Correct four documentation claims ahead of the 1.0.0 tag
All checks were successful
check / check (push) Successful in 7s

Final pre-tag verification found four places where the docs did not
match the shipped artifact.

`TODO.md` `# Status` said the tag was held on a durability defect that
has since landed. Rewritten to record that it landed, without naming a
remaining blocker whose issue number would falsify the file the moment
it closes. `# Next Step` opened with "Land <that issue>" and is
corrected the same way.

The README Package Layout tree omitted `internal/reqtls`,
`internal/ciscript` and `internal/versionscript`, all three of which
exist; `reqtls` is named repeatedly in the surrounding prose as the
TLS predicate.

The README Authenticated Endpoints table omitted three live routes,
verified against `internal/server/routes.go`: `GET` and `POST` on
`/source/{id}/targets/{targetID}/edit`, and `GET
/source/{id}/logs/{eventID}/body`.

The Rationale section sold "Guaranteed delivery" without saying which
guarantee. The engine deliberately implements at-least-once: a send
whose bookkeeping write fails is left recoverable and sent again by
the pending sweep or the next restart. Stated plainly, with the
consequence for receivers.

Documentation only; no behaviour change.
This commit is contained in:
2026-08-24 04:23:36 +00:00
parent af3703d748
commit 4008e15d43
2 changed files with 41 additions and 13 deletions

View File

@@ -1234,6 +1234,16 @@ webhooker solves this by acting as a durable intermediary:
backoff. Every delivery attempt is logged with status codes, response
bodies, and timing.
**That guarantee is at-least-once, not exactly-once.** When a send
reaches its target but the write recording that outcome fails, the
delivery is deliberately left in a recoverable state rather than
marked done — losing a delivery is the worse failure — so the
pending sweep picks it up about fifteen minutes later, or the next
restart does, and the target receives a payload it already got.
webhooker adds no delivery identifier of its own to an outbound
request, so **make your receiver idempotent** against whatever the
payload itself carries.
3. **Observability** — Full request/response logging for every webhook
received and every delivery attempted. Prometheus metrics expose
volume, latency, and error rates. The web UI provides real-time
@@ -2616,12 +2626,15 @@ abuse limit later; they are tracked as future work.
| `POST` | `/source/{id}/edit` | Edit webhook submission |
| `POST` | `/source/{id}/delete` | Delete webhook |
| `GET` | `/source/{id}/logs` | Webhook event logs |
| `GET` | `/source/{id}/logs/{eventID}/body` | Download an event's full stored body. The log page renders each body only up to its cap, so this is the only route that serves a whole one; it is offered wherever a body is shown truncated |
| `POST` | `/source/{id}/deliveries/{deliveryID}/replay` | Replay a finished delivery: creates a new delivery for the same event against the target's current configuration (30 per minute per bucket, then `429`) |
| `POST` | `/source/{id}/events/{eventID}/resubmit` | Resubmit a stored event: creates a new event copying it and fans that out to every currently active target (30 per minute per bucket, then `429`) |
| `POST` | `/source/{id}/entrypoints` | Add entrypoint to webhook |
| `POST` | `/source/{id}/entrypoints/{entrypointID}/delete` | Delete an entrypoint |
| `POST` | `/source/{id}/entrypoints/{entrypointID}/toggle` | Enable or disable an entrypoint |
| `POST` | `/source/{id}/targets` | Add target to webhook |
| `GET` | `/source/{id}/targets/{targetID}/edit` | Edit target form. The one page that renders a target's destination URL and header values in full, rather than masked |
| `POST` | `/source/{id}/targets/{targetID}/edit` | Edit target submission |
| `POST` | `/source/{id}/targets/{targetID}/delete` | Delete a target |
| `POST` | `/source/{id}/targets/{targetID}/toggle` | Enable or disable a target |
@@ -2660,6 +2673,8 @@ webhooker/
├── internal/
│ ├── banner/
│ │ └── banner.go # Ruled block for the one credential shown in the clear
│ ├── ciscript/
│ │ └── doc.go # Tests for the CI shell scripts in script/; no runtime code
│ ├── resetpw/
│ │ └── resetpw.go # `webhooker resetpw`: set an account's password, stopped deployments only
│ ├── config/
@@ -2729,13 +2744,17 @@ webhooker/
│ │ ├── ratelimit.go # Per-IP rate limiting middleware (go-chi/httprate)
│ │ ├── loginguard.go # Login failure counters and the Argon2id verification semaphore
│ │ └── testing.go # NewForTest: Middleware without the fx lifecycle
│ ├── reqtls/
│ │ └── reqtls.go # IsTLS: the one TLS predicate, r.TLS or X-Forwarded-Proto
│ ├── server/
│ │ ├── server.go # Server struct, fx lifecycle, signal handling
│ │ ├── http.go # HTTP server setup with timeouts
│ │ └── routes.go # All route definitions
── session/
├── session.go # Cookie-based session management
└── testing.go # NewForTest: Session without the fx lifecycle
── session/
├── session.go # Cookie-based session management
└── testing.go # NewForTest: Session without the fx lifecycle
│ └── versionscript/
│ └── doc.go # Tests for script/version and the build files that use it
├── static/
│ ├── static.go # //go:embed directive
│ ├── css/input.css # Tailwind input, source for tailwind.css (make css)

29
TODO.md
View File

@@ -25,11 +25,20 @@ file records where the project is, not what is in flight: a sentence
whose truth depends on a branch being unmerged is wrong the moment it
merges, and this file has been wrong that way before.
The tag is held on a durability defect
(https://git.eeqj.de/sneak/webhooker/issues/256): a concurrent reader
of a per-webhook event database strands delivered webhooks at
`pending`, and the next restart re-delivers them. That issue gates
`v1.0.0`, and is where the fix's own state is tracked.
The durability defect that held the tag has landed
(https://git.eeqj.de/sneak/webhooker/issues/256, commit `8d64259`).
Every SQLite handle opens with WAL journaling and a busy timeout, a
bookkeeping write that fails leaves its delivery in a recoverable
state rather than a lying one, and recovery skips a delivery that
already has a successful result row. Final pre-tag verification
exercised it and confirmed it holds. Whatever the milestone still
shows open is what remains before `v1.0.0`.
Delivery is at-least-once by design, not by accident: a send whose
result row does not land is attempted again, so a receiver can see a
duplicate. That is deliberate — the alternative is a silent lost
delivery — and the README says so under Rationale. It is not a defect
to re-file.
One caveat on reading a green check: a docs-only commit deliberately
replays from the layer cache
@@ -39,11 +48,11 @@ commit invalidates the `COPY` layer and genuinely executes.
# Next Step
Land https://git.eeqj.de/sneak/webhooker/issues/256, then clear the
rest of the open 1.0.0 milestone and tag `v1.0.0`. Merging `next` into
`main` is a separate act from tagging and waits on neither of those:
`next` is kept mergeable at all times, which is the point of the
branch.
Clear the rest of the open 1.0.0 milestone
(https://git.eeqj.de/sneak/webhooker/milestone/9) and tag `v1.0.0`.
Merging `next` into `main` is a separate act from tagging and waits on
neither of those: `next` is kept mergeable at all times, which is the
point of the branch.
# Completed Steps