@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 +56 -1
- package/lib/federation-bridge.js +5 -13
- package/lib/inbox-listeners.js +57 -27
- package/lib/timeline-store.js +12 -7
- package/package.json +1 -1
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** —
|
|
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
|
|
package/lib/federation-bridge.js
CHANGED
|
@@ -72,11 +72,11 @@ async function sendFedifyResponse(res, response, request) {
|
|
|
72
72
|
return;
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
-
// WORKAROUND:
|
|
76
|
-
//
|
|
77
|
-
//
|
|
78
|
-
//
|
|
79
|
-
//
|
|
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
|
}
|
package/lib/inbox-listeners.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
221
|
-
//
|
|
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 (
|
|
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
|
|
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
|
|
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 });
|
package/lib/timeline-store.js
CHANGED
|
@@ -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 (
|
|
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.
|
|
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",
|