fix: load Alpine.js in reader layout and allow private addresses for own host in author resolution (v2.9.2)
- 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 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
5806133dff
commit
b8e0beb5a3
+52
-4
@@ -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.
|
* Resolve the author Actor for a given post URL.
|
||||||
*
|
*
|
||||||
@@ -66,13 +107,20 @@ export async function resolveAuthor(
|
|||||||
documentLoader,
|
documentLoader,
|
||||||
collections,
|
collections,
|
||||||
) {
|
) {
|
||||||
|
const publicationLoader = createPublicationAwareDocumentLoader(
|
||||||
|
documentLoader,
|
||||||
|
ctx?.url?.href || "",
|
||||||
|
);
|
||||||
|
|
||||||
// Strategy 1: Look up remote post via Fedify (signed request)
|
// Strategy 1: Look up remote post via Fedify (signed request)
|
||||||
try {
|
try {
|
||||||
const remoteObject = await ctx.lookupObject(new URL(postUrl), {
|
const remoteObject = await ctx.lookupObject(new URL(postUrl), {
|
||||||
documentLoader,
|
documentLoader: publicationLoader,
|
||||||
});
|
});
|
||||||
if (remoteObject && typeof remoteObject.getAttributedTo === "function") {
|
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;
|
const recipient = Array.isArray(author) ? author[0] : author;
|
||||||
if (recipient) {
|
if (recipient) {
|
||||||
console.info(
|
console.info(
|
||||||
@@ -113,7 +161,7 @@ export async function resolveAuthor(
|
|||||||
if (authorUrl) {
|
if (authorUrl) {
|
||||||
try {
|
try {
|
||||||
const actor = await ctx.lookupObject(new URL(authorUrl), {
|
const actor = await ctx.lookupObject(new URL(authorUrl), {
|
||||||
documentLoader,
|
documentLoader: publicationLoader,
|
||||||
});
|
});
|
||||||
if (actor) {
|
if (actor) {
|
||||||
console.info(
|
console.info(
|
||||||
@@ -135,7 +183,7 @@ export async function resolveAuthor(
|
|||||||
if (extractedUrl) {
|
if (extractedUrl) {
|
||||||
try {
|
try {
|
||||||
const actor = await ctx.lookupObject(new URL(extractedUrl), {
|
const actor = await ctx.lookupObject(new URL(extractedUrl), {
|
||||||
documentLoader,
|
documentLoader: publicationLoader,
|
||||||
});
|
});
|
||||||
if (actor) {
|
if (actor) {
|
||||||
console.info(
|
console.info(
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@rmdes/indiekit-endpoint-activitypub",
|
"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.",
|
"description": "ActivityPub federation endpoint for Indiekit via Fedify. Adds full fediverse support: actor, inbox, outbox, followers, following, syndication, and Mastodon migration.",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"indiekit",
|
"indiekit",
|
||||||
|
|||||||
@@ -13,7 +13,8 @@
|
|||||||
{# Avatar fallback — remove broken images to reveal initials fallback underneath #}
|
{# Avatar fallback — remove broken images to reveal initials fallback underneath #}
|
||||||
<script>document.addEventListener("error",function(e){var t=e.target;if(t.tagName==="IMG"&&t.closest("[data-avatar-fallback]"))t.remove()},true)</script>
|
<script>document.addEventListener("error",function(e){var t=e.target;if(t.tagName==="IMG"&&t.closest("[data-avatar-fallback]"))t.remove()},true)</script>
|
||||||
|
|
||||||
{# 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 #}
|
||||||
|
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3/dist/cdn.min.js"></script>
|
||||||
|
|
||||||
{# Reader stylesheet — loaded in body is fine for modern browsers #}
|
{# Reader stylesheet — loaded in body is fine for modern browsers #}
|
||||||
<link rel="stylesheet" href="/assets/@rmdes-indiekit-endpoint-activitypub/reader.css">
|
<link rel="stylesheet" href="/assets/@rmdes-indiekit-endpoint-activitypub/reader.css">
|
||||||
|
|||||||
Reference in New Issue
Block a user