Skip to content

Domain Connect#3877

Open
pepeladeira wants to merge 11 commits into
mainfrom
domain-connect-auto-configure
Open

Domain Connect#3877
pepeladeira wants to merge 11 commits into
mainfrom
domain-connect-auto-configure

Conversation

@pepeladeira

@pepeladeira pepeladeira commented May 8, 2026

Copy link
Copy Markdown
Collaborator

Summary by CodeRabbit

  • New Features

    • Domain Connect one‑click auto‑configuration with discovery and signed apply links.
    • Forward DNS instructions: send DNS records via email and a client modal/hook to collect recipient.
    • UI: “Auto configure” and “Forward instructions” actions plus callback handling with success/error toasts.
  • Documentation

    • Example environment file documents Domain Connect keys, key‑generation guidance, and Veriff secret.
  • Style

    • Added Vercel and Cloudflare icons for clearer provider identification.

Review Change Stack

@coderabbitai

coderabbitai Bot commented May 8, 2026

Copy link
Copy Markdown
Contributor

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds Domain Connect: TXT discovery and provider mapping, RSA-signed apply URL generation, two workspace-scoped API endpoints (apply and forward-instructions), UI flows and modals for auto-config/forwarding, provider icons, email template, and callback handling.

Changes

Domain Connect Auto-Configuration

Layer / File(s) Summary
Type Definitions & Constants
apps/web/lib/domain-connect/types.ts, apps/web/lib/domain-connect/constants.ts
New DomainConnectProviderKind union and DomainConnectDiscovery type; constants for Dub DNS values and provider/service IDs.
Domain Connect Discovery
apps/web/lib/domain-connect/discover.ts
Resolves _domainconnect TXT, derives settings host, fetches /v2/{apex}/settings, validates urlSyncUX and providerId, maps providerId to vercel/cloudflare; discoverDomainConnectIfEligible gates discovery on env vars and domain status.
RSA Signing for Apply URLs
apps/web/lib/domain-connect/sign-apply-url.ts
buildSigningString canonicalizes and percent-encodes parameters; buildSignedApplyUrl constructs provider apply URL and signs canonical string with RSA-SHA256, appending key and sig.
Environment Configuration
apps/web/.env.example
Documents DOMAIN_CONNECT_PRIVATE_KEY and DOMAIN_CONNECT_KEY_HOST and adds generation instructions; adds VERIFF_SHARED_SECRET.
Verify Route Integration
apps/web/app/api/domains/[domain]/verify/route.ts
Verification GET computes apex and calls discoverDomainConnectIfEligible; includes domainConnect field (null or discovery) in JSON responses across branches.
Domain Connect Apply Endpoint
apps/web/app/api/domains/[domain]/domain-connect/apply/route.ts
POST handler validates recordType, loads domain/config, discovers Domain Connect, selects apex vs subdomain serviceId, composes query params (domain, verification, host), signs apply URL via buildSignedApplyUrl, and returns { applyUrl }.
Forward DNS Instructions Endpoint
apps/web/app/api/domains/[domain]/forward-instructions/route.ts
POST handler validates { email, recordType }, builds DNS records (A/CNAME and optional TXT), sends DomainDnsInstructions email via sendEmail, and returns { ok: true }.
Email Template
packages/email/src/templates/domain-dns-instructions.tsx
React Email template rendering DNS record table, explanatory text with sender and domain context, propagation timeline, and footer.
UI: Domain Card & Configuration Wiring
apps/web/ui/domains/domain-card.tsx, apps/web/ui/domains/domain-configuration.tsx
Domain verification response typing extended with optional domainConnect; DomainConfiguration converted to client component, accepts domain-connect/workspace context, adds handleAutoConfigure to POST to apply endpoint and redirect on applyUrl, and renders provider-specific "Auto configure" and "Forward instructions" actions.
Forward DNS Instructions Modal
apps/web/ui/modals/forward-dns-instructions-modal.tsx
Client modal + useForwardDnsInstructionsModal hook to collect email, POST to forward-instructions API, show toasts, and reset/close on success.
Callback Result Handling
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/domains/page-client.tsx
Effect watches domain_connect=callback, shows success/error toast (handles user_cancel), and clears callback/error parameters from the URL.
Partner Configuration Integration
apps/web/ui/partners/program-link-configuration.tsx
Partner onboarding types verification data with optional domainConnect and passes domain-connect/workspace/domain context into DomainConfiguration.
Brand Icons
packages/ui/src/icons/vercel.tsx, packages/ui/src/icons/cloudflare.tsx, packages/ui/src/icons/index.tsx
Adds Vercel and Cloudflare 16×16 icon components and updates barrel exports.

Sequence Diagram

sequenceDiagram
  participant User as User
  participant UI as DomainConfiguration UI
  participant ApplyAPI as POST /domain-connect/apply
  participant Discover as discoverDomainConnectIfEligible
  participant Signer as buildSignedApplyUrl
  participant Provider as Provider (Vercel/Cloudflare)
  participant Callback as Callback Handler

  User->>UI: Click "Auto configure"
  UI->>ApplyAPI: POST { recordType, returnTo? }
  ApplyAPI->>Discover: discoverDomainConnectIfEligible(apex, status)
  Discover-->>ApplyAPI: DomainConnectDiscovery (urlSyncUX, providerKind)
  ApplyAPI->>Signer: buildSignedApplyUrl(...)
  Signer-->>ApplyAPI: { applyUrl }
  ApplyAPI-->>UI: { applyUrl }
  UI->>Provider: Redirect browser to signed applyUrl
  Provider->>Callback: Redirect back with domain_connect=callback
  Callback->>UI: Show success/error toast and clear params
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • dubinc/dub#3871: Modifies domain onboarding and configuration UI in related components.
  • dubinc/dub#3614: Adds VERIFF_SHARED_SECRET to env example (overlapping env change).

Suggested reviewers

  • steven-tey

🐰 "A rabbit's verse on Domain Connect:"

TXT clues whisper where settings hide,
I sign the link and open wide,
Vercel, Cloudflare, icons gleam,
Click auto-config — the records stream,
A hop, a toast — your DNS aligned.

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 26.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title "Domain Connect" is vague and does not clearly convey the main objective of the changeset (adding Domain Connect auto-configuration functionality). Consider using a more descriptive title such as "Add Domain Connect auto-configuration support" or "Implement Domain Connect discovery and apply flows" to better reflect the comprehensive feature addition.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch domain-connect-auto-configure

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 and usage tips.

@vercel

vercel Bot commented May 8, 2026

Copy link
Copy Markdown
Contributor

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

Project Deployment Actions Updated (UTC)
dub Ready Ready Preview May 19, 2026 9:35pm

Request Review

@pepeladeira pepeladeira marked this pull request as ready for review May 11, 2026 21:53

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (4)
apps/web/app/api/domains/[domain]/forward-instructions/route.ts (1)

34-37: ⚡ Quick win

Remove the unused getConfigResponse call.

Line 36 performs an external call whose result is ignored, adding latency with no effect.

♻️ Proposed cleanup
-    const [domainJson] = await Promise.all([
-      getDomainResponse(domain),
-      getConfigResponse(domain),
-    ]);
+    const domainJson = await getDomainResponse(domain);
🤖 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 `@apps/web/app/api/domains/`[domain]/forward-instructions/route.ts around lines
34 - 37, The Promise.all currently calls getConfigResponse(domain) but only
destructures domainJson, so remove the unused getConfigResponse call to avoid
extra latency: replace the const [domainJson] = await Promise.all([
getDomainResponse(domain), getConfigResponse(domain), ]) pattern with a single
await for getDomainResponse (e.g., const domainJson = await
getDomainResponse(domain)); also remove any now-unused import or reference to
getConfigResponse to keep the module clean.
apps/web/ui/modals/forward-dns-instructions-modal.tsx (2)

122-128: ⚡ Quick win

Remove setShowModal from useMemo dependencies.

setShowModal is a React state setter and is guaranteed to be stable across renders. Including it in the dependency array is unnecessary.

♻️ Proposed fix
  return useMemo(
    () => ({
      setShowForwardDnsModal: setShowModal,
      ForwardDnsInstructionsModal: Modal,
    }),
-   [setShowModal, Modal],
+   [Modal],
  );
🤖 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 `@apps/web/ui/modals/forward-dns-instructions-modal.tsx` around lines 122 -
128, The useMemo call returning { setShowForwardDnsModal: setShowModal,
ForwardDnsInstructionsModal: Modal } incorrectly includes the stable React state
setter setShowModal in its dependency array; remove setShowModal from the
dependencies so the array only depends on Modal (i.e., change useMemo(...,
[Modal])) to avoid unnecessary recomputation while keeping
ForwardDnsInstructionsModal and setShowForwardDnsModal correct.

111-120: ⚡ Quick win

Simplify useCallback dependency array.

The dependency array destructures individual props instead of depending on the entire props object. If props has unstable object identity (recreated on each render), this pattern won't prevent unnecessary re-creation of the Modal component. Consider depending on props directly or ensuring props is memoized at the call site.

♻️ Proposed fix
  const Modal = useCallback(
    () => (
      <ForwardDnsInstructionsModal
        showModal={showModal}
        setShowModal={setShowModal}
        {...props}
      />
    ),
-   [showModal, props.domain, props.recordType, props.workspaceId],
+   [showModal, props],
  );
🤖 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 `@apps/web/ui/modals/forward-dns-instructions-modal.tsx` around lines 111 -
120, The useCallback for Modal currently lists individual props (props.domain,
props.recordType, props.workspaceId) which defeats the purpose if the props
object identity is unstable; update the dependency array to depend on props
directly (e.g., [showModal, props]) or ensure the caller memoizes props before
passing in; locate the useCallback that defines Modal (wrapping
ForwardDnsInstructionsModal and using showModal/setShowModal) and replace the
dependency entries for props.domain/props.recordType/props.workspaceId with a
single props dependency so the callback only re-creates when the props object
actually changes.
apps/web/lib/domain-connect/sign-apply-url.ts (1)

16-21: ⚡ Quick win

Verify safety of non-null assertion.

Line 19 uses params[k]! assuming values are always defined. While Object.keys(params) only iterates over existing keys, TypeScript's Record<string, string> type can still permit undefined values in some configurations. Consider adding a runtime guard or using a more explicit type.

🛡️ Proposed defensive check
export function buildSigningString(params: Record<string, string>): string {
  return Object.keys(params)
    .sort()
-   .map((k) => `${encodeURIComponent(k)}=${encodeURIComponent(params[k]!)}`)
+   .map((k) => {
+     const value = params[k];
+     if (value === undefined) throw new Error(`Missing value for key: ${k}`);
+     return `${encodeURIComponent(k)}=${encodeURIComponent(value)}`;
+   })
    .join("&");
}
🤖 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 `@apps/web/lib/domain-connect/sign-apply-url.ts` around lines 16 - 21, The
buildSigningString function uses a non-null assertion on params[k] which can be
unsafe; replace it with a runtime-safe approach: either tighten the type or
check values before encoding — e.g., filter Object.keys(params) to only keys
where params[k] !== undefined (or throw a clear error if a missing value is
exceptional), then use the guaranteed string value when calling
encodeURIComponent; update the codepaths around buildSigningString to reflect
the chosen behavior so callers provide valid data or receive an explicit error.
🤖 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 `@apps/web/app/api/domains/`[domain]/forward-instructions/route.ts:
- Around line 52-54: The A-record push always uses name: "@" which is wrong for
subdomains; update the logic in the route handler where recordType and records
are used so the name is computed: if the requested domain is the apex/root
domain use "@", otherwise extract the left-most label of the host (e.g., "go"
from "go.example.com") and use that as the name when calling records.push({
type: "A", name, value: DUB_CUSTOM_DOMAIN_A_RECORD }). Locate the code around
recordType and records.push in route.ts and replace the hard-coded "@" with this
computed name (ensure you handle single-label hosts and trailing dots
consistently).

---

Nitpick comments:
In `@apps/web/app/api/domains/`[domain]/forward-instructions/route.ts:
- Around line 34-37: The Promise.all currently calls getConfigResponse(domain)
but only destructures domainJson, so remove the unused getConfigResponse call to
avoid extra latency: replace the const [domainJson] = await Promise.all([
getDomainResponse(domain), getConfigResponse(domain), ]) pattern with a single
await for getDomainResponse (e.g., const domainJson = await
getDomainResponse(domain)); also remove any now-unused import or reference to
getConfigResponse to keep the module clean.

In `@apps/web/lib/domain-connect/sign-apply-url.ts`:
- Around line 16-21: The buildSigningString function uses a non-null assertion
on params[k] which can be unsafe; replace it with a runtime-safe approach:
either tighten the type or check values before encoding — e.g., filter
Object.keys(params) to only keys where params[k] !== undefined (or throw a clear
error if a missing value is exceptional), then use the guaranteed string value
when calling encodeURIComponent; update the codepaths around buildSigningString
to reflect the chosen behavior so callers provide valid data or receive an
explicit error.

In `@apps/web/ui/modals/forward-dns-instructions-modal.tsx`:
- Around line 122-128: The useMemo call returning { setShowForwardDnsModal:
setShowModal, ForwardDnsInstructionsModal: Modal } incorrectly includes the
stable React state setter setShowModal in its dependency array; remove
setShowModal from the dependencies so the array only depends on Modal (i.e.,
change useMemo(..., [Modal])) to avoid unnecessary recomputation while keeping
ForwardDnsInstructionsModal and setShowForwardDnsModal correct.
- Around line 111-120: The useCallback for Modal currently lists individual
props (props.domain, props.recordType, props.workspaceId) which defeats the
purpose if the props object identity is unstable; update the dependency array to
depend on props directly (e.g., [showModal, props]) or ensure the caller
memoizes props before passing in; locate the useCallback that defines Modal
(wrapping ForwardDnsInstructionsModal and using showModal/setShowModal) and
replace the dependency entries for
props.domain/props.recordType/props.workspaceId with a single props dependency
so the callback only re-creates when the props object actually changes.
🪄 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: 55d17398-ec1c-4bce-ba5c-d8fe913989ad

📥 Commits

Reviewing files that changed from the base of the PR and between d401eb2 and e080edb.

📒 Files selected for processing (17)
  • apps/web/.env.example
  • apps/web/app/api/domains/[domain]/domain-connect/apply/route.ts
  • apps/web/app/api/domains/[domain]/forward-instructions/route.ts
  • apps/web/app/api/domains/[domain]/verify/route.ts
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/domains/page-client.tsx
  • apps/web/lib/domain-connect/constants.ts
  • apps/web/lib/domain-connect/discover.ts
  • apps/web/lib/domain-connect/sign-apply-url.ts
  • apps/web/lib/domain-connect/types.ts
  • apps/web/ui/domains/domain-card.tsx
  • apps/web/ui/domains/domain-configuration.tsx
  • apps/web/ui/modals/forward-dns-instructions-modal.tsx
  • apps/web/ui/partners/program-link-configuration.tsx
  • packages/email/src/templates/domain-dns-instructions.tsx
  • packages/ui/src/icons/cloudflare.tsx
  • packages/ui/src/icons/index.tsx
  • packages/ui/src/icons/vercel.tsx

Comment thread apps/web/app/api/domains/[domain]/forward-instructions/route.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 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 `@apps/web/app/api/domains/`[domain]/forward-instructions/route.ts:
- Around line 34-37: The code awaits Promise.all([getDomainResponse(domain),
getConfigResponse(domain)]) but only uses the first result (domainJson), so
remove the unused getConfigResponse call to avoid extra latency and potential
failures: replace the Promise.all call with a single await
getDomainResponse(domain) (keeping the variable name domainJson) and remove any
related imports or references to getConfigResponse in this file (ensure
getDomainResponse is still awaited and error handling remains).
- Around line 69-81: The TXT DNS instruction block can push empty name or value
strings into records (variables: txtVerification, txtHostFqdn, apex, txtHost,
records); change the logic so you only push a TXT record when the resolved
txtHost is non-empty (treat "@" as valid) and txtVerification.value is non-blank
(e.g., value?.trim() !== ""); if either is empty, skip adding to records to
avoid emitting invalid instructions.
🪄 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: 0481af7f-2b73-46b3-848c-7f3daea3c7ab

📥 Commits

Reviewing files that changed from the base of the PR and between e080edb and 50887cf.

📒 Files selected for processing (1)
  • apps/web/app/api/domains/[domain]/forward-instructions/route.ts

Comment thread apps/web/app/api/domains/[domain]/forward-instructions/route.ts Outdated
Comment thread apps/web/app/api/domains/[domain]/forward-instructions/route.ts Outdated
@pepeladeira

Copy link
Copy Markdown
Collaborator Author

@coderabbitai full review. any breaking changes or security issues?

@coderabbitai

coderabbitai Bot commented May 11, 2026

Copy link
Copy Markdown
Contributor
🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Read critical security-sensitive files
echo "=== sign-apply-url.ts ==="
fd "sign-apply-url.ts" --exec cat {}

echo "=== apply/route.ts ==="
fd "route.ts" --full-path -g "*/domain-connect/apply/route.ts" --exec cat {}

echo "=== discover.ts ==="
fd "discover.ts" --full-path -g "*/domain-connect/discover.ts" --exec cat {}

echo "=== forward-instructions route.ts ==="
fd "route.ts" --full-path -g "*/forward-instructions/route.ts" --exec cat {}

echo "=== verify/route.ts ==="
fd "route.ts" --full-path -g "*/domains/*/verify/route.ts" --exec cat {}

echo "=== domain-connect/types.ts ==="
fd "types.ts" --full-path -g "*/domain-connect/types.ts" --exec cat {}

echo "=== domain-connect/constants.ts ==="
fd "constants.ts" --full-path -g "*/domain-connect/constants.ts" --exec cat {}

echo "=== domain-configuration.tsx ==="
fd "domain-configuration.tsx" --exec cat {}

Length of output: 12414


🏁 Script executed:

#!/bin/bash
echo "=== apply/route.ts ==="
find . -path "*/domain-connect/apply/route.ts" | xargs cat 2>/dev/null

echo "=== discover.ts ==="
find . -path "*/domain-connect/discover.ts" | xargs cat 2>/dev/null

echo "=== forward-instructions route.ts ==="
find . -path "*/forward-instructions/route.ts" | xargs cat 2>/dev/null

echo "=== verify route.ts ==="
find . -path "*/domains/*/verify/route.ts" | xargs cat 2>/dev/null

echo "=== constants.ts ==="
find . -path "*/domain-connect/constants.ts" | xargs cat 2>/dev/null

echo "=== types.ts ==="
find . -path "*/domain-connect/types.ts" | xargs cat 2>/dev/null

echo "=== page-client.tsx ==="
find . -path "*/settings/domains/page-client.tsx" | xargs cat 2>/dev/null

echo "=== forward-dns-instructions-modal.tsx ==="
find . -path "*/forward-dns-instructions-modal.tsx" | xargs cat 2>/dev/null

Length of output: 27819


@pepeladeira, here's my full analysis of security issues and breaking changes in this PR:


🔴 Security Issues

1. SSRF in discover.tsHigh

settingsHostFromTxtRecords prefers hostnames containing domainconnect, vercel, or cloudflare, but falls back to any candidate via candidates[0]. A workspace user who controls the DNS of their registered domain can set a _domainconnect.<domain> TXT record pointing to any internal hostname (e.g., internal-metadata.corp.local), causing the server to issue a credentialed HTTP request to that host.

// discover.ts — the fallback is the risk:
return (
  candidates.find(
    (h) =>
      h.includes("domainconnect") ||
      h.includes("vercel") ||
      h.includes("cloudflare"),
  ) ??
  candidates[0] ??   // ← SSRF: falls back to any host from user-controlled TXT
  null
);

The regex guard (/^[a-z0-9.-]+$/) only blocks bare IPv4; it doesn't block hostnames that resolve to RFC-1918/link-local addresses or IPv6.

Suggested fix: Remove the candidates[0] fallback. Only proceed if the host matches the preferred keyword set:

return (
  candidates.find(
    (h) =>
      h.includes("domainconnect") ||
      h.includes("vercel") ||
      h.includes("cloudflare"),
  ) ?? null  // ← no fallback to arbitrary hosts
);

2. Open Redirect in domain-configuration.tsxMedium

The client unconditionally redirects to applyUrl returned by the server:

// domain-configuration.tsx
if (json?.applyUrl) {
  window.location.assign(json.applyUrl as string);  // ← open redirect
  return;
}

applyUrl is constructed from discovery.urlSyncUX, which comes from a third-party HTTPS settings endpoint that is itself discovered via DNS TXT. A workspace user who controls their domain's DNS and runs a fake settings server can set urlSyncUX to any URL (e.g., a phishing page), which Dub then signs and sends back to the browser for redirect.

Suggested fix: Validate on the server that urlSyncUX is a known Vercel/Cloudflare URL before constructing the apply URL, or at minimum validate the applyUrl origin in the client before calling window.location.assign.


3. No Rate Limiting on Email Forwarding — Medium

POST /api/domains/[domain]/forward-instructions sends an email to any address supplied by the caller. Any workspace member with domains.read can loop this endpoint to spam arbitrary email addresses with Dub-branded DNS instructions.

Suggested fix: Add a rate limit (e.g., via ratelimit() used elsewhere in the codebase) keyed on workspaceId or session.user.id.


🟡 Breaking Changes

4. Stray export * in verify/route.tsLikely Bug

The first line of the new verify route is:

export * from "../../../../../../domains/[domain]/verify/route";

This re-exports everything from a different (likely the previous) verify route file. Since the current file also exports maxDuration and GET, this creates duplicate export conflicts, which will cause a runtime/build error. This looks like a development artefact that should be removed.


5. GET /api/domains/[domain]/verify Response Shape Change — Additive / Low Risk

All response branches now include a domainConnect field (null or DomainConnectDiscovery). This is additive and backward-compatible for most consumers, but any client using strict schema validation may need updating.


6. New Required Environment Variables — Low Risk

DOMAIN_CONNECT_PRIVATE_KEY and DOMAIN_CONNECT_KEY_HOST are gated (discoverDomainConnectIfEligible returns null without them), so existing deployments won't break — they'll simply never surface the auto-configure UI. Well-documented in .env.example. ✅


Summary Table

Severity Issue Location
🔴 High SSRF via DNS TXT → arbitrary host fetch lib/domain-connect/discover.ts
🟠 Medium Open redirect via server-returned applyUrl ui/domains/domain-configuration.tsx + apply/route.ts
🟠 Medium No rate limiting on email-sending endpoint api/domains/[domain]/forward-instructions/route.ts
🟡 Low-Bug Stray export * re-export conflicts api/domains/[domain]/verify/route.ts
🟢 Additive domainConnect field in verify response api/domains/[domain]/verify/route.ts

The stray export * (#4) is the one most likely to cause an immediate breakage in production and should be fixed before merging.

✅ Actions performed

Full review triggered.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (3)
apps/web/app/api/domains/[domain]/forward-instructions/route.ts (1)

74-74: 💤 Low value

Simplify redundant condition.

The condition (txtHost || txtHost === "@") is redundant because if txtHost === "@", then txtHost is already truthy. This can be simplified to just txtHost && txtValue.

♻️ Proposed simplification
-      if ((txtHost || txtHost === "@") && txtValue) {
+      if (txtHost && txtValue) {
         records.push({ type: "TXT", name: txtHost, value: txtValue });
       }
🤖 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 `@apps/web/app/api/domains/`[domain]/forward-instructions/route.ts at line 74,
The if-condition checking TXT inputs uses a redundant expression: replace the
check in the route handler where it currently reads ((txtHost || txtHost ===
"@") && txtValue) with a simplified truthy check txtHost && txtValue; update the
conditional that references variables txtHost and txtValue (inside the
forward-instructions route handler) so it just validates both are present
without the unnecessary txtHost === "@" clause.
apps/web/ui/domains/domain-configuration.tsx (1)

36-38: 💤 Low value

Simplify recordType initialization.

The double negation !!subdomain is unnecessary in a ternary expression.

♻️ Proposed simplification
- const [recordType, setRecordType] = useState<"A" | "CNAME">(
-   !!subdomain ? "CNAME" : "A",
- );
+ const [recordType, setRecordType] = useState<"A" | "CNAME">(
+   subdomain ? "CNAME" : "A",
+ );
🤖 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 `@apps/web/ui/domains/domain-configuration.tsx` around lines 36 - 38, The
initialization of the recordType state uses an unnecessary double negation
(!!subdomain); update the useState call for recordType to use subdomain directly
in the ternary (i.e., subdomain ? "CNAME" : "A") so it remains functionally
identical but simpler; locate the useState invocation that declares const
[recordType, setRecordType] = useState<"A" | "CNAME">(...) and replace the
!!subdomain expression with subdomain.
apps/web/app/api/domains/[domain]/domain-connect/apply/route.ts (1)

106-111: 💤 Low value

Simplify redundant groupId assignment.

Line 109 reassigns groupId = "subdomain" but this is already set on line 102. The else branch can be simplified.

♻️ Proposed simplification
  if (isApex) {
    queryParams.groupId = "apex";
  } else {
-   queryParams.groupId = "subdomain";
    queryParams.host = (subdomain ?? "www").toLowerCase();
  }
🤖 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 `@apps/web/app/api/domains/`[domain]/domain-connect/apply/route.ts around lines
106 - 111, The else branch redundantly sets queryParams.groupId = "subdomain"
even though queryParams.groupId was already assigned "subdomain" earlier;
simplify by removing that redundant assignment and only set queryParams.host =
(subdomain ?? "www").toLowerCase() inside the else branch—keep the isApex
conditional as-is and ensure queryParams.groupId is only set to "apex" in the if
branch and left unchanged otherwise.
🤖 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 `@apps/web/app/api/domains/`[domain]/domain-connect/apply/route.ts:
- Around line 70-78: The code currently uses discovery.urlSyncUX (returned by
discoverDomainConnect(apex)) to build applyUrl and return it to the client,
enabling an open redirect; update the server-side logic that reads
discovery.urlSyncUX so you parse and validate the URL origin before using it (in
the route handler that builds applyUrl) — e.g., ensure the URL's origin matches
an allowlist of known providers (Vercel/Cloudflare) or the discovery apex,
reject or ignore values that fail validation, and fallback to a safe internal
URL; alternatively implement an intermediate redirect endpoint on the server
that performs the same validation before redirecting (and/or add client-side
validation in domain-configuration.tsx before calling window.location.assign) so
untrusted DNS-provided values cannot cause a redirect to an arbitrary host.

In `@apps/web/app/api/domains/`[domain]/forward-instructions/route.ts:
- Around line 21-95: The POST handler wrapped by withWorkspace (the async
function that calls getDomainOrThrow, parses bodySchema, builds records and
calls sendEmail) currently allows unlimited emails to arbitrary addresses; add
per-user or per-workspace rate limiting (e.g., max 10 emails per hour) before
calling sendEmail: use a shared store (Redis, cache, or DB table) keyed by
workspace.id or session.user.id + endpoint identifier to increment and expire
counts, check the count and return a 429-style error/throw (instead of
proceeding) when the limit is exceeded, and only call sendEmail when under the
limit; ensure you reference the same identifiers used in the handler (workspace,
session.user.email) and enforce atomic increment/TTL (or transaction) to avoid
race conditions.

In `@apps/web/lib/domain-connect/discover.ts`:
- Around line 46-55: The current return uses candidates.find(... ) ??
candidates[0] ?? null which allows falling back to untrusted DNS TXT values and
enables SSRF; change the logic in discover.ts to remove the fallback to
candidates[0] so only values matching the trusted keywords ("domainconnect",
"vercel", "cloudflare") are returned (i.e., return the result of
candidates.find(...) or null), or alternatively enforce a strict
allowlist/validation step: validate the chosen host from candidates against an
explicit allowlist or perform IP resolution and reject private/internal
IPv4/IPv6 and localhost names before returning/using it (update the code paths
that reference candidates.find and candidates[0] accordingly).

In `@apps/web/ui/domains/domain-configuration.tsx`:
- Around line 68-70: The redirect uses json?.applyUrl with
window.location.assign without origin checks, enabling an open-redirect—before
calling window.location.assign in domain-configuration.tsx, parse and validate
json?.applyUrl: ensure it is an absolute URL whose origin equals
window.location.origin or is in an explicit server-approved whitelist (or else
only allow relative paths), and only call window.location.assign when that
validation passes; update the code path around json?.applyUrl and the early
return to enforce this check.

---

Nitpick comments:
In `@apps/web/app/api/domains/`[domain]/domain-connect/apply/route.ts:
- Around line 106-111: The else branch redundantly sets queryParams.groupId =
"subdomain" even though queryParams.groupId was already assigned "subdomain"
earlier; simplify by removing that redundant assignment and only set
queryParams.host = (subdomain ?? "www").toLowerCase() inside the else
branch—keep the isApex conditional as-is and ensure queryParams.groupId is only
set to "apex" in the if branch and left unchanged otherwise.

In `@apps/web/app/api/domains/`[domain]/forward-instructions/route.ts:
- Line 74: The if-condition checking TXT inputs uses a redundant expression:
replace the check in the route handler where it currently reads ((txtHost ||
txtHost === "@") && txtValue) with a simplified truthy check txtHost &&
txtValue; update the conditional that references variables txtHost and txtValue
(inside the forward-instructions route handler) so it just validates both are
present without the unnecessary txtHost === "@" clause.

In `@apps/web/ui/domains/domain-configuration.tsx`:
- Around line 36-38: The initialization of the recordType state uses an
unnecessary double negation (!!subdomain); update the useState call for
recordType to use subdomain directly in the ternary (i.e., subdomain ? "CNAME" :
"A") so it remains functionally identical but simpler; locate the useState
invocation that declares const [recordType, setRecordType] = useState<"A" |
"CNAME">(...) and replace the !!subdomain expression with subdomain.
🪄 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: 22b8dba1-70a8-4b46-b5d2-1136a2df6945

📥 Commits

Reviewing files that changed from the base of the PR and between d401eb2 and 6e8dd39.

📒 Files selected for processing (17)
  • apps/web/.env.example
  • apps/web/app/api/domains/[domain]/domain-connect/apply/route.ts
  • apps/web/app/api/domains/[domain]/forward-instructions/route.ts
  • apps/web/app/api/domains/[domain]/verify/route.ts
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/domains/page-client.tsx
  • apps/web/lib/domain-connect/constants.ts
  • apps/web/lib/domain-connect/discover.ts
  • apps/web/lib/domain-connect/sign-apply-url.ts
  • apps/web/lib/domain-connect/types.ts
  • apps/web/ui/domains/domain-card.tsx
  • apps/web/ui/domains/domain-configuration.tsx
  • apps/web/ui/modals/forward-dns-instructions-modal.tsx
  • apps/web/ui/partners/program-link-configuration.tsx
  • packages/email/src/templates/domain-dns-instructions.tsx
  • packages/ui/src/icons/cloudflare.tsx
  • packages/ui/src/icons/index.tsx
  • packages/ui/src/icons/vercel.tsx

Comment thread apps/web/app/api/domains/[domain]/domain-connect/apply/route.ts
Comment thread apps/web/app/api/domains/[domain]/forward-instructions/route.ts
Comment thread apps/web/lib/domain-connect/discover.ts
Comment thread apps/web/ui/domains/domain-configuration.tsx

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 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 `@apps/web/lib/domain-connect/discover.ts`:
- Around line 47-52: The current candidates.find call uses substring matching
(h.includes("domainconnect") etc.) which allows hosts like "vercel.evil.tld" to
pass; replace that check with a strict allowlist or suffix-based hostname
validation: parse the candidate into a URL/hostname (e.g., new URL(...).hostname
or equivalent), then accept only exact matches or suffix matches that ensure a
dot boundary (e.g., hostname === "vercel.com" || hostname ===
"domainconnect.org" || hostname.endsWith(".vercel.com") becomes hostname ===
trusted || hostname.endsWith("." + trusted)); update the code that uses
candidates.find to validate the parsed hostname this way before performing the
fetch.
🪄 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: c82a6839-d91d-40a1-8d80-610a3cdc46b7

📥 Commits

Reviewing files that changed from the base of the PR and between 6e8dd39 and fa0fd48.

📒 Files selected for processing (4)
  • apps/web/app/api/domains/[domain]/domain-connect/apply/route.ts
  • apps/web/app/api/domains/[domain]/forward-instructions/route.ts
  • apps/web/lib/domain-connect/discover.ts
  • apps/web/ui/domains/domain-configuration.tsx
🚧 Files skipped from review as they are similar to previous changes (3)
  • apps/web/app/api/domains/[domain]/forward-instructions/route.ts
  • apps/web/ui/domains/domain-configuration.tsx
  • apps/web/app/api/domains/[domain]/domain-connect/apply/route.ts

Comment thread apps/web/lib/domain-connect/discover.ts Outdated
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