Skip to content

feat(federation): NodeInfo + browser fallback for actor URLs (#1047) - #1048

Merged
fiddur merged 1 commit into
developfrom
1047-nodeinfo-actor-html
Aug 21, 2026
Merged

feat(federation): NodeInfo + browser fallback for actor URLs (#1047)#1048
fiddur merged 1 commit into
developfrom
1047-nodeinfo-actor-html

Conversation

@fiddur

@fiddur fiddur commented Aug 21, 2026

Copy link
Copy Markdown
Owner

Closes #1047 (user report relayed by Fredrik; verified live on aurboda.net).

Before: /.well-known/nodeinfo fell through nginx to the SPA and answered 200 with HTML (worse than a 404 for crawlers); GET /users/fiddur with a browser Accept header answered 406 Not acceptable. WebFinger and AP-Accept actor fetches were fine.

Changes:

  • NodeInfo (federation.setNodeInfoDispatcher('/nodeinfo/2.1', …)): software aurboda + BUILD_SHA version, activitypub protocol; Fedify serves the /.well-known/nodeinfo JRD automatically. Usage stats deliberately absent/zero — the per-user-database architecture has no cheap global counts, and NodeInfo allows omitting them. New nginx locations proxy both paths.
  • Browser fallback for actor URLs: new actor-html-router mounted right after integrateFederation (same fallthrough pattern as the feed tombstone router) — an HTML-preferring GET /users/:username 302-redirects to the /u/:username public profile page; AP requests and malformed usernames fall through untouched, so a fetch of a nonexistent actor still 404s.
  • Actor url property set to the profile page, so Mastodon-class clients link humans there directly.

Note for the FitPub thread (fitpub#455): FitPub gates follow-ability on NodeInfo software identity (name: fitpub), so this alone doesn't make Aurboda followable from FitPub — but it gives their side a standard hook to extend.

Tests: 4 new unit tests (redirect/fallthrough matrix), 2 new integration tests (JRD + 2.1 document) + actor url assertion; whole-monorepo pnpm check green, federation integration suite 23/23.

🤖 Generated with Claude Code

https://claude.ai/code/session_01CwoP1SqJhHgHiEEoEQtUjT

…1047)

User report: no NodeInfo (the well-known path even answered 200 with SPA
HTML via nginx fallthrough) and GET /users/{user} answered 406 to browsers.

- setNodeInfoDispatcher('/nodeinfo/2.1'): software aurboda + BUILD_SHA,
  activitypub protocol; no usage stats (per-user DBs have no cheap counts)
- nginx locations for /.well-known/nodeinfo + /nodeinfo/
- actor-html-router: browser-Accept GET /users/:username → 302 /u/:username
  (same post-integrateFederation fallthrough pattern as the tombstone router)
- actor Person gains url = profile page, so clients link humans there

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CwoP1SqJhHgHiEEoEQtUjT
@fiddur
fiddur marked this pull request as ready for review August 21, 2026 16:03

@fiddur fiddur left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

✅ Approved

Solid, well-scoped slice. I traced the whole fall-through path against the installed @fedify/express@2.3.1 and @fedify/fedify@2.3.1 rather than just the diff:

  • acceptsJsonLd returns false when the top-ranked type is text/html, so a browser hit really does land in onNotAcceptable, which next()s. The new router then matches, and — importantly — because it is a route (router.get), req.route is set, so the integration's if (notFound || notAcceptable && req.route != null) return skips writing its own 406 over your 302. No double-write / ERR_HTTP_HEADERS_SENT. Same mechanism the tombstone router relies on.
  • nodeInfoToJson validates software.name against /^[a-z0-9-]+$/ (aurboda passes), accepts any string version (so a BUILD_SHA is fine), allows an empty usage.users, and defaults openRegistrations to false — correct for an invite-only instance. The emitted document is schema-valid.
  • isFederationRequest already whitelists /.well-known/nodeinfo and /nodeinfo, so the gate doesn't swallow the new endpoints, and the nginx blocks match the existing per-path style. Redirect target is built from server-configured origin + a regex-validated username, so no open-redirect surface.

Non-blocking — fold into a later PR, don't re-roll this one

  1. The */* claim in the router comment is wrong. acceptsJsonLd computes preferredMediaTypes('*/*') === ['*/*'], which is neither text/html nor a member of the JSON-LD list, so Fedify treats a bare Accept: */* as not acceptable and falls through here — where req.accepts('html') is truthy for */*. So curl /users/fiddur now 302s to the profile page instead of 406ing. That behavior is arguably an improvement, but the comment says the opposite, and it also means the docstring's closing promise ("never a redirect to a 200 HTML page") doesn't hold for a nonexistent actor: onNotFound → this router → 302 → /u/<unknown> returns the SPA shell with generic meta (a soft-404). Worth correcting the two comments to describe what actually happens.
  2. No Vary: Accept on the redirect. Fedify sets it on the responses it negotiates; the 302 doesn't, so the same URL yields JSON-LD or a redirect with nothing telling a cache they differ. Harmless today (no proxy_cache in nginx), but cheap insurance if a CDN ever fronts this.
  3. The new router has no end-to-end test through integrateFederation. actor-html-router.test.ts proves the router redirects when reached; it never proves it is reached, and the thing that makes it reachable is the subtle notAcceptable && req.route != null branch in a third-party package. federation.integration.test.ts already has buildFederatedApp() for exactly this reason on the tombstone slice — one test there (Accept: text/html → 302 to /u/<user>) would pin the interaction against a Fedify upgrade.
  4. Minor: NodeInfo reports localPosts: 0 / localComments: 0 for a populated instance. Defensible given per-user DBs (and the comment explains it), just noting it's a visible untruth to crawlers rather than an omission.

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.

Publish NodeInfo + serve browsers something at actor URLs (user report)

1 participant