Correct four documentation claims ahead of the 1.0.0 tag
All checks were successful
check / check (push) Successful in 7s
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:
25
README.md
25
README.md
@@ -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
29
TODO.md
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user