@rmdes/indiekit-endpoint-activitypub 2.0.25 → 2.0.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -268,11 +268,66 @@ The JF2-to-ActivityStreams converter handles these Indiekit post types:
268
268
 
269
269
  Categories are converted to `Hashtag` tags. Bookmarks include a bookmark emoji and link.
270
270
 
271
+ ## Fedify Workarounds and Implementation Notes
272
+
273
+ This plugin uses [Fedify](https://fedify.dev) 2.0 but carries several workarounds for issues in Fedify or its Express integration. These are documented here so they can be revisited when Fedify upgrades.
274
+
275
+ ### Custom Express Bridge (instead of `@fedify/express`)
276
+
277
+ **File:** `lib/federation-bridge.js`
278
+ **Upstream issue:** `@fedify/express` uses `req.url` ([source](https://github.com/fedify-dev/fedify/blob/main/packages/express/src/index.ts), line 73), not `req.originalUrl`.
279
+
280
+ Indiekit plugins mount at a sub-path (e.g. `/activitypub`). Express strips the mount prefix from `req.url`, so Fedify's URI template matching breaks — WebFinger, actor endpoints, and inbox all return 404. The custom bridge uses `req.originalUrl` to preserve the full path.
281
+
282
+ The bridge also reconstructs POST bodies that Express's body parser has already consumed (`req.readable === false`). Without this, Fedify handlers like the `@fedify/debugger` login form receive empty bodies.
283
+
284
+ **Revisit when:** `@fedify/express` switches to `req.originalUrl`, or provides an option to pass a custom URL builder.
285
+
286
+ ### JSON-LD Attachment Array Compaction
287
+
288
+ **File:** `lib/federation-bridge.js` (in `sendFedifyResponse()`)
289
+ **Upstream issue:** JSON-LD compaction collapses single-element arrays to plain objects.
290
+
291
+ Mastodon's `update_account_fields` checks `attachment.is_a?(Array)` and silently skips profile links (PropertyValues) when `attachment` is a plain object instead of an array. The bridge intercepts actor JSON-LD responses and forces `attachment` to always be an array.
292
+
293
+ **Revisit when:** Fedify adds an option to preserve arrays during JSON-LD serialization, or Mastodon fixes their array check.
294
+
295
+ ### `.authorize()` Not Chained on Actor Dispatcher
296
+
297
+ **File:** `lib/federation-setup.js` (line ~254)
298
+ **Upstream issue:** No authenticated document loading for outgoing key fetches during signature verification.
299
+
300
+ Fedify's `.authorize()` predicate triggers HTTP Signature verification on every GET to the actor endpoint. When a remote server that requires Authorized Fetch (e.g. kobolds.online) requests our actor, Fedify tries to fetch *their* public key to verify the signature. Those servers return 401 on unsigned GETs, causing uncaught `FetchError` and 500 responses.
301
+
302
+ This means we do **not** enforce Authorized Fetch on our actor endpoint. Any server can read our actor document without signing the request.
303
+
304
+ **Revisit when:** Fedify supports using the instance actor's keys for outgoing document fetches during signature verification (i.e. authenticated document loading in the verification path, not just in inbox handlers).
305
+
306
+ ### `importSpkiPem()` / `importPkcs8Pem()` — Local PEM Import
307
+
308
+ **File:** `lib/federation-setup.js` (lines ~784–816)
309
+ **Upstream change:** Fedify 1.x exported `importSpki()` for loading PEM public keys. This was removed in Fedify 2.0.
310
+
311
+ The plugin carries local `importSpkiPem()` and `importPkcs8Pem()` functions that use the Web Crypto API directly (`crypto.subtle.importKey`) to load legacy RSA key pairs stored in MongoDB from the Fedify 1.x era. New key pairs are generated using Fedify 2.0's `generateCryptoKeyPair()` and stored as JWK, so these functions only matter for existing installations that migrated from Fedify 1.x.
312
+
313
+ **Revisit when:** All existing installations have been migrated to JWK-stored keys, or Fedify re-exports a PEM import utility.
314
+
315
+ ### Authenticated Document Loader for Inbox Handlers
316
+
317
+ **File:** `lib/inbox-listeners.js`
318
+ **Upstream behavior:** Fedify's personal inbox handlers do not automatically use authenticated (signed) HTTP fetches.
319
+
320
+ All `.getActor()`, `.getObject()`, and `.getTarget()` calls in inbox handlers must explicitly pass an authenticated `DocumentLoader` obtained via `ctx.getDocumentLoader({ identifier: handle })`. Without this, fetches to Authorized Fetch (Secure Mode) servers like hachyderm.io fail with 401, causing timeline items to show "Unknown" authors and missing content.
321
+
322
+ This is not a bug — Fedify requires explicit opt-in for signed fetches. But it's a pattern that every inbox handler must follow, and forgetting it silently degrades functionality.
323
+
324
+ **Revisit when:** Fedify provides an option to default to authenticated fetches in inbox handler context, or adds a middleware layer that handles this automatically.
325
+
271
326
  ## Known Limitations
272
327
 
273
328
  - **No automated tests** — Manual testing against real fediverse servers
274
329
  - **Single actor** — One fediverse identity per Indiekit instance
275
- - **No Authorized Fetch enforcement** — Disabled due to Fedify's current limitation with authenticated outgoing fetches (causes infinite loops with servers that require it)
330
+ - **No Authorized Fetch enforcement** — `.authorize()` disabled on actor dispatcher (see workarounds above)
276
331
  - **No image upload in reader** — Compose form is text-only
277
332
  - **In-process queue without Redis** — Activities may be lost on restart
278
333
 
@@ -72,11 +72,11 @@ async function sendFedifyResponse(res, response, request) {
72
72
  return;
73
73
  }
74
74
 
75
- // WORKAROUND: Fedify serializes endpoints with "type": "as:Endpoints"
76
- // which is not a real ActivityStreams type (fails browser.pub validation).
77
- // For actor JSON responses, buffer the body and strip the invalid type.
78
- // See: https://github.com/fedify-dev/fedify/issues/576
79
- // TODO: Remove this workaround when Fedify fixes the upstream issue.
75
+ // WORKAROUND: JSON-LD compaction collapses single-element arrays to a
76
+ // plain object. Mastodon's update_account_fields checks
77
+ // `attachment.is_a?(Array)` and skips if it's not an array, so
78
+ // profile links/PropertyValues are silently ignored.
79
+ // Force `attachment` to always be an array for Mastodon compatibility.
80
80
  const contentType = response.headers.get("content-type") || "";
81
81
  const isActorJson =
82
82
  contentType.includes("activity+json") ||
@@ -86,14 +86,6 @@ async function sendFedifyResponse(res, response, request) {
86
86
  const body = await response.text();
87
87
  try {
88
88
  const json = JSON.parse(body);
89
- if (json.endpoints?.type) {
90
- delete json.endpoints.type;
91
- }
92
- // WORKAROUND: Fedify's JSON-LD compaction collapses single-element
93
- // arrays to a plain object. Mastodon's update_account_fields checks
94
- // `attachment.is_a?(Array)` and skips if it's not an array, so
95
- // profile links/PropertyValues are silently ignored.
96
- // Force `attachment` to always be an array for Mastodon compatibility.
97
89
  if (json.attachment && !Array.isArray(json.attachment)) {
98
90
  json.attachment = [json.attachment];
99
91
  }
@@ -41,9 +41,20 @@ import { fetchAndStorePreviews } from "./og-unfurl.js";
41
41
  export function registerInboxListeners(inboxChain, options) {
42
42
  const { collections, handle, storeRawActivities } = options;
43
43
 
44
+ /**
45
+ * Get an authenticated DocumentLoader that signs outbound fetches with
46
+ * our actor's key. This allows .getActor()/.getObject() to succeed
47
+ * against Authorized Fetch (Secure Mode) servers like hachyderm.io.
48
+ *
49
+ * @param {import("@fedify/fedify").Context} ctx - Fedify context
50
+ * @returns {Promise<import("@fedify/fedify").DocumentLoader>}
51
+ */
52
+ const getAuthLoader = (ctx) => ctx.getDocumentLoader({ identifier: handle });
53
+
44
54
  inboxChain
45
55
  .on(Follow, async (ctx, follow) => {
46
- const followerActor = await follow.getActor();
56
+ const authLoader = await getAuthLoader(ctx);
57
+ const followerActor = await follow.getActor({ documentLoader: authLoader });
47
58
  if (!followerActor?.id) return;
48
59
 
49
60
  const followerUrl = followerActor.id.href;
@@ -90,7 +101,7 @@ export function registerInboxListeners(inboxChain, options) {
90
101
  });
91
102
 
92
103
  // Store notification
93
- const followerInfo = await extractActorInfo(followerActor);
104
+ const followerInfo = await extractActorInfo(followerActor, { documentLoader: authLoader });
94
105
  await addNotification(collections, {
95
106
  uid: follow.id?.href || `follow:${followerUrl}`,
96
107
  type: "follow",
@@ -104,9 +115,10 @@ export function registerInboxListeners(inboxChain, options) {
104
115
  })
105
116
  .on(Undo, async (ctx, undo) => {
106
117
  const actorUrl = undo.actorId?.href || "";
118
+ const authLoader = await getAuthLoader(ctx);
107
119
  let inner;
108
120
  try {
109
- inner = await undo.getObject();
121
+ inner = await undo.getObject({ documentLoader: authLoader });
110
122
  } catch {
111
123
  // Inner activity not dereferenceable — can't determine what was undone
112
124
  return;
@@ -150,7 +162,8 @@ export function registerInboxListeners(inboxChain, options) {
150
162
  // it to a Person (the Follow's target) rather than the Follow itself.
151
163
  // Instead, we match directly against ap_following — if we have a
152
164
  // pending follow for this actor, any Accept from them confirms it.
153
- const actorObj = await accept.getActor();
165
+ const authLoader = await getAuthLoader(ctx);
166
+ const actorObj = await accept.getActor({ documentLoader: authLoader });
154
167
  const actorUrl = actorObj?.id?.href || "";
155
168
  if (!actorUrl) return;
156
169
 
@@ -186,7 +199,8 @@ export function registerInboxListeners(inboxChain, options) {
186
199
  }
187
200
  })
188
201
  .on(Reject, async (ctx, reject) => {
189
- const actorObj = await reject.getActor();
202
+ const authLoader = await getAuthLoader(ctx);
203
+ const actorObj = await reject.getActor({ documentLoader: authLoader });
190
204
  const actorUrl = actorObj?.id?.href || "";
191
205
  if (!actorUrl) return;
192
206
 
@@ -217,20 +231,19 @@ export function registerInboxListeners(inboxChain, options) {
217
231
  }
218
232
  })
219
233
  .on(Like, async (ctx, like) => {
220
- // Use .objectId to get the URL without dereferencing the remote object.
221
- // Calling .getObject() would trigger an HTTP fetch to the remote server,
222
- // which fails with 404 when the server has Authorized Fetch (Secure Mode)
223
- // enabled — causing pointless retries and log spam.
234
+ // Use .objectId (non-fetching) for the liked URL we only need the
235
+ // URL to filter and log, not the full remote object.
224
236
  const objectId = like.objectId?.href || "";
225
237
 
226
238
  // Only log likes of our own content
227
239
  const pubUrl = collections._publicationUrl;
228
240
  if (!objectId || (pubUrl && !objectId.startsWith(pubUrl))) return;
229
241
 
242
+ const authLoader = await getAuthLoader(ctx);
230
243
  const actorUrl = like.actorId?.href || "";
231
244
  let actorObj;
232
245
  try {
233
- actorObj = await like.getActor();
246
+ actorObj = await like.getActor({ documentLoader: authLoader });
234
247
  } catch {
235
248
  actorObj = null;
236
249
  }
@@ -240,17 +253,20 @@ export function registerInboxListeners(inboxChain, options) {
240
253
  actorObj?.preferredUsername?.toString() ||
241
254
  actorUrl;
242
255
 
256
+ // Extract actor info (including avatar) before logging so we can store it
257
+ const actorInfo = await extractActorInfo(actorObj, { documentLoader: authLoader });
258
+
243
259
  await logActivity(collections, storeRawActivities, {
244
260
  direction: "inbound",
245
261
  type: "Like",
246
262
  actorUrl,
247
263
  actorName,
264
+ actorAvatar: actorInfo.photo || "",
248
265
  objectUrl: objectId,
249
266
  summary: `${actorName} liked ${objectId}`,
250
267
  });
251
268
 
252
269
  // Store notification
253
- const actorInfo = await extractActorInfo(actorObj);
254
270
  await addNotification(collections, {
255
271
  uid: like.id?.href || `like:${actorUrl}:${objectId}`,
256
272
  type: "like",
@@ -268,6 +284,7 @@ export function registerInboxListeners(inboxChain, options) {
268
284
  const objectId = announce.objectId?.href || "";
269
285
  if (!objectId) return;
270
286
 
287
+ const authLoader = await getAuthLoader(ctx);
271
288
  const actorUrl = announce.actorId?.href || "";
272
289
  const pubUrl = collections._publicationUrl;
273
290
 
@@ -277,7 +294,7 @@ export function registerInboxListeners(inboxChain, options) {
277
294
  if (pubUrl && objectId.startsWith(pubUrl)) {
278
295
  let actorObj;
279
296
  try {
280
- actorObj = await announce.getActor();
297
+ actorObj = await announce.getActor({ documentLoader: authLoader });
281
298
  } catch {
282
299
  actorObj = null;
283
300
  }
@@ -287,18 +304,21 @@ export function registerInboxListeners(inboxChain, options) {
287
304
  actorObj?.preferredUsername?.toString() ||
288
305
  actorUrl;
289
306
 
307
+ // Extract actor info (including avatar) before logging so we can store it
308
+ const actorInfo = await extractActorInfo(actorObj, { documentLoader: authLoader });
309
+
290
310
  // Log the boost activity
291
311
  await logActivity(collections, storeRawActivities, {
292
312
  direction: "inbound",
293
313
  type: "Announce",
294
314
  actorUrl,
295
315
  actorName,
316
+ actorAvatar: actorInfo.photo || "",
296
317
  objectUrl: objectId,
297
318
  summary: `${actorName} boosted ${objectId}`,
298
319
  });
299
320
 
300
321
  // Create notification
301
- const actorInfo = await extractActorInfo(actorObj);
302
322
  await addNotification(collections, {
303
323
  uid: announce.id?.href || `${actorUrl}#boost-${objectId}`,
304
324
  type: "boost",
@@ -319,8 +339,8 @@ export function registerInboxListeners(inboxChain, options) {
319
339
  const following = await collections.ap_following.findOne({ actorUrl });
320
340
  if (following) {
321
341
  try {
322
- // Fetch the original object being boosted
323
- const object = await announce.getObject();
342
+ // Fetch the original object being boosted (authenticated for Secure Mode servers)
343
+ const object = await announce.getObject({ documentLoader: authLoader });
324
344
  if (!object) return;
325
345
 
326
346
  // Skip non-content objects (Lemmy/PieFed like/create activities
@@ -329,13 +349,14 @@ export function registerInboxListeners(inboxChain, options) {
329
349
  if (!hasContent) return;
330
350
 
331
351
  // Get booster actor info
332
- const boosterActor = await announce.getActor();
333
- const boosterInfo = await extractActorInfo(boosterActor);
352
+ const boosterActor = await announce.getActor({ documentLoader: authLoader });
353
+ const boosterInfo = await extractActorInfo(boosterActor, { documentLoader: authLoader });
334
354
 
335
355
  // Extract and store with boost metadata
336
356
  const timelineItem = await extractObjectData(object, {
337
357
  boostedBy: boosterInfo,
338
358
  boostedAt: announce.published ? String(announce.published) : new Date().toISOString(),
359
+ documentLoader: authLoader,
339
360
  });
340
361
 
341
362
  await addTimelineItem(collections, timelineItem);
@@ -347,11 +368,12 @@ export function registerInboxListeners(inboxChain, options) {
347
368
  }
348
369
  })
349
370
  .on(Create, async (ctx, create) => {
371
+ const authLoader = await getAuthLoader(ctx);
350
372
  let object;
351
373
  try {
352
- object = await create.getObject();
374
+ object = await create.getObject({ documentLoader: authLoader });
353
375
  } catch {
354
- // Remote object not dereferenceable (Authorized Fetch, deleted, etc.)
376
+ // Remote object not dereferenceable (deleted, etc.)
355
377
  return;
356
378
  }
357
379
  if (!object) return;
@@ -359,7 +381,7 @@ export function registerInboxListeners(inboxChain, options) {
359
381
  const actorUrl = create.actorId?.href || "";
360
382
  let actorObj;
361
383
  try {
362
- actorObj = await create.getActor();
384
+ actorObj = await create.getActor({ documentLoader: authLoader });
363
385
  } catch {
364
386
  // Actor not dereferenceable — use URL as fallback
365
387
  actorObj = null;
@@ -376,11 +398,16 @@ export function registerInboxListeners(inboxChain, options) {
376
398
  const pubUrl = collections._publicationUrl;
377
399
  if (inReplyTo) {
378
400
  const content = object.content?.toString() || "";
401
+
402
+ // Extract actor info (including avatar) before logging so we can store it
403
+ const actorInfo = await extractActorInfo(actorObj, { documentLoader: authLoader });
404
+
379
405
  await logActivity(collections, storeRawActivities, {
380
406
  direction: "inbound",
381
407
  type: "Reply",
382
408
  actorUrl,
383
409
  actorName,
410
+ actorAvatar: actorInfo.photo || "",
384
411
  objectUrl: object.id?.href || "",
385
412
  targetUrl: inReplyTo,
386
413
  content,
@@ -389,7 +416,6 @@ export function registerInboxListeners(inboxChain, options) {
389
416
 
390
417
  // Create notification if reply is to one of OUR posts
391
418
  if (pubUrl && inReplyTo.startsWith(pubUrl)) {
392
- const actorInfo = await extractActorInfo(actorObj);
393
419
  const rawHtml = object.content?.toString() || "";
394
420
  const contentHtml = sanitizeContent(rawHtml);
395
421
  const contentText = rawHtml.replace(/<[^>]*>/g, "").substring(0, 200);
@@ -420,7 +446,7 @@ export function registerInboxListeners(inboxChain, options) {
420
446
 
421
447
  for (const tag of tags) {
422
448
  if (tag.type === "Mention" && tag.href?.href === ourActorUrl) {
423
- const actorInfo = await extractActorInfo(actorObj);
449
+ const actorInfo = await extractActorInfo(actorObj, { documentLoader: authLoader });
424
450
  const rawMentionHtml = object.content?.toString() || "";
425
451
  const mentionHtml = sanitizeContent(rawMentionHtml);
426
452
  const contentText = rawMentionHtml.replace(/<[^>]*>/g, "").substring(0, 200);
@@ -451,6 +477,7 @@ export function registerInboxListeners(inboxChain, options) {
451
477
  try {
452
478
  const timelineItem = await extractObjectData(object, {
453
479
  actorFallback: actorObj,
480
+ documentLoader: authLoader,
454
481
  });
455
482
  await addTimelineItem(collections, timelineItem);
456
483
 
@@ -479,9 +506,10 @@ export function registerInboxListeners(inboxChain, options) {
479
506
  }
480
507
  })
481
508
  .on(Move, async (ctx, move) => {
482
- const oldActorObj = await move.getActor();
509
+ const authLoader = await getAuthLoader(ctx);
510
+ const oldActorObj = await move.getActor({ documentLoader: authLoader });
483
511
  const oldActorUrl = oldActorObj?.id?.href || "";
484
- const target = await move.getTarget();
512
+ const target = await move.getTarget({ documentLoader: authLoader });
485
513
  const newActorUrl = target?.id?.href || "";
486
514
 
487
515
  if (oldActorUrl && newActorUrl) {
@@ -501,11 +529,12 @@ export function registerInboxListeners(inboxChain, options) {
501
529
  })
502
530
  .on(Update, async (ctx, update) => {
503
531
  // Update can be for a profile OR for a post (edited content)
532
+ const authLoader = await getAuthLoader(ctx);
504
533
 
505
534
  // Try to get the object being updated
506
535
  let object;
507
536
  try {
508
- object = await update.getObject();
537
+ object = await update.getObject({ documentLoader: authLoader });
509
538
  } catch {
510
539
  object = null;
511
540
  }
@@ -538,7 +567,7 @@ export function registerInboxListeners(inboxChain, options) {
538
567
  }
539
568
 
540
569
  // PATH 2: Otherwise, assume profile update — refresh stored follower data
541
- const actorObj = await update.getActor();
570
+ const actorObj = await update.getActor({ documentLoader: authLoader });
542
571
  const actorUrl = actorObj?.id?.href || "";
543
572
  if (!actorUrl) return;
544
573
 
@@ -564,7 +593,8 @@ export function registerInboxListeners(inboxChain, options) {
564
593
  })
565
594
  .on(Block, async (ctx, block) => {
566
595
  // Remote actor blocked us — remove them from followers
567
- const actorObj = await block.getActor();
596
+ const authLoader = await getAuthLoader(ctx);
597
+ const actorObj = await block.getActor({ documentLoader: authLoader });
568
598
  const actorUrl = actorObj?.id?.href || "";
569
599
  if (actorUrl) {
570
600
  await collections.ap_followers.deleteOne({ actorUrl });
@@ -36,9 +36,11 @@ export function sanitizeContent(html) {
36
36
  /**
37
37
  * Extract actor information from Fedify Person/Application/Service object
38
38
  * @param {object} actor - Fedify actor object
39
+ * @param {object} [options] - Options
40
+ * @param {object} [options.documentLoader] - Authenticated DocumentLoader for Secure Mode servers
39
41
  * @returns {object} { name, url, photo, handle }
40
42
  */
41
- export async function extractActorInfo(actor) {
43
+ export async function extractActorInfo(actor, options = {}) {
42
44
  if (!actor) {
43
45
  return {
44
46
  name: "Unknown",
@@ -54,10 +56,11 @@ export async function extractActorInfo(actor) {
54
56
  const url = actor.id?.href || "";
55
57
 
56
58
  // Extract photo URL from icon (Fedify uses async getters)
59
+ const loaderOpts = options.documentLoader ? { documentLoader: options.documentLoader } : {};
57
60
  let photo = "";
58
61
  try {
59
62
  if (typeof actor.getIcon === "function") {
60
- const iconObj = await actor.getIcon();
63
+ const iconObj = await actor.getIcon(loaderOpts);
61
64
  photo = iconObj?.url?.href || "";
62
65
  } else {
63
66
  const iconObj = await actor.icon;
@@ -89,6 +92,7 @@ export async function extractActorInfo(actor) {
89
92
  * @param {object} [options.boostedBy] - Actor info for boosts
90
93
  * @param {Date} [options.boostedAt] - Boost timestamp
91
94
  * @param {object} [options.actorFallback] - Fedify actor to use when object.getAttributedTo() fails
95
+ * @param {object} [options.documentLoader] - Authenticated DocumentLoader for Secure Mode servers
92
96
  * @returns {Promise<object>} Timeline item data
93
97
  */
94
98
  export async function extractObjectData(object, options = {}) {
@@ -130,14 +134,15 @@ export async function extractObjectData(object, options = {}) {
130
134
  : new Date().toISOString();
131
135
 
132
136
  // Extract author — try multiple strategies in order of reliability
137
+ const loaderOpts = options.documentLoader ? { documentLoader: options.documentLoader } : {};
133
138
  let authorObj = null;
134
139
  try {
135
140
  if (typeof object.getAttributedTo === "function") {
136
- const attr = await object.getAttributedTo();
141
+ const attr = await object.getAttributedTo(loaderOpts);
137
142
  authorObj = Array.isArray(attr) ? attr[0] : attr;
138
143
  }
139
144
  } catch {
140
- // getAttributedTo() failed (Authorized Fetch, unreachable, etc.)
145
+ // getAttributedTo() failed (unreachable, deleted, etc.)
141
146
  }
142
147
  // If getAttributedTo() returned nothing, use the actor from the wrapping activity
143
148
  if (!authorObj && options.actorFallback) {
@@ -150,7 +155,7 @@ export async function extractObjectData(object, options = {}) {
150
155
 
151
156
  let author;
152
157
  if (authorObj) {
153
- author = await extractActorInfo(authorObj);
158
+ author = await extractActorInfo(authorObj, loaderOpts);
154
159
  } else {
155
160
  // Last resort: use attributionIds (non-fetching) to get at least a URL
156
161
  const attrIds = object.attributionIds;
@@ -184,7 +189,7 @@ export async function extractObjectData(object, options = {}) {
184
189
  const category = [];
185
190
  try {
186
191
  if (typeof object.getTags === "function") {
187
- const tags = await object.getTags();
192
+ const tags = await object.getTags(loaderOpts);
188
193
  for await (const tag of tags) {
189
194
  if (tag.name) {
190
195
  const tagName = tag.name.toString().replace(/^#/, "");
@@ -203,7 +208,7 @@ export async function extractObjectData(object, options = {}) {
203
208
 
204
209
  try {
205
210
  if (typeof object.getAttachments === "function") {
206
- const attachments = await object.getAttachments();
211
+ const attachments = await object.getAttachments(loaderOpts);
207
212
  for await (const att of attachments) {
208
213
  const mediaUrl = att.url?.href || "";
209
214
  if (!mediaUrl) continue;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rmdes/indiekit-endpoint-activitypub",
3
- "version": "2.0.25",
3
+ "version": "2.0.27",
4
4
  "description": "ActivityPub federation endpoint for Indiekit via Fedify. Adds full fediverse support: actor, inbox, outbox, followers, following, syndication, and Mastodon migration.",
5
5
  "keywords": [
6
6
  "indiekit",