@rmdes/indiekit-endpoint-activitypub 2.0.25 → 2.0.26
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/lib/federation-bridge.js +5 -13
- package/lib/inbox-listeners.js +47 -27
- package/lib/timeline-store.js +12 -7
- package/package.json +1 -1
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
|
}
|
|
@@ -250,7 +263,7 @@ export function registerInboxListeners(inboxChain, options) {
|
|
|
250
263
|
});
|
|
251
264
|
|
|
252
265
|
// Store notification
|
|
253
|
-
const actorInfo = await extractActorInfo(actorObj);
|
|
266
|
+
const actorInfo = await extractActorInfo(actorObj, { documentLoader: authLoader });
|
|
254
267
|
await addNotification(collections, {
|
|
255
268
|
uid: like.id?.href || `like:${actorUrl}:${objectId}`,
|
|
256
269
|
type: "like",
|
|
@@ -268,6 +281,7 @@ export function registerInboxListeners(inboxChain, options) {
|
|
|
268
281
|
const objectId = announce.objectId?.href || "";
|
|
269
282
|
if (!objectId) return;
|
|
270
283
|
|
|
284
|
+
const authLoader = await getAuthLoader(ctx);
|
|
271
285
|
const actorUrl = announce.actorId?.href || "";
|
|
272
286
|
const pubUrl = collections._publicationUrl;
|
|
273
287
|
|
|
@@ -277,7 +291,7 @@ export function registerInboxListeners(inboxChain, options) {
|
|
|
277
291
|
if (pubUrl && objectId.startsWith(pubUrl)) {
|
|
278
292
|
let actorObj;
|
|
279
293
|
try {
|
|
280
|
-
actorObj = await announce.getActor();
|
|
294
|
+
actorObj = await announce.getActor({ documentLoader: authLoader });
|
|
281
295
|
} catch {
|
|
282
296
|
actorObj = null;
|
|
283
297
|
}
|
|
@@ -298,7 +312,7 @@ export function registerInboxListeners(inboxChain, options) {
|
|
|
298
312
|
});
|
|
299
313
|
|
|
300
314
|
// Create notification
|
|
301
|
-
const actorInfo = await extractActorInfo(actorObj);
|
|
315
|
+
const actorInfo = await extractActorInfo(actorObj, { documentLoader: authLoader });
|
|
302
316
|
await addNotification(collections, {
|
|
303
317
|
uid: announce.id?.href || `${actorUrl}#boost-${objectId}`,
|
|
304
318
|
type: "boost",
|
|
@@ -319,8 +333,8 @@ export function registerInboxListeners(inboxChain, options) {
|
|
|
319
333
|
const following = await collections.ap_following.findOne({ actorUrl });
|
|
320
334
|
if (following) {
|
|
321
335
|
try {
|
|
322
|
-
// Fetch the original object being boosted
|
|
323
|
-
const object = await announce.getObject();
|
|
336
|
+
// Fetch the original object being boosted (authenticated for Secure Mode servers)
|
|
337
|
+
const object = await announce.getObject({ documentLoader: authLoader });
|
|
324
338
|
if (!object) return;
|
|
325
339
|
|
|
326
340
|
// Skip non-content objects (Lemmy/PieFed like/create activities
|
|
@@ -329,13 +343,14 @@ export function registerInboxListeners(inboxChain, options) {
|
|
|
329
343
|
if (!hasContent) return;
|
|
330
344
|
|
|
331
345
|
// Get booster actor info
|
|
332
|
-
const boosterActor = await announce.getActor();
|
|
333
|
-
const boosterInfo = await extractActorInfo(boosterActor);
|
|
346
|
+
const boosterActor = await announce.getActor({ documentLoader: authLoader });
|
|
347
|
+
const boosterInfo = await extractActorInfo(boosterActor, { documentLoader: authLoader });
|
|
334
348
|
|
|
335
349
|
// Extract and store with boost metadata
|
|
336
350
|
const timelineItem = await extractObjectData(object, {
|
|
337
351
|
boostedBy: boosterInfo,
|
|
338
352
|
boostedAt: announce.published ? String(announce.published) : new Date().toISOString(),
|
|
353
|
+
documentLoader: authLoader,
|
|
339
354
|
});
|
|
340
355
|
|
|
341
356
|
await addTimelineItem(collections, timelineItem);
|
|
@@ -347,11 +362,12 @@ export function registerInboxListeners(inboxChain, options) {
|
|
|
347
362
|
}
|
|
348
363
|
})
|
|
349
364
|
.on(Create, async (ctx, create) => {
|
|
365
|
+
const authLoader = await getAuthLoader(ctx);
|
|
350
366
|
let object;
|
|
351
367
|
try {
|
|
352
|
-
object = await create.getObject();
|
|
368
|
+
object = await create.getObject({ documentLoader: authLoader });
|
|
353
369
|
} catch {
|
|
354
|
-
// Remote object not dereferenceable (
|
|
370
|
+
// Remote object not dereferenceable (deleted, etc.)
|
|
355
371
|
return;
|
|
356
372
|
}
|
|
357
373
|
if (!object) return;
|
|
@@ -359,7 +375,7 @@ export function registerInboxListeners(inboxChain, options) {
|
|
|
359
375
|
const actorUrl = create.actorId?.href || "";
|
|
360
376
|
let actorObj;
|
|
361
377
|
try {
|
|
362
|
-
actorObj = await create.getActor();
|
|
378
|
+
actorObj = await create.getActor({ documentLoader: authLoader });
|
|
363
379
|
} catch {
|
|
364
380
|
// Actor not dereferenceable — use URL as fallback
|
|
365
381
|
actorObj = null;
|
|
@@ -389,7 +405,7 @@ export function registerInboxListeners(inboxChain, options) {
|
|
|
389
405
|
|
|
390
406
|
// Create notification if reply is to one of OUR posts
|
|
391
407
|
if (pubUrl && inReplyTo.startsWith(pubUrl)) {
|
|
392
|
-
const actorInfo = await extractActorInfo(actorObj);
|
|
408
|
+
const actorInfo = await extractActorInfo(actorObj, { documentLoader: authLoader });
|
|
393
409
|
const rawHtml = object.content?.toString() || "";
|
|
394
410
|
const contentHtml = sanitizeContent(rawHtml);
|
|
395
411
|
const contentText = rawHtml.replace(/<[^>]*>/g, "").substring(0, 200);
|
|
@@ -420,7 +436,7 @@ export function registerInboxListeners(inboxChain, options) {
|
|
|
420
436
|
|
|
421
437
|
for (const tag of tags) {
|
|
422
438
|
if (tag.type === "Mention" && tag.href?.href === ourActorUrl) {
|
|
423
|
-
const actorInfo = await extractActorInfo(actorObj);
|
|
439
|
+
const actorInfo = await extractActorInfo(actorObj, { documentLoader: authLoader });
|
|
424
440
|
const rawMentionHtml = object.content?.toString() || "";
|
|
425
441
|
const mentionHtml = sanitizeContent(rawMentionHtml);
|
|
426
442
|
const contentText = rawMentionHtml.replace(/<[^>]*>/g, "").substring(0, 200);
|
|
@@ -451,6 +467,7 @@ export function registerInboxListeners(inboxChain, options) {
|
|
|
451
467
|
try {
|
|
452
468
|
const timelineItem = await extractObjectData(object, {
|
|
453
469
|
actorFallback: actorObj,
|
|
470
|
+
documentLoader: authLoader,
|
|
454
471
|
});
|
|
455
472
|
await addTimelineItem(collections, timelineItem);
|
|
456
473
|
|
|
@@ -479,9 +496,10 @@ export function registerInboxListeners(inboxChain, options) {
|
|
|
479
496
|
}
|
|
480
497
|
})
|
|
481
498
|
.on(Move, async (ctx, move) => {
|
|
482
|
-
const
|
|
499
|
+
const authLoader = await getAuthLoader(ctx);
|
|
500
|
+
const oldActorObj = await move.getActor({ documentLoader: authLoader });
|
|
483
501
|
const oldActorUrl = oldActorObj?.id?.href || "";
|
|
484
|
-
const target = await move.getTarget();
|
|
502
|
+
const target = await move.getTarget({ documentLoader: authLoader });
|
|
485
503
|
const newActorUrl = target?.id?.href || "";
|
|
486
504
|
|
|
487
505
|
if (oldActorUrl && newActorUrl) {
|
|
@@ -501,11 +519,12 @@ export function registerInboxListeners(inboxChain, options) {
|
|
|
501
519
|
})
|
|
502
520
|
.on(Update, async (ctx, update) => {
|
|
503
521
|
// Update can be for a profile OR for a post (edited content)
|
|
522
|
+
const authLoader = await getAuthLoader(ctx);
|
|
504
523
|
|
|
505
524
|
// Try to get the object being updated
|
|
506
525
|
let object;
|
|
507
526
|
try {
|
|
508
|
-
object = await update.getObject();
|
|
527
|
+
object = await update.getObject({ documentLoader: authLoader });
|
|
509
528
|
} catch {
|
|
510
529
|
object = null;
|
|
511
530
|
}
|
|
@@ -538,7 +557,7 @@ export function registerInboxListeners(inboxChain, options) {
|
|
|
538
557
|
}
|
|
539
558
|
|
|
540
559
|
// PATH 2: Otherwise, assume profile update — refresh stored follower data
|
|
541
|
-
const actorObj = await update.getActor();
|
|
560
|
+
const actorObj = await update.getActor({ documentLoader: authLoader });
|
|
542
561
|
const actorUrl = actorObj?.id?.href || "";
|
|
543
562
|
if (!actorUrl) return;
|
|
544
563
|
|
|
@@ -564,7 +583,8 @@ export function registerInboxListeners(inboxChain, options) {
|
|
|
564
583
|
})
|
|
565
584
|
.on(Block, async (ctx, block) => {
|
|
566
585
|
// Remote actor blocked us — remove them from followers
|
|
567
|
-
const
|
|
586
|
+
const authLoader = await getAuthLoader(ctx);
|
|
587
|
+
const actorObj = await block.getActor({ documentLoader: authLoader });
|
|
568
588
|
const actorUrl = actorObj?.id?.href || "";
|
|
569
589
|
if (actorUrl) {
|
|
570
590
|
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.26",
|
|
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",
|