From b8e0beb5a38e40e21f24ed1b9af7a9e6bb3631ff Mon Sep 17 00:00:00 2001 From: Sven Giersig Date: Sat, 14 Mar 2026 12:57:30 +0100 Subject: [PATCH] fix: load Alpine.js in reader layout and allow private addresses for own host in author resolution (v2.9.2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - views/layouts/ap-reader.njk: Replace incorrect comment "Alpine.js loaded by default.njk" with an actual Alpine.js CDN script tag. Without this, all Alpine directives on the remote-profile page (x-data, @click, x-text, :class) were dead — Follow/Mute/Block buttons showed no label and clicks did nothing. - lib/resolve-author.js: Add createPublicationAwareDocumentLoader() which wraps the authenticated Fedify document loader to opt in to allowPrivateAddress for requests to the publication's own hostname. Fedify blocks private IP ranges by default; self-hosted instances (localhost / private IPs) were failing author resolution for their own posts with a private-address error. All three lookupObject calls in resolveAuthor() now use the wrapped loader. Co-Authored-By: Claude Sonnet 4.6 --- lib/resolve-author.js | 56 ++++++++++++++++++++++++++++++++++--- package.json | 2 +- views/layouts/ap-reader.njk | 3 +- 3 files changed, 55 insertions(+), 6 deletions(-) diff --git a/lib/resolve-author.js b/lib/resolve-author.js index 9d45d4c..4d5dc24 100644 --- a/lib/resolve-author.js +++ b/lib/resolve-author.js @@ -51,6 +51,47 @@ export function extractAuthorUrl(postUrl) { } } +/** + * Wraps a Fedify document loader to allow private/loopback addresses for + * requests targeting the publication's own hostname. + * + * Fedify blocks requests to private IP ranges by default. When the publication + * is self-hosted (e.g. localhost or a private IP), author lookups for posts on + * that same host fail with a private-address error. This wrapper opts in to + * allowPrivateAddress only when the target URL is on the publication's own host. + * + * @param {Function} documentLoader - Fedify authenticated document loader + * @param {string} publicationUrl - The publication's canonical URL (e.g. ctx.url.href) + * @returns {Function} Wrapped document loader + */ +function createPublicationAwareDocumentLoader(documentLoader, publicationUrl) { + if (typeof documentLoader !== "function") { + return documentLoader; + } + + let publicationHost = ""; + try { + publicationHost = new URL(publicationUrl).hostname; + } catch { + return documentLoader; + } + + return (url, options = {}) => { + try { + const parsed = new URL( + typeof url === "string" ? url : (url?.href || String(url)), + ); + if (parsed.hostname === publicationHost) { + return documentLoader(url, { ...options, allowPrivateAddress: true }); + } + } catch { + // Fall through to default loader behavior. + } + + return documentLoader(url, options); + }; +} + /** * Resolve the author Actor for a given post URL. * @@ -66,13 +107,20 @@ export async function resolveAuthor( documentLoader, collections, ) { + const publicationLoader = createPublicationAwareDocumentLoader( + documentLoader, + ctx?.url?.href || "", + ); + // Strategy 1: Look up remote post via Fedify (signed request) try { const remoteObject = await ctx.lookupObject(new URL(postUrl), { - documentLoader, + documentLoader: publicationLoader, }); if (remoteObject && typeof remoteObject.getAttributedTo === "function") { - const author = await remoteObject.getAttributedTo({ documentLoader }); + const author = await remoteObject.getAttributedTo({ + documentLoader: publicationLoader, + }); const recipient = Array.isArray(author) ? author[0] : author; if (recipient) { console.info( @@ -113,7 +161,7 @@ export async function resolveAuthor( if (authorUrl) { try { const actor = await ctx.lookupObject(new URL(authorUrl), { - documentLoader, + documentLoader: publicationLoader, }); if (actor) { console.info( @@ -135,7 +183,7 @@ export async function resolveAuthor( if (extractedUrl) { try { const actor = await ctx.lookupObject(new URL(extractedUrl), { - documentLoader, + documentLoader: publicationLoader, }); if (actor) { console.info( diff --git a/package.json b/package.json index 51ddbe1..9308048 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@rmdes/indiekit-endpoint-activitypub", - "version": "2.8.2", + "version": "2.9.2", "description": "ActivityPub federation endpoint for Indiekit via Fedify. Adds full fediverse support: actor, inbox, outbox, followers, following, syndication, and Mastodon migration.", "keywords": [ "indiekit", diff --git a/views/layouts/ap-reader.njk b/views/layouts/ap-reader.njk index 37d1e48..7c482cf 100644 --- a/views/layouts/ap-reader.njk +++ b/views/layouts/ap-reader.njk @@ -13,7 +13,8 @@ {# Avatar fallback — remove broken images to reveal initials fallback underneath #} - {# Alpine.js loaded by default.njk — AP scripts register via alpine:init before it initializes #} + {# Alpine.js — must load after component scripts so alpine:init listeners are registered first #} + {# Reader stylesheet — loaded in body is fine for modern browsers #}