Skip to content

Add signed outbound webhooks - #14

Merged
DeveloperSarim merged 2 commits into
mainfrom
feat/webhooks
Sep 6, 2026
Merged

Add signed outbound webhooks#14
DeveloperSarim merged 2 commits into
mainfrom
feat/webhooks

Conversation

@DeveloperSarim

@DeveloperSarim DeveloperSarim commented Sep 6, 2026

Copy link
Copy Markdown
Owner

Telemetry that cannot leave the box is only useful to someone looking at a dashboard. This pushes every delivery event to systems that can act on it.

Events

message.sent · message.delivered · message.opened · message.clicked · message.bounced

Design notes

  • Signature is Stripe-shaped (t=…,v1=…) with the timestamp inside the signed material, so a captured request cannot be replayed against a receiver that checks the window.
  • Queued, not inline. A tracking pixel has to return in milliseconds regardless of how slow a receiver is, and a receiver that is down must not lose the event. Deliveries go to SQLite and drain separately.
  • Retries five times with backoff (1m, 5m, 25m, 2h, 10h). Every attempt is visible in the admin panel with its response code and error.
  • SSRF guard. Endpoint URLs are resolved before being accepted and refused if they point at a private or loopback address — otherwise any signed-in user could turn the server into a proxy into the internal network.
  • Signing is its own module with no persistence, so the verification path the tests exercise is exactly the one the sender uses, and a receiver can copy the file verbatim.

Verified

npx tsc --noEmit   clean
npm test           12 passed, 0 failed  (6 new)
npm run build      clean, 3 new routes

Summary by CodeRabbit

  • New Features

    • Added outbound webhooks for sent, delivered, opened, clicked, and bounced message events.
    • Added an admin Webhooks panel to create, enable, disable, delete, and test webhook endpoints.
    • Added delivery history, status tracking, automatic retries, and signed webhook requests.
    • Added protections against replayed requests and private or loopback destinations.
  • Documentation

    • Added webhook setup guidance, event details, payload examples, signature verification, retry behavior, and scheduled delivery instructions.
    • Updated the version badge, telemetry feature list, issue suggestions, and contributor presentation.

Telemetry that cannot leave the box is only useful to someone looking at a
dashboard. This pushes every delivery event to systems that can act on it: a
bounce suppresses a contact, a click notifies a channel, a delivery closes the
loop in a warehouse.

- Five events: message.sent, .delivered, .opened, .clicked, .bounced
- Stripe-shaped HMAC signature with the timestamp inside the signed material,
  so a captured request cannot be replayed
- Queued to SQLite rather than fired inline - a slow receiver must never delay
  a tracking pixel, and a receiver that is down must not lose the event
- Five retries with backoff (1m, 5m, 25m, 2h, 10h), every attempt visible in
  the admin panel with its response code
- Endpoint URLs are resolved and refused if they point at a private or
  loopback address, so a signed-in user cannot turn the server into a proxy
  into the internal network
- Signing lives in its own module with no persistence, so the verification
  path the tests exercise is the one the sender uses

Adds six tests covering tamper, wrong secret, replay outside the window, a
forged timestamp reusing a valid mac, and malformed headers.
Copilot AI lite review requested due to automatic review settings September 6, 2026 22:40
@coderabbitai

coderabbitai Bot commented Sep 6, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Currently processing new changes in this PR. This may take a few minutes, please wait...

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Team

Run ID: fd9cbc32-3239-48f6-9ab1-8894bd53091e

📥 Commits

Reviewing files that changed from the base of the PR and between f4a0628 and 6f8474b.

📒 Files selected for processing (15)
  • README.md
  • web/src/app/admin/page.tsx
  • web/src/app/api/send/route.ts
  • web/src/app/api/t/c/[token]/route.ts
  • web/src/app/api/t/o/[token]/route.ts
  • web/src/app/api/telemetry/route.ts
  • web/src/app/api/webhooks/[id]/route.ts
  • web/src/app/api/webhooks/drain/route.ts
  • web/src/app/api/webhooks/route.ts
  • web/src/components/admin/WebhooksPanel.tsx
  • web/src/lib/db.ts
  • web/src/lib/webhook-signature.ts
  • web/src/lib/webhooks.ts
  • web/test/webhook-signature.test.ts
  • web/tsconfig.tsbuildinfo
 _______________________________________________________________________________
< Design to test. Start thinking about testing before you write a line of code. >
 -------------------------------------------------------------------------------
  \
   \   (\__/)
       (•ㅅ•)
       /   づ
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/webhooks

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

There’s a confirmed retry backoff off-by-one bug and the SSRF URL validation needs tightening to reliably reject private/loopback addresses across multi-record DNS.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Adds a signed outbound webhook system to emit RayMail telemetry events to external systems, with an admin UI for endpoint management and a queued delivery mechanism backed by SQLite.

Changes:

  • Introduces a Stripe-shaped signing/verification module plus unit tests for signature correctness and replay protection.
  • Adds webhook endpoint CRUD + delivery queue/drainer with retries, and wires telemetry events (sent/delivered/opened/clicked/bounced) to enqueue deliveries.
  • Adds an Admin → Webhooks panel and documents webhook usage and verification in the README.
File summaries
File Description
web/test/webhook-signature.test.ts Adds tests covering signing, tampering, replay window, and malformed headers.
web/src/lib/webhooks.ts Implements endpoint storage helpers, enqueueing, and async draining with retries + signing.
web/src/lib/webhook-signature.ts Adds HMAC-based signature creation and verification with a tolerance window.
web/src/lib/db.ts Adds SQLite tables/indexes for webhook endpoints and deliveries.
web/src/components/admin/WebhooksPanel.tsx Provides admin UI to create/toggle/delete endpoints and view recent deliveries.
web/src/app/api/webhooks/route.ts Adds webhook endpoint listing/creation API and SSRF-oriented URL validation.
web/src/app/api/webhooks/drain/route.ts Adds an authenticated API to drain pending deliveries (optionally enqueueing a test event).
web/src/app/api/webhooks/[id]/route.ts Adds authenticated API to toggle enabled state and delete an endpoint.
web/src/app/api/telemetry/route.ts Enqueues webhook events for delivered/bounced telemetry updates.
web/src/app/api/t/o/[token]/route.ts Enqueues webhook events for opens (tracking pixel).
web/src/app/api/t/c/[token]/route.ts Enqueues webhook events for clicks (tracked links).
web/src/app/api/send/route.ts Enqueues webhook events when a message is accepted for sending.
web/src/app/admin/page.tsx Adds a “Webhooks” tab to the admin page and mounts the panel.
README.md Documents webhook events, payload format, verification guidance, and scheduling drain.
Review details
  • Files reviewed: 14/15 changed files
  • Comments generated: 3
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +36 to +48
// Payloads carry telemetry about real people; plaintext is a poor default.
if (url.protocol === "http:" && !isPrivateAddress(url.hostname)) {
return "Use https for a public endpoint";
}

try {
const { address } = await lookup(url.hostname);
if (isPrivateAddress(address)) {
return "That host resolves to a private address and cannot be used";
}
} catch {
return "That hostname does not resolve";
}
Comment thread web/src/lib/db.ts
Comment on lines +73 to +76
attempts INTEGER NOT NULL DEFAULT 0,
response_code INTEGER,
last_error TEXT,
next_attempt_at TEXT,
Comment thread web/src/lib/webhooks.ts
).run(attempt, code, error, row.id);
failed++;
} else {
const wait = BACKOFF_SECONDS[attempt] ?? 3600;
The secret was missing from the bind list, so every value shifted one place:
events received the description, description received the timestamp, and
created_at received nothing. Inserting an endpoint failed with a NOT NULL
constraint on events.

Typecheck and the unit tests both passed - neither touches the database - so
this only surfaced against a live instance.
@DeveloperSarim
DeveloperSarim merged commit f698419 into main Sep 6, 2026
1 check passed
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.

2 participants