Skip to content

feat: add provider-specific webhook ingestion endpoints - #822

Merged
thlpkee20-wq merged 3 commits into
Stellabill:mainfrom
supreme2580:feat/webhook-routes
Aug 31, 2026
Merged

feat: add provider-specific webhook ingestion endpoints#822
thlpkee20-wq merged 3 commits into
Stellabill:mainfrom
supreme2580:feat/webhook-routes

Conversation

@supreme2580

Copy link
Copy Markdown
Contributor

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 legacy POST /webhooks endpoint remains registered for backward compatibility.

Acceptance criteria → code/tests

Routes registered across the requested files

  • internal/routes/routes.go: /api/webhooks group registers /stripe and /generic via webhookVerificationFor(...); each resolves its provider config through middleware.ProviderConfig + middleware.WebhookVerificationMiddleware (HMAC-SHA256/384/512, 5-min tolerance, DefaultMaxBodySize cap, Stripe composite t=...,v1=... support). Legacy POST /webhooks preserved 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: NewPostgresRepository persists the raw event via Store(ctx, event) (the DB UNIQUE index on deduplication_id is the durable replay authority).
  • Persistence is skipped gracefully when no DB is configured (dev mode) → handler returns 503 outbox_unavailable instead of panicking.

Security, authorization, validation, data integrity

  • Signing secrets are resolved exclusively through secrets.Provider (Vault-first chain with env provider inside the abstraction). No raw os.Getenv reads for provider secrets — webhookVerificationFor drops the previous raw-env fallback. Missing secret ⇒ the route is registered to reject every request with 403 webhook_secret_not_configured (fail closed, never a placeholder secret).
  • Wrong/missing signature ⇒ 401; stale timestamp outside tolerance ⇒ 401; body over DefaultMaxBodySize (5MB) ⇒ 413.
  • Replay rejection keyed by event_id: header event id (generic X-Webhook-Event-Id) or, for Stripe (which signs timestamp+payload and sends the id in the body), the payload id/event_id field. Duplicates are rejected by the in-memory EventIDCache before persistence and additionally by the DB unique constraint — both layers tested.
  • handlers/webhooks.go now derives the event id from the payload for providers that don't send an id header, builds the outbox event with deduplication_id = event_id, and acknowledges idempotently on replay.

Failure/retry/concurrency/boundary behavior

  • Bad signature / stale timestamp / oversized body produced by the middleware (tested).
  • Store failure ⇒ idempotent 200 ack (event already recorded); nil store ⇒ 503.
  • Concurrent duplicate writes: in-memory cache + DB unique index make double-persistence safe; TestEventIDCache_SimultaneousWrites covers the race.

Regression coverage (empty, invalid, duplicate, boundary)

  • internal/middleware/webhook_verification_test.go: TestEventIDCache made deterministic (removed duplicated Len subtests 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 real outbox.Repository interface — 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

  • No schema changes; the deduplication_id column + partial UNIQUE index already exist (migrations/0009_add_outbox_deduplication.up.sql).
  • Legacy POST /webhooks (shared WEBHOOK_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'
  • Note: the repository baseline has pre-existing build breaks in unrelated files (tools/slo-lint mixed packages; several pre-existing broken test files in internal/handlers, internal/middleware, internal/repository); per scope they are left untouched. go test ./... in full is not green on the baseline.

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
@supreme2580

Copy link
Copy Markdown
Contributor Author

Alot of file changes were made because some other things on upstream are broken affecting my implementation

@thlpkee20-wq
thlpkee20-wq merged commit 4477a1f into Stellabill:main Aug 31, 2026
6 of 24 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add provider-specific webhook verification routes (Stripe + generic HMAC)

2 participants