feat(federation): NodeInfo + browser fallback for actor URLs (#1047) - #1048
Merged
Conversation
…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
marked this pull request as ready for review
August 21, 2026 16:03
fiddur
commented
Aug 21, 2026
fiddur
left a comment
Owner
Author
There was a problem hiding this comment.
✅ 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:
acceptsJsonLdreturns false when the top-ranked type istext/html, so a browser hit really does land inonNotAcceptable, whichnext()s. The new router then matches, and — importantly — because it is a route (router.get),req.routeis set, so the integration'sif (notFound || notAcceptable && req.route != null) returnskips writing its own 406 over your 302. No double-write /ERR_HTTP_HEADERS_SENT. Same mechanism the tombstone router relies on.nodeInfoToJsonvalidatessoftware.nameagainst/^[a-z0-9-]+$/(aurbodapasses), accepts any stringversion(so a BUILD_SHA is fine), allows an emptyusage.users, and defaultsopenRegistrationstofalse— correct for an invite-only instance. The emitted document is schema-valid.isFederationRequestalready whitelists/.well-known/nodeinfoand/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-configuredorigin+ a regex-validated username, so no open-redirect surface.
Non-blocking — fold into a later PR, don't re-roll this one
- The
*/*claim in the router comment is wrong.acceptsJsonLdcomputespreferredMediaTypes('*/*') === ['*/*'], which is neithertext/htmlnor a member of the JSON-LD list, so Fedify treats a bareAccept: */*as not acceptable and falls through here — wherereq.accepts('html')is truthy for*/*. Socurl /users/fiddurnow 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. - No
Vary: Accepton 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 (noproxy_cachein nginx), but cheap insurance if a CDN ever fronts this. - The new router has no end-to-end test through
integrateFederation.actor-html-router.test.tsproves the router redirects when reached; it never proves it is reached, and the thing that makes it reachable is the subtlenotAcceptable && req.route != nullbranch in a third-party package.federation.integration.test.tsalready hasbuildFederatedApp()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. - Minor: NodeInfo reports
localPosts: 0/localComments: 0for 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.
This was referenced Aug 21, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #1047 (user report relayed by Fredrik; verified live on aurboda.net).
Before:
/.well-known/nodeinfofell through nginx to the SPA and answered 200 with HTML (worse than a 404 for crawlers);GET /users/fiddurwith a browser Accept header answered 406 Not acceptable. WebFinger and AP-Accept actor fetches were fine.Changes:
federation.setNodeInfoDispatcher('/nodeinfo/2.1', …)): softwareaurboda+ BUILD_SHA version,activitypubprotocol; Fedify serves the/.well-known/nodeinfoJRD 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.actor-html-routermounted right afterintegrateFederation(same fallthrough pattern as the feed tombstone router) — an HTML-preferringGET /users/:username302-redirects to the/u/:usernamepublic profile page; AP requests and malformed usernames fall through untouched, so a fetch of a nonexistent actor still 404s.urlproperty 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
urlassertion; whole-monorepopnpm checkgreen, federation integration suite 23/23.🤖 Generated with Claude Code
https://claude.ai/code/session_01CwoP1SqJhHgHiEEoEQtUjT