Skip to content

fix(webhooks): stop self-feeding inbound relay loop - #66

Merged
ralyodio merged 1 commit into
masterfrom
fix/webhook-relay-loop
Jul 28, 2026
Merged

fix(webhooks): stop self-feeding inbound relay loop#66
ralyodio merged 1 commit into
masterfrom
fix/webhook-relay-loop

Conversation

@ralyodio

Copy link
Copy Markdown
Collaborator

The bug

The dashboard's inbound event log for moshcode.sh was full of inbound.inbound.inbound.…event entries, all from Bun/1.3.14.

Not a display bug. moshcode.sh has an active outbound target pointing at its own inbound receiver (https://moshcoding.com/api/webhooks/moshcode.sh), so:

  1. An inbound POST is recorded, then relayed as inbound.<type>.
  2. The relay lands back on the same receiver, which reads type: "inbound.event" out of our own envelope.
  3. It relays inbound.inbound.event — and so on, forever.

The chain resets to bare event at 10 hops because the name exceeds the 80-char limit in normalizeInboundEventType, which returns null. The Bun/1.3.14 user-agent was the tell: fireDomainEvent was the only sender not setting a UA, so the server was POSTing to itself.

All 200 stored events for that domain (the per-domain cap) are loop junk.

The fix

Three independent layers, so no single topology reproduces it:

  • relayEventType() — applies the inbound. prefix at most once, so prefix chains can't grow regardless of routing.
  • isSelfWebhookUrl() (lib/url-guard.ts) — targets aimed at our own /api/webhooks/* are skipped in fireDomainEvent, dead-lettered in deliverToEndpoint, and refused with an explanatory message when added via the dashboard.
  • x-moshcoding-hop counter, capped at MAX_RELAY_HOPS = 3 — bounds the same loop for hosts we can't statically recognize as ours (e.g. a custom domain proxied to this app).

fireDomainEvent now also sets a user-agent, so relayed events are identifiable in the log.

Trade-off

The self-target guard also blocks chaining domain A's outbound to domain B's receiver on moshcoding. That's a plausible-but-unused setup; the hop cap alone would have permitted it. Easy to relax if we ever want it.

Testing

bun test tests/ → 25 pass / 0 fail (was 22). bunx tsc --noEmit clean.

New coverage: prefix applied once across 12 simulated hops, hop-counter parsing/clamping, and self-URL detection (including the notmoshcoding.com near-miss).

Follow-up, not in this PR

  • The stale self-referential domain_webhooks row for moshcode.sh is inert once this ships (deliveries to it are skipped), but is worth deleting.
  • The 200 junk event rows age out under the existing per-domain cap.
  • .github/workflows/test.yml skips when package.json has no test script — which is the case here, so these tests don't run in CI.

🤖 Generated with Claude Code

A domain whose outbound target points back at its own inbound receiver
fed itself forever: the receiver recorded each event, relayed it as
`inbound.<type>`, and the relay re-entered the receiver, which read
`inbound.<type>` off our own envelope and relayed `inbound.inbound.<type>`.
The chain reset to bare `event` at 10 hops (past the 80-char event-type
limit) and started over. moshcode.sh hit this and filled its whole
200-row event log with junk.

Three layers, so no single topology can reproduce it:

- relayEventType() applies the `inbound.` prefix at most once.
- isSelfWebhookUrl() rejects targets aimed at our own /api/webhooks/*:
  skipped in fireDomainEvent, dead-lettered in deliverToEndpoint, and
  refused with an explanatory message when adding a domain target.
- An x-moshcoding-hop counter caps relays at 3 hops, which bounds the
  same loop for hosts we can't recognize as ours (a custom domain
  proxied to this app).

fireDomainEvent now also sends a user-agent, so a relayed event is
identifiable in the inbound log instead of showing up as bare `Bun/x.y`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@ralyodio
ralyodio merged commit a4e071b into master Jul 28, 2026
2 checks passed
@ralyodio
ralyodio deleted the fix/webhook-relay-loop branch July 28, 2026 20:31
@github-actions

Copy link
Copy Markdown

vu1nz Security Review

0 finding(s) in PR #?

No security issues found.

ralyodio added a commit that referenced this pull request Jul 28, 2026
First tagged release. Headline change: the inbound webhook relay loop
fix (#66).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant