Skip to content

feat: CNAME adblocker bypass#515

Merged
Blaumaus merged 9 commits intomainfrom
feat/cname-proxy
Apr 20, 2026
Merged

feat: CNAME adblocker bypass#515
Blaumaus merged 9 commits intomainfrom
feat/cname-proxy

Conversation

@Blaumaus
Copy link
Copy Markdown
Member

@Blaumaus Blaumaus commented Apr 17, 2026

Changes

If applicable, please describe what changes were made in this pull request.

Community Edition support

  • Your feature is implemented for the Swetrix Community Edition
  • This PR only updates the Cloud (Enterprise) Edition code (e.g. Paddle webhooks, blog, payouts, etc.)

Database migrations

  • Clickhouse / MySQL migrations added for this PR
  • No table schemas changed in this PR

Documentation

  • You have updated the documentation according to your PR
  • This PR did not change any publicly documented endpoints

Summary by CodeRabbit

  • New Features

    • Managed reverse-proxy: Project "Proxy" settings tab to add/list/verify/delete domains, status badges, polling, client-side validation, keyword warnings, copyable CNAME/snippet, limit up to 5 per project.
    • Automatic SSL issuance with status transitions, background verification/recheck cron jobs, public edge checks (allow/active) and edge auth guard.
    • Database table and new API endpoints for proxy domain management; UI install snippet and Cloudflare guidance.
    • New Cloudflare SVG icons for UI.
  • Documentation

    • New managed-proxy guide and updated adblockers guidance with setup, limits, and troubleshooting.
  • Translations

    • Added UI strings for proxy workflows.
  • Chores

    • New env var for edge shared-secret (edge API key).

@Blaumaus Blaumaus self-assigned this Apr 17, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 17, 2026

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
  • ✅ Review completed - (🔄 Check again to review again)
📝 Walkthrough

Walkthrough

Adds a managed reverse-proxy feature: DB migration and TypeORM entity, backend service with DNS/TLS verification and edge gating, controllers/DTOs and guard, scheduled verification tasks, frontend settings UI and routes, docs, localization, and client models/types.

Changes

Cohort / File(s) Summary
Backend DTOs
backend/apps/cloud/src/project/dto/index.ts, backend/apps/cloud/src/project/dto/proxy-domain-create.dto.ts
Added ProxyDomainCreateDTO and re-exported it from the DTO barrel.
Backend Entities
backend/apps/cloud/src/project/entity/index.ts, backend/apps/cloud/src/project/entity/proxy-domain.entity.ts
New ProxyDomain TypeORM entity and ProxyDomainStatus enum; UUID PK, projectId FK (ON DELETE CASCADE), unique hostname/proxyTargetId, status/timestamps and Swagger annotations.
Backend Module
backend/apps/cloud/src/project/project.module.ts
Registered ProxyDomain entity; added ProxyDomainService provider/export; added ProxyDomainController and ProxyDomainEdgeController to controllers.
Backend Service
backend/apps/cloud/src/project/proxy-domain.service.ts
New core service and helpers: PROXY_BASE_DOMAIN, validateHostname, CRUD, serialization, CNAME/TLS verification with status transitions, edge gating (isHostnameAllowedForIssuance, isHostnameLive), batch queries, and Redis cache cleanup.
Backend Controllers & Guard
backend/apps/cloud/src/project/proxy-domain.controller.ts, backend/apps/cloud/src/project/proxy-domain-edge.guard.ts
ProxyDomainController (authenticated project endpoints: list/create/delete/verify) and ProxyDomainEdgeController (edge allow/active checks) plus ProxyDomainEdgeGuard enforcing shared-secret header.
Backend Task Manager
backend/apps/cloud/src/task-manager/task-manager.service.ts
Injected ProxyDomainService; added cron tasks: verifyPendingProxyDomains (1m) and recheckLiveProxyDomains (6h) with concurrency guards and limited concurrency per domain.
Database Migration
backend/migrations/mysql/2026_04_17_managed_proxy_domains.sql
New proxy_domain table with columns, enum status, unique indexes on hostname/proxyTargetId, indexes on projectId/status, and FK to project(id) with ON DELETE CASCADE.
Frontend Models
web/app/lib/models/Project.ts
Added ProxyDomainStatus type and ProxyDomain interface for client models.
Frontend Settings UI
web/app/pages/Project/Settings/ProjectSettings.tsx, web/app/pages/Project/Settings/components/ProxyDomainsTab.tsx
Added "proxy" settings tab and ProxyDomainsTab component: list/add/delete/verify flows, client-side validation, polling while pending, snippet display when live, copy UI and toasts.
Frontend Routes / Actions
web/app/routes/projects.settings.$id.tsx
Extended ProjectSettingsActionData and added action handlers for list-proxy-domains, add-proxy-domain, delete-proxy-domain, verify-proxy-domain that call new backend endpoints.
Localization
web/public/locales/en.json
Added numerous translation keys for proxy UI, status labels, instructions, errors, and snippet strings.
Documentation
docs/content/docs/adblockers/how-to-deal-with-adblockers.mdx, docs/content/docs/adblockers/managed-proxy.mdx, docs/content/docs/adblockers/meta.json
Updated adblock guidance, added managed-proxy documentation page, and updated docs metadata to include the new page.
Environment example
backend/.env.example
Added MANAGED_PROXY_EDGE_API_KEY entry and documentation.

Sequence Diagram

sequenceDiagram
    participant Client
    participant Service as ProxyDomainService
    participant DB as Database
    participant DNS as DNS Resolver
    participant TLS as TLS Probe (443)
    participant Redis

    Client->>Service: request verify (domain id)
    activate Service

    Service->>DB: load domain record
    DB-->>Service: domain (status,timestamps)

    Service->>DNS: resolve CNAME (bounded timeout)
    activate DNS
    DNS-->>Service: resolved / not resolved
    deactivate DNS

    alt CNAME resolved
        Service->>TLS: probe TLS on hostname:443 (timeout)
        activate TLS
        TLS-->>Service: cert ok / fail / timeout
        deactivate TLS

        alt TLS ok
            Service->>DB: set status → LIVE, set liveSince/statusChangedAt
            Service->>Redis: purge related cert cache (best-effort)
        else TLS fail & grace < 30m
            Service->>DB: update lastCheckedAt, record errorMessage
        else TLS fail & elapsed ≥ 30m
            Service->>DB: set status → ERROR, set errorMessage/statusChangedAt
        end
    else CNAME missing & grace < 7d
        Service->>DB: update lastCheckedAt
    else CNAME missing & elapsed ≥ 7d
        Service->>DB: set status → ERROR, set errorMessage/statusChangedAt
    end

    Service-->>Client: return updated domain record
    deactivate Service
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

  • Paid trials #478 — touches task-manager.service.ts; related because both modify cron/task logic and the same service.

Poem

🐰 I nibble DNS and chase a CNAME,
I probe TLS beneath the moonlit name,
Domains hop from waiting to live,
I clear the cache so certs survive,
A little rabbit cheers your proxy drive.

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive The description follows the template structure with all required checkboxes addressed: Community Edition support properly marked (Cloud-only), database migrations confirmed (MySQL added), and documentation updates confirmed. However, the 'Changes' section is empty, leaving no implementation details about what was actually built. Add a summary of changes to the 'Changes' section describing the managed proxy domain feature, including the new backend service, controllers, entity, and frontend UI components added in this PR.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: CNAME adblocker bypass' directly summarizes the main feature: implementing a CNAME-based reverse proxy mechanism to bypass ad blockers. This aligns with the changeset's core functionality across backend controllers, services, entities, database migrations, and frontend UI for managing proxy domains.

✏️ 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 feat/cname-proxy

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.

Comment thread backend/apps/cloud/src/project/proxy-domain.service.ts Fixed
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@backend/apps/cloud/src/project/proxy-domain.service.ts`:
- Around line 388-412: Both queries are missing a stable ordering so batches can
starve rows; update findPendingForVerification and findLiveForRecheck to sort by
oldest check time (lastCheckedAt) and then by a stable tie-breaker (id). For
findPendingForVerification (method name) pass an order option to the repository
find call ordering lastCheckedAt ASC (NULLs first) and id ASC; for
findLiveForRecheck (method name) add an orderBy('pd.lastCheckedAt', 'ASC') and
addOrderBy('pd.id','ASC') on the query builder (ensure NULLs come first if your
DB supports it) so the scheduler processes the oldest-unchecked rows first and
ordering is deterministic.
- Around line 153-186: The create method can race because multiple callers can
pass proxyDomainRepository.count() then save; wrap the existence/count check and
save in a database transaction or row-level lock to serialize per-project domain
creation (e.g., beginTransaction, SELECT FOR UPDATE on the project row or a
dedicated lock table key for projectId), re-check the count and existing
hostname inside the transaction, then create/save the ProxyDomain entity (use
generateProxyTargetId, proxyDomainRepository.create/save) and commit; also keep
a unique DB constraint on hostname and translate duplicate-key/unique-constraint
DB errors thrown by proxyDomainRepository.save into ConflictException so
concurrent duplicate races remain surfaced as the same ConflictException.
- Around line 59-72: The validateHostname function currently uses a regex
replace on user-controlled input (hostname =
raw.trim().toLowerCase().replace(/\.+$/, '')) which CodeQL flagged; change
validateHostname to remove trailing dots without using regex by iteratively
trimming up to a small bounded number of characters (e.g., while the last
character is '.' and loop count < 10 remove the last char) after
trim().toLowerCase(), and then continue the existing checks (empty, length >
253) — reference validateHostname to locate and replace the .replace(/\.+$/, '')
step with a simple bounded loop that strips trailing '.' characters.

In `@backend/apps/cloud/src/task-manager/task-manager.service.ts`:
- Around line 2158-2186: The verifyPendingProxyDomains cron can overlap and
re-run while prior work is still in flight causing duplicate probes; add an
in-memory re-entrancy guard (e.g., a boolean property this.verifyingPending) at
the start of verifyPendingProxyDomains so the method returns immediately if
already running, set it true before processing and false in a finally block;
additionally change/follow up on findPendingForVerification to support atomic
claiming (mark rows as VERIFYING or implement short locks so overlapping runs
skip claimed domains) and wrap any dns.resolveCname calls used during
verifyDomain with a hard timeout (e.g., Promise.race with a 10s timer) to avoid
hanging DNS resolution.

In `@web/app/pages/Project/Settings/components/ProxyDomainsTab.tsx`:
- Line 585: The submitText is using t('common.add') which is not defined in the
translations; either add an "add": "Add" entry under the common namespace in
your English locale JSON or switch the call in ProxyDomainsTab.tsx to an
existing proxy-specific key (e.g., t('proxy.addDomain')) and add that key to the
locale file; update the translation JSON accordingly so the button renders a
proper label instead of the raw key.
- Around line 95-103: The catch block inside the onCopy async function uses a
hard-coded English string for the failure toast; replace the literal 'Failed to
copy' with the localized key by calling the translation function (t) — e.g., use
toast.error(t('common.failedToCopy')) — so the error toast uses i18n like the
success path (reference: onCopy, navigator.clipboard.writeText, toast.error, t).
🪄 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: 43d9fca9-41ab-4dfe-8d93-c0453bb06356

📥 Commits

Reviewing files that changed from the base of the PR and between 4db4cc6 and ea32633.

📒 Files selected for processing (17)
  • backend/apps/cloud/src/project/dto/index.ts
  • backend/apps/cloud/src/project/dto/proxy-domain-create.dto.ts
  • backend/apps/cloud/src/project/entity/index.ts
  • backend/apps/cloud/src/project/entity/proxy-domain.entity.ts
  • backend/apps/cloud/src/project/project.module.ts
  • backend/apps/cloud/src/project/proxy-domain.controller.ts
  • backend/apps/cloud/src/project/proxy-domain.service.ts
  • backend/apps/cloud/src/task-manager/task-manager.service.ts
  • backend/migrations/mysql/2026_04_17_managed_proxy_domains.sql
  • docs/content/docs/adblockers/how-to-deal-with-adblockers.mdx
  • docs/content/docs/adblockers/managed-proxy.mdx
  • docs/content/docs/adblockers/meta.json
  • web/app/lib/models/Project.ts
  • web/app/pages/Project/Settings/ProjectSettings.tsx
  • web/app/pages/Project/Settings/components/ProxyDomainsTab.tsx
  • web/app/routes/projects.settings.$id.tsx
  • web/public/locales/en.json

Comment thread backend/apps/cloud/src/project/proxy-domain.service.ts Outdated
Comment thread backend/apps/cloud/src/project/proxy-domain.service.ts Outdated
Comment thread backend/apps/cloud/src/project/proxy-domain.service.ts
Comment thread backend/apps/cloud/src/task-manager/task-manager.service.ts
Comment thread web/app/pages/Project/Settings/components/ProxyDomainsTab.tsx Outdated
Comment thread web/app/pages/Project/Settings/components/ProxyDomainsTab.tsx
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (2)
backend/apps/cloud/src/task-manager/task-manager.service.ts (1)

2156-2240: Past feedback addressed; consider hoisting the new guard fields to the top of the class.

The in-memory re-entrancy guards (verifyingPendingProxyDomains, recheckingLiveProxyDomains) correctly wrap the work in try/finally, so the flag is always reset even on early return / throw. Combined with isPrimaryNode() gating and the resolveCnameWithTimeout / TLS timeout on the service side, the previous overlapping-cron concern is resolved.

One small style nit: declaring private fields in the middle of the class body (between deleteOldRefreshTokens and verifyPendingProxyDomains) is unconventional and easy to miss when scanning. Consider moving them next to the other class-level state near the top of TaskManagerService.

♻️ Suggested field placement
 `@Injectable`()
 export class TaskManagerService {
+  // In-memory re-entrancy guards for the proxy-domain verifier crons.
+  // A single slow DNS/TLS probe can stretch a tick past the cron interval;
+  // without these flags the next tick would re-fire duplicate probes for
+  // everything still in flight.
+  private verifyingPendingProxyDomains = false
+  private recheckingLiveProxyDomains = false
+
   constructor(
     private readonly mailerService: MailerService,
     ...

And remove the declarations at lines 2156-2161.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/apps/cloud/src/task-manager/task-manager.service.ts` around lines
2156 - 2240, Move the two in-memory re-entrancy guard fields
(verifyingPendingProxyDomains and recheckingLiveProxyDomains) out of the middle
of the class and hoist them to the top of TaskManagerService next to other
class-level state; remove the original declarations between
deleteOldRefreshTokens and verifyPendingProxyDomains, and keep the rest of the
logic (the try/finally guards in verifyPendingProxyDomains and
recheckLiveProxyDomains) unchanged so behavior is preserved.
web/app/pages/Project/Settings/components/ProxyDomainsTab.tsx (1)

182-200: Polling interval is reset on every render, and the else if branch is unreachable.

fetchDomains is recreated on every render because it closes over listFetcher, which useFetcher returns as a new object reference each render. As a result, this effect re-runs on every render: the cleanup tears down the interval and a new setInterval(fetchDomains, 8000) is set up — so under frequent re-renders the 8s timer can keep being reset and never fire. Additionally, the else if (pollRef.current) branch is dead code: the previous effect's cleanup has already cleared and nulled pollRef.current before this effect body executes.

Depend only on the polling condition and call the latest fetchDomains via a ref.

♻️ Suggested refactor
+  const fetchDomainsRef = useRef(fetchDomains)
+  useEffect(() => {
+    fetchDomainsRef.current = fetchDomains
+  }, [fetchDomains])
+
+  const hasPending = domains.some(
+    (d) => d.status === 'waiting' || d.status === 'issuing',
+  )
+
   useEffect(() => {
-    const hasPending = domains.some(
-      (d) => d.status === 'waiting' || d.status === 'issuing',
-    )
-
-    if (hasPending) {
-      pollRef.current = setInterval(fetchDomains, 8000)
-    } else if (pollRef.current) {
-      clearInterval(pollRef.current)
-      pollRef.current = null
-    }
-
-    return () => {
-      if (pollRef.current) {
-        clearInterval(pollRef.current)
-        pollRef.current = null
-      }
-    }
-  }, [domains, fetchDomains])
+    if (!hasPending) return
+    const id = setInterval(() => fetchDomainsRef.current(), 8000)
+    return () => clearInterval(id)
+  }, [hasPending])
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/app/pages/Project/Settings/components/ProxyDomainsTab.tsx` around lines
182 - 200, The useEffect that manages polling re-runs every render because
fetchDomains changes each render (it closes over listFetcher), causing the
interval to be torn down and re-created so the 8s timer can be reset; also the
else-if branch is effectively unreachable because cleanup already nulls
pollRef.current. Fix by making the effect depend only on the polling condition
(domains) and not fetchDomains: keep pollRef and store the latest fetchDomains
in a ref (e.g., fetchDomainsRef.current) so the interval callback calls the
up-to-date fetchDomains, and remove the else-if branch; update useEffect to
set/clear interval based on hasPending only and use fetchDomainsRef inside the
interval.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@web/app/pages/Project/Settings/components/ProxyDomainsTab.tsx`:
- Around line 378-383: The hardcoded English string "Live since" in the JSX that
renders domain.liveSince should be replaced with an i18n key (e.g.
project.settings.proxy.liveSince) and the date passed as an interpolation;
update the Text rendering inside ProxyDomainsTab (the block referencing
domain.liveSince and dayjs(domain.liveSince)) to call the translation function
(t) with the key and a value like { date:
dayjs(domain.liveSince).locale(i18n.language).format('lll') } or equivalent
locale-aware formatting so the label is localized and the formatted date is
injected via interpolation.
- Around line 251-256: The validation error currently sets hostnameError to
t('project.settings.proxy.domainPlaceholder') inside submitAddDomain when
validateHostnameClient fails; replace that placeholder string with a dedicated
error key (e.g. t('project.settings.proxy.invalidDomain')) and update the
translations accordingly so setHostnameError uses the new error copy instead of
the placeholder; ensure you modify submitAddDomain (where validateHostnameClient
is checked) and any UI that displays hostnameError to show the new message.

---

Nitpick comments:
In `@backend/apps/cloud/src/task-manager/task-manager.service.ts`:
- Around line 2156-2240: Move the two in-memory re-entrancy guard fields
(verifyingPendingProxyDomains and recheckingLiveProxyDomains) out of the middle
of the class and hoist them to the top of TaskManagerService next to other
class-level state; remove the original declarations between
deleteOldRefreshTokens and verifyPendingProxyDomains, and keep the rest of the
logic (the try/finally guards in verifyPendingProxyDomains and
recheckLiveProxyDomains) unchanged so behavior is preserved.

In `@web/app/pages/Project/Settings/components/ProxyDomainsTab.tsx`:
- Around line 182-200: The useEffect that manages polling re-runs every render
because fetchDomains changes each render (it closes over listFetcher), causing
the interval to be torn down and re-created so the 8s timer can be reset; also
the else-if branch is effectively unreachable because cleanup already nulls
pollRef.current. Fix by making the effect depend only on the polling condition
(domains) and not fetchDomains: keep pollRef and store the latest fetchDomains
in a ref (e.g., fetchDomainsRef.current) so the interval callback calls the
up-to-date fetchDomains, and remove the else-if branch; update useEffect to
set/clear interval based on hasPending only and use fetchDomainsRef inside the
interval.
🪄 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: 6fe723ca-c5a2-44ed-9a63-44e4285d1127

📥 Commits

Reviewing files that changed from the base of the PR and between ea32633 and f7d0ec6.

📒 Files selected for processing (5)
  • backend/apps/cloud/src/project/proxy-domain.service.ts
  • backend/apps/cloud/src/task-manager/task-manager.service.ts
  • docs/content/docs/adblockers/how-to-deal-with-adblockers.mdx
  • web/app/pages/Project/Settings/components/ProxyDomainsTab.tsx
  • web/public/locales/en.json
🚧 Files skipped from review as they are similar to previous changes (1)
  • backend/apps/cloud/src/project/proxy-domain.service.ts

Comment thread web/app/pages/Project/Settings/components/ProxyDomainsTab.tsx
Comment thread web/app/pages/Project/Settings/components/ProxyDomainsTab.tsx Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

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 (1)
backend/apps/cloud/src/project/proxy-domain.controller.ts (1)

39-128: Optional: collapse the repeated project-load + authz boilerplate.

The 4-line prelude (isValidPIDgetFullProjectNotFoundExceptionallowedToManage) is copy-pasted across list, create, remove, and verify. A single private helper (or a param decorator / NestJS guard keyed on :pid) would centralize it, reduce drift risk (e.g., someone later forgetting the allowedToManage call in a new handler), and keep each handler focused on its own logic.

♻️ Example helper extraction
+  private async loadManageableProject(pid: string, uid: string) {
+    if (!isValidPID(pid)) {
+      throw new BadRequestException('The provided project ID is incorrect')
+    }
+    const project = await this.projectService.getFullProject(pid)
+    if (!project) {
+      throw new NotFoundException('Project not found')
+    }
+    this.projectService.allowedToManage(project, uid)
+    return project
+  }

Each handler then becomes e.g.:

   async list(`@Param`('pid') pid: string, `@CurrentUserId`() uid: string) {
-    if (!isValidPID(pid)) {
-      throw new BadRequestException('The provided project ID is incorrect')
-    }
-
-    const project = await this.projectService.getFullProject(pid)
-    if (!project) {
-      throw new NotFoundException('Project not found')
-    }
-    this.projectService.allowedToManage(project, uid)
-
+    await this.loadManageableProject(pid, uid)
     const domains = await this.proxyDomainService.listForProject(pid)
     return { domains, proxyBaseDomain: PROXY_BASE_DOMAIN }
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/apps/cloud/src/project/proxy-domain.controller.ts` around lines 39 -
128, Extract the repeated PID validation, project load and authorization into a
single private helper (e.g. ensureProjectManageable or getProjectForManage) that
encapsulates isValidPID(pid) -> throw BadRequestException, await
this.projectService.getFullProject(pid) -> throw NotFoundException, and
this.projectService.allowedToManage(project, uid); have it return the loaded
project (or throw) and replace the duplicated four-line prelude in list, create,
remove, and verify with a single await this.ensureProjectManageable(pid, uid)
call; alternatively note you can implement the same via a custom param decorator
or NestJS guard keyed on :pid, but keep handlers using the helper name above so
their bodies only handle proxyDomainService calls.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@backend/apps/cloud/src/project/proxy-domain.controller.ts`:
- Around line 131-185: The public edge endpoints in ProxyDomainEdgeController
(methods allow and active) leak customer state and are unauthenticated and
unthrottled; update these handlers to require an edge-auth header or other guard
(e.g., validate a shared-secret header against config or attach a custom Nest
guard) before calling validateHostname and
proxyDomainService.isHostnameAllowedForIssuance / isHostnameLive, collapse
success/failure semantics so callers cannot distinguish states (return a generic
404 or always return {allowed:false}/{active:false} on failure without distinct
messages), and add a rate-limiting guard (e.g., ThrottlerGuard) or document the
reliance on the edge cache to prevent direct DB hits via
findActiveDomainByHostname.

---

Nitpick comments:
In `@backend/apps/cloud/src/project/proxy-domain.controller.ts`:
- Around line 39-128: Extract the repeated PID validation, project load and
authorization into a single private helper (e.g. ensureProjectManageable or
getProjectForManage) that encapsulates isValidPID(pid) -> throw
BadRequestException, await this.projectService.getFullProject(pid) -> throw
NotFoundException, and this.projectService.allowedToManage(project, uid); have
it return the loaded project (or throw) and replace the duplicated four-line
prelude in list, create, remove, and verify with a single await
this.ensureProjectManageable(pid, uid) call; alternatively note you can
implement the same via a custom param decorator or NestJS guard keyed on :pid,
but keep handlers using the helper name above so their bodies only handle
proxyDomainService calls.
🪄 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: 0cec4251-a4fb-42a4-97de-b96d0de6301e

📥 Commits

Reviewing files that changed from the base of the PR and between f7d0ec6 and ca21147.

📒 Files selected for processing (1)
  • backend/apps/cloud/src/project/proxy-domain.controller.ts

Comment thread backend/apps/cloud/src/project/proxy-domain.controller.ts Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

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 (2)
web/app/pages/Project/Settings/components/ProxyDomainsTab.tsx (1)

157-202: Assign fetchDomainsRef.current inside an effect, not during render.

Line 164 mutates the ref as a render side-effect, which is not safe under StrictMode double-invocation or concurrent rendering. The polling interval captures fetchDomainsRef.current at interval-tick time, so writing the ref in useLayoutEffect (so the update is visible before the poll effect's cleanup/resubscribe) is both sufficient and idiomatic.

♻️ Proposed fix
-  const fetchDomains = useCallback(() => {
+  const fetchDomains = useCallback(() => {
     listFetcher.submit(
       { intent: 'list-proxy-domains' },
       { method: 'POST', action: settingsAction },
     )
   }, [listFetcher, settingsAction])

-  fetchDomainsRef.current = fetchDomains
+  useLayoutEffect(() => {
+    fetchDomainsRef.current = fetchDomains
+  }, [fetchDomains])

(and add useLayoutEffect to the react import)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/app/pages/Project/Settings/components/ProxyDomainsTab.tsx` around lines
157 - 202, Move the assignment fetchDomainsRef.current = fetchDomains out of
render and into a useLayoutEffect so the ref is updated as a layout side-effect
before any polling effects run; import useLayoutEffect from react, create a
useLayoutEffect that sets fetchDomainsRef.current = fetchDomains and cleans up
if needed, leaving fetchDomains as the same useCallback and keeping the poll
effect that reads fetchDomainsRef.current (pollRef and the setInterval logic
remain unchanged).
backend/apps/cloud/src/task-manager/task-manager.service.ts (1)

2164-2241: Re-entrancy guards and isPrimaryNode gate look good.

The try/finally pattern correctly releases the flag on every exit path (including the _isEmpty short‑circuits), which addresses the overlap concern from the earlier review. One operational note for later:

  • recheckLiveProxyDomains caps at 500 every 6h, so once live domains exceed ~500 the effective recheck interval stretches to 12h+ and a broken CNAME/cert could go unnoticed for that long. Worth monitoring and/or making the batch size / cadence configurable as adoption grows.
  • This file still depends on proxyDomainService.verifyDomain wrapping dns.resolveCname / TLS probes with hard timeouts; without that, a single hanging DNS server can still hold the guard for minutes and starve subsequent ticks. That fix lives in proxy-domain.service.ts (out of scope for this hunk).
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/apps/cloud/src/task-manager/task-manager.service.ts` around lines
2164 - 2241, The cron handlers work but need operational configurability: make
the batch sizes and cadence configurable instead of hardcoding 200/500 and
CronExpression.EVERY_MINUTE / EVERY_6_HOURS so you can tune them as adoption
grows; update verifyPendingProxyDomains and recheckLiveProxyDomains to read e.g.
config.get('proxyDomain.batchSize') and
config.get('proxyDomain.recheckIntervalCron') (or similar keys) and pass the
configurable batch size into proxyDomainService.findPendingForVerification and
findLiveForRecheck, and replace the fixed CronExpression constants with a value
loaded from configuration; also ensure proxyDomainService.verifyDomain
implements strict DNS/TLS timeouts (the verifyDomain method is the relevant
symbol) so a single slow resolver cannot hang the cron guard.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@web/app/pages/Project/Settings/components/ProxyDomainsTab.tsx`:
- Around line 281-519: The snippet UI only uses liveDomain = useMemo(() =>
domains.find(...)) which hides additional live domains; change this to collect
all live domains (e.g. liveDomains = useMemo(() => domains.filter(d => d.status
=== 'live'), [domains])) and then either (A) map over liveDomains to render a
snippet card per domain (reusing CopyableCode and projectId) or (B) add a small
selector state (e.g. selectedLiveDomain via useState) that lets the user pick
one hostname from liveDomains before rendering the CopyableCode blocks; update
usages of liveDomain to use the selected item or each item in the map and ensure
the script/init strings still use domain.hostname.

---

Nitpick comments:
In `@backend/apps/cloud/src/task-manager/task-manager.service.ts`:
- Around line 2164-2241: The cron handlers work but need operational
configurability: make the batch sizes and cadence configurable instead of
hardcoding 200/500 and CronExpression.EVERY_MINUTE / EVERY_6_HOURS so you can
tune them as adoption grows; update verifyPendingProxyDomains and
recheckLiveProxyDomains to read e.g. config.get('proxyDomain.batchSize') and
config.get('proxyDomain.recheckIntervalCron') (or similar keys) and pass the
configurable batch size into proxyDomainService.findPendingForVerification and
findLiveForRecheck, and replace the fixed CronExpression constants with a value
loaded from configuration; also ensure proxyDomainService.verifyDomain
implements strict DNS/TLS timeouts (the verifyDomain method is the relevant
symbol) so a single slow resolver cannot hang the cron guard.

In `@web/app/pages/Project/Settings/components/ProxyDomainsTab.tsx`:
- Around line 157-202: Move the assignment fetchDomainsRef.current =
fetchDomains out of render and into a useLayoutEffect so the ref is updated as a
layout side-effect before any polling effects run; import useLayoutEffect from
react, create a useLayoutEffect that sets fetchDomainsRef.current = fetchDomains
and cleans up if needed, leaving fetchDomains as the same useCallback and
keeping the poll effect that reads fetchDomainsRef.current (pollRef and the
setInterval logic remain 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: f2c85795-4936-4cea-9432-3212c4fff2fb

📥 Commits

Reviewing files that changed from the base of the PR and between ca21147 and 3fb3d14.

📒 Files selected for processing (3)
  • backend/apps/cloud/src/task-manager/task-manager.service.ts
  • web/app/pages/Project/Settings/components/ProxyDomainsTab.tsx
  • web/public/locales/en.json

Comment thread web/app/pages/Project/Settings/components/ProxyDomainsTab.tsx Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
backend/apps/cloud/src/project/proxy-domain.controller.ts (1)

37-126: Repeated pid-validation / project-load / authorization preamble — consider extracting.

Each of list, create, remove, and verify opens with the same 8-line block: isValidPIDgetFullProjectNotFoundExceptionallowedToManage. Pulling this into a small private helper (e.g. loadManageableProject(pid, uid)) would remove four near-identical copies and make it harder for future endpoints to forget one of the three checks.

♻️ Sketch
private async loadManageableProject(pid: string, uid: string) {
  if (!isValidPID(pid)) {
    throw new BadRequestException('The provided project ID is incorrect')
  }
  const project = await this.projectService.getFullProject(pid)
  if (!project) {
    throw new NotFoundException('Project not found')
  }
  this.projectService.allowedToManage(project, uid)
  return project
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/apps/cloud/src/project/proxy-domain.controller.ts` around lines 37 -
126, Extract the repeated pid validation, project load and authorization logic
used in list, create, remove and verify into a private helper (e.g.
loadManageableProject) that takes (pid: string, uid: string), runs isValidPID,
calls this.projectService.getFullProject(pid), throws
BadRequestException/NotFoundException as currently done, calls
this.projectService.allowedToManage(project, uid) and returns the project; then
replace the initial duplicate blocks in list, create, remove and verify with a
single await this.loadManageableProject(pid, uid) call to keep behavior
identical but remove duplication.
backend/apps/cloud/src/project/proxy-domain-edge.guard.ts (1)

22-30: Optional: avoid per-request warn spam when MANAGED_PROXY_EDGE_API_KEY is unset.

If the env var is missing in a managed deployment, every edge hit (including ACME probes and regular runtime /active checks) will emit a warn log. That can flood logs quickly. Consider either logging once on first miss (module-level latch) or resolving the expected secret in the constructor and failing fast at boot for managed deployments.

♻️ Illustrative change
 export class ProxyDomainEdgeGuard implements CanActivate {
   private readonly logger = new Logger(ProxyDomainEdgeGuard.name)
+  private warnedMissingSecret = false

   canActivate(context: ExecutionContext): boolean {
     const expected = process.env.MANAGED_PROXY_EDGE_API_KEY

     if (!expected) {
-      this.logger.warn(
-        'MANAGED_PROXY_EDGE_API_KEY is not configured; rejecting edge request',
-      )
+      if (!this.warnedMissingSecret) {
+        this.logger.warn(
+          'MANAGED_PROXY_EDGE_API_KEY is not configured; rejecting edge requests',
+        )
+        this.warnedMissingSecret = true
+      }
       throw new NotFoundException()
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/apps/cloud/src/project/proxy-domain-edge.guard.ts` around lines 22 -
30, The guard's canActivate currently warns on every request when
MANAGED_PROXY_EDGE_API_KEY is unset (in canActivate); change this to avoid
per-request warn spam by resolving the expected secret once and failing or
warning once: read process.env.MANAGED_PROXY_EDGE_API_KEY in the guard's
constructor (or at module top) and store it on the class (e.g., this.expected);
if running in a managed deployment and the secret is missing, either throw early
in the constructor to fail fast at boot or emit a single warning using a
module-level latch/boolean so subsequent calls to canActivate do not log; update
canActivate to reference the cached this.expected and remove the per-request
logger.warn call.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@backend/apps/cloud/src/project/proxy-domain-edge.guard.ts`:
- Around line 22-30: The guard's canActivate currently warns on every request
when MANAGED_PROXY_EDGE_API_KEY is unset (in canActivate); change this to avoid
per-request warn spam by resolving the expected secret once and failing or
warning once: read process.env.MANAGED_PROXY_EDGE_API_KEY in the guard's
constructor (or at module top) and store it on the class (e.g., this.expected);
if running in a managed deployment and the secret is missing, either throw early
in the constructor to fail fast at boot or emit a single warning using a
module-level latch/boolean so subsequent calls to canActivate do not log; update
canActivate to reference the cached this.expected and remove the per-request
logger.warn call.

In `@backend/apps/cloud/src/project/proxy-domain.controller.ts`:
- Around line 37-126: Extract the repeated pid validation, project load and
authorization logic used in list, create, remove and verify into a private
helper (e.g. loadManageableProject) that takes (pid: string, uid: string), runs
isValidPID, calls this.projectService.getFullProject(pid), throws
BadRequestException/NotFoundException as currently done, calls
this.projectService.allowedToManage(project, uid) and returns the project; then
replace the initial duplicate blocks in list, create, remove and verify with a
single await this.loadManageableProject(pid, uid) call to keep behavior
identical but remove duplication.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a9d24ce0-de8c-42dd-b947-675b87aab71e

📥 Commits

Reviewing files that changed from the base of the PR and between 3fb3d14 and 7ef8f87.

📒 Files selected for processing (3)
  • backend/.env.example
  • backend/apps/cloud/src/project/proxy-domain-edge.guard.ts
  • backend/apps/cloud/src/project/proxy-domain.controller.ts
✅ Files skipped from review due to trivial changes (1)
  • backend/.env.example

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

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 the current code and only fix it if needed.

Inline comments:
In `@backend/apps/cloud/src/project/proxy-domain.service.ts`:
- Around line 172-215: The create() path currently reserves a hostname as a
WAITING ProxyDomain and never frees it on verification failure, enabling
squatting; update create() (in proxy-domain.service.ts) to ignore or remove
expired unverified reservations when checking per-project limits and duplicate
hostnames by treating ProxyDomain rows with status WAITING (or ERROR) whose
statusChangedAt is older than your expiration threshold (e.g., 7 days) as
expired—either delete them inside the same transaction before counting/finding
duplicates or exclude them from projectDomains and duplicate checks so a new
owner can register the hostname; use the existing statusChangedAt,
ProxyDomainStatus enum, proxyTargetId and repo methods to implement this
cleanup/ignore logic atomically.

In `@web/app/pages/Project/Settings/components/ProxyDomainsTab.tsx`:
- Around line 330-333: The effect in ProxyDomainsTab currently only runs on
mount so when switching projects without remounting the UI shows stale domains
and uses stale IDs in settingsAction; update the useEffect that calls
fetchDomains to include projectId in its dependency array so it refetches
whenever projectId changes, and inside or before fetchDomains clear/reset the
local domains state (and any selectedDomainId) so previous project data isn't
visible during the load; ensure fetchDomains/settingsAction references the
current projectId variable (not a closed-over value) and consider aborting any
in-flight request if applicable.
🪄 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: 8676ed7e-f238-47ac-95fc-c537cb751051

📥 Commits

Reviewing files that changed from the base of the PR and between 3fb3d14 and 1f62048.

📒 Files selected for processing (8)
  • backend/.env.example
  • backend/apps/cloud/src/project/proxy-domain-edge.guard.ts
  • backend/apps/cloud/src/project/proxy-domain.controller.ts
  • backend/apps/cloud/src/project/proxy-domain.service.ts
  • docs/content/docs/adblockers/managed-proxy.mdx
  • web/app/pages/Project/Settings/components/ProxyDomainsTab.tsx
  • web/app/ui/icons/Cloudflare.tsx
  • web/public/locales/en.json
✅ Files skipped from review due to trivial changes (3)
  • backend/.env.example
  • web/app/ui/icons/Cloudflare.tsx
  • docs/content/docs/adblockers/managed-proxy.mdx
🚧 Files skipped from review as they are similar to previous changes (1)
  • backend/apps/cloud/src/project/proxy-domain-edge.guard.ts

Comment thread backend/apps/cloud/src/project/proxy-domain.service.ts
Comment thread web/app/pages/Project/Settings/components/ProxyDomainsTab.tsx
@Blaumaus Blaumaus merged commit 1530707 into main Apr 20, 2026
11 checks 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