You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix(webhooks): harden verify, create, replay, and relay edge cases
Adversarial audit follow-ups on the unreleased webhooks group:
- verify: reject an explicit empty --payload as a usage error instead of
hashing an `undefined` pre-image and silently failing (exit 2, not 1).
- create: propagate an AuthError from the post-create secret fetch instead
of masking it as "secret unavailable"; tag the genuine partial-failure
with the new webhook_secret_fetch_failed code for agent branching.
- replay: `--until` alone now points at the missing --since rather than
emitting the vaguer "pass <msg_id> or --since" hint.
- relay-client: route the 1008 token-collision redial through the standard
reconnect backoff (no zero-delay storm) and guard onopen against a stop()
that races socket construction.
- README: document at-least-once redelivery on reconnect (handlers must
key on svix-id).
Adds tests for each fix. Full suite 1934 pass / 0 fail.
Claude-Session: https://claude.ai/code/session_015J6Sduw5KeHz6SxLEBfViF
Copy file name to clipboardExpand all lines: packages/cli-core/src/commands/webhooks/README.md
+1Lines changed: 1 addition & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -250,6 +250,7 @@ Behavior notes:
250
250
- **Keepalive**: the relay server pings ~every 21s, but Bun's client WebSocket auto-pongs below the JS API (no ping events). A probe timer fires every `RELAY_SILENCE_TIMEOUT_MS / 2` (~15s) and, once at least `RELAY_SILENCE_TIMEOUT_MS` (30s) has elapsed with no inbound message, sends an active `ws.ping()` (so a probe lands 30–45s into a silence, depending on timer phase) — writes to a dead link surface as close/error, which redials with the same token. Reconnects never change the relay URL.
251
251
- **Per-delivery output**: human mode prints `time --> event_type msg_…` then `<-- status method path ms` via `log.ui` (bypasses the stderr throttle). Diagnostics: 401 → `clerkMiddleware` public-route hint; 400 → raw-body/`verifyWebhook()` order hint; 5xx → response body inline plus the exact `clerk webhooks replay <msg_id>` line; unreachable handler → synthetic **502** framed back to the relay.
252
252
- **Verification**: deliveries failing HMAC are warned about and still forwarded (the mismatch means the relay secret diverged, not that the local handler should silently miss events).
253
+
- **At-least-once**: forwarding is at-least-once, like any webhook stream. If the relay socket drops while a delivery is mid-forward, its response frame is sent on the closed socket and dropped, so Svix may redeliver it (and the new inbox URL after a 1008 rotation only appears in the next `ready` line on restart). Local handlers must key on `svix-id` and be idempotent.
253
254
- **Agent/`--json` mode**: NDJSON on stdout. Every line carries a `type` discriminator: one `{ "type": "ready", ... }` line (`relay_url`, `signing_secret`, `endpoint_id`, `events_filter`), then one `{ "type": "event", ... }` line per delivery (`svix_id`, `event_type`, `headers`, `body_b64`, `forward_status`, `latency_ms`). An event line saved to a file is directly consumable by `verify --delivery @file`.
254
255
- **SIGINT**: `listen` replaces the global cleanup-free handler before opening the socket: close socket, drain in-flight forwards, exit 130. The relay endpoint is **never** deleted on exit — its URL and `whsec_` stay stable across restarts. `listen` never exits 0.
0 commit comments