Skip to content

Commit bac5d13

Browse files
authored
chore(release): prep v0.1.5 — post-audit hardening cut (#49)
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.
1 parent f45c3c0 commit bac5d13

2 files changed

Lines changed: 28 additions & 3 deletions

File tree

CHANGELOG.md

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,33 @@ and this project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.ht
77

88
## [Unreleased]
99

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+
1025
### 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).
1237

1338
## [0.1.4] - 2026-05-05
1439

src/WebhookEngine.Sdk/WebhookEngine.Sdk.csproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
4-
<TargetFrameworks>net10.0;net8.0</TargetFrameworks>
4+
<TargetFramework>net10.0</TargetFramework>
55
<ImplicitUsings>enable</ImplicitUsings>
66
<Nullable>enable</Nullable>
77
<LangVersion>latest</LangVersion>
88

99
<!-- NuGet package metadata -->
1010
<PackageId>WebhookEngine.Sdk</PackageId>
11-
<Version>0.1.4</Version>
11+
<Version>0.1.5</Version>
1212
<Authors>WebhookEngine</Authors>
1313
<Description>.NET SDK for WebhookEngine — self-hosted webhook delivery platform. Send webhooks, manage endpoints and event types, retry failed deliveries.</Description>
1414
<PackageLicenseExpression>MIT</PackageLicenseExpression>

0 commit comments

Comments
 (0)