Files
indiekit-endpoint-activitypub/lib/controllers/resolve.js
Ricardo 76e9ba0b35 fix: centralize unsigned fallback in lookupWithSecurity
Some servers (e.g., tags.pub) return 400 for signed GET requests.
Previously only followActor had an unsigned fallback — all other
callers (resolve, unfollowActor, profile viewer, messages, post
detail, OG unfurl) would silently fail.

Fix: moved the fallback logic into lookupWithSecurity itself. When
an authenticated documentLoader is provided and the lookup fails,
it automatically retries without the loader (unsigned GET). This
fixes ALL AP resolution paths in one place — resolve, follow,
unfollow, profile viewing, message sending, quote fetching.

Removed individual fallbacks in followActor and resolve controller
since the central helper now handles it.
2026-03-21 19:16:05 +01:00

112 lines
3.1 KiB
JavaScript

/**
* Resolve controller — accepts any fediverse URL or handle, resolves it
* via lookupObject(), and redirects to the appropriate internal view.
*/
import { lookupWithSecurity } from "../lookup-helpers.js";
import {
Article,
Note,
Person,
Service,
Application,
Organization,
Group,
} from "@fedify/fedify/vocab";
/**
* GET /admin/reader/resolve?q=<url-or-handle>
* Resolves a fediverse URL or @user@domain handle and redirects to
* the post detail or remote profile view.
*/
export function resolveController(mountPath, plugin) {
return async (request, response, next) => {
try {
const query = (request.query.q || "").trim();
if (!query) {
return response.redirect(`${mountPath}/admin/reader`);
}
if (!plugin._federation) {
return response.status(503).render("error", {
title: "Error",
content: "Federation not initialized",
});
}
const handle = plugin.options.actor.handle;
const ctx = plugin._federation.createContext(
new URL(plugin._publicationUrl),
{ handle, publicationUrl: plugin._publicationUrl },
);
const documentLoader = await ctx.getDocumentLoader({
identifier: handle,
});
// Determine if input is a URL or a handle
// lookupObject accepts: URLs, @user@domain, user@domain, acct:user@domain
let lookupInput;
try {
// If it parses as a URL, pass as URL object
const parsed = new URL(query);
lookupInput = parsed;
} catch {
// Not a URL — treat as handle (strip leading @ if present)
lookupInput = query;
}
let object;
try {
// lookupWithSecurity handles signed→unsigned fallback automatically
object = await lookupWithSecurity(ctx, lookupInput, { documentLoader });
} catch (error) {
console.warn(
`[resolve] lookupObject failed for "${query}":`,
error.message,
);
}
if (!object) {
return response.status(404).render("error", {
title: response.locals.__("activitypub.reader.resolve.notFoundTitle"),
content: response.locals.__(
"activitypub.reader.resolve.notFound",
),
});
}
// Determine object type and redirect accordingly
const objectUrl =
object.id?.href || object.url?.href || query;
if (
object instanceof Person ||
object instanceof Service ||
object instanceof Application ||
object instanceof Organization ||
object instanceof Group
) {
return response.redirect(
`${mountPath}/admin/reader/profile?url=${encodeURIComponent(objectUrl)}`,
);
}
if (object instanceof Note || object instanceof Article) {
return response.redirect(
`${mountPath}/admin/reader/post?url=${encodeURIComponent(objectUrl)}`,
);
}
// Unknown type — try post detail as fallback
return response.redirect(
`${mountPath}/admin/reader/post?url=${encodeURIComponent(objectUrl)}`,
);
} catch (error) {
next(error);
}
};
}