Files
pad/internal/webhooks
xarmian 7ee0bddc45 fix(webhooks): whole-workspace access to manage; no delivery for a deleted workspace (#1727)
* fix(webhooks): whole-workspace access to manage them; none for a deleted workspace (BUG-3340)

A webhook receives item snapshots from every collection, but creating or
test-firing one required only the owner role, so an owner restricted to
specific collections could route every collection's content to a URL of
their choosing. Create and test now require unrestricted workspace
access, through the same check full export uses (BUG-1922), factored
into requireUnrestrictedAccess. Delete stays open: it is revocation.

ListWebhooks now returns nothing for a soft-deleted workspace. Both
dispatcher delivery paths (Dispatch and DeliverEvent) list through it,
so a deleted workspace's events are dropped instead of delivered during
the restore window; a restore brings the webhooks back.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(webhooks): re-check the workspace before every send; hide URLs from restricted owners (BUG-3340)

Codex r1 (no P0):
- P1: ListWebhooks checked deletion once, but a delivery reuses the hook
  list and payload across inline retries, later hooks in the fan-out,
  and redirect hops; codex reproduced POSTs after the delete committed.
  The dispatcher store gains WorkspaceLive, checked before every attempt
  (a new deliverySuppressed result: nothing sent, not the endpoint's
  failure, not counted as matched) and before every redirect hop (the
  request carries the hook's workspace). A lookup error is transient, so
  a blip delays rather than drops or sends unchecked.
- P2: the list still showed restricted owners every destination URL, and
  a URL can be the credential to a receiver's stored request bodies. A
  restricted owner now sees each webhook without its URL (the id is
  kept, so they can still delete it).

Tests: retry, later hook and redirect after a mid-delivery deletion each
send nothing (all three red with the checks disabled); the list hides
URLs from the restricted owner only.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(webhooks): check the workspace on every connection; defer on lookup errors (BUG-3340)

Codex r2 (no P0):
- P1: net/http can send more than once inside one Do (it re-dials and
  replays the body after a reused connection fails with nothing
  written), and a deletion can land during dial or TLS setup, both after
  the per-attempt check. An httptrace GotConn hook now re-checks the
  workspace on every connection the request is about to be written on,
  after TLS and before writing, and cancels the request if it is gone.
- P2 (two): a liveness lookup ERROR was treated as a deletion at the
  redirect check (the outbox then acked an undelivered event) and as an
  endpoint failure before a send (charged to the hook, which could
  auto-deactivate it). A new deliveryDeferred result sends nothing, keeps
  the event owed (Transient, so the outbox does not ack), and charges
  nothing to the hook. Suppression is reserved for a confirmed deletion.

Tests: deletion before the connection is ready sends nothing; a
pre-send lookup error and a redirect lookup error each defer without
sending or charging. Each goes red when its own fix is reverted.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(webhooks): close the connection in GotConn, a real write barrier (BUG-3340)

Codex r3 (no P0/P2): cancelling the request context in GotConn is not a
write barrier for HTTP/1: the transport queues the request before it
checks the context, so its writer could still send the whole request
(codex measured 10/10,000 in the zero-send test). GotConn now also closes
the connection it was handed when the workspace is gone or its liveness
unknown: nothing can be written to a closed socket, and the cancel stops
any transport retry on a fresh one.

Measured here: the cancel-only version fails the zero-send test 2/10,000;
with the close, 0/10,000 for both the deletion and lookup-error tests,
and the webhooks package passes under -race.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(webhooks): the connection check decides the outcome even if Do returns a response (BUG-3340)

Codex r4 (no P0/P1) P2: after GotConn closes and cancels, net/http can
still return an already-buffered response, and the suppressed/deferred
flags were only consulted when Do returned an error, so such a response
counted as a delivery. The flags are now checked right after Do,
whatever it returned, closing any response body first.

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-02 13:20:09 -04:00
..