feat: use authenticated document loader for all inbox handler fetches

Pass ctx.getDocumentLoader({ identifier: handle }) to every .getActor(),
.getObject(), and .getTarget() call in inbox handlers. This signs outbound
fetches with our actor's key, fixing silent failures against Authorized
Fetch (Secure Mode) servers like hachyderm.io.

The authenticated loader is also threaded through extractObjectData() and
extractActorInfo() in timeline-store.js so internal calls to
.getAttributedTo(), .getIcon(), .getTags(), and .getAttachments() also
use signed requests.

Also removes the endpoints.type workaround in federation-bridge.js since
Fedify 2.0 fixed issue #576 upstream. The attachment array workaround
for Mastodon compatibility remains.

Bumps version to 2.0.26.
This commit is contained in:
Ricardo
2026-02-25 09:41:29 +01:00
parent 17b0f582d1
commit fceac1f344
5 changed files with 81 additions and 57 deletions
+16 -9
View File
@@ -77,7 +77,7 @@ Reader: Followed account posts → Create inbox → timeline-store → ap_time
### 1. Express ↔ Fedify Bridge (CUSTOM — NOT @fedify/express) ### 1. Express ↔ Fedify Bridge (CUSTOM — NOT @fedify/express)
We **cannot** use `@fedify/express`'s `integrateFederation()` because Indiekit mounts plugins at sub-paths. Express strips the mount prefix from `req.url`, breaking Fedify's URI template matching. Instead, `federation-bridge.js` uses `req.originalUrl` to build the full URL. We **cannot** use `@fedify/express`'s `integrateFederation()` because Indiekit mounts plugins at sub-paths. Express strips the mount prefix from `req.url`, breaking Fedify's URI template matching. **Verified in Fedify 2.0**: `@fedify/express` still uses `req.url` (not `req.originalUrl`), so the custom bridge remains necessary. Instead, `federation-bridge.js` uses `req.originalUrl` to build the full URL.
The bridge also **reconstructs POST bodies** from `req.body` when Express body parser has already consumed the request stream (checked via `req.readable === false`). Without this, POST handlers in Fedify (e.g. the `@fedify/debugger` login form) receive empty bodies and fail with `"Response body object should not be disturbed or locked"`. The bridge also **reconstructs POST bodies** from `req.body` when Express body parser has already consumed the request stream (checked via `req.readable === false`). Without this, POST handlers in Fedify (e.g. the `@fedify/debugger` login form) receive empty bodies and fail with `"Response body object should not be disturbed or locked"`.
@@ -91,14 +91,19 @@ The `contentNegotiationRoutes` router is mounted at `/` (root). It MUST only pas
In `routesPublic`, the middleware skips paths starting with `/admin`. Without this, Fedify would intercept admin UI requests and return 404/406 responses instead of letting Express serve the authenticated pages. In `routesPublic`, the middleware skips paths starting with `/admin`. Without this, Fedify would intercept admin UI requests and return 404/406 responses instead of letting Express serve the authenticated pages.
### 4. Use .objectId/.actorId — NOT .getObject()/.getActor() in Inbox Handlers ### 4. Authenticated Document Loader for Inbox Handlers
Fedify's `.getObject()` and `.getActor()` trigger HTTP fetches to remote servers. This fails silently or retries ~10 times when: All `.getObject()` / `.getActor()` / `.getTarget()` calls in inbox handlers **must** pass an authenticated `DocumentLoader` to sign outbound fetches. Without this, requests to Authorized Fetch (Secure Mode) servers like hachyderm.io fail with 401.
- Remote server has **Authorized Fetch** enabled (returns 401)
- Server is down or unreachable
- Object has been deleted
**Always prefer** `.objectId?.href` and `.actorId?.href` (zero network requests) for Like, Announce, Undo, and Delete handlers. Only use `.getObject()` / `.getActor()` when you need the full object, and **always wrap in try-catch**. ```javascript
const authLoader = await ctx.getDocumentLoader({ identifier: handle });
const actor = await activity.getActor({ documentLoader: authLoader });
const object = await activity.getObject({ documentLoader: authLoader });
```
The `getAuthLoader` helper in `inbox-listeners.js` wraps this pattern. The authenticated loader is also passed through to `extractObjectData()` and `extractActorInfo()` in `timeline-store.js` so that `.getAttributedTo()`, `.getIcon()`, `.getTags()`, and `.getAttachments()` also sign their fetches.
**Still prefer** `.objectId?.href` and `.actorId?.href` (zero network requests) when you only need the URL — e.g. Like, Delete, and the filter check in Announce. Only use the fetching getters when you need the full object, and **always wrap in try-catch**.
### 5. Accept(Follow) Matching — Don't Check Inner Object Type ### 5. Accept(Follow) Matching — Don't Check Inner Object Type
@@ -118,9 +123,11 @@ Template names resolve across ALL registered plugin view directories. If two plu
Express 5 removed the `"back"` magic keyword from `response.redirect()`. It's treated as a literal URL, causing 404s at paths like `/admin/featured/back`. Always use explicit redirect paths. Express 5 removed the `"back"` magic keyword from `response.redirect()`. It's treated as a literal URL, causing 404s at paths like `/admin/featured/back`. Always use explicit redirect paths.
### 9. Fedify Endpoints Type Bug (Workaround) ### 9. Attachment Array Workaround (Mastodon Compatibility)
Fedify serializes `endpoints` with `"type": "as:Endpoints"` which is not a real ActivityStreams type. `sendFedifyResponse()` in `federation-bridge.js` strips this from actor JSON responses. Remove the workaround when [fedify#576](https://github.com/fedify-dev/fedify/issues/576) is fixed upstream. JSON-LD compaction collapses single-element arrays to plain objects. Mastodon's `update_account_fields` checks `attachment.is_a?(Array)` and silently skips if it's not an array. `sendFedifyResponse()` in `federation-bridge.js` forces `attachment` to always be an array.
**Note:** The old `endpoints.type` bug ([fedify#576](https://github.com/fedify-dev/fedify/issues/576)) was fixed in Fedify 2.0 — that workaround has been removed.
### 10. Profile Links — Express qs Body Parser Key Mismatch ### 10. Profile Links — Express qs Body Parser Key Mismatch
+5 -13
View File
@@ -72,11 +72,11 @@ async function sendFedifyResponse(res, response, request) {
return; return;
} }
// WORKAROUND: Fedify serializes endpoints with "type": "as:Endpoints" // WORKAROUND: JSON-LD compaction collapses single-element arrays to a
// which is not a real ActivityStreams type (fails browser.pub validation). // plain object. Mastodon's update_account_fields checks
// For actor JSON responses, buffer the body and strip the invalid type. // `attachment.is_a?(Array)` and skips if it's not an array, so
// See: https://github.com/fedify-dev/fedify/issues/576 // profile links/PropertyValues are silently ignored.
// TODO: Remove this workaround when Fedify fixes the upstream issue. // Force `attachment` to always be an array for Mastodon compatibility.
const contentType = response.headers.get("content-type") || ""; const contentType = response.headers.get("content-type") || "";
const isActorJson = const isActorJson =
contentType.includes("activity+json") || contentType.includes("activity+json") ||
@@ -86,14 +86,6 @@ async function sendFedifyResponse(res, response, request) {
const body = await response.text(); const body = await response.text();
try { try {
const json = JSON.parse(body); const json = JSON.parse(body);
if (json.endpoints?.type) {
delete json.endpoints.type;
}
// WORKAROUND: Fedify's JSON-LD compaction collapses single-element
// arrays to a plain object. Mastodon's update_account_fields checks
// `attachment.is_a?(Array)` and skips if it's not an array, so
// profile links/PropertyValues are silently ignored.
// Force `attachment` to always be an array for Mastodon compatibility.
if (json.attachment && !Array.isArray(json.attachment)) { if (json.attachment && !Array.isArray(json.attachment)) {
json.attachment = [json.attachment]; json.attachment = [json.attachment];
} }
+47 -27
View File
@@ -41,9 +41,20 @@ import { fetchAndStorePreviews } from "./og-unfurl.js";
export function registerInboxListeners(inboxChain, options) { export function registerInboxListeners(inboxChain, options) {
const { collections, handle, storeRawActivities } = options; const { collections, handle, storeRawActivities } = options;
/**
* Get an authenticated DocumentLoader that signs outbound fetches with
* our actor's key. This allows .getActor()/.getObject() to succeed
* against Authorized Fetch (Secure Mode) servers like hachyderm.io.
*
* @param {import("@fedify/fedify").Context} ctx - Fedify context
* @returns {Promise<import("@fedify/fedify").DocumentLoader>}
*/
const getAuthLoader = (ctx) => ctx.getDocumentLoader({ identifier: handle });
inboxChain inboxChain
.on(Follow, async (ctx, follow) => { .on(Follow, async (ctx, follow) => {
const followerActor = await follow.getActor(); const authLoader = await getAuthLoader(ctx);
const followerActor = await follow.getActor({ documentLoader: authLoader });
if (!followerActor?.id) return; if (!followerActor?.id) return;
const followerUrl = followerActor.id.href; const followerUrl = followerActor.id.href;
@@ -90,7 +101,7 @@ export function registerInboxListeners(inboxChain, options) {
}); });
// Store notification // Store notification
const followerInfo = await extractActorInfo(followerActor); const followerInfo = await extractActorInfo(followerActor, { documentLoader: authLoader });
await addNotification(collections, { await addNotification(collections, {
uid: follow.id?.href || `follow:${followerUrl}`, uid: follow.id?.href || `follow:${followerUrl}`,
type: "follow", type: "follow",
@@ -104,9 +115,10 @@ export function registerInboxListeners(inboxChain, options) {
}) })
.on(Undo, async (ctx, undo) => { .on(Undo, async (ctx, undo) => {
const actorUrl = undo.actorId?.href || ""; const actorUrl = undo.actorId?.href || "";
const authLoader = await getAuthLoader(ctx);
let inner; let inner;
try { try {
inner = await undo.getObject(); inner = await undo.getObject({ documentLoader: authLoader });
} catch { } catch {
// Inner activity not dereferenceable — can't determine what was undone // Inner activity not dereferenceable — can't determine what was undone
return; return;
@@ -150,7 +162,8 @@ export function registerInboxListeners(inboxChain, options) {
// it to a Person (the Follow's target) rather than the Follow itself. // it to a Person (the Follow's target) rather than the Follow itself.
// Instead, we match directly against ap_following — if we have a // Instead, we match directly against ap_following — if we have a
// pending follow for this actor, any Accept from them confirms it. // pending follow for this actor, any Accept from them confirms it.
const actorObj = await accept.getActor(); const authLoader = await getAuthLoader(ctx);
const actorObj = await accept.getActor({ documentLoader: authLoader });
const actorUrl = actorObj?.id?.href || ""; const actorUrl = actorObj?.id?.href || "";
if (!actorUrl) return; if (!actorUrl) return;
@@ -186,7 +199,8 @@ export function registerInboxListeners(inboxChain, options) {
} }
}) })
.on(Reject, async (ctx, reject) => { .on(Reject, async (ctx, reject) => {
const actorObj = await reject.getActor(); const authLoader = await getAuthLoader(ctx);
const actorObj = await reject.getActor({ documentLoader: authLoader });
const actorUrl = actorObj?.id?.href || ""; const actorUrl = actorObj?.id?.href || "";
if (!actorUrl) return; if (!actorUrl) return;
@@ -217,20 +231,19 @@ export function registerInboxListeners(inboxChain, options) {
} }
}) })
.on(Like, async (ctx, like) => { .on(Like, async (ctx, like) => {
// Use .objectId to get the URL without dereferencing the remote object. // Use .objectId (non-fetching) for the liked URL — we only need the
// Calling .getObject() would trigger an HTTP fetch to the remote server, // URL to filter and log, not the full remote object.
// which fails with 404 when the server has Authorized Fetch (Secure Mode)
// enabled — causing pointless retries and log spam.
const objectId = like.objectId?.href || ""; const objectId = like.objectId?.href || "";
// Only log likes of our own content // Only log likes of our own content
const pubUrl = collections._publicationUrl; const pubUrl = collections._publicationUrl;
if (!objectId || (pubUrl && !objectId.startsWith(pubUrl))) return; if (!objectId || (pubUrl && !objectId.startsWith(pubUrl))) return;
const authLoader = await getAuthLoader(ctx);
const actorUrl = like.actorId?.href || ""; const actorUrl = like.actorId?.href || "";
let actorObj; let actorObj;
try { try {
actorObj = await like.getActor(); actorObj = await like.getActor({ documentLoader: authLoader });
} catch { } catch {
actorObj = null; actorObj = null;
} }
@@ -250,7 +263,7 @@ export function registerInboxListeners(inboxChain, options) {
}); });
// Store notification // Store notification
const actorInfo = await extractActorInfo(actorObj); const actorInfo = await extractActorInfo(actorObj, { documentLoader: authLoader });
await addNotification(collections, { await addNotification(collections, {
uid: like.id?.href || `like:${actorUrl}:${objectId}`, uid: like.id?.href || `like:${actorUrl}:${objectId}`,
type: "like", type: "like",
@@ -268,6 +281,7 @@ export function registerInboxListeners(inboxChain, options) {
const objectId = announce.objectId?.href || ""; const objectId = announce.objectId?.href || "";
if (!objectId) return; if (!objectId) return;
const authLoader = await getAuthLoader(ctx);
const actorUrl = announce.actorId?.href || ""; const actorUrl = announce.actorId?.href || "";
const pubUrl = collections._publicationUrl; const pubUrl = collections._publicationUrl;
@@ -277,7 +291,7 @@ export function registerInboxListeners(inboxChain, options) {
if (pubUrl && objectId.startsWith(pubUrl)) { if (pubUrl && objectId.startsWith(pubUrl)) {
let actorObj; let actorObj;
try { try {
actorObj = await announce.getActor(); actorObj = await announce.getActor({ documentLoader: authLoader });
} catch { } catch {
actorObj = null; actorObj = null;
} }
@@ -298,7 +312,7 @@ export function registerInboxListeners(inboxChain, options) {
}); });
// Create notification // Create notification
const actorInfo = await extractActorInfo(actorObj); const actorInfo = await extractActorInfo(actorObj, { documentLoader: authLoader });
await addNotification(collections, { await addNotification(collections, {
uid: announce.id?.href || `${actorUrl}#boost-${objectId}`, uid: announce.id?.href || `${actorUrl}#boost-${objectId}`,
type: "boost", type: "boost",
@@ -319,8 +333,8 @@ export function registerInboxListeners(inboxChain, options) {
const following = await collections.ap_following.findOne({ actorUrl }); const following = await collections.ap_following.findOne({ actorUrl });
if (following) { if (following) {
try { try {
// Fetch the original object being boosted // Fetch the original object being boosted (authenticated for Secure Mode servers)
const object = await announce.getObject(); const object = await announce.getObject({ documentLoader: authLoader });
if (!object) return; if (!object) return;
// Skip non-content objects (Lemmy/PieFed like/create activities // Skip non-content objects (Lemmy/PieFed like/create activities
@@ -329,13 +343,14 @@ export function registerInboxListeners(inboxChain, options) {
if (!hasContent) return; if (!hasContent) return;
// Get booster actor info // Get booster actor info
const boosterActor = await announce.getActor(); const boosterActor = await announce.getActor({ documentLoader: authLoader });
const boosterInfo = await extractActorInfo(boosterActor); const boosterInfo = await extractActorInfo(boosterActor, { documentLoader: authLoader });
// Extract and store with boost metadata // Extract and store with boost metadata
const timelineItem = await extractObjectData(object, { const timelineItem = await extractObjectData(object, {
boostedBy: boosterInfo, boostedBy: boosterInfo,
boostedAt: announce.published ? String(announce.published) : new Date().toISOString(), boostedAt: announce.published ? String(announce.published) : new Date().toISOString(),
documentLoader: authLoader,
}); });
await addTimelineItem(collections, timelineItem); await addTimelineItem(collections, timelineItem);
@@ -347,11 +362,12 @@ export function registerInboxListeners(inboxChain, options) {
} }
}) })
.on(Create, async (ctx, create) => { .on(Create, async (ctx, create) => {
const authLoader = await getAuthLoader(ctx);
let object; let object;
try { try {
object = await create.getObject(); object = await create.getObject({ documentLoader: authLoader });
} catch { } catch {
// Remote object not dereferenceable (Authorized Fetch, deleted, etc.) // Remote object not dereferenceable (deleted, etc.)
return; return;
} }
if (!object) return; if (!object) return;
@@ -359,7 +375,7 @@ export function registerInboxListeners(inboxChain, options) {
const actorUrl = create.actorId?.href || ""; const actorUrl = create.actorId?.href || "";
let actorObj; let actorObj;
try { try {
actorObj = await create.getActor(); actorObj = await create.getActor({ documentLoader: authLoader });
} catch { } catch {
// Actor not dereferenceable — use URL as fallback // Actor not dereferenceable — use URL as fallback
actorObj = null; actorObj = null;
@@ -389,7 +405,7 @@ export function registerInboxListeners(inboxChain, options) {
// Create notification if reply is to one of OUR posts // Create notification if reply is to one of OUR posts
if (pubUrl && inReplyTo.startsWith(pubUrl)) { if (pubUrl && inReplyTo.startsWith(pubUrl)) {
const actorInfo = await extractActorInfo(actorObj); const actorInfo = await extractActorInfo(actorObj, { documentLoader: authLoader });
const rawHtml = object.content?.toString() || ""; const rawHtml = object.content?.toString() || "";
const contentHtml = sanitizeContent(rawHtml); const contentHtml = sanitizeContent(rawHtml);
const contentText = rawHtml.replace(/<[^>]*>/g, "").substring(0, 200); const contentText = rawHtml.replace(/<[^>]*>/g, "").substring(0, 200);
@@ -420,7 +436,7 @@ export function registerInboxListeners(inboxChain, options) {
for (const tag of tags) { for (const tag of tags) {
if (tag.type === "Mention" && tag.href?.href === ourActorUrl) { if (tag.type === "Mention" && tag.href?.href === ourActorUrl) {
const actorInfo = await extractActorInfo(actorObj); const actorInfo = await extractActorInfo(actorObj, { documentLoader: authLoader });
const rawMentionHtml = object.content?.toString() || ""; const rawMentionHtml = object.content?.toString() || "";
const mentionHtml = sanitizeContent(rawMentionHtml); const mentionHtml = sanitizeContent(rawMentionHtml);
const contentText = rawMentionHtml.replace(/<[^>]*>/g, "").substring(0, 200); const contentText = rawMentionHtml.replace(/<[^>]*>/g, "").substring(0, 200);
@@ -451,6 +467,7 @@ export function registerInboxListeners(inboxChain, options) {
try { try {
const timelineItem = await extractObjectData(object, { const timelineItem = await extractObjectData(object, {
actorFallback: actorObj, actorFallback: actorObj,
documentLoader: authLoader,
}); });
await addTimelineItem(collections, timelineItem); await addTimelineItem(collections, timelineItem);
@@ -479,9 +496,10 @@ export function registerInboxListeners(inboxChain, options) {
} }
}) })
.on(Move, async (ctx, move) => { .on(Move, async (ctx, move) => {
const oldActorObj = await move.getActor(); const authLoader = await getAuthLoader(ctx);
const oldActorObj = await move.getActor({ documentLoader: authLoader });
const oldActorUrl = oldActorObj?.id?.href || ""; const oldActorUrl = oldActorObj?.id?.href || "";
const target = await move.getTarget(); const target = await move.getTarget({ documentLoader: authLoader });
const newActorUrl = target?.id?.href || ""; const newActorUrl = target?.id?.href || "";
if (oldActorUrl && newActorUrl) { if (oldActorUrl && newActorUrl) {
@@ -501,11 +519,12 @@ export function registerInboxListeners(inboxChain, options) {
}) })
.on(Update, async (ctx, update) => { .on(Update, async (ctx, update) => {
// Update can be for a profile OR for a post (edited content) // Update can be for a profile OR for a post (edited content)
const authLoader = await getAuthLoader(ctx);
// Try to get the object being updated // Try to get the object being updated
let object; let object;
try { try {
object = await update.getObject(); object = await update.getObject({ documentLoader: authLoader });
} catch { } catch {
object = null; object = null;
} }
@@ -538,7 +557,7 @@ export function registerInboxListeners(inboxChain, options) {
} }
// PATH 2: Otherwise, assume profile update — refresh stored follower data // PATH 2: Otherwise, assume profile update — refresh stored follower data
const actorObj = await update.getActor(); const actorObj = await update.getActor({ documentLoader: authLoader });
const actorUrl = actorObj?.id?.href || ""; const actorUrl = actorObj?.id?.href || "";
if (!actorUrl) return; if (!actorUrl) return;
@@ -564,7 +583,8 @@ export function registerInboxListeners(inboxChain, options) {
}) })
.on(Block, async (ctx, block) => { .on(Block, async (ctx, block) => {
// Remote actor blocked us — remove them from followers // Remote actor blocked us — remove them from followers
const actorObj = await block.getActor(); const authLoader = await getAuthLoader(ctx);
const actorObj = await block.getActor({ documentLoader: authLoader });
const actorUrl = actorObj?.id?.href || ""; const actorUrl = actorObj?.id?.href || "";
if (actorUrl) { if (actorUrl) {
await collections.ap_followers.deleteOne({ actorUrl }); await collections.ap_followers.deleteOne({ actorUrl });
+12 -7
View File
@@ -36,9 +36,11 @@ export function sanitizeContent(html) {
/** /**
* Extract actor information from Fedify Person/Application/Service object * Extract actor information from Fedify Person/Application/Service object
* @param {object} actor - Fedify actor object * @param {object} actor - Fedify actor object
* @param {object} [options] - Options
* @param {object} [options.documentLoader] - Authenticated DocumentLoader for Secure Mode servers
* @returns {object} { name, url, photo, handle } * @returns {object} { name, url, photo, handle }
*/ */
export async function extractActorInfo(actor) { export async function extractActorInfo(actor, options = {}) {
if (!actor) { if (!actor) {
return { return {
name: "Unknown", name: "Unknown",
@@ -54,10 +56,11 @@ export async function extractActorInfo(actor) {
const url = actor.id?.href || ""; const url = actor.id?.href || "";
// Extract photo URL from icon (Fedify uses async getters) // Extract photo URL from icon (Fedify uses async getters)
const loaderOpts = options.documentLoader ? { documentLoader: options.documentLoader } : {};
let photo = ""; let photo = "";
try { try {
if (typeof actor.getIcon === "function") { if (typeof actor.getIcon === "function") {
const iconObj = await actor.getIcon(); const iconObj = await actor.getIcon(loaderOpts);
photo = iconObj?.url?.href || ""; photo = iconObj?.url?.href || "";
} else { } else {
const iconObj = await actor.icon; const iconObj = await actor.icon;
@@ -89,6 +92,7 @@ export async function extractActorInfo(actor) {
* @param {object} [options.boostedBy] - Actor info for boosts * @param {object} [options.boostedBy] - Actor info for boosts
* @param {Date} [options.boostedAt] - Boost timestamp * @param {Date} [options.boostedAt] - Boost timestamp
* @param {object} [options.actorFallback] - Fedify actor to use when object.getAttributedTo() fails * @param {object} [options.actorFallback] - Fedify actor to use when object.getAttributedTo() fails
* @param {object} [options.documentLoader] - Authenticated DocumentLoader for Secure Mode servers
* @returns {Promise<object>} Timeline item data * @returns {Promise<object>} Timeline item data
*/ */
export async function extractObjectData(object, options = {}) { export async function extractObjectData(object, options = {}) {
@@ -130,14 +134,15 @@ export async function extractObjectData(object, options = {}) {
: new Date().toISOString(); : new Date().toISOString();
// Extract author — try multiple strategies in order of reliability // Extract author — try multiple strategies in order of reliability
const loaderOpts = options.documentLoader ? { documentLoader: options.documentLoader } : {};
let authorObj = null; let authorObj = null;
try { try {
if (typeof object.getAttributedTo === "function") { if (typeof object.getAttributedTo === "function") {
const attr = await object.getAttributedTo(); const attr = await object.getAttributedTo(loaderOpts);
authorObj = Array.isArray(attr) ? attr[0] : attr; authorObj = Array.isArray(attr) ? attr[0] : attr;
} }
} catch { } catch {
// getAttributedTo() failed (Authorized Fetch, unreachable, etc.) // getAttributedTo() failed (unreachable, deleted, etc.)
} }
// If getAttributedTo() returned nothing, use the actor from the wrapping activity // If getAttributedTo() returned nothing, use the actor from the wrapping activity
if (!authorObj && options.actorFallback) { if (!authorObj && options.actorFallback) {
@@ -150,7 +155,7 @@ export async function extractObjectData(object, options = {}) {
let author; let author;
if (authorObj) { if (authorObj) {
author = await extractActorInfo(authorObj); author = await extractActorInfo(authorObj, loaderOpts);
} else { } else {
// Last resort: use attributionIds (non-fetching) to get at least a URL // Last resort: use attributionIds (non-fetching) to get at least a URL
const attrIds = object.attributionIds; const attrIds = object.attributionIds;
@@ -184,7 +189,7 @@ export async function extractObjectData(object, options = {}) {
const category = []; const category = [];
try { try {
if (typeof object.getTags === "function") { if (typeof object.getTags === "function") {
const tags = await object.getTags(); const tags = await object.getTags(loaderOpts);
for await (const tag of tags) { for await (const tag of tags) {
if (tag.name) { if (tag.name) {
const tagName = tag.name.toString().replace(/^#/, ""); const tagName = tag.name.toString().replace(/^#/, "");
@@ -203,7 +208,7 @@ export async function extractObjectData(object, options = {}) {
try { try {
if (typeof object.getAttachments === "function") { if (typeof object.getAttachments === "function") {
const attachments = await object.getAttachments(); const attachments = await object.getAttachments(loaderOpts);
for await (const att of attachments) { for await (const att of attachments) {
const mediaUrl = att.url?.href || ""; const mediaUrl = att.url?.href || "";
if (!mediaUrl) continue; if (!mediaUrl) continue;
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@rmdes/indiekit-endpoint-activitypub", "name": "@rmdes/indiekit-endpoint-activitypub",
"version": "2.0.25", "version": "2.0.26",
"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",