diff --git a/README.md b/README.md index 9b764f9..c702dcc 100644 --- a/README.md +++ b/README.md @@ -93,7 +93,7 @@ TTY detection, and security headers are always applied. | `METRICS_USERNAME` | Basic auth username for `/metrics` | `""` | | `METRICS_PASSWORD` | Basic auth password for `/metrics` | `""` | | `SENTRY_DSN` | Sentry error reporting DSN | `""` | -| `RETENTION_SWEEP_INTERVAL` | Retention reaper period (Go duration, must be positive) | `1h` | +| `RETENTION_SWEEP_INTERVAL` | How often the retention reaper and archive sweeper run (Go duration, must be positive) | `1h` | | `SESSION_IDLE_TIMEOUT` | Idle session timeout (Go duration) | `24h` | | `RECEIVER_RATE_LIMIT` | Receiver requests/minute per IP per entrypoint | `120` | | `TRUSTED_PROXIES` | CIDRs whose forwarded headers are trusted | `""` (none) | @@ -265,9 +265,10 @@ webhooker solves this by acting as a durable intermediary: targets simultaneously. This enables patterns like forwarding a GitHub webhook to both a deployment service and a Slack channel. -5. **Replay** — Stored events can be manually redelivered for debugging - or testing, without requiring the original sender to fire the webhook - again. +5. **Replay** (not yet implemented) — Every received event is stored in + full, which is what manual redelivery for debugging or testing will + be built on. No redelivery exists today, in the web UI or the API; + see [TODO.md](TODO.md). ### Use Cases @@ -277,6 +278,7 @@ webhooker solves this by acting as a durable intermediary: size, and delivery performance - **Debugging** and introspection of webhook payloads in the web UI - **Replay** of webhook events for application testing and development + (planned; not yet implemented) - **Fan-out** delivery of a single webhook to multiple downstream targets - **High-availability ingestion** for delivery to less reliable backend @@ -502,7 +504,7 @@ A programmatic access credential for API authentication. #### Event A captured incoming webhook request. Stores the complete HTTP request -data for replay and auditing. +data for auditing and for the planned replay capability. | Field | Type | Description | | -------------- | ------ | ----------- | @@ -784,9 +786,10 @@ unknown) one while one of its deliveries is still `retrying`, both recovery paths above terminally mark that delivery `failed` and record a `DeliveryResult` naming the current target type as the reason, logging it at warn level. The delivery is not re-dispatched under the new type — the -operator never asked for that delivery — and the event itself remains -stored in the per-webhook event database, so it can be redelivered -manually. +operator never asked for that delivery — and while the event itself +remains stored in the per-webhook event database, there is no way to +redeliver it: manual redelivery is planned, not implemented (see +[TODO.md](TODO.md)). ### Circuit Breaker (HTTP Targets with Retries) diff --git a/TODO.md b/TODO.md index 8b1ee6d..2b341dc 100644 --- a/TODO.md +++ b/TODO.md @@ -61,6 +61,34 @@ rests on. Profile settings placeholder removed, a progressive-enhancement copy button for the entrypoint URL, and retention form copy that states the actual policy (deletion by the reaper, 0 retains forever) (#57) +- 2026-08-11 Mask the webhook credential in delivery errors and logs: + Go embeds the request URL in `*url.Error`, so every transport failure + persisted the full Slack webhook URL into the per-webhook event + database via `DeliveryResult.Error`, a field a future REST API would + have served. `maskURLError` drops path, query and userinfo while + preserving the wrapped cause, so `errors.Is`/`As` and `Timeout()` + still work and DNS, TLS and timeout failures still read differently + (#118) +- 2026-08-11 Rate-limit the public webhook receiver endpoint + (`RECEIVER_RATE_LIMIT`, default 120/min), keyed on client IP plus + entrypoint path so one entrypoint cannot exhaust another's budget; + over-limit requests get 429 with `Retry-After`. It was the one + unauthenticated, internet-facing endpoint with no limit at all (#64) +- 2026-08-11 Enforce the body size limit before CSRF parses the form: + `MaxBodySize` is now first in all four form-parsing route groups, so + an oversized request is rejected with 413 instead of being read in + full by the CSRF middleware before any cap applied (#90) +- 2026-08-11 Mask target config on the source detail page, which + rendered the stored blob verbatim and so exposed the Slack + incoming-webhook URL — a bearer credential that cannot be revoked + per-holder. Config reaches the template only as a `TargetView` of + labelled fields, and header values are rendered as a count (#113) +- 2026-08-11 Allow `retention_days` of 0 to mean retain forever, via a + sentinel written in `BeforeSave` so the GORM column default cannot + win the race. Also bounds the reaper's cutoff arithmetic: day counts + above 106751 overflowed `time.Duration` and wrapped the cutoff into + the future, where every row matched and the sweep deleted everything + (#79) - 2026-08-09 Inactivity-based session timeout: sliding idle expiry (`SESSION_IDLE_TIMEOUT`, default `24h`) refreshed on authenticated requests, with the 7-day absolute cap kept as an independent @@ -115,6 +143,9 @@ rests on. # Future Steps +- Manual event redelivery from the web UI — the "Replay" capability the + README describes as planned. No redelivery code exists anywhere in the + tree; events are stored in full, which is all it would be built on - Delivery status and retry management UI - Per-webhook rate limiting in the receiver handler (per-webhook config plus handler enforcement; global limits must not apply to receiver diff --git a/internal/delivery/engine.go b/internal/delivery/engine.go index 564fd87..61f6ca5 100644 --- a/internal/delivery/engine.go +++ b/internal/delivery/engine.go @@ -785,9 +785,9 @@ func (e *Engine) sweepSingleRetry( // status retrying themselves. Re-dispatching under the new type // would be a delivery the operator never asked for, and leaving // the row retrying strands it forever, so the delivery is -// failed with a recorded reason and can be redelivered -// manually. Logged at warn, not error: this is operator-caused -// state, not a system fault. +// failed with a recorded reason. The event stays stored, but +// nothing redelivers it today. Logged at warn, not error: this +// is operator-caused state, not a system fault. func (e *Engine) failUnretryableRetry( webhookDB *gorm.DB, webhookID string, diff --git a/static/js/app.js b/static/js/app.js index a4d9fc8..ee4f7fd 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -1,5 +1,4 @@ // Webhooker client-side JavaScript -console.log("Webhooker loaded"); // Copy-to-clipboard, as progressive enhancement. //