You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Bumps SDK package version to 0.1.5 and finalises the SDK target framework
at net10.0 only (previously net10.0;net8.0; the second commit on PR #46
that consolidated to net10-only got squash-merged with only the csproj
change, dropping the matching CHANGELOG entry — re-added here).
CHANGELOG section [Unreleased] is renamed to [0.1.5] - 2026-05-05 and a
fresh empty [Unreleased] block opens above it. The 0.1.5 section now
documents the full audit-derived hardening that landed since 0.1.4: SSRF
guard with DNS-rebinding defense (F1), CAS-guarded message status
transitions (F2), advisory-locked endpoint health mutations (F3),
size-bounded IMemoryCache with per-app invalidation (F4), liveness /
readiness health probes + OpenTelemetry tracing + migration startup lock
+ admin default rejection (F5), security headers + /metrics auth gate +
cookie hardening (F6), idempotency UNIQUE index + Stripe-style replay
+ retention NULL-out (F7), frontend 401 redirect + ChunkLoadError
boundary (F8), HttpClient disposal + rate-limiter eviction +
WebhookVerifier in SDK (F9), graceful shutdown drain + custom-header
allow-list (F10), and the SDK net10-only simplification.
No breaking API changes — the v1 prefix and Standard Webhooks header
names are preserved.
Copy file name to clipboardExpand all lines: CHANGELOG.md
+26-1Lines changed: 26 additions & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -7,8 +7,33 @@ and this project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.ht
7
7
8
8
## [Unreleased]
9
9
10
+
## [0.1.5] - 2026-05-05
11
+
12
+
This release is the **post-audit hardening cut**. A multi-agent deep audit covered security, memory, concurrency, code quality, frontend, operations, timezone correctness, and NuGet SDK compliance; the ten resulting fixes (F1–F10) plus an idempotency race fix (F7) and an SDK target-framework simplification all land here. No breaking API changes — the `v1` route prefix and Standard Webhooks header names are preserved.
13
+
14
+
### Added
15
+
-**Liveness / readiness health probes (audit Ops).**`HealthController` now serves `/health/live` (process is up) and `/health/ready` (`AppReadinessGate` + `DbContext.CanConnectAsync`) alongside the original `/health`. `/health/ready` returns `503` with a reason while migrations or DI are still warming up so orchestrators (Kubernetes, Compose health checks) can wait correctly.
16
+
-**OpenTelemetry tracing with optional OTLP export (audit Ops).**`AddOpenTelemetry().WithTracing(...)` instruments ASP.NET Core; an OTLP exporter activates when `OpenTelemetry:OtlpEndpoint` is set, so deployments can pipe traces into Tempo / Honeycomb / Grafana Cloud without code changes.
17
+
-**`WebhookVerifier` in the .NET SDK (audit SDK).** Standard Webhooks signature verification ships in `WebhookEngine.Sdk` for the first time: 5-minute default tolerance, `CryptographicOperations.FixedTimeEquals` constant-time comparison, support for `whsec_` prefix and base64 secrets, and multi-value signatures for secret rotation. Receivers no longer need to roll their own verifier.
18
+
19
+
### Changed
20
+
-**SDK target framework simplified to `.NET 10` only.**`WebhookEngine.Sdk` previously multi-targeted `net8.0`, `net9.0`, and `net10.0`. The package now ships a single `net10.0` target, matching the rest of the project's stack lock. This is a pre-`v1.0` cleanup — the package has effectively zero installed user base, and self-hosted webhook deployments are overwhelmingly on `.NET 10` already. Side effects: NuGet badge flips from `.NET 8.0` to `.NET 10.0`, build cost drops 3×, and modern BCL APIs (`System.Threading.Lock`, `FrozenDictionary`, source-generated JSON, `OrderedDictionary<,>`) are unblocked for future SDK work.
21
+
-**`IMemoryCache` is bounded and per-app invalidatable (audit Memory C1 + C2).** The lookup cache is now configured with `SizeLimit = 10_000` and every entry sets `Size = 1`, so the cache cannot grow without bound under churn. `DeliveryLookupCache.InvalidateApplication(appId)` cancels the per-app `CancellationTokenSource` change-token so endpoint / event-type mutations evict stale entries immediately.
22
+
-**Graceful shutdown drain window (audit Ops H2).**`HostOptions.ShutdownTimeout = 45s` so in-flight HTTP deliveries can complete cleanly when the process receives SIGTERM, instead of being torn down at the default 5-second ceiling.
23
+
-**Dashboard admin default credentials are rejected outside Development.**`DashboardAdminSeeder` throws on startup if the seeded email is `admin@example.com` or the password is `admin`, `changeme`, `password`, or under 12 characters in non-Development environments. Operators must set real credentials before the API will start.
24
+
10
25
### Fixed
11
-
- **Idempotency race condition closed (audit Concurrency H1).** Two concurrent requests with the same `idempotencyKey` previously slipped past the time-window pre-check and double-enqueued the message — both threads saw an empty lookup and both inserted. A new partial unique index on `(app_id, endpoint_id, idempotency_key) WHERE idempotency_key IS NOT NULL` now serializes inserts at the database, and the controllers (`POST /api/v1/messages`, `POST /api/v1/messages/batch`, `POST /api/v1/dashboard/messages`) catch the resulting `23505` conflict to perform a Stripe-style replay: fetch the winning row for that `(app, endpoint, key)` triple and return its id as if it had been freshly enqueued. The window-based reuse semantics are preserved — `RetentionCleanupWorker` now NULL-outs `idempotency_key` on rows past the per-app `IdempotencyWindowMinutes` so the same key can be re-used in a fresh window without violating the index. The companion non-unique index `idx_messages_app_idempotency` is kept for the lookup path. Migration `20260505140607_AddIdempotencyUniqueIndex` is defensive: it NULL-outs any pre-existing duplicate triples (keeping the most-recent row per group) before creating the unique index, so the migration cannot fail on legacy data.
26
+
- **Idempotency race condition closed (audit Concurrency H1, F7).** Two concurrent requests with the same `idempotencyKey` previously slipped past the time-window pre-check and double-enqueued the message — both threads saw an empty lookup and both inserted. A new partial unique index on `(app_id, endpoint_id, idempotency_key) WHERE idempotency_key IS NOT NULL` now serializes inserts at the database, and the controllers (`POST /api/v1/messages`, `POST /api/v1/messages/batch`, `POST /api/v1/dashboard/messages`) catch the resulting `23505` conflict to perform a Stripe-style replay: fetch the winning row for that `(app, endpoint, key)` triple and return its id as if it had been freshly enqueued. The window-based reuse semantics are preserved — `RetentionCleanupWorker` now NULL-outs `idempotency_key` on rows past the per-app `IdempotencyWindowMinutes` so the same key can be re-used in a fresh window without violating the index. The companion non-unique index `idx_messages_app_idempotency` is kept for the lookup path. Migration `20260505140607_AddIdempotencyUniqueIndex` is defensive: it NULL-outs any pre-existing duplicate triples (keeping the most-recent row per group) before creating the unique index, so the migration cannot fail on legacy data.
27
+
-**Duplicate-attempt regression on lock loss closed (audit Concurrency C2, F2).**`MessageRepository.MarkDeliveredAsync` / `MarkFailedForRetryAsync` / `MarkDeadLetterAsync` are now compare-and-set (`WHERE Id = @id AND Status = Sending AND LockedBy = @lockedBy`) and return `bool`. `DeliveryWorker` checks the result and abandons silently when the row was stolen by stale-lock recovery, eliminating the previous duplicate `MessageAttempt` insertion path.
28
+
-**`EndpointHealthTracker` race serialized via PostgreSQL advisory lock (audit Concurrency C1, F3).** Circuit-breaker mutations on the same endpoint could interleave between concurrent workers, corrupting `ConsecutiveFailures` and the Open / HalfOpen / Closed state. `WithEndpointLockAsync` now wraps every mutation in a transaction with `pg_advisory_xact_lock(((100_001L << 32) | endpointHash))`, re-reads under lock, mutates, and commits. Behavior is unchanged on the InMemory test provider.
29
+
-**Frontend session-expiry redirect and chunk-load recovery (audit Frontend C1 + H4, F8).** A `webhookengine:auth-expired``CustomEvent` fires on any 401 outside `/api/v1/auth/*`; `AuthContext` listens, clears user state, and the router redirects to login. A new `RouteErrorBoundary` catches `ChunkLoadError` (e.g., after a deploy invalidates cached lazy chunks) and renders an "Update available — Reload" prompt instead of crashing the SPA.
30
+
-**`HttpResponseMessage` disposal and rate-limiter eviction (audit Memory H1 + H2, F9).**`HttpDeliveryService` now uses `using var` on both the `HttpRequestMessage` and `HttpResponseMessage` so socket-pool entries don't leak under high failure rates. `EndpointRateLimiter` evicts inactive endpoint windows after 15 minutes (5-minute sweep) so the dictionary cannot grow unbounded across an endpoint's lifetime.
31
+
-**Migration startup race serialized via advisory lock (audit Ops, F5).** Concurrent API replicas could race the migrator at startup; the migration block in `Program.cs` now wraps `Database.MigrateAsync()` in `pg_advisory_lock(((200_000L << 32) | 1))` so only one replica runs the migrator while the others wait.
32
+
33
+
### Security
34
+
-**SSRF guard with DNS-rebinding defense (audit Security C1, F1).** Endpoint URL validation now performs a DNS resolve and rejects any address that maps to RFC1918, loopback, link-local, CGNAT, multicast, reserved, or IPv6 unique-local / link-local / IPv4-mapped private ranges. The `webhook-delivery``HttpClient` uses a `SocketsHttpHandler.ConnectCallback` that pins the resolved IP for the lifetime of the request, so a malicious DNS server cannot return a public IP at validate-time and a private IP at connect-time. A new `WebhookEngine:SsrfGuard` options section (`Enabled`, `AllowLoopbackInDevelopment`) gates the policy.
35
+
-**Security headers, `/metrics` auth gate, and cookie hardening (audit Security H1 + H3 + Medium 2, F6).**`SecurityHeadersMiddleware` now sets `Strict-Transport-Security` (in non-Dev), `Content-Security-Policy`, `X-Frame-Options: DENY`, `X-Content-Type-Options: nosniff`, `Referrer-Policy`, and `Permissions-Policy`. `MetricsAuthMiddleware` requires `Authorization: Bearer <token>` on `/metrics` when `WebhookEngine:Metrics:ScrapeToken` is configured. The dashboard cookie now uses `SecurePolicy = Always` outside Development and Testing, so an HTTPS-only deployment cannot accidentally issue cookies over plaintext.
36
+
-**Custom-header allow-list and reserved-header rejection (audit Security M3, F10).**`CustomHeaderPolicy` rejects per-endpoint custom headers that would override engine-set headers (`Authorization`, `Cookie`, `Set-Cookie`, `Host`, `Content-*`, `Transfer-Encoding`, `User-Agent`, `webhook-id`, `webhook-timestamp`, `webhook-signature`), strips CR/LF, and enforces size bounds (name ≤128, value ≤1024). Applied to all four endpoint validators (public + dashboard, create + update).
0 commit comments