Skip to content

refactor(identity): one source of truth for federation identity (#333, #326)#337

Merged
TemujinCalidius merged 1 commit into
devfrom
refactor/identity-accessor
Jul 23, 2026
Merged

refactor(identity): one source of truth for federation identity (#333, #326)#337
TemujinCalidius merged 1 commit into
devfrom
refactor/identity-accessor

Conversation

@TemujinCalidius

Copy link
Copy Markdown
Owner

Closes #333. Phase 0 of #326.

Why

SITE_URL / FEDI_HANDLE / FEDI_DOMAIN were read straight from process.env in 23 places and captured into module-level constants in 12 more:

// src/lib/federation.ts — evaluated once, at import
const siteUrl = siteConfig.url;

A module-level constant can never observe a value resolved at runtime, so a DB-backed identity is impossible until every consumer goes through one accessor. This is that accessor — still env-backed, so resolution is unchanged.

What

src/lib/identity.ts exposes the identity triple plus the three values that must agree with one another: actorId, keyId, webfingerSubject. site.config.ts now derives from it as well, so there is exactly one derivation in the codebase. http-signatures.ts takes keyId from it rather than rebuilding the string in two places.

It found a real bug

The WebFinger route resolved identity on its own:

const domain = process.env.FEDI_DOMAIN || "localhost";   // everything else derives this from SITE_URL

So on an instance with SITE_URL set but no FEDI_DOMAIN:

  • the site advertised @you@yourdomain.com on every page, served a valid actor, and federated posts
  • WebFinger — the endpoint every other server uses to find you — answered only to acct:you@localhost

Every lookup from Mastodon got a bare 404. Nobody could follow the instance, and nothing logged a problem — from the inside the site looked completely healthy. Exactly the silent-failure class #333 was filed to prevent, sitting in the code already.

Two smaller divergences fixed with it:

  • Trailing slashes stripped — an actor id differing by one slash is a different id to a remote server.
  • The domain fallback keeps a non-default port, matching what the setup wizard already writes (u.host). Dropping it sent remote lookups to :443, which isn't us.

Also: unreachable addresses in the wizard

Setup could be completed on localhost or a private range with only a warning. That bakes an identity nobody can follow into the actor id — and, because Post.apId stores absolute URLs, into every post published before the move.

Still permitted (testing locally is legitimate), but it now takes an explicit tick — "I'm only testing locally" — rather than a warning you can walk straight past. Enforced server-side; the checkbox only surfaces it. Covers loopback, 10/8, 172.16/12, 192.168/16, link-local and IPv6 unique-local.

Verification

Check Result
npx tsc --noEmit clean
npx eslint . 0 errors
npx vitest run 681 passed (73 files) — +25
npm run build clean
identity env reads outside identity.ts 0
module-level identity captures 0
changelog sync

New tests cover the invariant that matters — keyId stays anchored to actorId, and the WebFinger self href equals the actor id used for signing — plus the regression itself: a lookup that previously 404'd now resolves, and the bogus acct:me@localhost identity is gone.

update-profile.test.ts stubbed @/../site.config to inject an origin; identity no longer comes from there, so it now sets the environment variable the accessor actually reads.

What this does NOT do

No DB overlay, no UI, no behaviour change to how values resolve (beyond the three divergences above, each deliberate). Phases 1–3 of #326 — the overlay, the change-domain Move/alsoKnownAs flow, the admin UI — build on this.

🤖 Generated with Claude Code

…#326)

Phase 0 of #326. SITE_URL / FEDI_HANDLE / FEDI_DOMAIN were read directly from
process.env in 23 places and captured into module-level constants in 12 more. A
module-level `const siteUrl = process.env.SITE_URL` is evaluated at import, so it
can never observe a value resolved at runtime — which makes a DB-backed identity
impossible until everything goes through one accessor. This is that accessor,
still env-backed.

Adds src/lib/identity.ts exposing the identity triple plus the values that MUST
agree with each other: actorId, keyId and the WebFinger subject. site.config.ts
now derives from it too, so there is exactly one derivation in the codebase.
http-signatures.ts takes keyId from it directly rather than rebuilding the string.

This surfaced a real bug, now fixed. The WebFinger route resolved identity on its
own with `FEDI_DOMAIN || "localhost"`, while everything else derived the domain
from SITE_URL. An instance that set SITE_URL but not FEDI_DOMAIN therefore
advertised @you@yourdomain everywhere and answered WebFinger only for
acct:you@localhost — a bare 404 to every remote lookup. Undiscoverable and
unfollowable, with a site that looked entirely healthy from the inside and
nothing logged anywhere.

Two smaller divergences went with it: trailing slashes are stripped (an actor id
differing by a slash is a different id to a remote server), and the domain
fallback keeps a non-default port, matching what the setup wizard already writes
— dropping it sent remote lookups to :443, which isn't us.

Also gates unreachable addresses in the setup wizard. Finishing setup on
localhost or a private range bakes an unfollowable identity into the actor id,
and into every post published before moving, since Post.apId stores absolute
URLs. Still permitted — testing locally is legitimate — but it now requires an
explicit acknowledgement rather than a warning that can be walked past. Enforced
server-side; the checkbox only surfaces it.

update-profile.test.ts stubbed @/../site.config to inject the origin. Identity no
longer comes from there, so it now sets the environment variable the accessor
actually reads.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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