Skip to content

Add optional Resend delivery event logging - #198

Merged
aspiers merged 16 commits into
mainfrom
feat/resend-delivery-webhooks
Jul 20, 2026
Merged

Add optional Resend delivery event logging#198
aspiers merged 16 commits into
mainfrom
feat/resend-delivery-webhooks

Conversation

@aspiers

@aspiers aspiers commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Summary

Add an explicitly optional Resend webhook integration for operators who already use Resend and want delivery diagnostics. ePDS remains compatible with any SMTP provider; when enabled, the signed receiver emits structured logs and does not persist webhook data.

Addresses #185.

Changes

  • Add opt-in POST /webhooks/resend, enabled only when RESEND_WEBHOOK_SECRET is set.
  • Verify the raw request body using Svix before inspecting or logging the payload.
  • Normalize logs as provider, eventId, eventType, occurredAt, messageId, recipients, and subject; delayed-delivery events use warning level.
  • Preserve retry and event timestamps so external log analysis can deduplicate, order events, and calculate delivery latency.
  • Filter account-wide Resend events to the exact configured SMTP_FROM, acknowledging other senders without logging their payloads.
  • Apply a dedicated webhook rate limit without affecting the global request limiter.
  • Document clearly that Resend is not required and that no webhook data is stored by ePDS.

Testing

  • pnpm format:check
  • pnpm lint
  • pnpm typecheck
  • pnpm build
  • pnpm test — 1,054 tests passed
  • pnpm test:coverage — 58.02% statements, 57.48% branches, 71.08% functions, 56.75% lines

Notes

A live production SMTP-to-webhook test was not performed because it requires the production Resend dashboard, signing secret, and SMTP credentials. Resend does not explicitly document whether SMTP-submitted messages emit these webhooks, so Resend users must complete the documented test before relying on the logs.

Retries intentionally produce repeated log entries carrying the same provider-neutral eventId; downstream log analysis should deduplicate on that field. ePDS instances sharing a Resend account must use distinct SMTP_FROM addresses because events sent from the same address cannot be separated.

Summary by CodeRabbit

  • New Features
    • Added optional Resend delivery event logging for sent, delivered, delayed, bounced, and failed emails.
    • Added signed Resend webhook verification with sender filtering, structured logging, and dedicated rate limiting.
  • Bug Fixes
    • Isolated rate limits between different request categories.
  • Documentation
    • Updated environment examples and configuration docs with step-by-step enablement and logging/monitoring guidance.
  • Tests
    • Added comprehensive tests for webhook validation, signature handling, filtering, logging, and rate-limit isolation.

aspiers and others added 3 commits July 15, 2026 10:10
Store Resend email lifecycle events idempotently by Svix ID and expose delivery latency and outcome metrics. Share the ten-minute OTP lifetime between Better Auth and the latency threshold so the two cannot drift.

Refs #185

Co-authored-by: OpenAI Codex <noreply@openai.com>
Add a raw-body webhook endpoint that verifies Svix signatures before validating and persisting supported Resend email events. Retries are acknowledged idempotently and delayed-delivery events are surfaced in logs.

Refs #185

Co-authored-by: OpenAI Codex <noreply@openai.com>
Document the webhook URL, event subscriptions, signing secret, metrics output, and the required live SMTP verification. Propagate RESEND_WEBHOOK_SECRET into generated auth-service environments and add release notes.

Refs #185

Co-authored-by: OpenAI Codex <noreply@openai.com>
Copilot AI review requested due to automatic review settings July 15, 2026 09:11
@changeset-bot

changeset-bot Bot commented Jul 15, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: d275b33

The changes in this PR will be included in the next version bump.

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel

vercel Bot commented Jul 15, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
epds-demo Ready Ready Preview, Comment Jul 20, 2026 1:52pm

Request Review

@coderabbitai

coderabbitai Bot commented Jul 15, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@aspiers, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 36 seconds

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 66a0a243-1732-4344-af85-f7163ddb3599

📥 Commits

Reviewing files that changed from the base of the PR and between 6bfa3db and d275b33.

📒 Files selected for processing (7)
  • .changeset/resend-delivery-webhooks.md
  • .env.example
  • docs/configuration.md
  • packages/auth-service/.env.example
  • packages/auth-service/src/__tests__/resend-webhook.test.ts
  • packages/auth-service/src/routes/resend-webhook.ts
  • scripts/setup.sh
📝 Walkthrough

Walkthrough

Adds an optional /webhooks/resend endpoint that verifies Svix signatures, validates and filters delivery events, emits structured logs, applies isolated rate limiting, and is covered by HTTP integration tests and operator documentation.

Changes

Resend delivery webhooks

Layer / File(s) Summary
Runtime configuration and route wiring
packages/auth-service/package.json, packages/auth-service/src/context.ts, packages/auth-service/src/index.ts, .env.example, packages/auth-service/.env.example, scripts/setup.sh
Adds RESEND_WEBHOOK_SECRET, the Svix dependency, conditional route mounting, and shared environment propagation.
Signed webhook validation and logging
packages/auth-service/src/routes/resend-webhook.ts
Verifies raw request signatures, validates supported event payloads, filters senders, logs delivery events, and returns success or HTTP 400 responses.
Webhook rate-limit namespace
packages/auth-service/src/middleware/rate-limit.ts, packages/auth-service/src/__tests__/rate-limit.test.ts
Adds prefixed rate-limit buckets and tests separation between global and webhook counters.
HTTP receiver integration coverage
packages/auth-service/src/__tests__/resend-webhook.test.ts
Tests signed requests, logging behavior, sender filtering, repeated identifiers, delayed events, invalid signatures, missing headers, and unsupported payloads.
Operator setup and coverage documentation
docs/configuration.md, docs/design/testing-gaps.md, .changeset/resend-delivery-webhooks.md, vitest.config.ts, packages/shared/src/__tests__/db-extended.test.ts
Documents configuration and webhook operations, updates route coverage notes, records the release change, raises coverage thresholds, and tightens filesystem cleanup assertions.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Resend
  participant AuthService
  participant Svix
  participant Logger
  Resend->>AuthService: POST /webhooks/resend
  AuthService->>Svix: Verify signed raw payload
  Svix-->>AuthService: Return validated event
  AuthService->>Logger: Log matching delivery event
  AuthService-->>Resend: Return acknowledgement
Loading

Possibly related issues

Possibly related PRs

  • hypercerts-org/ePDS#14 — Adds the OTP configuration used by the webhook logger to redact OTP-like subject content.

Suggested reviewers: copilot

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 31.25% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately summarizes the main change: optional Resend delivery event logging.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/resend-delivery-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.

@coveralls-official

coveralls-official Bot commented Jul 15, 2026

Copy link
Copy Markdown

Coverage Report for CI Build 29748067755

Warning

Build has drifted: This PR's base is out of sync with its target branch, so coverage data may include unrelated changes.
Quick fix: rebase this PR. Learn more →

Coverage increased (+0.8%) to 57.186%

Details

  • Coverage increased (+0.8%) from the base build.
  • Patch coverage: 9 uncovered changes across 2 files (70 of 79 lines covered, 88.61%).
  • 5 coverage regressions across 2 files.

Uncovered Changes

File Changed Covered %
packages/auth-service/src/routes/resend-webhook.ts 72 67 93.06%
packages/auth-service/src/index.ts 4 0 0.0%
Total (3 files) 79 70 88.61%

Coverage Regressions

5 previously-covered lines in 2 files lost coverage.

File Lines Losing Coverage Coverage
packages/auth-service/src/email/sender.ts 3 64.2%
packages/auth-service/src/better-auth.ts 2 22.31%

Coverage Stats

Coverage Status
Relevant Lines: 3060
Covered Lines: 1744
Line Coverage: 56.99%
Relevant Branches: 1908
Covered Branches: 1097
Branch Coverage: 57.49%
Branches in Coverage %: Yes
Coverage Strength: 5.99 hits per line

💛 - Coveralls

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.

Pull request overview

Adds first-class Resend delivery webhook ingestion to the auth-service so operators can measure OTP email delivery latency (sent→delivered) and distinguish SMTP handoff from downstream delivery delays. This extends the shared SQLite DB with persistent, idempotent event storage and exposes aggregated delivery/latency stats via the existing authenticated /metrics endpoint, with documentation and tests.

Changes:

  • Introduce a signed POST /webhooks/resend receiver (Svix verification over raw body) and persist supported lifecycle events idempotently in SQLite.
  • Aggregate delivery outcomes and latency statistics (including “over OTP lifetime” fraction) into the existing /metrics response as resendDelivery.
  • Add/ratchet tests, coverage thresholds, and operator documentation + env propagation for RESEND_WEBHOOK_SECRET.

Reviewed changes

Copilot reviewed 18 out of 19 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
vitest.config.ts Ratchets coverage thresholds to the new floor.
scripts/setup.sh Propagates RESEND_WEBHOOK_SECRET into per-package .env files when applicable.
pnpm-lock.yaml Locks new svix dependency (and transitive deps).
packages/shared/src/types.ts Adds shared OTP_LIFETIME_SECONDS constant to avoid drift.
packages/shared/src/index.ts Re-exports new shared constant and Resend-related DB/metrics types.
packages/shared/src/db.ts Adds resend_email_event table/migration, persistence helpers, and resendDelivery metrics aggregation.
packages/shared/src/tests/db-extended.test.ts Adds DB-level tests for event ordering/deduplication and delivery metrics aggregation.
packages/auth-service/src/routes/resend-webhook.ts New webhook route: raw-body ingestion, Svix signature verification, payload validation, DB persistence, and logging.
packages/auth-service/src/lib/auth-flow.ts Updates comment to reference shared OTP lifetime constant.
packages/auth-service/src/index.ts Mounts webhook router before body parsing; wires RESEND_WEBHOOK_SECRET into config.
packages/auth-service/src/context.ts Extends config with optional resendWebhookSecret.
packages/auth-service/src/better-auth.ts Uses shared OTP_LIFETIME_SECONDS for Better Auth OTP expiry.
packages/auth-service/src/tests/resend-webhook.test.ts Adds HTTP-level integration tests for signature verification, idempotency, and payload filtering.
packages/auth-service/package.json Adds svix dependency.
packages/auth-service/.env.example Documents RESEND_WEBHOOK_SECRET usage for the auth service.
docs/design/testing-gaps.md Updates coverage notes to reflect new route-level test coverage.
docs/configuration.md Documents webhook setup steps and resulting /metrics fields.
.env.example Documents RESEND_WEBHOOK_SECRET at the repo level.
.changeset/resend-delivery-webhooks.md Adds operator-facing changeset for the new delivery metrics feature.
Files not reviewed (1)
  • pnpm-lock.yaml: Generated file

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/shared/src/db.ts Outdated

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 3

🧹 Nitpick comments (1)
packages/shared/src/__tests__/db-extended.test.ts (1)

283-303: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Import ResendEmailEventType instead of re-declaring the union inline.

The eventType parameter re-lists the five literal event types instead of importing ResendEmailEventType from ../db.js. If a new Resend event type is added later, this local copy can silently drift out of sync with the source of truth.

♻️ Suggested fix
+import type { ResendEmailEventType } from '../db.js'
...
   function record(
     svixId: string,
     emailId: string,
-    eventType:
-      | 'email.sent'
-      | 'email.delivered'
-      | 'email.delivery_delayed'
-      | 'email.bounced'
-      | 'email.failed',
+    eventType: ResendEmailEventType,
     eventCreatedAt: number,
   ): boolean {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/shared/src/__tests__/db-extended.test.ts` around lines 283 - 303,
Update the test helper function record to import and use ResendEmailEventType
from ../db.js for its eventType parameter, removing the duplicated inline
literal union and keeping the existing event values and behavior unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/auth-service/src/index.ts`:
- Around line 51-56: In the webhook setup block before
createResendWebhookRouter, mount a dedicated rate limiter for /webhooks/resend
before the router handles requests. Configure it to allow legitimate Resend
retry bursts while limiting repeated raw-body parsing, signature verification,
and warning logs, without changing the global limiter or webhook verification
flow.

In `@packages/auth-service/src/routes/resend-webhook.ts`:
- Around line 125-127: Remove recipients, sender, and subject from the persisted
record constructed in the resend webhook route, retaining only fields required
by delivery metrics and latency queries such as email_id, event type, and
timestamps.

In `@packages/shared/src/db.ts`:
- Around line 224-243: The resend_email_event migration creates indefinitely
retained recipient data without cleanup. Add a TTL purge for resend_email_event,
using received_at or the established retention policy, and invoke it from the
existing database maintenance path near deleteAccountData or related cleanup
methods; preserve the table and indexes while ensuring expired rows are
periodically deleted.

---

Nitpick comments:
In `@packages/shared/src/__tests__/db-extended.test.ts`:
- Around line 283-303: Update the test helper function record to import and use
ResendEmailEventType from ../db.js for its eventType parameter, removing the
duplicated inline literal union and keeping the existing event values and
behavior unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c1e829c3-1ae1-432c-a951-0eb5563eabcc

📥 Commits

Reviewing files that changed from the base of the PR and between 78d9769 and 2bf9af4.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (18)
  • .changeset/resend-delivery-webhooks.md
  • .env.example
  • docs/configuration.md
  • docs/design/testing-gaps.md
  • packages/auth-service/.env.example
  • packages/auth-service/package.json
  • packages/auth-service/src/__tests__/resend-webhook.test.ts
  • packages/auth-service/src/better-auth.ts
  • packages/auth-service/src/context.ts
  • packages/auth-service/src/index.ts
  • packages/auth-service/src/lib/auth-flow.ts
  • packages/auth-service/src/routes/resend-webhook.ts
  • packages/shared/src/__tests__/db-extended.test.ts
  • packages/shared/src/db.ts
  • packages/shared/src/index.ts
  • packages/shared/src/types.ts
  • scripts/setup.sh
  • vitest.config.ts

Comment thread packages/auth-service/src/index.ts
Comment thread packages/auth-service/src/routes/resend-webhook.ts Outdated
Comment thread packages/shared/src/db.ts Outdated
@railway-app

railway-app Bot commented Jul 15, 2026

Copy link
Copy Markdown

🚅 Deployed to the ePDS-pr-198 environment in ePDS

Service Status Web Updated (UTC)
@certified-app/auth-service ✅ Success (View Logs) Web Jul 20, 2026 at 1:09 pm
@certified-app/pds-core ✅ Success (View Logs) Web Jul 15, 2026 at 10:19 am
@certified-app/demo untrusted ✅ Success (View Logs) Web Jul 15, 2026 at 9:13 am
@certified-app/demo ✅ Success (View Logs) Web Jul 15, 2026 at 9:13 am

Clarify that ePDS supports any SMTP provider and expose the Resend route and metrics only after an operator opts in with a webhook secret. Bound the optional integration with dedicated rate limiting, 30-day retention, and reduced persisted payload data.

Refs #185

Co-authored-by: OpenAI Codex <noreply@openai.com>
Copilot AI review requested due to automatic review settings July 15, 2026 10:07
@railway-app
railway-app Bot temporarily deployed to ePDS / ePDS-pr-198 July 15, 2026 10:07 Destroyed

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.

Pull request overview

Copilot reviewed 20 out of 21 changed files in this pull request and generated 1 comment.

Files not reviewed (1)
  • pnpm-lock.yaml: Generated file
Comments suppressed due to low confidence (1)

packages/auth-service/src/index.ts:70

  • requestRateLimit() keys off req.ip, but trust proxy is configured after the Resend webhook rate limiter is mounted. In deployments behind a reverse proxy, this will rate-limit all webhook calls under the proxy’s IP (or otherwise ignore X-Forwarded-For), potentially throttling legitimate delivery events.
  // Middleware
  app.set('trust proxy', 1)
  app.use(express.urlencoded({ extended: true }))
  app.use(express.json())
  app.use(cookieParser())

Comment thread packages/shared/src/db.ts Outdated
Remove the webhook database schema and in-process delivery metrics. The optional signed receiver now emits structured event logs with Svix IDs so external log analysis can deduplicate retries, order events, and calculate latency without ePDS retaining webhook data.

Refs #185

Co-authored-by: OpenAI Codex <noreply@openai.com>
Copilot AI review requested due to automatic review settings July 15, 2026 10:14
@railway-app
railway-app Bot temporarily deployed to ePDS / ePDS-pr-198 July 15, 2026 10:14 Destroyed
@aspiers aspiers changed the title Capture Resend delivery events for OTP latency metrics Add optional Resend delivery event logging Jul 15, 2026
@aspiers

aspiers commented Jul 15, 2026

Copy link
Copy Markdown
Contributor Author

(comment generated by OpenAI Codex)

Follow-up 8048eb0 changes the design to logging-only: the SQLite event schema, database methods, retention job, and in-process delivery metrics were removed. It also makes the Resend integration explicitly opt-in throughout the examples, documentation, changeset, title, and PR description. The CodeRabbit test-type duplication nitpick became obsolete when the database event test helper was removed.

Ignore only missing temporary database directories during teardown instead of swallowing every filesystem error.

Co-authored-by: OpenAI Codex <noreply@openai.com>
@railway-app
railway-app Bot temporarily deployed to ePDS / ePDS-pr-198 July 15, 2026 10:18 Destroyed
@aspiers
aspiers marked this pull request as ready for review July 16, 2026 19:55
Copilot AI review requested due to automatic review settings July 16, 2026 19:55

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@aspiers

aspiers commented Jul 16, 2026

Copy link
Copy Markdown
Contributor Author

Given that our main Resend team only delivers to staging and production I guess we can tolerate this with suitable filters on both.

Resend models recipients as an array, which made Railway display the single ePDS recipient as recipients[0]. Webhook subjects also include the sign-in code and would expose it in logs.\n\nNormalize the recipient to the existing email field and redact only OTP tokens matching the configured length and charset while preserving the rest of the subject.\n\nFollow-up to #185.\n\nCo-authored-by: OpenAI <noreply@openai.com>
Copilot AI review requested due to automatic review settings July 20, 2026 12:52
@railway-app
railway-app Bot temporarily deployed to ePDS / ePDS-pr-198 July 20, 2026 12:52 Destroyed

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

aspiers added 2 commits July 20, 2026 13:58
Use concise digit character classes so SonarCloud accepts the OTP subject sanitizer added for #185.\n\nCo-authored-by: OpenAI <noreply@openai.com>
SonarCloud reports intentional localhost, Docker, and Railway-private HTTP URLs as clear-text protocol vulnerabilities. Mark those exact lines with justified NOSONAR annotations because the traffic never crosses the public network.\n\nCo-authored-by: OpenAI <noreply@openai.com>
Copilot AI review requested due to automatic review settings July 20, 2026 12:58
@railway-app
railway-app Bot temporarily deployed to ePDS / ePDS-pr-198 July 20, 2026 12:58 Destroyed

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@coderabbitai coderabbitai Bot 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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
packages/auth-service/src/__tests__/resend-webhook.test.ts (1)

50-104: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Use supertest for testing route handlers.

The manual spinning up of an ephemeral Express server with app.listen(0) and making a raw fetch request violates the coding guideline to cover route handlers via supertest integration tests. Using supertest removes the need for manual server lifecycle management and port allocation boilerplate.

Please remember to add import request from 'supertest' at the top of the file.

🛠️ Proposed refactor to use supertest
-  const app = express()
-  app.use(
-    createResendWebhookRouter(
-      WEBHOOK_SECRET,
-      'login@example.org',
-      options.otpLength ?? 6,
-      options.otpCharset ?? 'numeric',
-    ),
-  )
-  const server = app.listen(0)
-
-  try {
-    server.unref()
-    const port = await new Promise<number>((resolve, reject) => {
-      server.once('error', reject)
-      server.once('listening', () => {
-        const address = server.address()
-        if (typeof address === 'object' && address) resolve(address.port)
-        else reject(new Error('Failed to resolve ephemeral port'))
-      })
-    })
-
-    const payload = JSON.stringify(event)
-    const svixId = options.svixId ?? 'msg_test_123'
-    const timestamp = new Date()
-    const signature =
-      options.validSignature === false
-        ? 'v1,invalid'
-        : new Webhook(WEBHOOK_SECRET).sign(svixId, timestamp, payload)
-
-    const headers: Record<string, string> = {
-      'Content-Type': 'application/json',
-    }
-    if (options.includeHeaders !== false) {
-      headers['svix-id'] = svixId
-      headers['svix-timestamp'] = String(Math.floor(timestamp.getTime() / 1000))
-      headers['svix-signature'] = signature
-    }
-
-    const response = await fetch(`http://127.0.0.1:${port}/webhooks/resend`, {
-      method: 'POST',
-      headers,
-      body: payload,
-    })
-    return {
-      status: response.status,
-      json: (await response.json()) as Record<string, unknown>,
-    }
-  } finally {
-    await new Promise<void>((resolve) => {
-      server.close(() => {
-        resolve()
-      })
-    })
-  }
+  const app = express()
+  app.use(
+    createResendWebhookRouter(
+      WEBHOOK_SECRET,
+      'login@example.org',
+      options.otpLength ?? 6,
+      options.otpCharset ?? 'numeric',
+    ),
+  )
+
+  const payload = JSON.stringify(event)
+  const svixId = options.svixId ?? 'msg_test_123'
+  const timestamp = new Date()
+  const signature =
+    options.validSignature === false
+      ? 'v1,invalid'
+      : new Webhook(WEBHOOK_SECRET).sign(svixId, timestamp, payload)
+
+  const req = request(app)
+    .post('/webhooks/resend')
+    .set('Content-Type', 'application/json')
+
+  if (options.includeHeaders !== false) {
+    req.set('svix-id', svixId)
+    req.set('svix-timestamp', String(Math.floor(timestamp.getTime() / 1000)))
+    req.set('svix-signature', signature)
+  }
+
+  const response = await req.send(payload)
+
+  return {
+    status: response.status,
+    json: response.body as Record<string, unknown>,
+  }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/auth-service/src/__tests__/resend-webhook.test.ts` around lines 50 -
104, Refactor the test helper around createResendWebhookRouter to use
supertest(request) against the Express app instead of app.listen, ephemeral port
resolution, fetch, and manual server cleanup. Preserve the existing payload,
headers, signature, and returned status/json behavior, and add the supertest
import at the top of the test file.

Source: Coding guidelines

packages/auth-service/src/routes/resend-webhook.ts (1)

119-126: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Missing-header / non-buffer branch swallows the failure silently.

Unlike the signature-invalid and payload-invalid branches below (which both call logger.warn before returning), this branch returns { ok: false, error: 'Invalid webhook request' } with no logging at all. As per coding guidelines, "Never swallow errors silently; log at minimum debug level" — a misconfigured proxy stripping Svix headers, or a body-parser mismatch, would go completely unobserved here.

🔧 Proposed fix
   const headers = getSvixHeaders(req)
   if (!headers || !Buffer.isBuffer(req.body)) {
+    logger.warn(
+      { hasHeaders: !!headers, isBuffer: Buffer.isBuffer(req.body) },
+      'Rejected email webhook with missing headers or body',
+    )
     return { ok: false, error: 'Invalid webhook request' }
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/auth-service/src/routes/resend-webhook.ts` around lines 119 - 126,
Add a warning log in the missing-header/non-buffer branch of verifyResendWebhook
before returning the existing invalid-request result, matching the logging
behavior of the signature-invalid and payload-invalid branches. Include enough
context to distinguish missing Svix headers from an invalid request body.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@packages/auth-service/src/__tests__/resend-webhook.test.ts`:
- Around line 50-104: Refactor the test helper around createResendWebhookRouter
to use supertest(request) against the Express app instead of app.listen,
ephemeral port resolution, fetch, and manual server cleanup. Preserve the
existing payload, headers, signature, and returned status/json behavior, and add
the supertest import at the top of the test file.

In `@packages/auth-service/src/routes/resend-webhook.ts`:
- Around line 119-126: Add a warning log in the missing-header/non-buffer branch
of verifyResendWebhook before returning the existing invalid-request result,
matching the logging behavior of the signature-invalid and payload-invalid
branches. Include enough context to distinguish missing Svix headers from an
invalid request body.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1c08af21-1745-48d3-ad83-2a47c4a4227c

📥 Commits

Reviewing files that changed from the base of the PR and between fc369fe and 6bfa3db.

📒 Files selected for processing (4)
  • docs/configuration.md
  • packages/auth-service/src/__tests__/resend-webhook.test.ts
  • packages/auth-service/src/index.ts
  • packages/auth-service/src/routes/resend-webhook.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/auth-service/src/index.ts
  • docs/configuration.md

@blacksmith-sh

This comment has been minimized.

Operators using Resend open tracking could see delivery outcomes but not whether the message's tracking pixel was fetched.\n\nAccept email.opened webhooks, normalize them to opened, and document the required tracking opt-in and reliability caveats. The event uses the existing sender filter and sanitized provider-neutral log fields.\n\nFollow-up to #185.\n\nCo-authored-by: OpenAI <noreply@openai.com>
Copilot AI review requested due to automatic review settings July 20, 2026 13:51
@railway-app
railway-app Bot temporarily deployed to ePDS / ePDS-pr-198 July 20, 2026 13:51 Destroyed

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@sonarqubecloud

Copy link
Copy Markdown

@aspiers
aspiers merged commit fd558aa into main Jul 20, 2026
23 checks passed
@aspiers
aspiers deleted the feat/resend-delivery-webhooks branch July 20, 2026 13:57
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