Record landed 1.0.0 work and drop the removed signing roadmap in TODO.md #284
170
TODO.md
170
TODO.md
@@ -18,29 +18,18 @@ Issue branches do NOT touch this file — the manager maintains it on
|
||||
|
||||
# Status
|
||||
|
||||
1.0.0 is complete: 55 closed, 0 open. `next` (6874059) is 62 commits
|
||||
ahead of `main` and a strict fast-forward. No git tags exist yet.
|
||||
1.0.0 is open, with work remaining. The milestone
|
||||
(https://git.eeqj.de/sneak/webhooker/milestone/9) is the authoritative
|
||||
list, and the only place to read a count or a state of play from. This
|
||||
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 bar was not "the milestone is empty" but "sneak can deploy this and
|
||||
use it in low-volume production". Every gap the deployability audit
|
||||
named against that bar is now closed:
|
||||
|
||||
- `DATA_DIR` locking, so two instances cannot both deliver
|
||||
(https://git.eeqj.de/sneak/webhooker/issues/201)
|
||||
- shutdown on listener failure, rather than a live non-serving process
|
||||
(https://git.eeqj.de/sneak/webhooker/issues/200)
|
||||
- inbound signature verification
|
||||
(https://git.eeqj.de/sneak/webhooker/issues/67)
|
||||
- per-attempt delivery detail in the event log
|
||||
(https://git.eeqj.de/sneak/webhooker/issues/202)
|
||||
- replay of a terminally failed delivery
|
||||
(https://git.eeqj.de/sneak/webhooker/issues/203)
|
||||
- `ALLOWED_EGRESS_CIDRS`, an allowlist escape hatch for the SSRF guard
|
||||
(https://git.eeqj.de/sneak/webhooker/issues/204)
|
||||
- the three credential exposures
|
||||
(https://git.eeqj.de/sneak/webhooker/issues/205,
|
||||
https://git.eeqj.de/sneak/webhooker/issues/206,
|
||||
https://git.eeqj.de/sneak/webhooker/issues/207)
|
||||
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.
|
||||
|
||||
One caveat on reading a green check: a docs-only commit deliberately
|
||||
replays from the layer cache
|
||||
@@ -50,23 +39,129 @@ commit invalidates the `COPY` layer and genuinely executes.
|
||||
|
||||
# Next Step
|
||||
|
||||
Merge the milestone PR (https://git.eeqj.de/sneak/webhooker/pulls/111)
|
||||
and tag `v1.0.0`. It is `merge-ready` and assigned to sneak; nothing
|
||||
else gates it.
|
||||
|
||||
Post-1.0 follow-ups are open, none blocking the tag:
|
||||
https://git.eeqj.de/sneak/webhooker/issues/245,
|
||||
https://git.eeqj.de/sneak/webhooker/issues/246,
|
||||
https://git.eeqj.de/sneak/webhooker/issues/247 and
|
||||
https://git.eeqj.de/sneak/webhooker/issues/248. Also still open and
|
||||
unmilestoned: https://git.eeqj.de/sneak/webhooker/issues/193 (a design
|
||||
question, not a defect), https://git.eeqj.de/sneak/webhooker/issues/198
|
||||
(`make test` is past the org 20s target) and
|
||||
https://git.eeqj.de/sneak/webhooker/issues/212 (encrypting target config
|
||||
at rest).
|
||||
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.
|
||||
|
||||
# Completed Steps
|
||||
|
||||
- 2026-08-24 Bind the plaintext HTTP listener deliberately, via
|
||||
`BIND_ADDRESS` defaulting to `127.0.0.1`, and document the
|
||||
reverse-proxy deployment. A hostname, an empty value or a value
|
||||
carrying a port is a startup error, and the `Dockerfile` sets
|
||||
`0.0.0.0` because a loopback bind inside a container is unreachable
|
||||
(https://git.eeqj.de/sneak/webhooker/issues/268). The same commit
|
||||
removed the shutdown race: `httpServer` is built in the constructor
|
||||
rather than assigned from the serving goroutine, which orders the
|
||||
write before every fx hook and rules out the nil dereference a
|
||||
SIGTERM arriving first would have caused, and `sentryEnabled` is an
|
||||
`atomic.Bool` (https://git.eeqj.de/sneak/webhooker/issues/226)
|
||||
- 2026-08-24 Remove inbound request signature verification. The
|
||||
entrypoint UUID is the authentication secret, so the per-entrypoint
|
||||
shared secret, the `internal/signature` package, the receiver check,
|
||||
the model fields and the forms are all gone. This reverses the
|
||||
feature that landed earlier in the same milestone
|
||||
(https://git.eeqj.de/sneak/webhooker/issues/67,
|
||||
https://git.eeqj.de/sneak/webhooker/issues/279)
|
||||
- 2026-08-24 Stamp the build version into the binary and render it in
|
||||
the UI footer. `script/version` is the single source — `$VERSION`,
|
||||
else `git describe --tags --always --dirty`, else `unknown` — so a
|
||||
`make build` binary and a `make docker` image from one checkout
|
||||
report the same thing, and nothing in it varies between two builds
|
||||
of the same commit, which the release gate's byte-identical
|
||||
assertion would catch
|
||||
(https://git.eeqj.de/sneak/webhooker/issues/253)
|
||||
- 2026-08-24 Derive cookie `Secure` and CSRF strictness from the
|
||||
request transport rather than from `WEBHOOKER_ENVIRONMENT`. Behind a
|
||||
real TLS proxy with the environment left at its `dev` default, the
|
||||
session cookie silently lost `Secure` while the CSRF cookie on the
|
||||
same response kept it. `X-Forwarded-Proto` is now matched
|
||||
case-insensitively on its first comma-separated element, so `HTTPS`
|
||||
and `https, http` no longer fall to the relaxed CSRF path
|
||||
(https://git.eeqj.de/sneak/webhooker/issues/269)
|
||||
- 2026-08-24 Roll back a failed webhook deletion instead of committing
|
||||
it. A failing delete committed whatever had already succeeded,
|
||||
hard-deleted the per-webhook event database anyway, and redirected as
|
||||
though it had worked — orphaned config plus permanently destroyed
|
||||
history, reported as success. All three delete positions now roll
|
||||
back with the event database intact
|
||||
(https://git.eeqj.de/sneak/webhooker/issues/262)
|
||||
- 2026-08-24 Name a deleted target on its historical deliveries, marked
|
||||
`(deleted)`, rather than leaving the event log unable to say where a
|
||||
delivery went. A deleted target's credentials stay masked exactly as
|
||||
a live one's, and it cannot become deliverable again through the
|
||||
receiver, resubmit, replay, the edit form or the toggle
|
||||
(https://git.eeqj.de/sneak/webhooker/issues/211)
|
||||
- 2026-08-24 Bound both request-controlled `/metrics` label dimensions,
|
||||
so the unauthenticated receiver is no longer a memory-exhaustion
|
||||
vector: `handler` carries the chi route pattern, and `method` folds
|
||||
anything chi cannot route onto a single `(unmatched)` sentinel. Both
|
||||
were reproduced before the fix — 300 random method tokens took the
|
||||
series count from 106 to 7,631, and path flooding reached 62,532 —
|
||||
and a label audit across a live scrape found no third unbounded
|
||||
dimension (https://git.eeqj.de/sneak/webhooker/issues/254,
|
||||
https://git.eeqj.de/sneak/webhooker/issues/261)
|
||||
- 2026-08-24 Validate `max_retries` on both target forms. `abc`, `2.7`
|
||||
and `-5` silently became 0 — fire-and-forget — including on the edit
|
||||
path, where it destroyed a working value, and `999999999` stored
|
||||
verbatim. The ceiling of 20 is the `max` both templates already
|
||||
declared (https://git.eeqj.de/sneak/webhooker/issues/221)
|
||||
- 2026-08-24 Resubmit a stored event as a new undelivered event, so a
|
||||
backend under development can be tested against real captured
|
||||
traffic. Per-delivery replay cannot serve that: it re-sends one
|
||||
finished delivery to its own original target, and a target created
|
||||
for a dev backend has no prior delivery to replay. Resubmit
|
||||
re-injects the stored event at the top of the receiver path and fans
|
||||
it out to whatever targets are active now
|
||||
(https://git.eeqj.de/sneak/webhooker/issues/250)
|
||||
- 2026-08-20 Take an exclusive lock on `DATA_DIR` at startup, so two
|
||||
instances on one directory cannot both deliver
|
||||
(https://git.eeqj.de/sneak/webhooker/issues/201)
|
||||
- 2026-08-20 Shut down the app when the HTTP listener fails. The
|
||||
`OnStart` hook returned as soon as the serving goroutine was
|
||||
spawned, so a failed listen left fx reporting RUNNING and a live
|
||||
process with nothing bound — invisible to systemd and Docker restart
|
||||
policies (https://git.eeqj.de/sneak/webhooker/issues/200)
|
||||
- 2026-08-20 Stop target credentials leaking into the per-webhook event
|
||||
databases (https://git.eeqj.de/sneak/webhooker/issues/206), log SQL
|
||||
with placeholders rather than bound values
|
||||
(https://git.eeqj.de/sneak/webhooker/issues/207), and fail loudly on
|
||||
half-set metrics auth credentials
|
||||
(https://git.eeqj.de/sneak/webhooker/issues/205)
|
||||
- 2026-08-20 Read queue depths with `Find`, not `Scan`. `Scan` swaps
|
||||
GORM's own trace recorder in for the logging adapter, and that
|
||||
recorder does not implement `gorm.ParamsFilter`, so those statements
|
||||
logged their bound values interpolated and bypassed the suppression
|
||||
above. The two units gated green against a `next` that lacked the
|
||||
other, and `next` went red when both landed
|
||||
(https://git.eeqj.de/sneak/webhooker/issues/234)
|
||||
- 2026-08-20 Render per-attempt delivery detail in the event log
|
||||
(https://git.eeqj.de/sneak/webhooker/issues/202) and add replay of a
|
||||
terminally failed delivery
|
||||
(https://git.eeqj.de/sneak/webhooker/issues/203)
|
||||
- 2026-08-20 Expose delivery metrics on `/metrics`
|
||||
(https://git.eeqj.de/sneak/webhooker/issues/209) and document the
|
||||
backup, restore and upgrade procedures
|
||||
(https://git.eeqj.de/sneak/webhooker/issues/210)
|
||||
- 2026-08-20 Add a `webhooker resetpw` subcommand and a bootstrap
|
||||
banner. The admin bootstrap password was printed once among roughly
|
||||
45 fx lines, and under `docker run -d` went to container logs subject
|
||||
to rotation; there was no reset path at all, so recovery meant
|
||||
hand-deleting the users row, documented nowhere. The password is read
|
||||
from stdin or generated, never from argv where `/proc` would publish
|
||||
it (https://git.eeqj.de/sneak/webhooker/issues/208)
|
||||
- 2026-08-20 Add `ALLOWED_EGRESS_CIDRS`, an allowlist-only escape hatch
|
||||
for the SSRF guard, so a self-hosted proxy can forward into the
|
||||
operator's own network. The guard's always-blocked set cannot be
|
||||
reopened by configuration
|
||||
(https://git.eeqj.de/sneak/webhooker/issues/204)
|
||||
- 2026-08-20 Harden operator-set target headers, which were carried
|
||||
unsafely across a redirect
|
||||
(https://git.eeqj.de/sneak/webhooker/issues/233)
|
||||
- 2026-08-20 Add a target edit form with headers and timeout fields
|
||||
(https://git.eeqj.de/sneak/webhooker/issues/127)
|
||||
- 2026-08-18 Raise `script/test`'s per-package timeout from 30s to 90s,
|
||||
matching the org-wide backstop. `go test` applies `-timeout` per
|
||||
package, and `internal/handlers` had grown past the old budget: a
|
||||
@@ -299,9 +394,6 @@ at rest).
|
||||
- Per-webhook rate limiting in the receiver handler (per-webhook config
|
||||
plus handler enforcement; global limits must not apply to receiver
|
||||
endpoints)
|
||||
- Stripe HMAC signature verification. The GitHub and GitLab schemes
|
||||
landed with inbound verification
|
||||
(https://git.eeqj.de/sneak/webhooker/issues/67)
|
||||
- API key authentication for programmatic access (APIKey model exists;
|
||||
Bearer token middleware does not)
|
||||
- REST API v1
|
||||
|
||||
Reference in New Issue
Block a user