Output of make fmt alone, so make fmt-check starts green. It changes line wrapping, table padding, list markers and emphasis markers, and no words. REPO_POLICIES.md was already formatted and is unchanged. Model: opus-5-5
This commit is contained in:
@@ -2,403 +2,367 @@
|
|||||||
|
|
||||||
One issue per unit of work, one branch and one PR per issue:
|
One issue per unit of work, one branch and one PR per issue:
|
||||||
|
|
||||||
* ensure a tracked issue exists with a definition of done
|
- ensure a tracked issue exists with a definition of done
|
||||||
* branch from `next` (never from `main`)
|
- branch from `next` (never from `main`)
|
||||||
* do the work; open a PR based on `next` (never on `main`)
|
- do the work; open a PR based on `next` (never on `main`)
|
||||||
* pass an independent review, then the manager squash-merges into `next`
|
- pass an independent review, then the manager squash-merges into `next`
|
||||||
* push; nothing stays local-only
|
- push; nothing stays local-only
|
||||||
|
|
||||||
`next` is the branch for the next milestone and must stay green and
|
`next` is the branch for the next milestone and must stay green and mergeable to
|
||||||
mergeable to `main` without notice. One `next` -> `main` PR accumulates
|
`main` without notice. One `next` -> `main` PR accumulates the milestone;
|
||||||
the milestone; releases are cut from `main` separately.
|
releases are cut from `main` separately.
|
||||||
|
|
||||||
Issue branches do NOT touch this file — the manager maintains it on
|
Issue branches do NOT touch this file — the manager maintains it on `next`.
|
||||||
`next`. Every branch editing `TODO.md` conflicts with every other
|
Every branch editing `TODO.md` conflicts with every other (#112).
|
||||||
(#112).
|
|
||||||
|
|
||||||
# Status
|
# Status
|
||||||
|
|
||||||
The milestone (https://git.eeqj.de/sneak/webhooker/milestone/9) is the
|
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
|
authoritative list, and the only place to read a count or a state of play from.
|
||||||
play from. This file records where the project is, not what is in
|
This file records where the project is, not what is in flight: a sentence whose
|
||||||
flight: a sentence whose truth depends on a branch being unmerged is
|
truth depends on a branch being unmerged is wrong the moment it merges, and this
|
||||||
wrong the moment it merges, and this file has been wrong that way
|
file has been wrong that way before.
|
||||||
before.
|
|
||||||
|
|
||||||
The durability defect that held the tag has landed
|
The durability defect that held the tag has landed
|
||||||
(https://git.eeqj.de/sneak/webhooker/issues/256, commit `8d64259`).
|
(https://git.eeqj.de/sneak/webhooker/issues/256, commit `8d64259`). Every SQLite
|
||||||
Every SQLite handle opens with WAL journaling and a busy timeout, a
|
handle opens with WAL journaling and a busy timeout, a bookkeeping write that
|
||||||
bookkeeping write that fails leaves its delivery in a recoverable
|
fails leaves its delivery in a recoverable state rather than a lying one, and
|
||||||
state rather than a lying one, and recovery skips a delivery that
|
recovery skips a delivery that already has a successful result row. Final
|
||||||
already has a successful result row. Final pre-tag verification
|
pre-tag verification exercised it and confirmed it holds. Whatever the milestone
|
||||||
exercised it and confirmed it holds. Whatever the milestone still
|
still shows open is what remains before `v1.0.0`.
|
||||||
shows open is what remains before `v1.0.0`.
|
|
||||||
|
|
||||||
Delivery is at-least-once by design, not by accident: a send whose
|
Delivery is at-least-once by design, not by accident: a send whose result row
|
||||||
result row does not land is attempted again, so a receiver can see a
|
does not land is attempted again, so a receiver can see a duplicate. That is
|
||||||
duplicate. That is deliberate — the alternative is a silent lost
|
deliberate — the alternative is a silent lost delivery — and the README says so
|
||||||
delivery — and the README says so under Rationale. It is not a defect
|
under Rationale. It is not a defect to re-file.
|
||||||
to re-file.
|
|
||||||
|
|
||||||
# Next Step
|
# Next Step
|
||||||
|
|
||||||
Clear the rest of the open 1.0.0 milestone
|
Clear the rest of the open 1.0.0 milestone
|
||||||
(https://git.eeqj.de/sneak/webhooker/milestone/9) and tag `v1.0.0`.
|
(https://git.eeqj.de/sneak/webhooker/milestone/9) and tag `v1.0.0`. Merging
|
||||||
Merging `next` into `main` is a separate act from tagging and waits on
|
`next` into `main` is a separate act from tagging and waits on neither of those:
|
||||||
neither of those: `next` is kept mergeable at all times, which is the
|
`next` is kept mergeable at all times, which is the point of the branch.
|
||||||
point of the branch.
|
|
||||||
|
|
||||||
# Completed Steps
|
# Completed Steps
|
||||||
|
|
||||||
- 2026-08-24 Bind the plaintext HTTP listener deliberately, via
|
- 2026-08-24 Bind the plaintext HTTP listener deliberately, via `BIND_ADDRESS`
|
||||||
`BIND_ADDRESS` defaulting to `127.0.0.1`, and document the
|
defaulting to `127.0.0.1`, and document the reverse-proxy deployment. A
|
||||||
reverse-proxy deployment. A hostname, an empty value or a value
|
hostname, an empty value or a value carrying a port is a startup error, and
|
||||||
carrying a port is a startup error, and the `Dockerfile` sets
|
the `Dockerfile` sets `0.0.0.0` because a loopback bind inside a container is
|
||||||
`0.0.0.0` because a loopback bind inside a container is unreachable
|
unreachable (https://git.eeqj.de/sneak/webhooker/issues/268). The same commit
|
||||||
(https://git.eeqj.de/sneak/webhooker/issues/268). The same commit
|
removed the shutdown race: `httpServer` is built in the constructor rather
|
||||||
removed the shutdown race: `httpServer` is built in the constructor
|
than assigned from the serving goroutine, which orders the write before every
|
||||||
rather than assigned from the serving goroutine, which orders the
|
fx hook and rules out the nil dereference a SIGTERM arriving first would have
|
||||||
write before every fx hook and rules out the nil dereference a
|
caused, and `sentryEnabled` is an `atomic.Bool`
|
||||||
SIGTERM arriving first would have caused, and `sentryEnabled` is an
|
(https://git.eeqj.de/sneak/webhooker/issues/226)
|
||||||
`atomic.Bool` (https://git.eeqj.de/sneak/webhooker/issues/226)
|
- 2026-08-24 Remove inbound request signature verification. The entrypoint UUID
|
||||||
- 2026-08-24 Remove inbound request signature verification. The
|
is the authentication secret, so the per-entrypoint shared secret, the
|
||||||
entrypoint UUID is the authentication secret, so the per-entrypoint
|
`internal/signature` package, the receiver check, the model fields and the
|
||||||
shared secret, the `internal/signature` package, the receiver check,
|
forms are all gone. This reverses the feature that landed earlier in the same
|
||||||
the model fields and the forms are all gone. This reverses the
|
milestone (https://git.eeqj.de/sneak/webhooker/issues/67,
|
||||||
feature that landed earlier in the same milestone
|
|
||||||
(https://git.eeqj.de/sneak/webhooker/issues/67,
|
|
||||||
https://git.eeqj.de/sneak/webhooker/issues/279)
|
https://git.eeqj.de/sneak/webhooker/issues/279)
|
||||||
- 2026-08-24 Stamp the build version into the binary and render it in
|
- 2026-08-24 Stamp the build version into the binary and render it in the UI
|
||||||
the UI footer. `script/version` is the single source — `$VERSION`,
|
footer. `script/version` is the single source — `$VERSION`, else
|
||||||
else `git describe --tags --always --dirty`, else `unknown` — so a
|
`git describe --tags --always --dirty`, else `unknown` — so a `make build`
|
||||||
`make build` binary and a `make docker` image from one checkout
|
binary and a `make docker` image from one checkout report the same thing, and
|
||||||
report the same thing, and nothing in it varies between two builds
|
nothing in it varies between two builds of the same commit, which the release
|
||||||
of the same commit, which the release gate's byte-identical
|
gate's byte-identical assertion would catch
|
||||||
assertion would catch
|
|
||||||
(https://git.eeqj.de/sneak/webhooker/issues/253)
|
(https://git.eeqj.de/sneak/webhooker/issues/253)
|
||||||
- 2026-08-24 Derive cookie `Secure` and CSRF strictness from the
|
- 2026-08-24 Derive cookie `Secure` and CSRF strictness from the request
|
||||||
request transport rather than from `WEBHOOKER_ENVIRONMENT`. Behind a
|
transport rather than from `WEBHOOKER_ENVIRONMENT`. Behind a real TLS proxy
|
||||||
real TLS proxy with the environment left at its `dev` default, the
|
with the environment left at its `dev` default, the session cookie silently
|
||||||
session cookie silently lost `Secure` while the CSRF cookie on the
|
lost `Secure` while the CSRF cookie on the same response kept it.
|
||||||
same response kept it. `X-Forwarded-Proto` is now matched
|
`X-Forwarded-Proto` is now matched case-insensitively on its first
|
||||||
case-insensitively on its first comma-separated element, so `HTTPS`
|
comma-separated element, so `HTTPS` and `https, http` no longer fall to the
|
||||||
and `https, http` no longer fall to the relaxed CSRF path
|
relaxed CSRF path (https://git.eeqj.de/sneak/webhooker/issues/269)
|
||||||
(https://git.eeqj.de/sneak/webhooker/issues/269)
|
- 2026-08-24 Roll back a failed webhook deletion instead of committing it. A
|
||||||
- 2026-08-24 Roll back a failed webhook deletion instead of committing
|
failing delete committed whatever had already succeeded, hard-deleted the
|
||||||
it. A failing delete committed whatever had already succeeded,
|
per-webhook event database anyway, and redirected as though it had worked —
|
||||||
hard-deleted the per-webhook event database anyway, and redirected as
|
orphaned config plus permanently destroyed history, reported as success. All
|
||||||
though it had worked — orphaned config plus permanently destroyed
|
three delete positions now roll back with the event database intact
|
||||||
history, reported as success. All three delete positions now roll
|
|
||||||
back with the event database intact
|
|
||||||
(https://git.eeqj.de/sneak/webhooker/issues/262)
|
(https://git.eeqj.de/sneak/webhooker/issues/262)
|
||||||
- 2026-08-24 Name a deleted target on its historical deliveries, marked
|
- 2026-08-24 Name a deleted target on its historical deliveries, marked
|
||||||
`(deleted)`, rather than leaving the event log unable to say where a
|
`(deleted)`, rather than leaving the event log unable to say where a delivery
|
||||||
delivery went. A deleted target's credentials stay masked exactly as
|
went. A deleted target's credentials stay masked exactly as a live one's, and
|
||||||
a live one's, and it cannot become deliverable again through the
|
it cannot become deliverable again through the receiver, resubmit, replay, the
|
||||||
receiver, resubmit, replay, the edit form or the toggle
|
edit form or the toggle (https://git.eeqj.de/sneak/webhooker/issues/211)
|
||||||
(https://git.eeqj.de/sneak/webhooker/issues/211)
|
- 2026-08-24 Bound both request-controlled `/metrics` label dimensions, so the
|
||||||
- 2026-08-24 Bound both request-controlled `/metrics` label dimensions,
|
unauthenticated receiver is no longer a memory-exhaustion vector: `handler`
|
||||||
so the unauthenticated receiver is no longer a memory-exhaustion
|
carries the chi route pattern, and `method` folds anything chi cannot route
|
||||||
vector: `handler` carries the chi route pattern, and `method` folds
|
onto a single `(unmatched)` sentinel. Both were reproduced before the fix —
|
||||||
anything chi cannot route onto a single `(unmatched)` sentinel. Both
|
300 random method tokens took the series count from 106 to 7,631, and path
|
||||||
were reproduced before the fix — 300 random method tokens took the
|
flooding reached 62,532 — and a label audit across a live scrape found no
|
||||||
series count from 106 to 7,631, and path flooding reached 62,532 —
|
third unbounded dimension (https://git.eeqj.de/sneak/webhooker/issues/254,
|
||||||
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)
|
https://git.eeqj.de/sneak/webhooker/issues/261)
|
||||||
- 2026-08-24 Validate `max_retries` on both target forms. `abc`, `2.7`
|
- 2026-08-24 Validate `max_retries` on both target forms. `abc`, `2.7` and `-5`
|
||||||
and `-5` silently became 0 — fire-and-forget — including on the edit
|
silently became 0 — fire-and-forget — including on the edit path, where it
|
||||||
path, where it destroyed a working value, and `999999999` stored
|
destroyed a working value, and `999999999` stored verbatim. The ceiling of 20
|
||||||
verbatim. The ceiling of 20 is the `max` both templates already
|
is the `max` both templates already declared
|
||||||
declared (https://git.eeqj.de/sneak/webhooker/issues/221)
|
(https://git.eeqj.de/sneak/webhooker/issues/221)
|
||||||
- 2026-08-24 Resubmit a stored event as a new undelivered event, so a
|
- 2026-08-24 Resubmit a stored event as a new undelivered event, so a backend
|
||||||
backend under development can be tested against real captured
|
under development can be tested against real captured traffic. Per-delivery
|
||||||
traffic. Per-delivery replay cannot serve that: it re-sends one
|
replay cannot serve that: it re-sends one finished delivery to its own
|
||||||
finished delivery to its own original target, and a target created
|
original target, and a target created for a dev backend has no prior delivery
|
||||||
for a dev backend has no prior delivery to replay. Resubmit
|
to replay. Resubmit re-injects the stored event at the top of the receiver
|
||||||
re-injects the stored event at the top of the receiver path and fans
|
path and fans it out to whatever targets are active now
|
||||||
it out to whatever targets are active now
|
|
||||||
(https://git.eeqj.de/sneak/webhooker/issues/250)
|
(https://git.eeqj.de/sneak/webhooker/issues/250)
|
||||||
- 2026-08-20 Take an exclusive lock on `DATA_DIR` at startup, so two
|
- 2026-08-20 Take an exclusive lock on `DATA_DIR` at startup, so two instances
|
||||||
instances on one directory cannot both deliver
|
on one directory cannot both deliver
|
||||||
(https://git.eeqj.de/sneak/webhooker/issues/201)
|
(https://git.eeqj.de/sneak/webhooker/issues/201)
|
||||||
- 2026-08-20 Shut down the app when the HTTP listener fails. The
|
- 2026-08-20 Shut down the app when the HTTP listener fails. The `OnStart` hook
|
||||||
`OnStart` hook returned as soon as the serving goroutine was
|
returned as soon as the serving goroutine was spawned, so a failed listen left
|
||||||
spawned, so a failed listen left fx reporting RUNNING and a live
|
fx reporting RUNNING and a live process with nothing bound — invisible to
|
||||||
process with nothing bound — invisible to systemd and Docker restart
|
systemd and Docker restart policies
|
||||||
policies (https://git.eeqj.de/sneak/webhooker/issues/200)
|
(https://git.eeqj.de/sneak/webhooker/issues/200)
|
||||||
- 2026-08-20 Stop target credentials leaking into the per-webhook event
|
- 2026-08-20 Stop target credentials leaking into the per-webhook event
|
||||||
databases (https://git.eeqj.de/sneak/webhooker/issues/206), log SQL
|
databases (https://git.eeqj.de/sneak/webhooker/issues/206), log SQL with
|
||||||
with placeholders rather than bound values
|
placeholders rather than bound values
|
||||||
(https://git.eeqj.de/sneak/webhooker/issues/207), and fail loudly on
|
(https://git.eeqj.de/sneak/webhooker/issues/207), and fail loudly on half-set
|
||||||
half-set metrics auth credentials
|
metrics auth credentials (https://git.eeqj.de/sneak/webhooker/issues/205)
|
||||||
(https://git.eeqj.de/sneak/webhooker/issues/205)
|
- 2026-08-20 Read queue depths with `Find`, not `Scan`. `Scan` swaps GORM's own
|
||||||
- 2026-08-20 Read queue depths with `Find`, not `Scan`. `Scan` swaps
|
trace recorder in for the logging adapter, and that recorder does not
|
||||||
GORM's own trace recorder in for the logging adapter, and that
|
implement `gorm.ParamsFilter`, so those statements logged their bound values
|
||||||
recorder does not implement `gorm.ParamsFilter`, so those statements
|
interpolated and bypassed the suppression above. The two units gated green
|
||||||
logged their bound values interpolated and bypassed the suppression
|
against a `next` that lacked the other, and `next` went red when both landed
|
||||||
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)
|
(https://git.eeqj.de/sneak/webhooker/issues/234)
|
||||||
- 2026-08-20 Render per-attempt delivery detail in the event log
|
- 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
|
(https://git.eeqj.de/sneak/webhooker/issues/202) and add replay of a
|
||||||
terminally failed delivery
|
terminally failed delivery (https://git.eeqj.de/sneak/webhooker/issues/203)
|
||||||
(https://git.eeqj.de/sneak/webhooker/issues/203)
|
|
||||||
- 2026-08-20 Expose delivery metrics on `/metrics`
|
- 2026-08-20 Expose delivery metrics on `/metrics`
|
||||||
(https://git.eeqj.de/sneak/webhooker/issues/209) and document the
|
(https://git.eeqj.de/sneak/webhooker/issues/209) and document the backup,
|
||||||
backup, restore and upgrade procedures
|
restore and upgrade procedures
|
||||||
(https://git.eeqj.de/sneak/webhooker/issues/210)
|
(https://git.eeqj.de/sneak/webhooker/issues/210)
|
||||||
- 2026-08-20 Add a `webhooker resetpw` subcommand and a bootstrap
|
- 2026-08-20 Add a `webhooker resetpw` subcommand and a bootstrap banner. The
|
||||||
banner. The admin bootstrap password was printed once among roughly
|
admin bootstrap password was printed once among roughly 45 fx lines, and under
|
||||||
45 fx lines, and under `docker run -d` went to container logs subject
|
`docker run -d` went to container logs subject to rotation; there was no reset
|
||||||
to rotation; there was no reset path at all, so recovery meant
|
path at all, so recovery meant hand-deleting the users row, documented
|
||||||
hand-deleting the users row, documented nowhere. The password is read
|
nowhere. The password is read from stdin or generated, never from argv where
|
||||||
from stdin or generated, never from argv where `/proc` would publish
|
`/proc` would publish it (https://git.eeqj.de/sneak/webhooker/issues/208)
|
||||||
it (https://git.eeqj.de/sneak/webhooker/issues/208)
|
- 2026-08-20 Add `ALLOWED_EGRESS_CIDRS`, an allowlist-only escape hatch for the
|
||||||
- 2026-08-20 Add `ALLOWED_EGRESS_CIDRS`, an allowlist-only escape hatch
|
SSRF guard, so a self-hosted proxy can forward into the operator's own
|
||||||
for the SSRF guard, so a self-hosted proxy can forward into the
|
network. The guard's always-blocked set cannot be reopened by configuration
|
||||||
operator's own network. The guard's always-blocked set cannot be
|
|
||||||
reopened by configuration
|
|
||||||
(https://git.eeqj.de/sneak/webhooker/issues/204)
|
(https://git.eeqj.de/sneak/webhooker/issues/204)
|
||||||
- 2026-08-20 Harden operator-set target headers, which were carried
|
- 2026-08-20 Harden operator-set target headers, which were carried unsafely
|
||||||
unsafely across a redirect
|
across a redirect (https://git.eeqj.de/sneak/webhooker/issues/233)
|
||||||
(https://git.eeqj.de/sneak/webhooker/issues/233)
|
|
||||||
- 2026-08-20 Add a target edit form with headers and timeout fields
|
- 2026-08-20 Add a target edit form with headers and timeout fields
|
||||||
(https://git.eeqj.de/sneak/webhooker/issues/127)
|
(https://git.eeqj.de/sneak/webhooker/issues/127)
|
||||||
- 2026-08-18 Raise `script/test`'s per-package timeout from 30s to 90s,
|
- 2026-08-18 Raise `script/test`'s per-package timeout from 30s to 90s, matching
|
||||||
matching the org-wide backstop. `go test` applies `-timeout` per
|
the org-wide backstop. `go test` applies `-timeout` per package, and
|
||||||
package, and `internal/handlers` had grown past the old budget: a
|
`internal/handlers` had grown past the old budget: a cache-defeated build
|
||||||
cache-defeated build failed outright at `GOMAXPROCS=4`, and every run
|
failed outright at `GOMAXPROCS=4`, and every run under deliberate host load
|
||||||
under deliberate host load breached 30s. The measurement table lives
|
breached 30s. The measurement table lives in the script (#194)
|
||||||
in the script (#194)
|
- 2026-08-18 Re-sync `REPO_POLICIES.md` from `prompts`. The local copy was stale
|
||||||
- 2026-08-18 Re-sync `REPO_POLICIES.md` from `prompts`. The local copy
|
and still mandated a 20s test target with a 30s timeout, which the org
|
||||||
was stale and still mandated a 20s test target with a 30s timeout,
|
replaced with a 60s cap and a 90s backstop. A synced copy is not a source;
|
||||||
which the org replaced with a 60s cap and a 90s backstop. A synced
|
reading it as one nearly produced a PR against `prompts` proposing a change
|
||||||
copy is not a source; reading it as one nearly produced a PR against
|
already merged there (#196)
|
||||||
`prompts` proposing a change already merged there (#196)
|
- 2026-08-18 Report handler panics through the logger and answer 500. chi
|
||||||
- 2026-08-18 Report handler panics through the logger and answer 500.
|
v1.5.5's `Recoverer` scans for a `panic(0x` frame the runtime no longer emits,
|
||||||
chi v1.5.5's `Recoverer` scans for a `panic(0x` frame the runtime no
|
then indexes `pkg[-1:]`, so it panicked inside its own stack printer before
|
||||||
longer emits, then indexes `pkg[-1:]`, so it panicked inside its own
|
writing a byte: the recovery never ran, the client got a dropped connection
|
||||||
stack printer before writing a byte: the recovery never ran, the
|
instead of a 500, and the original panic was lost. A local middleware replaces
|
||||||
client got a dropped connection instead of a 500, and the original
|
it, bounded by `MaxPanicLogLineBytes` (#187)
|
||||||
panic was lost. A local middleware replaces it, bounded by
|
- 2026-08-18 Route GORM's logger through `slog` and bound it. Every `gorm.Open`
|
||||||
`MaxPanicLogLineBytes` (#187)
|
left `logger.Default` in place at `Warn` with `IgnoreRecordNotFoundError`
|
||||||
- 2026-08-18 Route GORM's logger through `slog` and bound it. Every
|
false, so **every record-not-found printed the fully interpolated SQL to
|
||||||
`gorm.Open` left `logger.Default` in place at `Warn` with
|
stdout** — including the client-chosen path on `/webhook/{uuid}` and the
|
||||||
`IgnoreRecordNotFoundError` false, so **every record-not-found
|
submitted username on the login form, at no level the operator set and outside
|
||||||
printed the fully interpolated SQL to stdout** — including the
|
`internal/logger` entirely. Three call sites, not the two the issue named
|
||||||
client-chosen path on `/webhook/{uuid}` and the submitted username on
|
(#178)
|
||||||
the login form, at no level the operator set and outside
|
- 2026-08-18 Bound every `slog` line against client-chosen text. Eight sites
|
||||||
`internal/logger` entirely. Three call sites, not the two the issue
|
reachable unauthenticated, found by reading every `slog` call in the tree
|
||||||
named (#178)
|
rather than only the one reported; the budget moved to a shared
|
||||||
- 2026-08-18 Bound every `slog` line against client-chosen text. Eight
|
`internal/logfield` so no second truncation exists. `DEBUG` being off by
|
||||||
sites reachable unauthenticated, found by reading every `slog` call in
|
default is not a bound and is not treated as one (#176)
|
||||||
the tree rather than only the one reported; the budget moved to a
|
- 2026-08-18 Stop a slow host turning a login-guard test into a segfault. A
|
||||||
shared `internal/logfield` so no second truncation exists. `DEBUG`
|
non-fatal `assert` on an acquire result was dereferenced on the next line, so
|
||||||
being off by default is not a bound and is not treated as one (#176)
|
one timing miss killed the whole `internal/middleware` binary and reddened CI
|
||||||
- 2026-08-18 Stop a slow host turning a login-guard test into a
|
for unrelated PRs. The fix also removed a real production race — `acquire`
|
||||||
segfault. A non-fatal `assert` on an acquire result was dereferenced
|
could shed a request with a slot standing free, because Go picks uniformly
|
||||||
on the next line, so one timing miss killed the whole
|
among ready `select` cases (#186)
|
||||||
`internal/middleware` binary and reddened CI for unrelated PRs. The
|
- 2026-08-18 Send the chi route pattern to Sentry rather than the concrete path.
|
||||||
fix also removed a real production race — `acquire` could shed a
|
The receiver's path carries the entrypoint capability token, so every Sentry
|
||||||
request with a slot standing free, because Go picks uniformly among
|
event from `/webhook/{uuid}` shipped a live credential to a third party.
|
||||||
ready `select` cases (#186)
|
Request `Data`, `QueryString`, `Cookies` and `Env` are dropped and headers
|
||||||
- 2026-08-18 Send the chi route pattern to Sentry rather than the
|
reduced to an allowlist (#179)
|
||||||
concrete path. The receiver's path carries the entrypoint capability
|
- 2026-08-18 Read form fields from the POST body only. `r.FormValue` merges the
|
||||||
token, so every Sentry event from `/webhook/{uuid}` shipped a live
|
query string, so a login could be driven by URL parameters — putting the
|
||||||
credential to a third party. Request `Data`, `QueryString`, `Cookies`
|
password somewhere that lands in access logs, proxy logs and browser history
|
||||||
and `Env` are dropped and headers reduced to an allowlist (#179)
|
(#160)
|
||||||
- 2026-08-18 Read form fields from the POST body only. `r.FormValue`
|
- 2026-08-18 Verify login credentials before spending rate-limit budget, so a
|
||||||
merges the query string, so a login could be driven by URL parameters
|
flood of wrong passwords cannot lock out the account it is guessing at. The
|
||||||
— putting the password somewhere that lands in access logs, proxy
|
manager took this decision rather than stall the queue; it is flagged on the
|
||||||
logs and browser history (#160)
|
issue for reversal (#150)
|
||||||
- 2026-08-18 Verify login credentials before spending rate-limit
|
- 2026-08-18 Run all linting in Docker via `Dockerfile.lint`. Host lint was
|
||||||
budget, so a flood of wrong passwords cannot lock out the account it
|
wrong in both directions from version skew and shared caches. `script/lint`
|
||||||
is guessing at. The manager took this decision rather than stall the
|
asserts the summary line, because `--no-cache-filter` silently ignores a stage
|
||||||
queue; it is flagged on the issue for reversal (#150)
|
name it does not match — the flag that makes the gate meaningful fails open
|
||||||
- 2026-08-18 Run all linting in Docker via `Dockerfile.lint`. Host lint
|
(#109)
|
||||||
was wrong in both directions from version skew and shared caches.
|
- 2026-08-18 Serve an event's full stored body over HTTP. The list query
|
||||||
`script/lint` asserts the summary line, because `--no-cache-filter`
|
truncates for rendering, and that truncated value was the only way to read a
|
||||||
silently ignores a stage name it does not match — the flag that makes
|
body, so the full payload was unreachable (#157)
|
||||||
the gate meaningful fails open (#109)
|
|
||||||
- 2026-08-18 Serve an event's full stored body over HTTP. The list
|
|
||||||
query truncates for rendering, and that truncated value was the only
|
|
||||||
way to read a body, so the full payload was unreachable (#157)
|
|
||||||
- 2026-08-18 Bound the access log line against client-chosen text.
|
- 2026-08-18 Bound the access log line against client-chosen text.
|
||||||
`internal/logfield` budgets by *encoded* bytes, not runes, so a
|
`internal/logfield` budgets by _encoded_ bytes, not runes, so a handler's JSON
|
||||||
handler's JSON escaping cannot multiply a field past its allowance
|
escaping cannot multiply a field past its allowance (#146)
|
||||||
(#146)
|
- 2026-08-18 Mark superseded CI commits `failure` rather than `skipped`. A
|
||||||
- 2026-08-18 Mark superseded CI commits `failure` rather than
|
skipped run rolls up green, so a commit that was never tested reported success
|
||||||
`skipped`. A skipped run rolls up green, so a commit that was never
|
(#152)
|
||||||
tested reported success (#152)
|
- 2026-08-18 Set `fx.StopTimeout` inside the container stop grace, so shutdown
|
||||||
- 2026-08-18 Set `fx.StopTimeout` inside the container stop grace, so
|
hooks are bounded by a deadline the orchestrator will actually honour rather
|
||||||
shutdown hooks are bounded by a deadline the orchestrator will
|
than being killed mid-flush (#134)
|
||||||
actually honour rather than being killed mid-flush (#134)
|
- 2026-08-17 Bucket IPv6 rate-limit keys by `/64`. A single allocation hands out
|
||||||
- 2026-08-17 Bucket IPv6 rate-limit keys by `/64`. A single allocation
|
2^64 addresses, so per-address keying let one client mint unlimited buckets.
|
||||||
hands out 2^64 addresses, so per-address keying let one client mint
|
Manager decision, recorded on the issue (#125)
|
||||||
unlimited buckets. Manager decision, recorded on the issue (#125)
|
- 2026-08-17 Correct release-blocking README and startup-warning inaccuracies,
|
||||||
- 2026-08-17 Correct release-blocking README and startup-warning
|
including claims about behaviour the code does not have (#151)
|
||||||
inaccuracies, including claims about behaviour the code does not have
|
|
||||||
(#151)
|
|
||||||
- 2026-08-17 Fetch and verify Alpine.js at build time against
|
- 2026-08-17 Fetch and verify Alpine.js at build time against
|
||||||
`static/vendor.sha256` instead of committing the minified blob, so
|
`static/vendor.sha256` instead of committing the minified blob, so the
|
||||||
the dependency is pinned by hash rather than by trust (#145)
|
dependency is pinned by hash rather than by trust (#145)
|
||||||
- 2026-08-17 Bound the event log's rendered bodies in the query itself,
|
- 2026-08-17 Bound the event log's rendered bodies in the query itself, so a
|
||||||
so a large stored payload cannot be read into memory just to be
|
large stored payload cannot be read into memory just to be truncated for
|
||||||
truncated for display (#135)
|
display (#135)
|
||||||
- 2026-08-17 Mask the `http` target's destination URL in the UI: it can
|
- 2026-08-17 Mask the `http` target's destination URL in the UI: it can carry a
|
||||||
carry a bearer credential in its path or query, and was rendered
|
bearer credential in its path or query, and was rendered verbatim. Manager
|
||||||
verbatim. Manager decision to mask unconditionally (#115)
|
decision to mask unconditionally (#115)
|
||||||
- 2026-08-14 Bound shutdown hooks by their stop context, so a hook that
|
- 2026-08-14 Bound shutdown hooks by their stop context, so a hook that hangs
|
||||||
hangs cannot hold the process past its grace period (#102)
|
cannot hold the process past its grace period (#102)
|
||||||
- 2026-08-14 Render templates via a buffer rather than the
|
- 2026-08-14 Render templates via a buffer rather than the `ResponseWriter`, so
|
||||||
`ResponseWriter`, so a template error part-way through cannot commit
|
a template error part-way through cannot commit a 200 and then fail — the
|
||||||
a 200 and then fail — the response is written only once it is whole
|
response is written only once it is whole (#123)
|
||||||
(#123)
|
- 2026-08-14 Align the session codec's max-age with the 7-day absolute cap. The
|
||||||
- 2026-08-14 Align the session codec's max-age with the 7-day absolute
|
codec accepted cookies the session layer considered expired, so the cap was
|
||||||
cap. The codec accepted cookies the session layer considered expired,
|
enforced in one place and not the other (#108)
|
||||||
so the cap was enforced in one place and not the other (#108)
|
- 2026-08-12 Warn when `TRUSTED_PROXIES` is empty in production, where the safe
|
||||||
- 2026-08-12 Warn when `TRUSTED_PROXIES` is empty in production, where
|
default silently discards forwarded headers and every client rate-limits as
|
||||||
the safe default silently discards forwarded headers and every client
|
the proxy's address (#149)
|
||||||
rate-limits as the proxy's address (#149)
|
- 2026-08-12 Bound the receiver rate limit per client IP across the whole
|
||||||
- 2026-08-12 Bound the receiver rate limit per client IP across the
|
`/webhook/*` route. The existing limiter keyed on the request path and
|
||||||
whole `/webhook/*` route. The existing limiter keyed on the request
|
`/webhook/{uuid}` matches any single segment, so a client that invented a
|
||||||
path and `/webhook/{uuid}` matches any single segment, so a client
|
fresh path per request minted a fresh bucket per request: the limit on the
|
||||||
that invented a fresh path per request minted a fresh bucket per
|
only unauthenticated endpoint bounded nothing in aggregate, and every request
|
||||||
request: the limit on the only unauthenticated endpoint bounded
|
still cost an entrypoint lookup before it 404ed. An outer limiter keyed on the
|
||||||
nothing in aggregate, and every request still cost an entrypoint
|
client address alone now bounds that, chained in front of the unchanged
|
||||||
lookup before it 404ed. An outer limiter keyed on the client address
|
|
||||||
alone now bounds that, chained in front of the unchanged
|
|
||||||
per-entrypoint limiter (#139)
|
per-entrypoint limiter (#139)
|
||||||
- 2026-08-12 Correct release-blocking documentation inaccuracies: the
|
- 2026-08-12 Correct release-blocking documentation inaccuracies: the README
|
||||||
README promised manual redelivery in the present tense in three
|
promised manual redelivery in the present tense in three places when nothing
|
||||||
places when nothing implements it (the same false claim also sat in
|
implements it (the same false claim also sat in the doc comment that was its
|
||||||
the doc comment that was its source text), the env table omitted
|
source text), the env table omitted `RETENTION_SWEEP_INTERVAL`, and `TODO.md`
|
||||||
`RETENTION_SWEEP_INTERVAL`, and `TODO.md` itself omitted five landed
|
itself omitted five landed units (#141)
|
||||||
units (#141)
|
- 2026-08-12 Make the CI gate execute the checks it reports on. The workflow now
|
||||||
- 2026-08-12 Make the CI gate execute the checks it reports on. The
|
writes a build-context fingerprint before calling `script/cibuild`, so a code
|
||||||
workflow now writes a build-context fingerprint before calling
|
commit invalidates the `COPY` layer of the lint and builder stages while a
|
||||||
`script/cibuild`, so a code commit invalidates the `COPY` layer of
|
docs-only commit still replays from cache; a superseding run also rewrites the
|
||||||
the lint and builder stages while a docs-only commit still replays
|
`failure` status Gitea leaves on commits it cancelled and never tested.
|
||||||
from cache; a superseding run also rewrites the `failure` status
|
Verified by pushing a deliberately broken test and watching CI go red (#119)
|
||||||
Gitea leaves on commits it cancelled and never tested. Verified by
|
- 2026-08-12 Require a positive `RETENTION_SWEEP_INTERVAL`: a non-positive value
|
||||||
pushing a deliberately broken test and watching CI go red (#119)
|
reached `time.NewTicker` in both the retention reaper and the archive sweeper,
|
||||||
- 2026-08-12 Require a positive `RETENTION_SWEEP_INTERVAL`: a
|
panicking two goroutines with no recover after startup had already reported
|
||||||
non-positive value reached `time.NewTicker` in both the retention
|
success (#140)
|
||||||
reaper and the archive sweeper, panicking two goroutines with no
|
- 2026-08-12 Bound the `X-Forwarded-For` scan's allocation to the hop cap: the
|
||||||
recover after startup had already reported success (#140)
|
reverse walk cuts entries with `strings.LastIndexByte` instead of joining and
|
||||||
- 2026-08-12 Bound the `X-Forwarded-For` scan's allocation to the hop
|
splitting, so a 1 MB header allocates 16 bytes rather than 1.6 MB per request
|
||||||
cap: the reverse walk cuts entries with `strings.LastIndexByte`
|
on the unauthenticated receiver. Semantics proven unchanged by differential
|
||||||
instead of joining and splitting, so a 1 MB header allocates 16 bytes
|
testing against the previous implementation (#133)
|
||||||
rather than 1.6 MB per request on the unauthenticated receiver.
|
|
||||||
Semantics proven unchanged by differential testing against the
|
|
||||||
previous implementation (#133)
|
|
||||||
- 2026-08-12 Cap the `X-Forwarded-For` hop walk at 64 entries, so an
|
- 2026-08-12 Cap the `X-Forwarded-For` hop walk at 64 entries, so an
|
||||||
attacker-supplied chain cannot burn unbounded CPU in the rate-limit
|
attacker-supplied chain cannot burn unbounded CPU in the rate-limit key
|
||||||
key function; running off the end falls back to the peer address
|
function; running off the end falls back to the peer address (#124)
|
||||||
(#124)
|
- 2026-08-12 Gate forwarded-header trust behind a `TRUSTED_PROXIES` CIDR list:
|
||||||
- 2026-08-12 Gate forwarded-header trust behind a `TRUSTED_PROXIES` CIDR
|
all three rate limiters key on the connection's own address unless the direct
|
||||||
list: all three rate limiters key on the connection's own address
|
peer is a configured proxy, in which case `X-Forwarded-For` is walked right to
|
||||||
unless the direct peer is a configured proxy, in which case
|
left for the first non-proxy hop. Default trusts nothing, and a
|
||||||
`X-Forwarded-For` is walked right to left for the first non-proxy hop.
|
set-but-unparseable value aborts startup. Before this, any client could mint a
|
||||||
Default trusts nothing, and a set-but-unparseable value aborts
|
fresh bucket or drain another's by rotating a spoofed header (#88)
|
||||||
startup. Before this, any client could mint a fresh bucket or drain
|
- 2026-08-11 Web UI cleanup: nav terminology unified on Webhooks, the Profile
|
||||||
another's by rotating a spoofed header (#88)
|
settings placeholder removed, a progressive-enhancement copy button for the
|
||||||
- 2026-08-11 Web UI cleanup: nav terminology unified on Webhooks, the
|
entrypoint URL, and retention form copy that states the actual policy
|
||||||
Profile settings placeholder removed, a progressive-enhancement copy
|
(deletion by the reaper, 0 retains forever) (#57)
|
||||||
button for the entrypoint URL, and retention form copy that states the
|
- 2026-08-11 Mask the webhook credential in delivery errors and logs: Go embeds
|
||||||
actual policy (deletion by the reaper, 0 retains forever) (#57)
|
the request URL in `*url.Error`, so every transport failure persisted the full
|
||||||
- 2026-08-11 Mask the webhook credential in delivery errors and logs:
|
Slack webhook URL into the per-webhook event database via
|
||||||
Go embeds the request URL in `*url.Error`, so every transport failure
|
`DeliveryResult.Error`, a field a future REST API would have served.
|
||||||
persisted the full Slack webhook URL into the per-webhook event
|
`maskURLError` drops path, query and userinfo while preserving the wrapped
|
||||||
database via `DeliveryResult.Error`, a field a future REST API would
|
cause, so `errors.Is`/`As` and `Timeout()` still work and DNS, TLS and timeout
|
||||||
have served. `maskURLError` drops path, query and userinfo while
|
failures still read differently (#118)
|
||||||
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
|
- 2026-08-11 Rate-limit the public webhook receiver endpoint
|
||||||
(`RECEIVER_RATE_LIMIT`, default 120/min), keyed on client IP plus
|
(`RECEIVER_RATE_LIMIT`, default 120/min), keyed on client IP plus entrypoint
|
||||||
entrypoint path so one entrypoint cannot exhaust another's budget;
|
path so one entrypoint cannot exhaust another's budget; over-limit requests
|
||||||
over-limit requests get 429 with `Retry-After`. It was the one
|
get 429 with `Retry-After`. It was the one unauthenticated, internet-facing
|
||||||
unauthenticated, internet-facing endpoint with no limit at all (#64)
|
endpoint with no limit at all (#64)
|
||||||
- 2026-08-11 Enforce the body size limit before CSRF parses the form:
|
- 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
|
`MaxBodySize` is now first in all four form-parsing route groups, so an
|
||||||
an oversized request is rejected with 413 instead of being read in
|
oversized request is rejected with 413 instead of being read in full by the
|
||||||
full by the CSRF middleware before any cap applied (#90)
|
CSRF middleware before any cap applied (#90)
|
||||||
- 2026-08-11 Mask target config on the source detail page, which
|
- 2026-08-11 Mask target config on the source detail page, which rendered the
|
||||||
rendered the stored blob verbatim and so exposed the Slack
|
stored blob verbatim and so exposed the Slack incoming-webhook URL — a bearer
|
||||||
incoming-webhook URL — a bearer credential that cannot be revoked
|
credential that cannot be revoked per-holder. Config reaches the template only
|
||||||
per-holder. Config reaches the template only as a `TargetView` of
|
as a `TargetView` of labelled fields, and header values are rendered as a
|
||||||
labelled fields, and header values are rendered as a count (#113)
|
count (#113)
|
||||||
- 2026-08-11 Allow `retention_days` of 0 to mean retain forever, via a
|
- 2026-08-11 Allow `retention_days` of 0 to mean retain forever, via a sentinel
|
||||||
sentinel written in `BeforeSave` so the GORM column default cannot
|
written in `BeforeSave` so the GORM column default cannot win the race. Also
|
||||||
win the race. Also bounds the reaper's cutoff arithmetic: day counts
|
bounds the reaper's cutoff arithmetic: day counts above 106751 overflowed
|
||||||
above 106751 overflowed `time.Duration` and wrapped the cutoff into
|
`time.Duration` and wrapped the cutoff into the future, where every row
|
||||||
the future, where every row matched and the sweep deleted everything
|
matched and the sweep deleted everything (#79)
|
||||||
(#79)
|
|
||||||
- 2026-08-09 Inactivity-based session timeout: sliding idle expiry
|
- 2026-08-09 Inactivity-based session timeout: sliding idle expiry
|
||||||
(`SESSION_IDLE_TIMEOUT`, default `24h`) refreshed on authenticated
|
(`SESSION_IDLE_TIMEOUT`, default `24h`) refreshed on authenticated requests,
|
||||||
requests, with the 7-day absolute cap kept as an independent
|
with the 7-day absolute cap kept as an independent backstop that activity
|
||||||
backstop that activity never extends (#66)
|
never extends (#66)
|
||||||
- 2026-08-09 Restart recovery and the 60s retry sweep terminally fail an
|
- 2026-08-09 Restart recovery and the 60s retry sweep terminally fail an
|
||||||
orphaned `retrying` delivery whose target type no longer supports
|
orphaned `retrying` delivery whose target type no longer supports retries,
|
||||||
retries, recording a `DeliveryResult` with the reason instead of
|
recording a `DeliveryResult` with the reason instead of leaving the delivery
|
||||||
leaving the delivery stuck forever (#82)
|
stuck forever (#82)
|
||||||
- 2026-08-09 Root the delivery engine's worker pool and the retention
|
- 2026-08-09 Root the delivery engine's worker pool and the retention reaper's
|
||||||
reaper's sweep loop at `context.Background()` rather than the fx
|
sweep loop at `context.Background()` rather than the fx `OnStart` hook context
|
||||||
`OnStart` hook context (#97), which carries fx's 15s start timeout and
|
(#97), which carries fx's 15s start timeout and killed both roughly fifteen
|
||||||
killed both roughly fifteen seconds after boot: the proxy silently
|
seconds after boot: the proxy silently stopped delivering webhooks entirely,
|
||||||
stopped delivering webhooks entirely, and the reaper never ran a
|
and the reaper never ran a single sweep under its default one-hour interval
|
||||||
single sweep under its default one-hour interval
|
- 2026-08-09 Archive writer lifecycle (#89): deleting a webhook (or its last
|
||||||
- 2026-08-09 Archive writer lifecycle (#89): deleting a webhook (or its
|
`database` target) evicts the cached archive writer and closes its handle
|
||||||
last `database` target) evicts the cached archive writer and closes
|
while deliberately leaving `archive-{webhookID}.db` on disk, and a new
|
||||||
its handle while deliberately leaving `archive-{webhookID}.db` on
|
`ArchiveSweeper` prunes idle archives on the existing
|
||||||
disk, and a new `ArchiveSweeper` prunes idle archives on the existing
|
|
||||||
`RETENTION_SWEEP_INTERVAL` without ever creating an archive file
|
`RETENTION_SWEEP_INTERVAL` without ever creating an archive file
|
||||||
- 2026-08-09 Configuration parsing fails loudly on set-but-unparseable
|
- 2026-08-09 Configuration parsing fails loudly on set-but-unparseable
|
||||||
environment values: `envInt` removed in favour of `envPositiveInt`
|
environment values: `envInt` removed in favour of `envPositiveInt` plus a
|
||||||
plus a `PORT` range check, `envBool` now parses with
|
`PORT` range check, `envBool` now parses with `strconv.ParseBool`, and
|
||||||
`strconv.ParseBool`, and defaults apply only to unset variables (#80)
|
defaults apply only to unset variables (#80)
|
||||||
- 2026-08-07 Automatic event retention cleanup based on
|
- 2026-08-07 Automatic event retention cleanup based on `retention_days`,
|
||||||
`retention_days`, deleting expired events, deliveries, and delivery
|
deleting expired events, deliveries, and delivery results from each
|
||||||
results from each per-webhook event database (#63)
|
per-webhook event database (#63)
|
||||||
- 2026-08-07 Update golangci-lint to v2.12.2 (Docker image digest in
|
- 2026-08-07 Update golangci-lint to v2.12.2 (Docker image digest in
|
||||||
`Dockerfile`, release-archive sha256 pins in `script/bootstrap`),
|
`Dockerfile`, release-archive sha256 pins in `script/bootstrap`), adopt the
|
||||||
adopt the canonical `.golangci.yml` (v2 `linters.settings` layout so
|
canonical `.golangci.yml` (v2 `linters.settings` layout so
|
||||||
`lll`/`funlen`/`cyclop`/`dupl` thresholds actually apply), and fix
|
`lll`/`funlen`/`cyclop`/`dupl` thresholds actually apply), and fix all newly
|
||||||
all newly surfaced lint findings
|
surfaced lint findings
|
||||||
- 2026-07-07 Adopted scripts-to-rule-them-all: `script/` entrypoints,
|
- 2026-07-07 Adopted scripts-to-rule-them-all: `script/` entrypoints, Makefile
|
||||||
Makefile shims, README Entrypoints section
|
shims, README Entrypoints section
|
||||||
- 2026-03-25 pin golangci-lint Docker image for linting (#55)
|
- 2026-03-25 pin golangci-lint Docker image for linting (#55)
|
||||||
- 2026-03-18 CSRF middleware detects TLS per-request, fixing login over
|
- 2026-03-18 CSRF middleware detects TLS per-request, fixing login over plain
|
||||||
plain HTTP and behind reverse proxies (#54)
|
HTTP and behind reverse proxies (#54)
|
||||||
- 2026-03-17 root path redirects based on auth state (#52)
|
- 2026-03-17 root path redirects based on auth state (#52)
|
||||||
- 2026-03-17 CSRF protection, SSRF prevention for HTTP delivery targets
|
- 2026-03-17 CSRF protection, SSRF prevention for HTTP delivery targets with DNS
|
||||||
with DNS rebinding defense, and per-IP login rate limiting (#42)
|
rebinding defense, and per-IP login rate limiting (#42)
|
||||||
- 2026-03-17 Slack target type for incoming webhook notifications (#47)
|
- 2026-03-17 Slack target type for incoming webhook notifications (#47)
|
||||||
- 2026-03-17 Dockerfile absolute paths and static linking (#49);
|
- 2026-03-17 Dockerfile absolute paths and static linking (#49); absolute dev
|
||||||
absolute dev DATA_DIR default and clarified env docs (#46)
|
DATA_DIR default and clarified env docs (#46)
|
||||||
- 2026-03-05 security headers middleware, session regeneration on
|
- 2026-03-05 security headers middleware, session regeneration on login, request
|
||||||
login, request body size limits (#41)
|
body size limits (#41)
|
||||||
- 2026-03-04 tests for delivery, middleware, and session packages
|
- 2026-03-04 tests for delivery, middleware, and session packages (#32); removed
|
||||||
(#32); removed the build-architecture global (#31)
|
the build-architecture global (#31)
|
||||||
- 2026-03-04 1.0 MVP merge: Webhook/Entrypoint/Target rename, core
|
- 2026-03-04 1.0 MVP merge: Webhook/Entrypoint/Target rename, core delivery
|
||||||
delivery engine with bounded worker pool and circuit breaker,
|
engine with bounded worker pool and circuit breaker, parallel fan-out,
|
||||||
parallel fan-out, per-webhook event databases, management UI (#16)
|
per-webhook event databases, management UI (#16)
|
||||||
- 2026-03-01 repo brought to REPO_POLICIES standards; TODO.md folded
|
- 2026-03-01 repo brought to REPO_POLICIES standards; TODO.md folded into README
|
||||||
into README (#6)
|
(#6)
|
||||||
|
|
||||||
# Future Steps
|
# Future Steps
|
||||||
|
|
||||||
- Delivery status and retry management UI. Replay of a terminally
|
- Delivery status and retry management UI. Replay of a terminally failed
|
||||||
failed delivery and per-attempt detail already landed
|
delivery and per-attempt detail already landed
|
||||||
(https://git.eeqj.de/sneak/webhooker/issues/203,
|
(https://git.eeqj.de/sneak/webhooker/issues/203,
|
||||||
https://git.eeqj.de/sneak/webhooker/issues/202)
|
https://git.eeqj.de/sneak/webhooker/issues/202)
|
||||||
- Per-webhook rate limiting in the receiver handler (per-webhook config
|
- Per-webhook rate limiting in the receiver handler (per-webhook config plus
|
||||||
plus handler enforcement; global limits must not apply to receiver
|
handler enforcement; global limits must not apply to receiver endpoints)
|
||||||
endpoints)
|
- API key authentication for programmatic access (APIKey model exists; Bearer
|
||||||
- API key authentication for programmatic access (APIKey model exists;
|
token middleware does not)
|
||||||
Bearer token middleware does not)
|
|
||||||
- REST API v1
|
- REST API v1
|
||||||
- CRUD for webhooks, entrypoints, targets
|
- CRUD for webhooks, entrypoints, targets
|
||||||
- event viewing and filtering endpoints
|
- event viewing and filtering endpoints
|
||||||
@@ -406,9 +370,9 @@ point of the branch.
|
|||||||
- OpenAPI specification
|
- OpenAPI specification
|
||||||
- Analytics dashboard: success rates, response times, volume
|
- Analytics dashboard: success rates, response times, volume
|
||||||
- A remember-me option at login
|
- A remember-me option at login
|
||||||
- Password reset flow for a forgotten password over the web. The
|
- Password reset flow for a forgotten password over the web. The authenticated
|
||||||
authenticated password *change* flow already landed, and a lost
|
password _change_ flow already landed, and a lost password is recoverable from
|
||||||
password is recoverable from the console with `webhooker resetpw`
|
the console with `webhooker resetpw`
|
||||||
(https://git.eeqj.de/sneak/webhooker/issues/208)
|
(https://git.eeqj.de/sneak/webhooker/issues/208)
|
||||||
- Later, nice to have
|
- Later, nice to have
|
||||||
- email delivery target type
|
- email delivery target type
|
||||||
|
|||||||
Reference in New Issue
Block a user