Skip to content

Add alternate WebFinger domain support#1821

Open
JohnONolan wants to merge 1 commit into
mainfrom
codex/alternate-webfinger-domain
Open

Add alternate WebFinger domain support#1821
JohnONolan wants to merge 1 commit into
mainfrom
codex/alternate-webfinger-domain

Conversation

@JohnONolan
Copy link
Copy Markdown
Member

@JohnONolan JohnONolan commented May 21, 2026

Summary

Adds support for alternate Social Web/WebFinger handle domains, so a Ghost site running ActivityPub from blog.site.com can expose the handle as @index@site.com while keeping the actor URL, keys, inbox/outbox URLs, and existing identity rooted at the original ActivityPub URL.

Ref implementation in Mastodon
https://docs-p.joinmastodon.org/admin/config/#web_domain

So (eg) on your root domain, like onolan.org you forward requests like:

location = /.well-known/webfinger {
  return 301 https://john.onolan.org$request_uri;
}

and they will resolve to the correct place.

Future PR to Ghost will add UI for making domain portion of your handle configurable, which will validate that the necessary redirect is in place before you can add it.

What changed

  • Added accounts.webfinger_host storage, generated hash lookup, and uniqueness for claimed custom handles.
  • Added account normalization, conflict checks, live WebFinger validation, and save/clear service methods.
  • Added authenticated GET/PUT /.ghost/activitypub/v1/account/domain endpoints.
  • Moved WebFinger handling before host-data middleware and added custom-domain lookup plus actor-host validation fallback.
  • Updated account/post/feed/notification/search DTO rendering to use the configured handle domain.
  • Added focused unit and integration coverage for validation, routing, persistence, WebFinger behavior, and custom-domain search.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 21, 2026

Review Change Stack

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

This PR adds support for custom WebFinger hosts on accounts. The implementation adds a webfinger_host column and hashes with indexes, extends Account entity/types and repository persistence, provides normalization and remote WebFinger validation in AccountService, exposes GET/PUT domain endpoints, updates WebFinger resolution to prefer configured hosts (including direct handle lookups), and propagates the host through queries and DTOs for handle construction while fixing route mounting order to avoid endpoint shadowing.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • TryGhost/ActivityPub#1815: Touches account search test setup related to FULLTEXT query behavior which can interact with webfinger-host-aware search.
  • TryGhost/ActivityPub#1804: Modifies route mounting/order comparator logic similar to the deterministic ordering added here.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 10.00% 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
Title check ✅ Passed The title 'Add alternate WebFinger domain support' clearly and concisely summarizes the main change—introducing support for alternate WebFinger/Social Web handle domains. It is specific, meaningful, and directly reflects the primary objective of the PR.
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.
Description check ✅ Passed The PR description clearly explains the feature (alternate WebFinger domain support), provides implementation context (Mastodon reference), documents configuration needs, and summarizes all key changes and test coverage.

✏️ 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 codex/alternate-webfinger-domain

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

@JohnONolan JohnONolan force-pushed the codex/alternate-webfinger-domain branch from 8bb9401 to d6e922a Compare May 21, 2026 07:04
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: 5

🤖 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 `@src/account/account.service.ts`:
- Around line 78-83: The WebfingerHostError union of string literals should be
replaced with structured error objects and the validateWebfingerHost() and
setWebfingerHost() flows should return those objects inside the existing Result
type; update the WebfingerHostError definition to an object-based discriminated
union (e.g., { kind: 'invalid-domain', host: string } | { kind: 'conflict',
host: string, conflictingActor?: string } | { kind: 'not-reachable', host:
string, cause?: Error } | { kind: 'invalid-webfinger', host: string, response?:
unknown } | { kind: 'wrong-actor', expected: string, actual: string }) and
change all returns in validateWebfingerHost and setWebfingerHost (and related
logic between lines ~270-410) to construct and return these error objects
instead of bare strings so callers receive contextual fields like host or actor
URL inside the Result error branch.
- Around line 442-459: The pre-create check uses
accountRepository.getByWebfingerHandle(draft.username, normalizedHost) which
misses accounts that implicitly own the handle via their domain (webfinger_host
=== null); replace that call and the subsequent conflict check with
accountRepository.hasWebfingerHandleConflict(draft.username, normalizedHost,
draft.apId) (or equivalent API on accountRepository) so it detects both explicit
webfinger_host and default-domain conflicts before creating in
createInternalAccount(), and add a regression test that sets up an existing
account with webfinger_host = null and domain = "www.example.com" to assert
creation of `@username`@www.example.com is rejected.

In `@src/account/utils.ts`:
- Line 1: This file fails the repository formatting checks across lines 1–134;
run the project's formatter (e.g., npm run format, prettier --write, or eslint
--fix as configured) on src/account/utils.ts to reformat imports and the entire
file so CI passes; ensure the top import (isIP from 'node:net') and all
functions/exports in this file are formatted consistently with the repo rules
before committing.

In `@src/http/api/webfinger.controller.ts`:
- Around line 67-70: The site lookup uses the original resourceHost instead of
the normalized value; update the fallback to call this.siteService.getSiteByHost
with normalizedResourceHost for both attempts (i.e., replace the second await
this.siteService.getSiteByHost(`www.${resourceHost}`) with await
this.siteService.getSiteByHost(`www.${normalizedResourceHost}`)) so the lookup
always uses the normalized host when using SiteService.getSiteByHost in the
WebfingerController logic.
- Around line 112-116: The current try/catch around
this.accountRepository.getBySite(site) swallows all errors and returns null,
turning backend failures into 404s; remove the blanket catch or replace it with
a targeted check that only maps known "not found" repository responses to null
(e.g. catch a specific NotFoundError or check error.code/message) and rethrow
any other exceptions so they propagate; update the code path around getBySite to
either let errors bubble up or explicitly rethrow unexpected errors while only
converting explicit not-found conditions to null.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 97c6e91b-efd3-4c6c-9ad1-4b292d993bb5

📥 Commits

Reviewing files that changed from the base of the PR and between fc34790 and 8bb9401.

📒 Files selected for processing (41)
  • migrate/migrations/000081_add-webfinger-host-to-accounts.down.sql
  • migrate/migrations/000081_add-webfinger-host-to-accounts.up.sql
  • src/account/account.entity.ts
  • src/account/account.repository.knex.integration.test.ts
  • src/account/account.repository.knex.ts
  • src/account/account.service.integration.test.ts
  • src/account/account.service.ts
  • src/account/account.service.unit.test.ts
  • src/account/types.ts
  • src/account/utils.ts
  • src/account/utils.unit.test.ts
  • src/app.ts
  • src/configuration/registrations.ts
  • src/dispatchers.unit.test.ts
  • src/feed/feed.service.ts
  • src/helpers/activitypub/activity.ts
  • src/http/api/account.controller.ts
  • src/http/api/account.controller.unit.test.ts
  • src/http/api/feed.controller.ts
  • src/http/api/feed.unit.test.ts
  • src/http/api/helpers/post.ts
  • src/http/api/notification.controller.ts
  • src/http/api/site.controller.ts
  • src/http/api/views/account.follows.view.ts
  • src/http/api/views/account.posts.view.ts
  • src/http/api/views/account.search.view.integration.test.ts
  • src/http/api/views/account.search.view.ts
  • src/http/api/views/account.view.ts
  • src/http/api/views/blocks.view.ts
  • src/http/api/views/explore.view.ts
  • src/http/api/views/recommendations.view.ts
  • src/http/api/views/reply.chain.view.ts
  • src/http/api/webfinger.controller.ts
  • src/http/api/webfinger.unit.test.ts
  • src/http/host-data-context-loader.ts
  • src/http/routing/route-registry.ts
  • src/http/routing/route-registry.unit.test.ts
  • src/notification/__snapshots__/get-notifications-data.json
  • src/notification/notification.service.ts
  • src/post/post.entity.ts
  • src/post/post.repository.knex.ts

Comment thread src/account/account.service.ts Outdated
Comment thread src/account/account.service.ts
Comment thread src/account/utils.ts
Comment thread src/http/api/webfinger.controller.ts
Comment thread src/http/api/webfinger.controller.ts
@JohnONolan JohnONolan force-pushed the codex/alternate-webfinger-domain branch from d6e922a to 493a04a Compare May 21, 2026 07:14
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: 3

🧹 Nitpick comments (1)
src/http/api/webfinger.controller.ts (1)

57-72: 🏗️ Heavy lift

Move the new account-resolution path behind a service.

This change adds more repository-driven lookup logic in the controller, which pushes account resolution rules into the HTTP layer. Please keep the controller focused on request/response handling and expose this lookup flow through an AccountService method instead.

As per coding guidelines, "Repositories should handle all data access and not be used directly by controllers; use services as the abstraction layer."

🤖 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 `@src/http/api/webfinger.controller.ts` around lines 57 - 72, The controller
currently performs repository/service lookups
(accountRepository.getByWebfingerHandle, siteService.getSiteByHost and
getAccountBySiteOrNull) to resolve accounts; move this lookup logic into a new
AccountService method (e.g., resolveAccountForWebfinger(resourceUsername,
normalizedResourceHost, resourceHost)) that encapsulates the custom-domain
lookup, fallback to site/www.host, and account resolution, and returns either
the resolved account or null. Replace the inline logic in the controller with a
single call to AccountService.resolveAccountForWebfinger and then call
createWebfingerResponse only if that service returns an account; remove direct
repository calls from the controller so the controller only handles
request/response flow.
🤖 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 `@src/http/api/account.controller.ts`:
- Around line 445-469: The route decorators APIRoute('GET', 'account/domain')
and APIRoute('PUT', 'account/domain') on the current-user handlers
handleGetAccountDomain and handleUpdateAccountDomain should be changed to
top-level paths (e.g., 'domain') because these operate on the current user, not
a specific account; update the APIRoute values to 'domain', ensure any
references to accountDomainResponse remain valid, and adjust any
routing/README/OpenAPI docs or tests that reference the old 'account/domain'
paths to the new top-level route.

In `@src/http/api/site.controller.ts`:
- Around line 37-45: The responseBody currently sets domain to
account.webfingerHost which can be null; change it to use the same fallback as
handle by assigning domain using getAccountHandleHost(account) (the same
function used to derive handle) so domain always returns the actor host when no
custom WebFinger host exists; update the response construction in the block that
builds responseBody (where domain, handle, actorUrl are set) to use
getAccountHandleHost(account) for domain.

In `@src/http/api/views/recommendations.view.ts`:
- Around line 87-91: The query exposes the effective account domain via
COALESCE(accounts.webfinger_host, accounts.domain) but still joins domain_blocks
on accounts.domain_hash, causing mismatches when a webfinger_host is used;
update the domain_blocks join to use the effective-hash equivalent
COALESCE(accounts.webfinger_host_hash, accounts.domain_hash) so the block check
uses the same resolved domain as the select (adjust the join condition where
domain_blocks is joined to reference COALESCE(accounts.webfinger_host_hash,
accounts.domain_hash) instead of accounts.domain_hash).

---

Nitpick comments:
In `@src/http/api/webfinger.controller.ts`:
- Around line 57-72: The controller currently performs repository/service
lookups (accountRepository.getByWebfingerHandle, siteService.getSiteByHost and
getAccountBySiteOrNull) to resolve accounts; move this lookup logic into a new
AccountService method (e.g., resolveAccountForWebfinger(resourceUsername,
normalizedResourceHost, resourceHost)) that encapsulates the custom-domain
lookup, fallback to site/www.host, and account resolution, and returns either
the resolved account or null. Replace the inline logic in the controller with a
single call to AccountService.resolveAccountForWebfinger and then call
createWebfingerResponse only if that service returns an account; remove direct
repository calls from the controller so the controller only handles
request/response flow.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d31d9dad-43e5-44c6-b069-5be7b2f8fd4f

📥 Commits

Reviewing files that changed from the base of the PR and between 8bb9401 and d6e922a.

📒 Files selected for processing (41)
  • migrate/migrations/000081_add-webfinger-host-to-accounts.down.sql
  • migrate/migrations/000081_add-webfinger-host-to-accounts.up.sql
  • src/account/account.entity.ts
  • src/account/account.repository.knex.integration.test.ts
  • src/account/account.repository.knex.ts
  • src/account/account.service.integration.test.ts
  • src/account/account.service.ts
  • src/account/account.service.unit.test.ts
  • src/account/types.ts
  • src/account/utils.ts
  • src/account/utils.unit.test.ts
  • src/app.ts
  • src/configuration/registrations.ts
  • src/dispatchers.unit.test.ts
  • src/feed/feed.service.ts
  • src/helpers/activitypub/activity.ts
  • src/http/api/account.controller.ts
  • src/http/api/account.controller.unit.test.ts
  • src/http/api/feed.controller.ts
  • src/http/api/feed.unit.test.ts
  • src/http/api/helpers/post.ts
  • src/http/api/notification.controller.ts
  • src/http/api/site.controller.ts
  • src/http/api/views/account.follows.view.ts
  • src/http/api/views/account.posts.view.ts
  • src/http/api/views/account.search.view.integration.test.ts
  • src/http/api/views/account.search.view.ts
  • src/http/api/views/account.view.ts
  • src/http/api/views/blocks.view.ts
  • src/http/api/views/explore.view.ts
  • src/http/api/views/recommendations.view.ts
  • src/http/api/views/reply.chain.view.ts
  • src/http/api/webfinger.controller.ts
  • src/http/api/webfinger.unit.test.ts
  • src/http/host-data-context-loader.ts
  • src/http/routing/route-registry.ts
  • src/http/routing/route-registry.unit.test.ts
  • src/notification/__snapshots__/get-notifications-data.json
  • src/notification/notification.service.ts
  • src/post/post.entity.ts
  • src/post/post.repository.knex.ts
✅ Files skipped from review due to trivial changes (3)
  • src/account/account.repository.knex.integration.test.ts
  • src/dispatchers.unit.test.ts
  • src/http/api/notification.controller.ts

Comment thread src/http/api/account.controller.ts Outdated
Comment thread src/http/api/site.controller.ts Outdated
Comment thread src/http/api/views/recommendations.view.ts
@JohnONolan JohnONolan force-pushed the codex/alternate-webfinger-domain branch from 493a04a to 4f52fea Compare May 21, 2026 07:29
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.

Caution

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

⚠️ Outside diff range comments (1)
src/http/api/views/account.search.view.ts (1)

158-167: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Domain-block filtering misses custom WebFinger domains.

Line 158 still joins domain_blocks only via accounts.domain_hash. After introducing webfinger_host-based handles, accounts blocked by their custom handle domain can leak into search results.

Suggested fix
 .leftJoin('domain_blocks', function () {
-    this.on(
-        'domain_blocks.domain_hash',
-        'accounts.domain_hash',
-    ).andOnVal(
-        'domain_blocks.blocker_id',
-        '=',
-        viewerAccountId.toString(),
-    );
+    this.on(function () {
+        this.on('domain_blocks.domain_hash', 'accounts.domain_hash').orOn(
+            'domain_blocks.domain_hash',
+            'accounts.webfinger_host_hash',
+        );
+    }).andOnVal('domain_blocks.blocker_id', '=', viewerAccountId.toString());
 })
🤖 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 `@src/http/api/views/account.search.view.ts` around lines 158 - 167, The
leftJoin on 'domain_blocks' currently only matches domain_blocks.domain_hash to
accounts.domain_hash, which misses custom WebFinger domains stored in
accounts.webfinger_host; update the join condition in the leftJoin callback so
domain_blocks.domain_hash is matched against both accounts.domain_hash and
accounts.webfinger_host (e.g., keep the existing
this.on('domain_blocks.domain_hash','accounts.domain_hash').orOn('domain_blocks.domain_hash','accounts.webfinger_host'))
while preserving the this.andOnVal('domain_blocks.blocker_id','=',
viewerAccountId.toString()) condition so blocks by the viewer are correctly
applied.
♻️ Duplicate comments (4)
src/http/api/views/recommendations.view.ts (1)

87-91: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Align domain-block filtering with the effective handle domain.

You now expose domain from COALESCE(accounts.webfinger_host, accounts.domain), but the domain_blocks join (Line 126) still checks accounts.domain_hash. That can let accounts with blocked custom WebFinger domains slip into recommendations.

Suggested fix
 .leftJoin('domain_blocks', function () {
-    this.on(
-        'domain_blocks.domain_hash',
-        'accounts.domain_hash',
-    ).andOnVal(
+    this.on(
+        'domain_blocks.domain_hash',
+        this.db.raw(
+            'COALESCE(accounts.webfinger_host_hash, accounts.domain_hash)',
+        ),
+    ).andOnVal(
         'domain_blocks.blocker_id',
         '=',
         viewerAccountId.toString(),
     );
 })
🤖 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 `@src/http/api/views/recommendations.view.ts` around lines 87 - 91, The query
exposes domain via COALESCE(accounts.webfinger_host, accounts.domain) but the
domain_blocks join still filters on accounts.domain_hash, allowing blocked
webfinger domains to bypass filtering; update the domain_blocks join condition
to use the hash of the effective domain (i.e.
hash(COALESCE(accounts.webfinger_host, accounts.domain))) instead of
accounts.domain_hash so the same computed domain value used in the select is
used for blocking checks; locate the COALESCE usage in the select, the
domain_blocks join condition, and the accounts.webfinger_host/accounts.domain
fields to implement the replacement.
src/http/api/webfinger.controller.ts (2)

121-125: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don’t turn repository failures into 404s.

This catch-all converts any getBySite() failure into null, so DB/query outages become a misleading not-found response. Only explicit not-found conditions should map to null; everything else should bubble.

Suggested fix
     private async getAccountBySiteOrNull(site: {
         id: number;
         host: string;
         webhook_secret: string;
         ghost_uuid: string | null;
     }) {
-        try {
-            return await this.accountRepository.getBySite(site);
-        } catch (_error) {
-            return null;
-        }
+        return await this.accountRepository.getBySite(site);
     }
🤖 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 `@src/http/api/webfinger.controller.ts` around lines 121 - 125, The current
try/catch around this.accountRepository.getBySite(site) in the webfinger
controller swallows all errors and returns null; change it so only repository
"not found" cases are converted to null and all other exceptions are rethrown.
Specifically, update the getBySite call handling in the method that uses
this.accountRepository.getBySite to inspect the thrown error (e.g., check for a
NotFoundError, record not found sentinel, or error.code === 'NOT_FOUND') and
return null only for that case; for any other error rethrow or propagate the
original error so outages/DB errors are not masked.

67-69: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Reuse normalizedResourceHost for the site fallback lookup.

The controller normalizes the requested host, but this branch still queries SiteService with the raw resourceHost. Requests like mixed-case hosts can pass validation and still false-404 here.

Suggested fix
-        const site =
-            (await this.siteService.getSiteByHost(resourceHost)) ||
-            (await this.siteService.getSiteByHost(`www.${resourceHost}`));
+        const site =
+            (await this.siteService.getSiteByHost(normalizedResourceHost)) ||
+            (await this.siteService.getSiteByHost(
+                `www.${normalizedResourceHost}`,
+            ));
🤖 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 `@src/http/api/webfinger.controller.ts` around lines 67 - 69, The site lookup
uses the raw resourceHost for the fallback which can mismatch normalized casing;
change the second getSiteByHost call to use the already-normalized
normalizedResourceHost (i.e. call
this.siteService.getSiteByHost(`www.${normalizedResourceHost}`)) so the fallback
lookup and the initial lookup both use the normalized host; update the code
around the WebfingerController's site resolution (variables:
normalizedResourceHost, resourceHost, site, and method
siteService.getSiteByHost) accordingly.
src/http/api/account.controller.ts (1)

445-469: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Move the current-user domain endpoints to top-level paths.

These handlers operate on the current account, so account/domain breaks the route namespace convention used elsewhere in this codebase.

Suggested fix
-    `@APIRoute`('GET', 'account/domain')
+    `@APIRoute`('GET', 'domain')
@@
-    `@APIRoute`('PUT', 'account/domain')
+    `@APIRoute`('PUT', 'domain')

As per coding guidelines "HTTP routes that operate on the current user should live at top-level paths (e.g., aliases, blocks/accounts, notifications); routes that operate on a specific account should use account/:handle namespace; don't nest current-user resources under account/...."

🤖 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 `@src/http/api/account.controller.ts` around lines 445 - 469, The current-user
domain endpoints are incorrectly namespaced under 'account/domain'; update the
APIRoute decorators on handleGetAccountDomain and handleUpdateAccountDomain to
use the top-level path (e.g., change '`@APIRoute`('GET', 'account/domain')' to
'`@APIRoute`('GET', 'domain')' and similarly for the PUT) so they follow the
project's convention for current-user resources, and ensure any route
metadata/docs or tests referencing 'account/domain' are updated to the new
'domain' top-level path.
🤖 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 `@src/http/api/views/account.search.view.ts`:
- Around line 158-167: The leftJoin on 'domain_blocks' currently only matches
domain_blocks.domain_hash to accounts.domain_hash, which misses custom WebFinger
domains stored in accounts.webfinger_host; update the join condition in the
leftJoin callback so domain_blocks.domain_hash is matched against both
accounts.domain_hash and accounts.webfinger_host (e.g., keep the existing
this.on('domain_blocks.domain_hash','accounts.domain_hash').orOn('domain_blocks.domain_hash','accounts.webfinger_host'))
while preserving the this.andOnVal('domain_blocks.blocker_id','=',
viewerAccountId.toString()) condition so blocks by the viewer are correctly
applied.

---

Duplicate comments:
In `@src/http/api/account.controller.ts`:
- Around line 445-469: The current-user domain endpoints are incorrectly
namespaced under 'account/domain'; update the APIRoute decorators on
handleGetAccountDomain and handleUpdateAccountDomain to use the top-level path
(e.g., change '`@APIRoute`('GET', 'account/domain')' to '`@APIRoute`('GET',
'domain')' and similarly for the PUT) so they follow the project's convention
for current-user resources, and ensure any route metadata/docs or tests
referencing 'account/domain' are updated to the new 'domain' top-level path.

In `@src/http/api/views/recommendations.view.ts`:
- Around line 87-91: The query exposes domain via
COALESCE(accounts.webfinger_host, accounts.domain) but the domain_blocks join
still filters on accounts.domain_hash, allowing blocked webfinger domains to
bypass filtering; update the domain_blocks join condition to use the hash of the
effective domain (i.e. hash(COALESCE(accounts.webfinger_host, accounts.domain)))
instead of accounts.domain_hash so the same computed domain value used in the
select is used for blocking checks; locate the COALESCE usage in the select, the
domain_blocks join condition, and the accounts.webfinger_host/accounts.domain
fields to implement the replacement.

In `@src/http/api/webfinger.controller.ts`:
- Around line 121-125: The current try/catch around
this.accountRepository.getBySite(site) in the webfinger controller swallows all
errors and returns null; change it so only repository "not found" cases are
converted to null and all other exceptions are rethrown. Specifically, update
the getBySite call handling in the method that uses
this.accountRepository.getBySite to inspect the thrown error (e.g., check for a
NotFoundError, record not found sentinel, or error.code === 'NOT_FOUND') and
return null only for that case; for any other error rethrow or propagate the
original error so outages/DB errors are not masked.
- Around line 67-69: The site lookup uses the raw resourceHost for the fallback
which can mismatch normalized casing; change the second getSiteByHost call to
use the already-normalized normalizedResourceHost (i.e. call
this.siteService.getSiteByHost(`www.${normalizedResourceHost}`)) so the fallback
lookup and the initial lookup both use the normalized host; update the code
around the WebfingerController's site resolution (variables:
normalizedResourceHost, resourceHost, site, and method
siteService.getSiteByHost) accordingly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 490bd08b-2f0b-4d0f-8c23-7646322366ec

📥 Commits

Reviewing files that changed from the base of the PR and between d6e922a and 4f52fea.

📒 Files selected for processing (42)
  • migrate/migrations/000081_add-webfinger-host-to-accounts.down.sql
  • migrate/migrations/000081_add-webfinger-host-to-accounts.up.sql
  • src/account/account.entity.ts
  • src/account/account.repository.knex.integration.test.ts
  • src/account/account.repository.knex.ts
  • src/account/account.service.integration.test.ts
  • src/account/account.service.ts
  • src/account/account.service.unit.test.ts
  • src/account/types.ts
  • src/account/utils.ts
  • src/account/utils.unit.test.ts
  • src/app.ts
  • src/configuration/registrations.ts
  • src/dispatchers.unit.test.ts
  • src/feed/feed.service.ts
  • src/helpers/activitypub/activity.ts
  • src/http/api/account.controller.ts
  • src/http/api/account.controller.unit.test.ts
  • src/http/api/feed.controller.ts
  • src/http/api/feed.unit.test.ts
  • src/http/api/helpers/post.ts
  • src/http/api/notification.controller.ts
  • src/http/api/site.controller.ts
  • src/http/api/views/account.follows.view.ts
  • src/http/api/views/account.posts.view.ts
  • src/http/api/views/account.search.view.integration.test.ts
  • src/http/api/views/account.search.view.ts
  • src/http/api/views/account.view.ts
  • src/http/api/views/blocks.view.ts
  • src/http/api/views/explore.view.ts
  • src/http/api/views/recommendations.view.ts
  • src/http/api/views/reply.chain.view.ts
  • src/http/api/webfinger.controller.ts
  • src/http/api/webfinger.unit.test.ts
  • src/http/host-data-context-loader.ts
  • src/http/routing/route-registry.ts
  • src/http/routing/route-registry.unit.test.ts
  • src/notification/__snapshots__/get-notifications-data.json
  • src/notification/notification.service.ts
  • src/post/post.entity.ts
  • src/post/post.repository.knex.integration.test.ts
  • src/post/post.repository.knex.ts
✅ Files skipped from review due to trivial changes (1)
  • src/helpers/activitypub/activity.ts

@JohnONolan JohnONolan force-pushed the codex/alternate-webfinger-domain branch from 4f52fea to 27d8de5 Compare May 21, 2026 07:41
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: 3

🧹 Nitpick comments (1)
src/http/api/webfinger.controller.ts (1)

117-128: 💤 Low value

Backend failures are still swallowed and converted to 404s.

The try/catch returns null for any error from getBySite(), which masks backend failures (database outages, connection errors) as "not found" responses. Consider either letting unexpected errors propagate or only catching specific not-found conditions.

🤖 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 `@src/http/api/webfinger.controller.ts` around lines 117 - 128, The helper
getAccountBySiteOrNull currently swallows all errors from
accountRepository.getBySite(), turning backend failures into not-found results;
change it to only convert a "not found" condition to null (e.g., detect a
specific NotFoundError/RowNotFound error from getBySite()) and rethrow any other
errors so they propagate as server errors. Locate getAccountBySiteOrNull and
accountRepository.getBySite and update the catch to inspect the error
type/message and return null only for the not-found case, otherwise throw the
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 `@src/http/api/site.controller.ts`:
- Around line 33-45: When enriching the site payload in the block that calls
this.accountService.getAccountForSite(site), guard against a null/undefined
account before accessing account.username or account.apId.href; if account is
falsy, build responseBody using the original site (or sensible defaults) and
skip calling getAccountHandleHost/getAccountHandle and reading
account.apId.href, otherwise proceed with the current enrichment logic. Locate
the code around responseBody, the call to this.accountService.getAccountForSite,
and the helpers getAccountHandleHost/getAccountHandle to implement the
conditional check and fallback behavior.

In `@src/http/api/views/blocks.view.ts`:
- Around line 18-22: The code selects an effective domain using
COALESCE(accounts.webfinger_host, accounts.domain) but still joins domain_blocks
on accounts.domain, causing domainBlockedByMe to be computed against the raw
domain instead of the effective (webfinger) domain; update the domain_blocks
join condition and any references used to compute domainBlockedByMe to use the
same COALESCE(accounts.webfinger_host, accounts.domain) expression or the alias
produced by the select so both the selected "domain" and the join/filter use the
identical effective-domain value (look for the COALESCE(...) select and the join
against domain_blocks to change the join condition to use that COALESCE/alias).

In `@src/http/api/views/explore.view.ts`:
- Around line 27-31: The query selects the effective handle domain via
COALESCE(accounts.webfinger_host, accounts.domain) but the block join still
checks accounts.domain_hash, which can miss blocks for custom webfinger_host
values; update the block join used in the Explore query to compare against the
hash of the same COALESCE(accounts.webfinger_host, accounts.domain) expression
(i.e., compute the effective domain the same way the select does) instead of
accounts.domain_hash so blocked custom WebFinger domains are filtered
consistently (locate the join/filter that references accounts.domain_hash in
this file and replace its input with the hashed
COALESCE(accounts.webfinger_host, accounts.domain) expression).

---

Nitpick comments:
In `@src/http/api/webfinger.controller.ts`:
- Around line 117-128: The helper getAccountBySiteOrNull currently swallows all
errors from accountRepository.getBySite(), turning backend failures into
not-found results; change it to only convert a "not found" condition to null
(e.g., detect a specific NotFoundError/RowNotFound error from getBySite()) and
rethrow any other errors so they propagate as server errors. Locate
getAccountBySiteOrNull and accountRepository.getBySite and update the catch to
inspect the error type/message and return null only for the not-found case,
otherwise throw the error.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 31632d14-80b9-4053-9da5-12908d190a6e

📥 Commits

Reviewing files that changed from the base of the PR and between 4f52fea and 27d8de5.

📒 Files selected for processing (43)
  • migrate/migrations/000081_add-webfinger-host-to-accounts.down.sql
  • migrate/migrations/000081_add-webfinger-host-to-accounts.up.sql
  • src/account/account.entity.ts
  • src/account/account.repository.knex.integration.test.ts
  • src/account/account.repository.knex.ts
  • src/account/account.service.integration.test.ts
  • src/account/account.service.ts
  • src/account/account.service.unit.test.ts
  • src/account/types.ts
  • src/account/utils.ts
  • src/account/utils.unit.test.ts
  • src/app.ts
  • src/configuration/registrations.ts
  • src/dispatchers.unit.test.ts
  • src/feed/feed.service.ts
  • src/helpers/activitypub/activity.ts
  • src/http/api/account.controller.ts
  • src/http/api/account.controller.unit.test.ts
  • src/http/api/feed.controller.ts
  • src/http/api/feed.unit.test.ts
  • src/http/api/helpers/post.ts
  • src/http/api/notification.controller.ts
  • src/http/api/site.controller.ts
  • src/http/api/views/account.follows.view.ts
  • src/http/api/views/account.posts.view.ts
  • src/http/api/views/account.search.view.integration.test.ts
  • src/http/api/views/account.search.view.ts
  • src/http/api/views/account.view.ts
  • src/http/api/views/blocks.view.ts
  • src/http/api/views/explore.view.ts
  • src/http/api/views/recommendations.view.integration.test.ts
  • src/http/api/views/recommendations.view.ts
  • src/http/api/views/reply.chain.view.ts
  • src/http/api/webfinger.controller.ts
  • src/http/api/webfinger.unit.test.ts
  • src/http/host-data-context-loader.ts
  • src/http/routing/route-registry.ts
  • src/http/routing/route-registry.unit.test.ts
  • src/notification/__snapshots__/get-notifications-data.json
  • src/notification/notification.service.ts
  • src/post/post.entity.ts
  • src/post/post.repository.knex.integration.test.ts
  • src/post/post.repository.knex.ts
✅ Files skipped from review due to trivial changes (4)
  • src/http/routing/route-registry.unit.test.ts
  • src/http/api/views/account.follows.view.ts
  • src/helpers/activitypub/activity.ts
  • src/http/api/feed.unit.test.ts

Comment thread src/http/api/site.controller.ts Outdated
Comment thread src/http/api/views/blocks.view.ts
Comment thread src/http/api/views/explore.view.ts
@JohnONolan JohnONolan force-pushed the codex/alternate-webfinger-domain branch 2 times, most recently from f1ad394 to 3ff9533 Compare May 21, 2026 08:02
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

♻️ Duplicate comments (1)
src/http/api/site.controller.ts (1)

33-45: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Guard account before building the enriched response.

getAccountForSite(site) can return nullish; the current dereference path throws and turns this endpoint into a 500.

Proposed minimal fix
             if (this.accountService) {
                 const account =
                     await this.accountService.getAccountForSite(site);

-                responseBody = {
-                    ...site,
-                    domain: getAccountHandleHost(account),
-                    handle: getAccountHandle(
-                        getAccountHandleHost(account),
-                        account.username,
-                    ),
-                    actorUrl: account.apId.href,
-                };
+                if (account) {
+                    const handleHost = getAccountHandleHost(account);
+                    responseBody = {
+                        ...site,
+                        domain: handleHost,
+                        handle: getAccountHandle(
+                            handleHost,
+                            account.username,
+                        ),
+                        actorUrl: account.apId.href,
+                    };
+                }
             }
🤖 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 `@src/http/api/site.controller.ts` around lines 33 - 45, The code dereferences
account returned from this.accountService.getAccountForSite(site) without
guarding for null/undefined which can throw; update the block that builds
responseBody so it checks that account is truthy before calling
getAccountHandleHost, getAccountHandle, or accessing account.apId.href (methods:
getAccountForSite, getAccountHandleHost, getAccountHandle, and the responseBody
construction). If account is nullish, supply safe fallbacks (e.g., omit
domain/handle/actorUrl or use empty strings/defaults) or return the original
site object unchanged; ensure responseBody construction only uses account fields
when account exists to avoid a 500.
🧹 Nitpick comments (1)
src/http/api/feed.unit.test.ts (1)

64-69: ⚡ Quick win

Add a focused assertion for WebFinger-host precedence.

These fixtures only cover null WebFinger hosts, so the new precedence path isn’t directly validated. Please add one test where author_webfinger_host/reposter_webfinger_host are set and assert body.posts[n].author.handle and body.posts[n].repostedBy.handle explicitly.

As per coding guidelines, “Assertions should target the specific field being tested, not stringify the whole object; prefer expect(result.field).toEqual(...)...”.

Also applies to: 108-113

🤖 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 `@src/http/api/feed.unit.test.ts` around lines 64 - 69, Add a focused test case
in src/http/api/feed.unit.test.ts that includes non-null author_webfinger_host
and reposter_webfinger_host values in the fixture rows (instead of null) and
assert the computed handles directly using
expect(body.posts[i].author.handle).toEqual(...) and
expect(body.posts[i].repostedBy.handle).toEqual(...); target the specific post
index you want to validate and avoid stringifying the whole post object—this
verifies WebFinger-host precedence for author and reposter handling.
🤖 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 `@src/account/account.repository.knex.ts`:
- Around line 427-464: getByWebfingerHandle only matches accounts.username,
causing custom-domain lookups like acct:index@custom.example to fail after
display-name changes; update the query in getByWebfingerHandle to also allow
matching the final path segment of accounts.ap_id (the actor path username) in
addition to accounts.username when the webfinger_host_hash matches: add an OR
condition that compares LOWER(SUBSTRING_INDEX(accounts.ap_id, '/', -1)) (or
equivalent SQL to extract the last path segment) to LOWER(?) (the incoming
username) while preserving the existing webfinger_host_hash check and still
returning the same selected columns so mapRowToAccountEntity can be used
unchanged.

---

Duplicate comments:
In `@src/http/api/site.controller.ts`:
- Around line 33-45: The code dereferences account returned from
this.accountService.getAccountForSite(site) without guarding for null/undefined
which can throw; update the block that builds responseBody so it checks that
account is truthy before calling getAccountHandleHost, getAccountHandle, or
accessing account.apId.href (methods: getAccountForSite, getAccountHandleHost,
getAccountHandle, and the responseBody construction). If account is nullish,
supply safe fallbacks (e.g., omit domain/handle/actorUrl or use empty
strings/defaults) or return the original site object unchanged; ensure
responseBody construction only uses account fields when account exists to avoid
a 500.

---

Nitpick comments:
In `@src/http/api/feed.unit.test.ts`:
- Around line 64-69: Add a focused test case in src/http/api/feed.unit.test.ts
that includes non-null author_webfinger_host and reposter_webfinger_host values
in the fixture rows (instead of null) and assert the computed handles directly
using expect(body.posts[i].author.handle).toEqual(...) and
expect(body.posts[i].repostedBy.handle).toEqual(...); target the specific post
index you want to validate and avoid stringifying the whole post object—this
verifies WebFinger-host precedence for author and reposter handling.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: edfac374-2b0b-4135-948a-a150a4135b1e

📥 Commits

Reviewing files that changed from the base of the PR and between 27d8de5 and f1ad394.

📒 Files selected for processing (43)
  • migrate/migrations/000081_add-webfinger-host-to-accounts.down.sql
  • migrate/migrations/000081_add-webfinger-host-to-accounts.up.sql
  • src/account/account.entity.ts
  • src/account/account.repository.knex.integration.test.ts
  • src/account/account.repository.knex.ts
  • src/account/account.service.integration.test.ts
  • src/account/account.service.ts
  • src/account/account.service.unit.test.ts
  • src/account/types.ts
  • src/account/utils.ts
  • src/account/utils.unit.test.ts
  • src/app.ts
  • src/configuration/registrations.ts
  • src/dispatchers.unit.test.ts
  • src/feed/feed.service.ts
  • src/helpers/activitypub/activity.ts
  • src/http/api/account.controller.ts
  • src/http/api/account.controller.unit.test.ts
  • src/http/api/feed.controller.ts
  • src/http/api/feed.unit.test.ts
  • src/http/api/helpers/post.ts
  • src/http/api/notification.controller.ts
  • src/http/api/site.controller.ts
  • src/http/api/views/account.follows.view.ts
  • src/http/api/views/account.posts.view.ts
  • src/http/api/views/account.search.view.integration.test.ts
  • src/http/api/views/account.search.view.ts
  • src/http/api/views/account.view.ts
  • src/http/api/views/blocks.view.ts
  • src/http/api/views/explore.view.ts
  • src/http/api/views/recommendations.view.integration.test.ts
  • src/http/api/views/recommendations.view.ts
  • src/http/api/views/reply.chain.view.ts
  • src/http/api/webfinger.controller.ts
  • src/http/api/webfinger.unit.test.ts
  • src/http/host-data-context-loader.ts
  • src/http/routing/route-registry.ts
  • src/http/routing/route-registry.unit.test.ts
  • src/notification/__snapshots__/get-notifications-data.json
  • src/notification/notification.service.ts
  • src/post/post.entity.ts
  • src/post/post.repository.knex.integration.test.ts
  • src/post/post.repository.knex.ts
✅ Files skipped from review due to trivial changes (2)
  • src/dispatchers.unit.test.ts
  • src/notification/snapshots/get-notifications-data.json

Comment thread src/account/account.repository.knex.ts
Allow Ghost ActivityPub sites hosted on subdomains to expose handles on an alternate domain while preserving the existing actor URL and keys.

This adds nullable account-level WebFinger host storage, strict live validation, authenticated domain APIs, WebFinger lookup behavior before host-data loading, and handle rendering updates across API views.

Validation remains synchronous so a custom handle is only saved after the requested domain resolves to the current actor.
@JohnONolan JohnONolan force-pushed the codex/alternate-webfinger-domain branch from 3ff9533 to 020e60f Compare May 21, 2026 08:18
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