diff --git a/README.md b/README.md
index 6fcf3b4..9bf10b1 100644
--- a/README.md
+++ b/README.md
@@ -4,53 +4,28 @@
[](https://github.com/rodri-oliveira-dev/ReliableWebhooks/actions/workflows/release.yml)
[](https://www.nuget.org/packages/ReliableWebhooks/)
[](https://dotnet.microsoft.com/)
-[](LICENSE)
+[](https://github.com/rodri-oliveira-dev/ReliableWebhooks/blob/main/LICENSE)
**English** | [Português (Brasil)](https://github.com/rodri-oliveira-dev/ReliableWebhooks/blob/main/README.pt-BR.md)
-ReliableWebhooks is a .NET 10 library for building reliable outbound webhook delivery.
+ReliableWebhooks is a .NET 10 library for reliable outbound webhook delivery. It provides explicit primitives for durable enqueueing, lease-based dispatch, bounded concurrency, HTTP delivery, retry classification and scheduling, HMAC-SHA256 signing, observability, and Microsoft dependency injection/hosting.
-It provides composable primitives for stable webhook identities, persistence and lease coordination, bounded concurrent dispatching, one-attempt HTTP delivery, HMAC-SHA256 request signing, response classification, deterministic retry scheduling, backend-neutral observability, and standard .NET dependency-injection/hosting integration. The goal is to make the reliability concerns around webhook delivery explicit instead of hiding them inside an opaque background loop.
+Use it when an HTTP `POST` is not enough: deliveries must survive failures, coordinate multiple workers, retry deterministically, and expose operational state without pretending that exactly-once HTTP delivery exists.
-> **v1.0.0 scope:** the first public release provides the reliable-delivery engine, technology-agnostic `IWebhookDeliveryStore` contract, retries, leasing, signing, observability, dependency injection, hosted dispatching, and production guidance. Production restart durability is supplied by a consumer-provided conforming durable store.
+## Guarantees and boundaries
-## Why ReliableWebhooks?
+- With a conforming **durable** `IWebhookDeliveryStore`, the intended delivery model is **at-least-once**.
+- ReliableWebhooks does **not** guarantee exactly-once delivery. Receivers must be idempotent and tolerate duplicates.
+- The core package does **not** ship a production durable store. Durability across process restarts depends on the store supplied and configured by the consuming application.
+- `InMemoryWebhookDeliveryStore` is process-local, **not durable**, and intended only for tests, samples, and local development.
+- One `IWebhookDeliveryTransport.SendAsync` call represents one HTTP attempt; retry timing is handled separately by the retry policy and dispatcher.
+- Automatic dead-letter replay, receiver-side idempotency, and secret-rotation policy remain application responsibilities.
-Sending an HTTP `POST` is simple. Delivering a webhook reliably is not.
-
-Real applications must handle transient HTTP failures, network errors, process crashes, concurrent workers, retry storms, abandoned work, duplicate delivery, and servers that ask clients to retry later. A reliable solution also needs to preserve enough state to resume safely after a failure without pretending that exactly-once delivery is possible over HTTP.
-
-ReliableWebhooks addresses those concerns by separating the delivery workflow into explicit responsibilities:
-
-- a stable webhook message identity;
-- a persistence contract for enqueueing, claiming, leasing, retrying, and completing deliveries;
-- a concurrent dispatcher that claims only available capacity and coordinates graceful shutdown;
-- an HTTP transport that performs exactly one request attempt per call;
-- optional HMAC-SHA256 request signing over generated metadata and the exact outbound payload bytes;
-- transport-neutral success, retryable-failure, and permanent-failure outcomes;
-- a retry policy that calculates the next attempt without sleeping or blocking a worker;
-- structured logs, traces, and metrics built on standard .NET diagnostics APIs;
-- standard Microsoft DI and Generic Host integration with an optional hosted dispatcher;
-- replaceable abstractions for persistence, transport, dispatch timing, signing, classification, and retry behavior.
-
-## How it works
-
-A ReliableWebhooks delivery is designed around a small state machine rather than a fire-and-forget HTTP call:
-
-1. Create a `WebhookMessage` with a stable ID, event type, destination, exact payload bytes, content type, and optional headers.
-2. Enqueue it through `IWebhookDeliveryStore` or the application-facing `IWebhookEnqueueService`. Duplicate enqueue attempts with the same stable ID are deterministic. Wrap the store with `InstrumentedWebhookDeliveryStore` when enqueue logs and the queued metric are required independently of the persistence implementation.
-3. `WebhookDispatcher` atomically claims due deliveries up to its available concurrency slots and receives expiring `WebhookDeliveryLease` instances.
-4. `WebhookHttpTransport` performs one HTTP `POST` per claimed delivery; when a signer is configured, it signs the exact payload buffer used by the request and adds the delivery identity, event type, timestamp, and signature headers.
-5. The transport returns a `WebhookDeliveryResult` with a transport-neutral outcome.
-6. Successful and permanent failures are persisted immediately. Retryable failures are passed to `IWebhookRetryPolicy`, which returns either a future `NextAttemptAt` or a dead-letter decision.
-7. The store records the resulting state so abandoned or failed work can be resumed safely. Lease ownership prevents stale workers from overwriting the current owner.
-8. The lifecycle emits safe structured logs, an activity for each delivery attempt, and bounded metrics without requiring a telemetry backend.
-
-When combined with a durable store, the intended delivery model is **at-least-once**, not exactly-once. Receivers must therefore be idempotent and tolerate duplicate deliveries.
+For the complete production contract and operational guidance, see [Production usage](https://github.com/rodri-oliveira-dev/ReliableWebhooks/blob/main/docs/production-usage.md) and the [Persistence contract](https://github.com/rodri-oliveira-dev/ReliableWebhooks/blob/main/docs/persistence.md).
## Installation
-The NuGet package ID is `ReliableWebhooks` and the library targets `net10.0`. Install v1.0.0 with:
+The package targets `net10.0`:
```bash
dotnet add package ReliableWebhooks --version 1.0.0
@@ -62,21 +37,9 @@ or:
```
-## v1.0.0 boundaries
-
-The first public release intentionally keeps persistence technology-agnostic:
-
-- no production durable store is bundled; applications register a conforming `IWebhookDeliveryStore`;
-- `InMemoryWebhookDeliveryStore` is non-durable and intended only for tests, samples, and local development;
-- delivery is at-least-once when backed by a conforming durable store, so receivers must be idempotent;
-- exactly-once delivery, receiver-side idempotency, automatic dead-letter replay, and secret-rotation policy are not guaranteed;
-- EF Core, Dapper, ADO.NET, Redis, files, document databases, and other persistence technologies are optional consumer choices, not core dependencies.
-
-See [`docs/production-usage.md`](docs/production-usage.md) for production integration and [`docs/release-v1.0.0.md`](docs/release-v1.0.0.md) for release/distribution details.
-
## Quick start
-The current API exposes the delivery primitives directly. The example below uses the in-memory store only to demonstrate the workflow.
+This minimal example uses the in-memory store so it can run without external infrastructure. Replace it with a conforming durable `IWebhookDeliveryStore` before using ReliableWebhooks in production.
```csharp
using System.Text.Json;
@@ -97,6 +60,7 @@ var message = new WebhookMessage(
IWebhookDeliveryStore store = new InstrumentedWebhookDeliveryStore(
new InMemoryWebhookDeliveryStore());
+
await store.EnqueueAsync(message, DateTimeOffset.UtcNow);
using var handler = new HttpClientHandler
@@ -118,270 +82,57 @@ var dispatcher = new WebhookDispatcher(
MaxConcurrency = 4,
LeaseDuration = TimeSpan.FromMinutes(1),
PollInterval = TimeSpan.FromSeconds(1),
- ShutdownGracePeriod = TimeSpan.FromSeconds(30),
+ ShutdownGracePeriod = TimeSpan.FromSeconds(5),
});
-await dispatcher.RunAsync(stoppingToken);
+using var stop = new CancellationTokenSource(TimeSpan.FromSeconds(10));
+await dispatcher.RunAsync(stop.Token);
```
-`WebhookDispatcher` does not create unbounded work: it claims at most the number of currently available concurrency slots. On shutdown it stops new claims, lets in-flight deliveries finish during the configured grace period, and then cancels remaining attempts. Canceled work is not marked successful; its lease can expire and be reclaimed later.
+`AddHostedDispatcher()` is opt-in. Applications can instead resolve and run `WebhookDispatcher` directly when they need to own its lifecycle.
-`InMemoryWebhookDeliveryStore` is process-local and **not durable**. It exists for tests and samples only and must not be used as production persistence.
+A runnable end-to-end application with signing, success, retries, dead-letter behavior, and diagnostics is available in [`samples/ReliableWebhooks.Sample`](https://github.com/rodri-oliveira-dev/ReliableWebhooks/tree/main/samples/ReliableWebhooks.Sample).
-For the full persistence contract and conformance guidance, see [`docs/persistence.md`](docs/persistence.md).
+## Delivery flow
-A runnable end-to-end sample is available in [`samples/ReliableWebhooks.Sample`](samples/ReliableWebhooks.Sample). For production integration, configuration, receiver idempotency, signing verification, and troubleshooting, see [`docs/production-usage.md`](docs/production-usage.md).
+1. Create a `WebhookMessage` with a stable ID, event type, destination, exact payload bytes, content type, and optional headers.
+2. Enqueue it through `IWebhookEnqueueService` or the lower-level `IWebhookDeliveryStore`.
+3. `WebhookDispatcher` claims only due work up to its available concurrency and receives expiring leases.
+4. `WebhookHttpTransport` performs one request attempt and returns a transport-neutral result.
+5. Success and permanent failure become terminal states; retryable failures are scheduled by `IWebhookRetryPolicy`.
+6. A conforming durable store persists the resulting state so expired or abandoned work can be reclaimed safely.
-### Dependency injection and hosted dispatcher
+Stable IDs make enqueueing idempotent at the store boundary, while leases prevent stale workers from overwriting the current owner. These mechanisms support at-least-once delivery; they do not remove the need for receiver idempotency.
-Applications using the .NET Generic Host can register the standard integration through `AddReliableWebhooks`. A production application must register its own `IWebhookDeliveryStore`; ReliableWebhooks intentionally does not select the non-durable in-memory store as a production default.
+## Minimal production registration
+
+A production application owns the persistence adapter and explicitly registers it:
```csharp
services.AddSingleton();
-ReliableWebhooksBuilder webhooks = services.AddReliableWebhooks(options =>
-{
- options.Dispatcher.MaxConcurrency = 8;
- options.Dispatcher.LeaseDuration = TimeSpan.FromMinutes(2);
- options.Dispatcher.PollInterval = TimeSpan.FromSeconds(1);
- options.MessageLimits.MaxPayloadBytes = 1024 * 1024;
- options.Transport = new WebhookHttpTransportOptions
- {
- AttemptTimeout = TimeSpan.FromSeconds(30),
- };
-});
-
+ReliableWebhooksBuilder webhooks = services.AddReliableWebhooks();
webhooks.AddHostedDispatcher();
```
-`AddHostedDispatcher()` is opt-in. Without it, applications can resolve and run `WebhookDispatcher` themselves. The default transport uses `IHttpClientFactory`, requires HTTPS destinations, disables automatic redirects and cookies, removes the default `HttpClientFactory` request loggers to avoid leaking secret-bearing destination paths or query strings, and sets `HttpClient.Timeout` to infinite so `WebhookHttpTransportOptions.AttemptTimeout` remains the authoritative per-attempt timeout. Additional handlers or client configuration can be added through `ReliableWebhooksBuilder.HttpClientBuilder`. Direct `WebhookHttpTransport` construction uses the caller-provided `HttpClient` as configured, including any handler cookie policy.
-
-ReliableWebhooks treats destinations as operator-trusted by default after validating that they are absolute HTTP/HTTPS URIs, but the transport rejects plaintext `http://` unless `WebhookHttpTransportOptions.AllowInsecureHttp` is explicitly set. Use that opt-in only for development, loopback, or a deliberately trusted plaintext network. HMAC signing does not provide confidentiality or TLS server authentication. Applications that accept tenant-provided or otherwise untrusted webhook URLs should also configure `WebhookHttpTransportOptions.DestinationPolicy`, for example with `new PublicNetworkWebhookDestinationPolicy(allowedHosts: ["internal-webhooks.example"])`. That policy resolves DNS names before each attempt and denies loopback, unspecified, multicast, link-local, private, shared carrier-grade, documentation, benchmarking, transition, reserved, and other special-use targets for IPv4 and IPv6 unless a host is explicitly allow-listed. A deterministic destination denial is a permanent delivery failure and is not retried. With a standard `HttpClient`, validation happens before `SendAsync`; keep automatic redirects disabled and use an exact allow-list for any intended intranet destinations to avoid broad SSRF bypasses.
-
-The default response classifier, retry policy, transport, and dispatcher are registered with replaceable DI registrations. Stores and signers are application-provided, and dispatcher timing or retry jitter abstractions can also be replaced. Invalid dispatcher, retry, transport, or signing options are validated when options are resolved and by Generic Host startup validation.
-
-Application code can enqueue without depending directly on persistence scheduling details:
-
-```csharp
-IWebhookEnqueueService webhookEnqueue =
- serviceProvider.GetRequiredService();
-
-await webhookEnqueue.EnqueueAsync(message, cancellationToken);
-```
-
-`IWebhookEnqueueService` enforces `ReliableWebhooksOptions.MessageLimits` before a delivery is persisted. Defaults allow a 1 MiB payload, 32 persisted custom headers, 16 KiB of aggregate custom header name/value bytes, 128-character webhook IDs, 128-character event types, 256-character content types, and 2048-character destination URIs. Increase these limits only for receivers and tenants that are expected to need larger messages, and pair them with application-level quotas so one tenant cannot consume the whole queue. Code that bypasses this service and calls `IWebhookDeliveryStore.EnqueueAsync` directly is also bypassing the standard production limit gate; call `WebhookMessageLimits.Validate(message)` explicitly in that path or enforce equivalent limits at the application boundary.
-
-The integration depends only on `Microsoft.Extensions.*`; it does not require ASP.NET Core.
-
-### Signing webhooks
-
-Signing is enabled by supplying an `IWebhookRequestSigner`. The built-in `HmacSha256WebhookRequestSigner` resolves secret bytes through `IWebhookSigningSecretProvider`, so the library does not need to know whether secrets come from configuration, a secret manager, or another source. It requires at least 32 bytes (256 bits) of cryptographically generated HMAC key material and rejects empty or shorter secrets before signing. Generate these bytes with a CSPRNG and store them in a secret manager; do not use passwords, passphrases, tenant names, or other low-entropy strings as signing secrets.
-
-```csharp
-IWebhookSigningSecretProvider secretProvider = GetApplicationSecretProvider();
-IWebhookRequestSigner signer = new HmacSha256WebhookRequestSigner(secretProvider);
-
-var transport = new WebhookHttpTransport(
- httpClient,
- classifier: null,
- options: null,
- signer: signer);
-```
-
-With the default `WebhookSigningOptions`, each signed request contains:
-
-- `X-Webhook-Id`: the stable `WebhookMessage.Id`;
-- `X-Webhook-Event`: the `WebhookMessage.EventType`;
-- `Content-Type`: the normalized `WebhookMessage.ContentType`;
-- `X-Webhook-Timestamp`: the UTC Unix timestamp in seconds;
-- `X-Webhook-Signature`: `v1=`.
-
-Header names can be customized through `WebhookHttpTransportOptions.Signing`. Generated signing headers take precedence over custom message headers with the same names.
-
-`WebhookMessage.Id` and `WebhookMessage.EventType` must be non-empty and must not contain control characters such as CR, LF, or NUL because they are used in generated headers and telemetry. `WebhookMessage.ContentType` must be a syntactically valid HTTP media type, including vendor media types such as `application/vnd.example+json`.
-
-Custom headers supplied to `WebhookMessage` must use valid HTTP token names, are compared case-insensitively for duplicates, and must not contain control characters such as CR, LF, or NUL in their values. The default transport reserves routing and framing headers that message data must not control: `Host`, `Content-Length`, `Transfer-Encoding`, `Connection`, `TE`, `Trailer`, `Upgrade`, `Expect`, `Keep-Alive`, `Proxy-Authenticate`, `Proxy-Authorization`, and `Proxy-Connection`. Sensitive credential headers such as `Authorization` and `Cookie` are not accepted in the persisted message; resolve them at send time through `IWebhookRequestHeaderProvider` so queued attempts pick up rotation without rewriting stored deliveries. Treat custom header values as sensitive whenever they come from tenants, subscribers, or other external configuration. Advanced users who need lower-level HTTP control should provide a custom `IWebhookDeliveryTransport`.
-
-The `v1` canonical HMAC input is versioned and length-prefixed:
-
-```text
-ASCII("rw-hmac-sha256/v1\0")
-|| frame(UTF8(unixTimestampSeconds))
-|| frame(UTF8(webhookId))
-|| frame(UTF8(eventType))
-|| frame(UTF8(contentType))
-|| frame(exactRequestPayloadBytes)
-```
+The store must implement the durability, atomic claim, lease, stale-owner rejection, retry scheduling, and terminal-state semantics defined by the persistence contract.
-Each `frame(value)` is an eight-byte big-endian length followed by the exact value bytes. The payload bytes are not reserialized or normalized before signing. The same byte array is used both for HMAC calculation and for the HTTP request content. The content type is normalized with the platform HTTP media-type parser when the `WebhookMessage` is created, so the authenticated content-type frame matches the serialized `Content-Type` header sent by the default transport. The authenticated values are the timestamp, webhook ID, event type, content type, and body bytes. Custom headers, destination URI, and the configurable signing header names are not part of the built-in HMAC envelope.
+## Read next
-A receiver can verify a delivery independently by:
-
-1. reading the generated ID, event, timestamp, signature, and content type without modifying the request body;
-2. parsing the timestamp as Unix seconds and rejecting timestamps outside the receiver's replay-tolerance window;
-3. rebuilding the `rw-hmac-sha256/v1` canonical frames with those metadata values and the raw request body bytes;
-4. calculating HMAC-SHA256 with the shared secret;
-5. encoding the digest as lowercase hexadecimal and prefixing it with `v1=`;
-6. comparing the calculated signature with the received signature using a constant-time comparison.
-
-Signing secrets are never included in library-generated exception messages or automatic telemetry. Applications should follow the same rule in custom secret providers and signers. Rotate secrets through your application secret provider and receiver configuration, and use different secrets for distinct trust audiences or receivers so one receiver compromise does not allow signatures to be forged for another.
-
-## Observability
-
-ReliableWebhooks emits telemetry through standard .NET APIs and does **not** depend on the OpenTelemetry SDK, a collector, or any exporter. Applications decide whether telemetry is collected and where it is sent.
-
-The public `ReliableWebhooksInstrumentation` class exposes the canonical names used by the library:
-
-```csharp
-ReliableWebhooksInstrumentation.ActivitySourceName // "ReliableWebhooks"
-ReliableWebhooksInstrumentation.MeterName // "ReliableWebhooks"
-```
-
-The same class exposes stable names for the delivery-attempt activity and the published metrics, so integrations do not need to duplicate instrumentation strings.
-
-### Structured logs
-
-`WebhookDispatcher` has an additive constructor overload that accepts `ILogger`. Existing constructors remain valid and use a no-op logger. Enqueue telemetry is provided by `InstrumentedWebhookDeliveryStore`, a decorator that can wrap any `IWebhookDeliveryStore`, including a custom durable production store.
-
-```csharp
-ILogger logger = loggerFactory.CreateLogger("ReliableWebhooks");
-
-IWebhookDeliveryStore durableStore = GetApplicationWebhookStore();
-IWebhookDeliveryStore store = new InstrumentedWebhookDeliveryStore(
- durableStore,
- logger);
-
-var dispatcher = new WebhookDispatcher(
- store,
- transport,
- retryPolicy,
- options,
- delay: null,
- logger);
-```
-
-`InstrumentedWebhookDeliveryStore` emits the enqueue event and queued metric only when a new delivery is actually persisted. Duplicate idempotent enqueue calls do not double-count queued deliveries. Claim telemetry is emitted by `WebhookDispatcher`, so the same claim is logged exactly once and custom stores receive the same lifecycle coverage.
-
-The lifecycle uses stable event IDs for enqueue, claim, attempt start, success, retry scheduling, permanent failure, dead letter, cancellation, lease loss, and unexpected failure. Log properties are structured and deliberately exclude payload bodies, destination URLs/query strings, signing secrets, and signatures.
-
-### Traces
-
-Each dispatcher delivery attempt creates an activity named `ReliableWebhooks.DeliveryAttempt` from the `ReliableWebhooks` activity source. Safe attributes include:
-
-- `webhook.id` for log/trace correlation;
-- `webhook.event_type`;
-- `webhook.attempt`;
-- `webhook.outcome`;
-- `http.response.status_code` when an HTTP response exists;
-- `error.type` for unexpected exception types.
-
-Payloads, secrets, signatures, and destination URLs are never added automatically. Webhook IDs are allowed in traces for correlation but are intentionally excluded from metric dimensions.
-
-### Metrics
-
-| Instrument | Type | Tags |
-| --- | --- | --- |
-| `reliablewebhooks.delivery.queued` | Counter | None by default |
-| `reliablewebhooks.delivery.attempted` | Counter | None by default |
-| `reliablewebhooks.delivery.succeeded` | Counter | None by default |
-| `reliablewebhooks.delivery.retried` | Counter | None by default |
-| `reliablewebhooks.delivery.permanently_failed` | Counter | None by default |
-| `reliablewebhooks.delivery.dead_lettered` | Counter | None by default |
-| `reliablewebhooks.delivery.duration` | Histogram in milliseconds | `webhook.outcome` |
-
-Raw `WebhookMessage.EventType` is not used as a metric dimension by default. To opt into an event-type metric tag, configure `WebhookMetricsOptions.EventTypeTagAllowList` with a bounded vocabulary; values outside the allow-list are reported as `other`. Never include customer IDs, request IDs, destinations, webhook IDs, payload data, signatures, or other high-cardinality/sensitive values in metric dimensions.
-
-### OpenTelemetry integration
-
-A consumer can opt into OpenTelemetry with its own OpenTelemetry packages and exporter configuration. ReliableWebhooks itself does not require them:
-
-```csharp
-builder.Services
- .AddOpenTelemetry()
- .WithTracing(tracing =>
- tracing.AddSource(ReliableWebhooksInstrumentation.ActivitySourceName))
- .WithMetrics(metrics =>
- metrics.AddMeter(ReliableWebhooksInstrumentation.MeterName));
-```
-
-The application can then add OTLP, Azure Monitor, Prometheus, Grafana/Tempo, Datadog, Dynatrace, or another supported exporter/backend without changing ReliableWebhooks. Logging continues through standard `ILogger` and can be connected to the application's chosen logging/OpenTelemetry pipeline independently.
-
-## Default behavior
-
-| Area | Default |
-| --- | --- |
-| Dispatcher maximum concurrency | 4 |
-| Dispatcher lease duration | 1 minute |
-| Dispatcher poll interval | 1 second |
-| Dispatcher shutdown grace period | 30 seconds |
-| HTTP attempt timeout | 30 seconds |
-| Captured response body | Up to 16 KiB |
-| Outbound payload size | Up to 1 MiB through `IWebhookEnqueueService` |
-| Persisted custom headers | Up to 32 headers and 16 KiB aggregate name/value bytes through `IWebhookEnqueueService` |
-| Persisted metadata length | ID/event type up to 128 characters, content type up to 256, destination URI up to 2048 through `IWebhookEnqueueService` |
-| Plain HTTP destinations | Rejected unless `AllowInsecureHttp = true` |
-| Success classification | Any `2xx` response |
-| Retryable HTTP responses | `408`, `425`, `429`, and `5xx` |
-| Permanent HTTP responses | Other status codes, including redirects |
-| Signing algorithm | HMAC-SHA256 when a signer is configured |
-| Signing canonical format | `rw-hmac-sha256/v1` marker plus length-prefixed timestamp, webhook ID, event type, content type, and payload bytes |
-| Signing headers | `X-Webhook-Id`, `X-Webhook-Event`, `X-Webhook-Timestamp`, `X-Webhook-Signature` |
-| Maximum attempts | 5, including the current attempt |
-| Base retry delay | 1 second |
-| Maximum retry delay | 5 minutes |
-| Jitter | 0 to 20% positive jitter before the maximum-delay cap |
-| `Retry-After` | Honored when it schedules later than the local retry delay; `MaxDelay` caps only local backoff/jitter |
-
-Automatic redirects and automatic cookies must be disabled on the `HttpClient` handler so a single transport invocation cannot silently become multiple HTTP requests or retain receiver-controlled state for later deliveries. The default DI-managed client configures this automatically.
-
-Malformed `Retry-After` values are ignored. Valid delta-seconds and HTTP-date values are exposed through `WebhookDeliveryResult` and consumed by the default retry policy.
-
-## Delivery guarantees and boundaries
-
-ReliableWebhooks is designed around explicit delivery semantics:
-
-- **At-least-once, not exactly-once.** Duplicate delivery can occur, especially when a worker fails after sending but before persisting the result.
-- **Stable IDs support duplicate-safe enqueueing.** Receivers still need application-level idempotency.
-- **Message resource limits protect the queue.** The standard enqueue service rejects oversized payloads, headers, and persisted metadata before store writes. Applications should still apply tenant quotas and business-level payload constraints.
-- **Leases coordinate active ownership.** While a lease is valid, two workers should not own the same delivery simultaneously; expired work can be reclaimed.
-- **Dispatcher concurrency is bounded.** Claims are limited to currently available slots instead of creating unbounded background tasks.
-- **Shutdown is two-phase.** New claims stop first; in-flight work can finish within the grace period before remaining attempts are canceled.
-- **One transport call means one HTTP attempt.** Retry loops are intentionally outside the transport.
-- **Delivery-scoped extension failures are isolated.** Exceptions from transport/signing/classification for one delivery are logged by exception type and move through retry/dead-letter handling; store claim and state-transition failures remain infrastructure failures.
-- **Signed payloads use exact request bytes.** Receivers must verify the raw body rather than a parsed/reserialized representation.
-- **Retry policies schedule; they do not wait.** `DefaultWebhookRetryPolicy` returns a future timestamp or dead-letter decision and never calls `Task.Delay`.
-- **Telemetry is backend-neutral.** Logs, traces, and metrics use standard .NET APIs; exporters remain an application concern.
-- **Persistence is replaceable.** The core package does not depend on a specific database provider.
-
-`ReliableWebhooks` v1.0.0 does not require a built-in production persistence adapter. Applications provide a conforming durable `IWebhookDeliveryStore`; optional EF Core, Dapper, Redis, file-backed, or other adapters may be introduced independently.
+- **Production usage:** [`docs/production-usage.md`](https://github.com/rodri-oliveira-dev/ReliableWebhooks/blob/main/docs/production-usage.md) — DI/hosting, retry and response classification, signing and receiver verification, destination security, observability, configuration, tuning, and troubleshooting.
+- **Persistence contract:** [`docs/persistence.md`](https://github.com/rodri-oliveira-dev/ReliableWebhooks/blob/main/docs/persistence.md) — durable state, idempotent enqueue, atomic claims, leases, ownership, transitions, and conformance testing.
+- **Runnable sample:** [`samples/ReliableWebhooks.Sample`](https://github.com/rodri-oliveira-dev/ReliableWebhooks/tree/main/samples/ReliableWebhooks.Sample) — end-to-end integration using the public API.
+- **v1.0.0 scope:** [`docs/release-v1.0.0.md`](https://github.com/rodri-oliveira-dev/ReliableWebhooks/blob/main/docs/release-v1.0.0.md) — release boundaries, distribution, and supported surface.
+- **Security policy:** [`SECURITY.md`](https://github.com/rodri-oliveira-dev/ReliableWebhooks/blob/main/SECURITY.md) — supported security reporting process and project security guidance.
## Extensibility
-The main behaviors are exposed through public abstractions:
-
-- `IWebhookDeliveryStore` — persistence and lease coordination;
-- `InstrumentedWebhookDeliveryStore` — persistence decorator that adds enqueue logs and the queued metric to any store implementation;
-- `IWebhookEnqueueService` — application-facing enqueue API registered by the DI integration;
-- `IWebhookDeliveryTransport` — one-attempt delivery transport used by the dispatcher;
-- `IWebhookDispatcherDelay` — replaceable polling/grace-period timing for deterministic tests or custom scheduling;
-- `IWebhookHttpResponseClassifier` — HTTP response classification;
-- `IWebhookRetryPolicy` — retry and dead-letter decisions;
-- `IWebhookRetryJitterSource` — deterministic or custom jitter generation;
-- `IWebhookDestinationPolicy` — destination authorization for untrusted webhook URLs;
-- `IWebhookRequestSigner` — request-signing strategy;
-- `IWebhookSigningSecretProvider` — per-message signing secret resolution;
-- `ReliableWebhooksInstrumentation` — stable public diagnostics names for tracing and metrics integration.
-
-`ReliableWebhooksOptions` groups dispatcher, message limits, retry, transport, and signing configuration for DI consumers. `WebhookDispatcherOptions` exposes `MaxConcurrency`, `LeaseDuration`, `PollInterval`, `ShutdownGracePeriod`, and `TimeProvider` so concurrency and timing remain configurable and testable. `WebhookMessageLimits` exposes the standard per-message resource boundary used by `IWebhookEnqueueService` and can also be applied explicitly by applications that enqueue directly through a store.
-
-This keeps persistence, dispatching, transport behavior, signing, retry strategy, hosting, and telemetry backend selection independently replaceable and testable.
-
-## Support and contribution
+The core behaviors are exposed through replaceable abstractions, including persistence, delivery transport, response classification, retry policy/jitter, request signing and secret resolution, destination policy, request-header resolution, dispatcher timing, and observability integration. The production guide documents the built-in defaults and the boundaries that custom implementations must preserve.
-Use [GitHub Issues](https://github.com/rodri-oliveira-dev/ReliableWebhooks/issues) for bugs, questions, and feature discussions.
+## Contributing
-For security issues, follow [SECURITY.md](https://github.com/rodri-oliveira-dev/ReliableWebhooks/blob/main/SECURITY.md) instead of opening a public issue.
+See [`CONTRIBUTING.md`](https://github.com/rodri-oliveira-dev/ReliableWebhooks/blob/main/CONTRIBUTING.md) for local development, validation, and contribution guidance.
-Contributions are welcome. See [CONTRIBUTING.md](https://github.com/rodri-oliveira-dev/ReliableWebhooks/blob/main/CONTRIBUTING.md) for the contribution workflow and [CHANGELOG.md](https://github.com/rodri-oliveira-dev/ReliableWebhooks/blob/main/CHANGELOG.md) for notable changes.
+## License
ReliableWebhooks is licensed under the [MIT License](https://github.com/rodri-oliveira-dev/ReliableWebhooks/blob/main/LICENSE).
diff --git a/README.pt-BR.md b/README.pt-BR.md
index 6c7580e..89fae15 100644
--- a/README.pt-BR.md
+++ b/README.pt-BR.md
@@ -4,53 +4,28 @@
[](https://github.com/rodri-oliveira-dev/ReliableWebhooks/actions/workflows/release.yml)
[](https://www.nuget.org/packages/ReliableWebhooks/)
[](https://dotnet.microsoft.com/)
-[](LICENSE)
+[](https://github.com/rodri-oliveira-dev/ReliableWebhooks/blob/main/LICENSE)
[English](https://github.com/rodri-oliveira-dev/ReliableWebhooks/blob/main/README.md) | **Português (Brasil)**
-ReliableWebhooks é uma biblioteca .NET 10 para construção de entrega confiável de webhooks de saída.
+ReliableWebhooks é uma biblioteca .NET 10 para entrega confiável de webhooks de saída. Ela fornece primitivas explícitas para enqueue durável, dispatch baseado em leases, concorrência limitada, entrega HTTP, classificação e agendamento de retries, assinatura HMAC-SHA256, observabilidade e integração com dependency injection/hosting da Microsoft.
-Ela fornece componentes combináveis para identidade estável de webhooks, persistência e coordenação por lease, dispatch concorrente limitado, envio HTTP de uma única tentativa, assinatura HMAC-SHA256, classificação de respostas, agendamento determinístico de retries, observabilidade independente de backend e integração com injeção de dependência/hosting padrão do .NET. O objetivo é tornar explícitas as preocupações de confiabilidade da entrega de webhooks, em vez de escondê-las dentro de um loop de background opaco.
+Use-a quando um `POST` HTTP não é suficiente: as entregas precisam sobreviver a falhas, coordenar múltiplos workers, repetir de forma determinística e expor estado operacional sem fingir que entrega exactly-once existe sobre HTTP.
-> **Escopo da v1.0.0:** a primeira release pública fornece o engine de entrega confiável, contrato `IWebhookDeliveryStore` independente de tecnologia, retries, leases, assinatura, observabilidade, injeção de dependência, hosted dispatcher e orientação de produção. A durabilidade entre reinícios é fornecida por um store durável e aderente ao contrato escolhido pela aplicação.
+## Garantias e limites
-## Por que ReliableWebhooks?
+- Com um `IWebhookDeliveryStore` **durável** e aderente ao contrato, o modelo de entrega pretendido é **at-least-once**.
+- ReliableWebhooks **não** garante entrega exactly-once. Receivers precisam ser idempotentes e tolerar duplicidades.
+- O pacote core **não** inclui um store durável de produção. A durabilidade após reinício do processo depende do store fornecido e configurado pela aplicação consumidora.
+- `InMemoryWebhookDeliveryStore` é local ao processo, **não é durável**, e serve apenas para testes, samples e desenvolvimento local.
+- Uma chamada a `IWebhookDeliveryTransport.SendAsync` representa uma tentativa HTTP; o agendamento de retries é tratado separadamente pela política de retry e pelo dispatcher.
+- Replay automático de dead letters, idempotência no receiver e política de rotação de secrets continuam sendo responsabilidades da aplicação.
-Enviar um `POST` HTTP é simples. Entregar um webhook de forma confiável não é.
-
-Aplicações reais precisam lidar com falhas HTTP transitórias, erros de rede, queda do processo, workers concorrentes, picos de retry, trabalho abandonado, entregas duplicadas e servidores que pedem ao cliente para tentar novamente mais tarde. Uma solução confiável também precisa preservar estado suficiente para continuar com segurança após uma falha, sem fingir que exactly-once é possível sobre HTTP.
-
-ReliableWebhooks trata esses pontos separando o fluxo de entrega em responsabilidades explícitas:
-
-- uma identidade estável para cada mensagem de webhook;
-- um contrato de persistência para enqueue, claim, lease, retry e conclusão de entregas;
-- um dispatcher concorrente que faz claim apenas da capacidade disponível e coordena shutdown gracioso;
-- um transporte HTTP que executa exatamente uma tentativa de requisição por chamada;
-- assinatura HMAC-SHA256 opcional sobre metadados gerados e os bytes exatos do payload enviado;
-- resultados independentes do transporte para sucesso, falha retryable e falha permanente;
-- uma política de retry que calcula a próxima tentativa sem aguardar nem bloquear um worker;
-- logs estruturados, traces e métricas baseados nas APIs padrão de diagnostics do .NET;
-- integração com Microsoft DI e Generic Host, com hosted dispatcher opcional;
-- abstrações substituíveis para persistência, transporte, temporização do dispatcher, assinatura, classificação e comportamento de retry.
-
-## Como funciona
-
-Uma entrega com ReliableWebhooks é estruturada como uma pequena máquina de estados, e não como uma chamada HTTP fire-and-forget:
-
-1. Crie um `WebhookMessage` com ID estável, tipo de evento, destino, bytes exatos do payload, content type e headers opcionais.
-2. Faça o enqueue através de `IWebhookDeliveryStore` ou da API de aplicação `IWebhookEnqueueService`. Tentativas duplicadas com o mesmo ID estável têm comportamento determinístico. Envolva o store com `InstrumentedWebhookDeliveryStore` quando precisar de logs de enqueue e da métrica de queued independentemente da implementação de persistência.
-3. `WebhookDispatcher` faz claim atômico das entregas vencidas até o limite de slots de concorrência disponíveis e recebe instâncias expirantes de `WebhookDeliveryLease`.
-4. `WebhookHttpTransport` executa um único `POST` HTTP por entrega em lease; quando um signer está configurado, ele assina exatamente o mesmo buffer usado na requisição e adiciona headers com ID, tipo do evento, timestamp e assinatura.
-5. O transporte retorna um `WebhookDeliveryResult` com resultado independente do transporte.
-6. Sucessos e falhas permanentes são persistidos imediatamente. Falhas retryable são passadas para `IWebhookRetryPolicy`, que retorna um `NextAttemptAt` futuro ou uma decisão de dead letter.
-7. O store registra o estado resultante para que trabalhos abandonados ou com falha possam ser retomados com segurança. A posse da lease impede que workers obsoletos sobrescrevam o dono atual.
-8. O ciclo de vida emite logs estruturados seguros, uma activity por tentativa e métricas limitadas sem exigir backend de telemetria.
-
-Quando combinado com um store durável, o modelo de entrega pretendido é **at-least-once**, e não exactly-once. Portanto, os receptores precisam ser idempotentes e tolerar entregas duplicadas.
+Para o contrato completo de produção e orientações operacionais, veja [Uso em produção](https://github.com/rodri-oliveira-dev/ReliableWebhooks/blob/main/docs/production-usage.md) e o [Contrato de persistência](https://github.com/rodri-oliveira-dev/ReliableWebhooks/blob/main/docs/persistence.pt-BR.md).
## Instalação
-O Package ID no NuGet é `ReliableWebhooks` e a biblioteca tem como target `net10.0`. Instale a v1.0.0 com:
+O pacote tem como target `net10.0`:
```bash
dotnet add package ReliableWebhooks --version 1.0.0
@@ -62,21 +37,9 @@ ou:
```
-## Limites da v1.0.0
-
-A primeira release pública mantém a persistência intencionalmente independente de tecnologia:
-
-- nenhum store durável de produção é incluído; a aplicação registra um `IWebhookDeliveryStore` aderente ao contrato;
-- `InMemoryWebhookDeliveryStore` não é durável e serve apenas para testes, samples e desenvolvimento local;
-- a entrega é at-least-once quando apoiada por um store durável aderente, portanto receivers devem ser idempotentes;
-- exactly-once, idempotência no receiver, replay automático de dead letters e política de rotação de secrets não são garantidos;
-- EF Core, Dapper, ADO.NET, Redis, arquivos, bancos de documentos e outras tecnologias são escolhas opcionais do consumidor, não dependências do core.
-
-Consulte [`docs/production-usage.md`](docs/production-usage.md) para integração de produção e [`docs/release-v1.0.0.pt-BR.md`](docs/release-v1.0.0.pt-BR.md) para detalhes da release/distribuição.
+## Quick Start
-## Começando
-
-A API também permite montar os componentes diretamente. O exemplo abaixo usa o store em memória apenas para demonstrar o fluxo.
+Este exemplo mínimo usa o store em memória para poder executar sem infraestrutura externa. Substitua-o por um `IWebhookDeliveryStore` durável e aderente ao contrato antes de usar ReliableWebhooks em produção.
```csharp
using System.Text.Json;
@@ -97,6 +60,7 @@ var message = new WebhookMessage(
IWebhookDeliveryStore store = new InstrumentedWebhookDeliveryStore(
new InMemoryWebhookDeliveryStore());
+
await store.EnqueueAsync(message, DateTimeOffset.UtcNow);
using var handler = new HttpClientHandler
@@ -118,261 +82,57 @@ var dispatcher = new WebhookDispatcher(
MaxConcurrency = 4,
LeaseDuration = TimeSpan.FromMinutes(1),
PollInterval = TimeSpan.FromSeconds(1),
- ShutdownGracePeriod = TimeSpan.FromSeconds(30),
+ ShutdownGracePeriod = TimeSpan.FromSeconds(5),
});
-await dispatcher.RunAsync(stoppingToken);
+using var stop = new CancellationTokenSource(TimeSpan.FromSeconds(10));
+await dispatcher.RunAsync(stop.Token);
```
-`WebhookDispatcher` não cria trabalho de forma ilimitada: ele faz claim no máximo da quantidade de slots de concorrência disponíveis. No shutdown, ele interrompe novos claims, permite que entregas em andamento terminem durante o grace period configurado e cancela as tentativas restantes depois desse limite. Trabalho cancelado não é marcado como sucesso; sua lease pode expirar e ser recuperada posteriormente.
+`AddHostedDispatcher()` é opt-in. A aplicação também pode resolver e executar `WebhookDispatcher` diretamente quando precisa controlar seu ciclo de vida.
+
+Um sample end-to-end executável com assinatura, sucesso, retries, dead letter e diagnósticos está disponível em [`samples/ReliableWebhooks.Sample`](https://github.com/rodri-oliveira-dev/ReliableWebhooks/tree/main/samples/ReliableWebhooks.Sample).
-`InMemoryWebhookDeliveryStore` é local ao processo e **não é durável**. Ele existe apenas para testes e exemplos e não deve ser usado como persistência de produção.
+## Fluxo de entrega
-Para o contrato completo de persistência e as orientações de conformidade, consulte [`docs/persistence.pt-BR.md`](docs/persistence.pt-BR.md).
+1. Crie um `WebhookMessage` com ID estável, tipo do evento, destino, bytes exatos do payload, content type e headers opcionais.
+2. Faça o enqueue por `IWebhookEnqueueService` ou pelo `IWebhookDeliveryStore` de nível mais baixo.
+3. `WebhookDispatcher` faz claim apenas do trabalho vencido até o limite de concorrência disponível e recebe leases com expiração.
+4. `WebhookHttpTransport` executa uma tentativa de request e retorna um resultado independente do transporte.
+5. Sucesso e falha permanente tornam-se estados terminais; falhas retryable são reagendadas por `IWebhookRetryPolicy`.
+6. Um store durável aderente ao contrato persiste o estado resultante para que trabalho expirado ou abandonado possa ser recuperado com segurança.
-Um exemplo end-to-end executável está disponível em [`samples/ReliableWebhooks.Sample`](samples/ReliableWebhooks.Sample). Para integração em produção, configuração, idempotência do receiver, verificação de assinatura e troubleshooting, consulte [`docs/production-usage.md`](docs/production-usage.md).
+IDs estáveis tornam o enqueue idempotente na fronteira do store, enquanto leases impedem que workers antigos sobrescrevam o owner atual. Esses mecanismos suportam entrega at-least-once; eles não eliminam a necessidade de idempotência no receiver.
-### Injeção de dependência e hosted dispatcher
+## Registro mínimo para produção
-Aplicações que usam o Generic Host do .NET podem registrar a integração padrão através de `AddReliableWebhooks`. Uma aplicação de produção deve registrar seu próprio `IWebhookDeliveryStore`; ReliableWebhooks não escolhe implicitamente o store em memória, que não é durável, como default de produção.
+Uma aplicação de produção é responsável pelo adapter de persistência e o registra explicitamente:
```csharp
services.AddSingleton();
-ReliableWebhooksBuilder webhooks = services.AddReliableWebhooks(options =>
-{
- options.Dispatcher.MaxConcurrency = 8;
- options.Dispatcher.LeaseDuration = TimeSpan.FromMinutes(2);
- options.Dispatcher.PollInterval = TimeSpan.FromSeconds(1);
- options.MessageLimits.MaxPayloadBytes = 1024 * 1024;
- options.Transport = new WebhookHttpTransportOptions
- {
- AttemptTimeout = TimeSpan.FromSeconds(30),
- };
-});
-
+ReliableWebhooksBuilder webhooks = services.AddReliableWebhooks();
webhooks.AddHostedDispatcher();
```
-`AddHostedDispatcher()` é opt-in. Sem ele, a aplicação pode resolver e executar `WebhookDispatcher` diretamente. O transporte padrão usa `IHttpClientFactory`, exige destinos HTTPS, desabilita redirects automáticos e cookies, remove os loggers padrão de requisição do `HttpClientFactory` para evitar vazamento de paths ou queries de destino que contenham secrets, e configura `HttpClient.Timeout` como infinito para que `WebhookHttpTransportOptions.AttemptTimeout` continue sendo o timeout autoritativo de cada tentativa. Handlers ou configurações adicionais podem ser aplicados através de `ReliableWebhooksBuilder.HttpClientBuilder`. Construção direta de `WebhookHttpTransport` usa o `HttpClient` fornecido pelo chamador do jeito que ele estiver configurado, incluindo qualquer política de cookies do handler.
-
-ReliableWebhooks trata destinos como confiáveis pelo operador por padrão depois de validar que são URIs HTTP/HTTPS absolutas, mas o transporte rejeita `http://` em texto claro salvo quando `WebhookHttpTransportOptions.AllowInsecureHttp` é configurado explicitamente. Use esse opt-in apenas para desenvolvimento, loopback ou uma rede em texto claro deliberadamente confiável. Assinatura HMAC não oferece confidencialidade nem autenticação de servidor TLS. Aplicações que aceitam URLs de tenants ou outra origem não confiável também devem configurar `WebhookHttpTransportOptions.DestinationPolicy`, por exemplo com `new PublicNetworkWebhookDestinationPolicy(allowedHosts: ["internal-webhooks.example"])`. Essa policy resolve DNS antes de cada tentativa e nega destinos loopback, unspecified, multicast, link-local, privados, carrier-grade compartilhados, de documentação, benchmarking, transição, reservados e outros destinos de uso especial para IPv4 e IPv6, salvo quando um host é explicitamente permitido. Uma negação determinística de destino é uma falha permanente e não é retentada. Com `HttpClient` padrão, a validação acontece antes de `SendAsync`; mantenha redirects automáticos desabilitados e use allow-list exata para destinos de intranet intencionais para evitar bypasses amplos de SSRF.
-
-Classifier de resposta, política de retry, transporte e dispatcher usam registros substituíveis por DI. Store e signer são fornecidos pela aplicação, e as abstrações de temporização do dispatcher ou fonte de jitter também podem ser substituídas. Opções inválidas de dispatcher, retry, transporte ou assinatura são validadas na resolução das opções e pela validação de startup do Generic Host.
-
-O código da aplicação pode fazer enqueue sem conhecer os detalhes de agendamento da persistência:
-
-```csharp
-IWebhookEnqueueService webhookEnqueue =
- serviceProvider.GetRequiredService();
-
-await webhookEnqueue.EnqueueAsync(message, cancellationToken);
-```
-
-`IWebhookEnqueueService` aplica `ReliableWebhooksOptions.MessageLimits` antes de persistir uma entrega. Os padrões permitem payload de 1 MiB, 32 headers customizados persistidos, 16 KiB de bytes agregados de nomes/valores de headers customizados, IDs de webhook com 128 caracteres, tipos de evento com 128 caracteres, content types com 256 caracteres e URIs de destino com 2048 caracteres. Aumente esses limites apenas para receivers e tenants que realmente precisem de mensagens maiores, e combine-os com quotas da aplicação para que um tenant não consuma toda a fila. Código que chama `IWebhookDeliveryStore.EnqueueAsync` diretamente também contorna o gate padrão de produção; nesse caminho, chame `WebhookMessageLimits.Validate(message)` explicitamente ou aplique limites equivalentes na borda da aplicação.
-
-A integração depende apenas de `Microsoft.Extensions.*`; ela não exige ASP.NET Core.
-
-### Assinando webhooks
-
-A assinatura é habilitada fornecendo um `IWebhookRequestSigner`. O `HmacSha256WebhookRequestSigner` nativo resolve os bytes do segredo através de `IWebhookSigningSecretProvider`, portanto a biblioteca não precisa saber se o segredo vem de configuração, secret manager ou outra fonte segura. Ele exige pelo menos 32 bytes (256 bits) de material de chave HMAC gerado criptograficamente e rejeita segredos vazios ou mais curtos antes de assinar. Gere esses bytes com um CSPRNG e armazene-os em um secret manager; não use senhas, passphrases, nomes de tenants ou outras strings de baixa entropia como segredos de assinatura.
-
-```csharp
-IWebhookSigningSecretProvider secretProvider = GetApplicationSecretProvider();
-IWebhookRequestSigner signer = new HmacSha256WebhookRequestSigner(secretProvider);
-
-var transport = new WebhookHttpTransport(
- httpClient,
- classifier: null,
- options: null,
- signer: signer);
-```
-
-Com o `WebhookSigningOptions` padrão, cada requisição assinada contém:
-
-- `X-Webhook-Id`: o `WebhookMessage.Id` estável;
-- `X-Webhook-Event`: o `WebhookMessage.EventType`;
-- `Content-Type`: o `WebhookMessage.ContentType` normalizado;
-- `X-Webhook-Timestamp`: o timestamp UTC em Unix seconds;
-- `X-Webhook-Signature`: `v1=`.
-
-Os nomes dos headers podem ser customizados através de `WebhookHttpTransportOptions.Signing`. Headers gerados pela assinatura têm precedência sobre headers customizados da mensagem com o mesmo nome.
-
-`WebhookMessage.Id` e `WebhookMessage.EventType` devem ser não vazios e não podem conter caracteres de controle como CR, LF ou NUL porque são usados em headers gerados e telemetria. `WebhookMessage.ContentType` deve ser um media type HTTP sintaticamente válido, incluindo media types de fornecedor como `application/vnd.example+json`.
-
-Headers customizados fornecidos ao `WebhookMessage` devem usar nomes válidos de token HTTP, são comparados sem diferenciar maiúsculas/minúsculas para detectar duplicidade e não podem conter caracteres de controle como CR, LF ou NUL nos valores. O transporte padrão reserva headers de roteamento e framing que dados da mensagem não podem controlar: `Host`, `Content-Length`, `Transfer-Encoding`, `Connection`, `TE`, `Trailer`, `Upgrade`, `Expect`, `Keep-Alive`, `Proxy-Authenticate`, `Proxy-Authorization` e `Proxy-Connection`. Headers sensíveis de credencial, como `Authorization` e `Cookie`, não são aceitos na mensagem persistida; resolva-os no momento do envio por meio de `IWebhookRequestHeaderProvider` para que tentativas enfileiradas observem rotação sem regravar entregas armazenadas. Trate valores de headers customizados como sensíveis quando vierem de tenants, assinantes ou outra configuração externa. Usuários avançados que precisem de controle HTTP de nível mais baixo devem fornecer um `IWebhookDeliveryTransport` customizado.
-
-A entrada canônica `v1` do HMAC é versionada e usa frames com tamanho prefixado:
-
-```text
-ASCII("rw-hmac-sha256/v1\0")
-|| frame(UTF8(unixTimestampSeconds))
-|| frame(UTF8(webhookId))
-|| frame(UTF8(eventType))
-|| frame(UTF8(contentType))
-|| frame(exactRequestPayloadBytes)
-```
+O store precisa implementar as semânticas de durabilidade, claim atômico, lease, rejeição de owner obsoleto, agendamento de retry e estados terminais definidas pelo contrato de persistência.
-Cada `frame(value)` usa oito bytes big-endian para o tamanho, seguidos pelos bytes exatos do valor. O payload não é serializado novamente nem normalizado antes da assinatura. O mesmo array de bytes é usado tanto no cálculo do HMAC quanto no conteúdo da requisição HTTP. O content type é normalizado pelo parser de media type HTTP da plataforma quando o `WebhookMessage` é criado, então o frame autenticado de content type corresponde ao header `Content-Type` serializado pelo transporte padrão. Os valores autenticados são timestamp, ID do webhook, tipo de evento, content type e bytes do corpo. Headers customizados, URI de destino e os nomes configuráveis dos headers de assinatura não fazem parte do envelope HMAC nativo.
+## Leia em seguida
-Um receptor pode validar uma entrega de forma independente:
-
-1. lendo ID, evento, timestamp, assinatura e content type gerados sem modificar o corpo da requisição;
-2. interpretando o timestamp como Unix seconds e rejeitando timestamps fora da janela de tolerância contra replay;
-3. reconstruindo os frames canônicos `rw-hmac-sha256/v1` com esses metadados e os bytes brutos do corpo;
-4. calculando HMAC-SHA256 com o segredo compartilhado;
-5. codificando o digest em hexadecimal minúsculo e prefixando com `v1=`;
-6. comparando a assinatura calculada com a recebida em tempo constante.
-
-Segredos de assinatura nunca são incluídos em mensagens de exceção geradas pela biblioteca nem em telemetria automática. Aplicações devem manter a mesma regra em providers de segredo e signers customizados. Faça rotação de segredos pelo provider da aplicação e pela configuração do receiver, e use segredos diferentes para públicos de confiança ou receivers distintos para que o comprometimento de um receiver não permita forjar assinaturas para outro.
-
-## Observabilidade
-
-ReliableWebhooks emite telemetria pelas APIs padrão do .NET e **não** depende do SDK do OpenTelemetry, de collector ou de exporter. A aplicação decide se a telemetria será coletada e para onde ela será enviada.
-
-A classe pública `ReliableWebhooksInstrumentation` expõe os nomes canônicos usados pela biblioteca:
-
-```csharp
-ReliableWebhooksInstrumentation.ActivitySourceName // "ReliableWebhooks"
-ReliableWebhooksInstrumentation.MeterName // "ReliableWebhooks"
-```
-
-A mesma classe expõe nomes estáveis para a activity de tentativa de entrega e para as métricas publicadas, evitando que integrações dupliquem strings de instrumentação.
-
-### Logs estruturados
-
-`WebhookDispatcher` possui um overload aditivo de construtor que recebe `ILogger`. Os construtores existentes continuam válidos e usam um logger no-op. A telemetria de enqueue é fornecida por `InstrumentedWebhookDeliveryStore`, um decorator que pode envolver qualquer `IWebhookDeliveryStore`, inclusive um store durável customizado de produção.
-
-```csharp
-ILogger logger = loggerFactory.CreateLogger("ReliableWebhooks");
-
-IWebhookDeliveryStore durableStore = GetApplicationWebhookStore();
-IWebhookDeliveryStore store = new InstrumentedWebhookDeliveryStore(
- durableStore,
- logger);
-
-var dispatcher = new WebhookDispatcher(
- store,
- transport,
- retryPolicy,
- options,
- delay: null,
- logger);
-```
-
-`InstrumentedWebhookDeliveryStore` emite o evento de enqueue e a métrica de queued apenas quando uma nova entrega é persistida de fato. Chamadas idempotentes duplicadas de enqueue não contam duas vezes a mesma entrega. A telemetria de claim é emitida por `WebhookDispatcher`, então o mesmo claim é registrado uma única vez e stores customizados recebem a mesma cobertura de ciclo de vida.
-
-O ciclo de vida usa event IDs estáveis para enqueue, claim, início de tentativa, sucesso, agendamento de retry, falha permanente, dead letter, cancelamento, perda de lease e falha inesperada. As propriedades de log excluem deliberadamente payload, URLs de destino/query string, segredos de assinatura e assinaturas.
-
-### Traces
-
-Cada tentativa do dispatcher cria uma activity `ReliableWebhooks.DeliveryAttempt` no activity source `ReliableWebhooks`. Atributos seguros incluem ID do webhook para correlação, tipo de evento, número da tentativa, resultado, status HTTP quando houver e tipo da exceção inesperada.
-
-Payloads, segredos, assinaturas e URLs de destino não são adicionados automaticamente. IDs de webhook podem aparecer em traces para correlação, mas não são usados como dimensões de métricas.
-
-### Métricas
-
-| Instrumento | Tipo | Tags |
-| --- | --- | --- |
-| `reliablewebhooks.delivery.queued` | Counter | Nenhuma por padrão |
-| `reliablewebhooks.delivery.attempted` | Counter | Nenhuma por padrão |
-| `reliablewebhooks.delivery.succeeded` | Counter | Nenhuma por padrão |
-| `reliablewebhooks.delivery.retried` | Counter | Nenhuma por padrão |
-| `reliablewebhooks.delivery.permanently_failed` | Counter | Nenhuma por padrão |
-| `reliablewebhooks.delivery.dead_lettered` | Counter | Nenhuma por padrão |
-| `reliablewebhooks.delivery.duration` | Histogram em milissegundos | `webhook.outcome` |
-
-`WebhookMessage.EventType` bruto não é usado como dimensão de métrica por padrão. Para habilitar uma tag de tipo de evento, configure `WebhookMetricsOptions.EventTypeTagAllowList` com um vocabulário limitado; valores fora da allow-list são reportados como `other`. Nunca inclua IDs de cliente, request IDs, destinos, IDs de webhook, payloads, assinaturas ou outros valores sensíveis/de alta cardinalidade em dimensões de métricas.
-
-### Integração com OpenTelemetry
-
-O consumidor pode habilitar OpenTelemetry com seus próprios pacotes e exporters. ReliableWebhooks não exige nenhum deles:
-
-```csharp
-builder.Services
- .AddOpenTelemetry()
- .WithTracing(tracing =>
- tracing.AddSource(ReliableWebhooksInstrumentation.ActivitySourceName))
- .WithMetrics(metrics =>
- metrics.AddMeter(ReliableWebhooksInstrumentation.MeterName));
-```
-
-A aplicação pode então adicionar OTLP, Azure Monitor, Prometheus, Grafana/Tempo, Datadog, Dynatrace ou outro exporter/backend suportado sem alterar ReliableWebhooks. Logs continuam usando `ILogger` e podem ser conectados de forma independente ao pipeline de logging/OpenTelemetry escolhido pela aplicação.
-
-## Comportamento padrão
-
-| Área | Padrão |
-| --- | --- |
-| Concorrência máxima do dispatcher | 4 |
-| Duração da lease | 1 minuto |
-| Intervalo de polling | 1 segundo |
-| Grace period de shutdown | 30 segundos |
-| Timeout de tentativa HTTP | 30 segundos |
-| Corpo de resposta capturado | Até 16 KiB |
-| Tamanho de payload de saída | Até 1 MiB via `IWebhookEnqueueService` |
-| Headers customizados persistidos | Até 32 headers e 16 KiB agregados de nomes/valores via `IWebhookEnqueueService` |
-| Tamanho de metadados persistidos | ID/tipo do evento até 128 caracteres, content type até 256 e URI de destino até 2048 via `IWebhookEnqueueService` |
-| Destinos HTTP sem TLS | Rejeitados salvo com `AllowInsecureHttp = true` |
-| Classificação de sucesso | Qualquer resposta `2xx` |
-| Respostas HTTP retryable | `408`, `425`, `429` e `5xx` |
-| Respostas HTTP permanentes | Outros status codes, incluindo redirects |
-| Algoritmo de assinatura | HMAC-SHA256 quando um signer está configurado |
-| Formato canônico de assinatura | marcador `rw-hmac-sha256/v1` mais timestamp, ID do webhook, tipo do evento, content type e bytes do payload com tamanho prefixado |
-| Headers de assinatura | `X-Webhook-Id`, `X-Webhook-Event`, `X-Webhook-Timestamp`, `X-Webhook-Signature` |
-| Máximo de tentativas | 5, incluindo a tentativa atual |
-| Delay base de retry | 1 segundo |
-| Delay máximo de retry | 5 minutos |
-| Jitter | 0 a 20% de jitter positivo antes do limite máximo |
-| `Retry-After` | Respeitado quando agenda depois do delay local; `MaxDelay` limita apenas backoff/jitter locais |
-
-Redirects automáticos e cookies automáticos precisam ficar desabilitados para que uma chamada do transporte não se transforme silenciosamente em múltiplas requisições nem preserve estado controlado pelo receptor para entregas futuras. O cliente gerenciado pela integração de DI já aplica essa configuração.
-
-Valores malformados de `Retry-After` são ignorados. Valores válidos em delta-seconds e HTTP-date são expostos por `WebhookDeliveryResult` e consumidos pela política de retry padrão.
-
-## Garantias e limites de entrega
-
-ReliableWebhooks trabalha com semântica explícita de entrega:
-
-- **At-least-once, não exactly-once.** Entregas duplicadas podem acontecer, especialmente quando um worker envia e falha antes de persistir o resultado.
-- **IDs estáveis tornam o enqueue duplicate-safe.** O receptor ainda precisa de idempotência no nível da aplicação.
-- **Limites de recursos da mensagem protegem a fila.** O serviço padrão de enqueue rejeita payloads, headers e metadados persistidos grandes demais antes da escrita no store. A aplicação ainda deve aplicar quotas por tenant e restrições de payload de negócio.
-- **Leases coordenam ownership ativo.** Enquanto uma lease é válida, dois workers não devem possuir a mesma entrega simultaneamente; trabalho expirado pode ser recuperado.
-- **A concorrência do dispatcher é limitada.** Claims respeitam os slots disponíveis e não criam tasks ilimitadas.
-- **O shutdown ocorre em duas fases.** Novos claims param primeiro; trabalho em andamento pode terminar durante o grace period antes de ser cancelado.
-- **Uma chamada ao transporte corresponde a uma tentativa HTTP.** Loops de retry ficam fora do transporte.
-- **Falhas de extensões por entrega são isoladas.** Exceções de transporte/assinatura/classificação de uma entrega são registradas pelo tipo da exceção e seguem retry/dead letter; falhas de claim e transição no store continuam sendo falhas de infraestrutura.
-- **Payloads assinados usam os bytes exatos da requisição.** O receptor deve validar o corpo bruto, não uma versão parseada e serializada novamente.
-- **Políticas de retry agendam; elas não aguardam.** `DefaultWebhookRetryPolicy` retorna um timestamp futuro ou decisão de dead letter.
-- **A telemetria é independente de backend.** Exporters continuam sob responsabilidade da aplicação.
-- **A persistência é substituível.** O pacote principal não depende de um provider de banco específico.
-
-A v1.0.0 de `ReliableWebhooks` não exige um adapter de persistência de produção nativo. A aplicação fornece um `IWebhookDeliveryStore` durável compatível; adapters opcionais para EF Core, Dapper, Redis, arquivos ou outras tecnologias podem ser introduzidos de forma independente.
+- **Uso em produção:** [`docs/production-usage.md`](https://github.com/rodri-oliveira-dev/ReliableWebhooks/blob/main/docs/production-usage.md) — DI/hosting, retry e classificação de responses, assinatura e verificação no receiver, segurança de destino, observabilidade, configuração, tuning e troubleshooting.
+- **Contrato de persistência:** [`docs/persistence.pt-BR.md`](https://github.com/rodri-oliveira-dev/ReliableWebhooks/blob/main/docs/persistence.pt-BR.md) — estado durável, enqueue idempotente, claims atômicos, leases, ownership, transições e testes de conformidade.
+- **Sample executável:** [`samples/ReliableWebhooks.Sample`](https://github.com/rodri-oliveira-dev/ReliableWebhooks/tree/main/samples/ReliableWebhooks.Sample) — integração end-to-end usando a API pública.
+- **Escopo da v1.0.0:** [`docs/release-v1.0.0.pt-BR.md`](https://github.com/rodri-oliveira-dev/ReliableWebhooks/blob/main/docs/release-v1.0.0.pt-BR.md) — limites da release, distribuição e superfície suportada.
+- **Política de segurança:** [`SECURITY.md`](https://github.com/rodri-oliveira-dev/ReliableWebhooks/blob/main/SECURITY.md) — processo de reporte de segurança e orientações de segurança do projeto.
## Extensibilidade
-Os principais comportamentos são expostos por abstrações públicas:
-
-- `IWebhookDeliveryStore` — persistência e coordenação por lease;
-- `InstrumentedWebhookDeliveryStore` — decorator que adiciona logs de enqueue e métrica queued a qualquer store;
-- `IWebhookEnqueueService` — API de enqueue voltada para a aplicação e registrada pela integração de DI;
-- `IWebhookDeliveryTransport` — transporte de uma tentativa usado pelo dispatcher;
-- `IWebhookDispatcherDelay` — temporização substituível para polling/grace period;
-- `IWebhookHttpResponseClassifier` — classificação de respostas HTTP;
-- `IWebhookRetryPolicy` — decisões de retry e dead letter;
-- `IWebhookRetryJitterSource` — geração de jitter substituível;
-- `IWebhookDestinationPolicy` — autorização de destinos para URLs de webhook não confiáveis;
-- `IWebhookRequestSigner` — estratégia de assinatura;
-- `IWebhookSigningSecretProvider` — resolução de segredo por mensagem;
-- `ReliableWebhooksInstrumentation` — nomes públicos e estáveis de diagnostics.
-
-`ReliableWebhooksOptions` agrupa a configuração de dispatcher, limites de mensagem, retry, transporte e signing para consumidores via DI. `WebhookDispatcherOptions` mantém `MaxConcurrency`, `LeaseDuration`, `PollInterval`, `ShutdownGracePeriod` e `TimeProvider` configuráveis e testáveis. `WebhookMessageLimits` expõe a fronteira padrão de recursos por mensagem usada por `IWebhookEnqueueService` e também pode ser aplicada explicitamente por aplicações que fazem enqueue diretamente pelo store.
-
-## Suporte e contribuição
+Os comportamentos centrais são expostos por abstrações substituíveis, incluindo persistência, transport de entrega, classificação de responses, política de retry/jitter, assinatura e resolução de secrets, política de destino, resolução de headers, temporização do dispatcher e integração de observabilidade. O guia de produção documenta os defaults internos e os limites que implementações customizadas precisam preservar.
-Use [GitHub Issues](https://github.com/rodri-oliveira-dev/ReliableWebhooks/issues) para bugs, dúvidas e discussão de funcionalidades.
+## Contribuindo
-Para problemas de segurança, siga [SECURITY.md](https://github.com/rodri-oliveira-dev/ReliableWebhooks/blob/main/SECURITY.md) em vez de abrir uma issue pública.
+Veja [`CONTRIBUTING.md`](https://github.com/rodri-oliveira-dev/ReliableWebhooks/blob/main/CONTRIBUTING.md) para desenvolvimento local, validação e orientações de contribuição.
-Contribuições são bem-vindas. Consulte [CONTRIBUTING.md](https://github.com/rodri-oliveira-dev/ReliableWebhooks/blob/main/CONTRIBUTING.md) para o fluxo de contribuição e [CHANGELOG.md](https://github.com/rodri-oliveira-dev/ReliableWebhooks/blob/main/CHANGELOG.md) para mudanças relevantes.
+## Licença
ReliableWebhooks é licenciado sob a [MIT License](https://github.com/rodri-oliveira-dev/ReliableWebhooks/blob/main/LICENSE).