@rmdes/indiekit-endpoint-activitypub 2.14.0 → 2.15.0
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-setup.js +8 -17
- package/lib/inbox-handlers.js +82 -1
- package/lib/inbox-listeners.js +48 -1
- package/lib/outbox-failure.js +139 -0
- package/package.json +1 -1
package/lib/federation-setup.js
CHANGED
|
@@ -41,6 +41,7 @@ import { MongoKvStore } from "./kv-store.js";
|
|
|
41
41
|
import { registerInboxListeners } from "./inbox-listeners.js";
|
|
42
42
|
import { jf2ToAS2Activity, resolvePostUrl } from "./jf2-to-as2.js";
|
|
43
43
|
import { cachedQuery } from "./redis-cache.js";
|
|
44
|
+
import { onOutboxPermanentFailure } from "./outbox-failure.js";
|
|
44
45
|
|
|
45
46
|
const COLLECTION_CACHE_TTL = 300; // 5 minutes
|
|
46
47
|
|
|
@@ -342,25 +343,15 @@ export function setupFederation(options) {
|
|
|
342
343
|
});
|
|
343
344
|
|
|
344
345
|
// Handle permanent delivery failures (Fedify 2.0).
|
|
345
|
-
// Fires when a remote inbox returns 404/410
|
|
346
|
-
//
|
|
346
|
+
// Fires when a remote inbox returns 404/410.
|
|
347
|
+
// 410: immediate full cleanup. 404: strike system (3 strikes over 7 days).
|
|
347
348
|
federation.setOutboxPermanentFailureHandler(async (_ctx, values) => {
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
(actors.length ? ` (actors: ${actors.join(", ")})` : ""),
|
|
349
|
+
await onOutboxPermanentFailure(
|
|
350
|
+
values.statusCode,
|
|
351
|
+
values.actorIds,
|
|
352
|
+
values.inbox,
|
|
353
|
+
collections,
|
|
354
354
|
);
|
|
355
|
-
collections.ap_activities.insertOne({
|
|
356
|
-
direction: "outbound",
|
|
357
|
-
type: "DeliveryFailed",
|
|
358
|
-
actorUrl: publicationUrl,
|
|
359
|
-
objectUrl: inboxUrl,
|
|
360
|
-
summary: `Permanent delivery failure to ${inboxUrl}: ${error?.message || "unknown"}`,
|
|
361
|
-
affectedActors: actors,
|
|
362
|
-
receivedAt: new Date().toISOString(),
|
|
363
|
-
}).catch(() => {});
|
|
364
355
|
});
|
|
365
356
|
|
|
366
357
|
// Wrap with debug dashboard if enabled. The debugger proxies the
|
package/lib/inbox-handlers.js
CHANGED
|
@@ -152,6 +152,73 @@ function isDirectMessage(object, ourActorUrl, followersUrl) {
|
|
|
152
152
|
return true;
|
|
153
153
|
}
|
|
154
154
|
|
|
155
|
+
/**
|
|
156
|
+
* Compute post visibility from to/cc addressing fields.
|
|
157
|
+
* Matches Hollo's write-time visibility classification.
|
|
158
|
+
*
|
|
159
|
+
* @param {object} object - Fedify object (Note, Article, etc.)
|
|
160
|
+
* @returns {"public"|"unlisted"|"private"|"direct"}
|
|
161
|
+
*/
|
|
162
|
+
function computeVisibility(object) {
|
|
163
|
+
const to = new Set((object.toIds || []).map((u) => u.href));
|
|
164
|
+
const cc = new Set((object.ccIds || []).map((u) => u.href));
|
|
165
|
+
|
|
166
|
+
if (to.has(PUBLIC)) return "public";
|
|
167
|
+
if (cc.has(PUBLIC)) return "unlisted";
|
|
168
|
+
// Without knowing the remote actor's followers URL, we can't distinguish
|
|
169
|
+
// "private" (followers-only) from "direct". Both are non-public.
|
|
170
|
+
if (to.size > 0 || cc.size > 0) return "private";
|
|
171
|
+
return "direct";
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Recursively fetch and store ancestor posts for a reply chain.
|
|
176
|
+
* Each ancestor is stored with isContext: true so it can be filtered
|
|
177
|
+
* from the main timeline while being available for thread views.
|
|
178
|
+
*
|
|
179
|
+
* @param {object} object - Fedify object (Note, Article, etc.)
|
|
180
|
+
* @param {object} collections - MongoDB collections
|
|
181
|
+
* @param {object} authLoader - Authenticated document loader
|
|
182
|
+
* @param {number} maxDepth - Maximum recursion depth
|
|
183
|
+
*/
|
|
184
|
+
async function fetchReplyChain(object, collections, authLoader, maxDepth) {
|
|
185
|
+
if (maxDepth <= 0) return;
|
|
186
|
+
const parentUrl = object.replyTargetId?.href;
|
|
187
|
+
if (!parentUrl) return;
|
|
188
|
+
|
|
189
|
+
// Skip if we already have this post
|
|
190
|
+
if (collections.ap_timeline) {
|
|
191
|
+
const existing = await collections.ap_timeline.findOne({ uid: parentUrl });
|
|
192
|
+
if (existing) return;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Fetch the parent post
|
|
196
|
+
let parent;
|
|
197
|
+
try {
|
|
198
|
+
parent = await object.getReplyTarget({ documentLoader: authLoader });
|
|
199
|
+
} catch {
|
|
200
|
+
// Remote server unreachable — stop climbing
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
if (!parent || !parent.id) return;
|
|
204
|
+
|
|
205
|
+
// Store as context item
|
|
206
|
+
try {
|
|
207
|
+
const timelineItem = await extractObjectData(parent, {
|
|
208
|
+
documentLoader: authLoader,
|
|
209
|
+
});
|
|
210
|
+
timelineItem.isContext = true;
|
|
211
|
+
timelineItem.visibility = computeVisibility(parent);
|
|
212
|
+
await addTimelineItem(collections, timelineItem);
|
|
213
|
+
} catch {
|
|
214
|
+
// Extraction failed — stop climbing
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Recurse for the parent's parent
|
|
219
|
+
await fetchReplyChain(parent, collections, authLoader, maxDepth - 1);
|
|
220
|
+
}
|
|
221
|
+
|
|
155
222
|
// ---------------------------------------------------------------------------
|
|
156
223
|
// Individual handlers
|
|
157
224
|
// ---------------------------------------------------------------------------
|
|
@@ -515,7 +582,7 @@ export async function handleAnnounce(item, collections, ctx, handle) {
|
|
|
515
582
|
boostedAt: announce.published ? String(announce.published) : new Date().toISOString(),
|
|
516
583
|
documentLoader: authLoader,
|
|
517
584
|
});
|
|
518
|
-
|
|
585
|
+
timelineItem.visibility = computeVisibility(object);
|
|
519
586
|
await addTimelineItem(collections, timelineItem);
|
|
520
587
|
|
|
521
588
|
// Fire-and-forget quote enrichment for boosted posts
|
|
@@ -688,6 +755,18 @@ export async function handleCreate(item, collections, ctx, handle) {
|
|
|
688
755
|
}
|
|
689
756
|
}
|
|
690
757
|
|
|
758
|
+
// --- Recursive reply chain fetching ---
|
|
759
|
+
// Fetch and store ancestor posts so conversation threads have context.
|
|
760
|
+
// Each ancestor is stored with isContext: true to distinguish from organic timeline items.
|
|
761
|
+
if (inReplyTo) {
|
|
762
|
+
try {
|
|
763
|
+
await fetchReplyChain(object, collections, authLoader, 5);
|
|
764
|
+
} catch (error) {
|
|
765
|
+
// Non-critical — incomplete context is acceptable
|
|
766
|
+
console.warn("[inbox-handlers] Reply chain fetch failed:", error.message);
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
|
|
691
770
|
// Check for mentions of our actor
|
|
692
771
|
if (object.tag) {
|
|
693
772
|
const tags = Array.isArray(object.tag) ? object.tag : [object.tag];
|
|
@@ -728,6 +807,7 @@ export async function handleCreate(item, collections, ctx, handle) {
|
|
|
728
807
|
actorFallback: actorObj,
|
|
729
808
|
documentLoader: authLoader,
|
|
730
809
|
});
|
|
810
|
+
timelineItem.visibility = computeVisibility(object);
|
|
731
811
|
await addTimelineItem(collections, timelineItem);
|
|
732
812
|
|
|
733
813
|
// Fire-and-forget OG unfurling for notes and articles (not boosts)
|
|
@@ -768,6 +848,7 @@ export async function handleCreate(item, collections, ctx, handle) {
|
|
|
768
848
|
actorFallback: actorObj,
|
|
769
849
|
documentLoader: authLoader,
|
|
770
850
|
});
|
|
851
|
+
timelineItem.visibility = computeVisibility(object);
|
|
771
852
|
await addTimelineItem(collections, timelineItem);
|
|
772
853
|
}
|
|
773
854
|
}
|
package/lib/inbox-listeners.js
CHANGED
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
|
|
28
28
|
import { isServerBlocked } from "./storage/server-blocks.js";
|
|
29
29
|
import { touchKeyFreshness } from "./key-refresh.js";
|
|
30
|
+
import { resetDeliveryStrikes } from "./outbox-failure.js";
|
|
30
31
|
import { enqueueActivity } from "./inbox-queue.js";
|
|
31
32
|
import { extractActorInfo } from "./timeline-store.js";
|
|
32
33
|
import { addNotification } from "./storage/notifications.js";
|
|
@@ -53,6 +54,7 @@ export function registerInboxListeners(inboxChain, options) {
|
|
|
53
54
|
const actorUrl = follow.actorId?.href || "";
|
|
54
55
|
if (await isServerBlocked(actorUrl, collections)) return;
|
|
55
56
|
await touchKeyFreshness(collections, actorUrl);
|
|
57
|
+
await resetDeliveryStrikes(collections, actorUrl);
|
|
56
58
|
|
|
57
59
|
const authLoader = await getAuthLoader(ctx);
|
|
58
60
|
const followerActor = await follow.getActor({ documentLoader: authLoader });
|
|
@@ -152,6 +154,7 @@ export function registerInboxListeners(inboxChain, options) {
|
|
|
152
154
|
const actorUrl = undo.actorId?.href || "";
|
|
153
155
|
if (await isServerBlocked(actorUrl, collections)) return;
|
|
154
156
|
await touchKeyFreshness(collections, actorUrl);
|
|
157
|
+
await resetDeliveryStrikes(collections, actorUrl);
|
|
155
158
|
|
|
156
159
|
await enqueueActivity(collections, {
|
|
157
160
|
activityType: "Undo",
|
|
@@ -165,6 +168,7 @@ export function registerInboxListeners(inboxChain, options) {
|
|
|
165
168
|
const actorUrl = accept.actorId?.href || "";
|
|
166
169
|
if (await isServerBlocked(actorUrl, collections)) return;
|
|
167
170
|
await touchKeyFreshness(collections, actorUrl);
|
|
171
|
+
await resetDeliveryStrikes(collections, actorUrl);
|
|
168
172
|
|
|
169
173
|
await enqueueActivity(collections, {
|
|
170
174
|
activityType: "Accept",
|
|
@@ -178,6 +182,7 @@ export function registerInboxListeners(inboxChain, options) {
|
|
|
178
182
|
const actorUrl = reject.actorId?.href || "";
|
|
179
183
|
if (await isServerBlocked(actorUrl, collections)) return;
|
|
180
184
|
await touchKeyFreshness(collections, actorUrl);
|
|
185
|
+
await resetDeliveryStrikes(collections, actorUrl);
|
|
181
186
|
|
|
182
187
|
await enqueueActivity(collections, {
|
|
183
188
|
activityType: "Reject",
|
|
@@ -191,6 +196,7 @@ export function registerInboxListeners(inboxChain, options) {
|
|
|
191
196
|
const actorUrl = like.actorId?.href || "";
|
|
192
197
|
if (await isServerBlocked(actorUrl, collections)) return;
|
|
193
198
|
await touchKeyFreshness(collections, actorUrl);
|
|
199
|
+
await resetDeliveryStrikes(collections, actorUrl);
|
|
194
200
|
|
|
195
201
|
await enqueueActivity(collections, {
|
|
196
202
|
activityType: "Like",
|
|
@@ -205,6 +211,7 @@ export function registerInboxListeners(inboxChain, options) {
|
|
|
205
211
|
const actorUrl = announce.actorId?.href || "";
|
|
206
212
|
if (await isServerBlocked(actorUrl, collections)) return;
|
|
207
213
|
await touchKeyFreshness(collections, actorUrl);
|
|
214
|
+
await resetDeliveryStrikes(collections, actorUrl);
|
|
208
215
|
|
|
209
216
|
await enqueueActivity(collections, {
|
|
210
217
|
activityType: "Announce",
|
|
@@ -219,11 +226,47 @@ export function registerInboxListeners(inboxChain, options) {
|
|
|
219
226
|
const actorUrl = create.actorId?.href || "";
|
|
220
227
|
if (await isServerBlocked(actorUrl, collections)) return;
|
|
221
228
|
await touchKeyFreshness(collections, actorUrl);
|
|
229
|
+
await resetDeliveryStrikes(collections, actorUrl);
|
|
230
|
+
|
|
231
|
+
// Forward public replies to our posts to our followers.
|
|
232
|
+
// Must happen here (not in async handler) because forwardActivity
|
|
233
|
+
// is only available on InboxContext, not base Context.
|
|
234
|
+
const objectUrl = create.objectId?.href || "";
|
|
235
|
+
try {
|
|
236
|
+
const obj = await create.getObject();
|
|
237
|
+
const inReplyTo = obj?.replyTargetId?.href || "";
|
|
238
|
+
if (
|
|
239
|
+
inReplyTo &&
|
|
240
|
+
collections._publicationUrl &&
|
|
241
|
+
inReplyTo.startsWith(collections._publicationUrl)
|
|
242
|
+
) {
|
|
243
|
+
// Check if the reply is public (to/cc includes PUBLIC collection)
|
|
244
|
+
const toUrls = (obj.toIds || []).map((u) => u.href);
|
|
245
|
+
const ccUrls = (obj.ccIds || []).map((u) => u.href);
|
|
246
|
+
const isPublic = [...toUrls, ...ccUrls].includes(
|
|
247
|
+
"https://www.w3.org/ns/activitystreams#Public",
|
|
248
|
+
);
|
|
249
|
+
if (isPublic) {
|
|
250
|
+
await ctx.forwardActivity(
|
|
251
|
+
{ identifier: handle },
|
|
252
|
+
"followers",
|
|
253
|
+
{
|
|
254
|
+
skipIfUnsigned: true,
|
|
255
|
+
preferSharedInbox: true,
|
|
256
|
+
excludeBaseUris: [new URL(ctx.origin)],
|
|
257
|
+
},
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
} catch (error) {
|
|
262
|
+
// Non-critical — forwarding failure shouldn't block processing
|
|
263
|
+
console.warn("[inbox-listeners] Reply forwarding failed:", error.message);
|
|
264
|
+
}
|
|
222
265
|
|
|
223
266
|
await enqueueActivity(collections, {
|
|
224
267
|
activityType: "Create",
|
|
225
268
|
actorUrl,
|
|
226
|
-
objectUrl
|
|
269
|
+
objectUrl,
|
|
227
270
|
rawJson: await create.toJsonLd(),
|
|
228
271
|
});
|
|
229
272
|
})
|
|
@@ -233,6 +276,7 @@ export function registerInboxListeners(inboxChain, options) {
|
|
|
233
276
|
const actorUrl = del.actorId?.href || "";
|
|
234
277
|
if (await isServerBlocked(actorUrl, collections)) return;
|
|
235
278
|
await touchKeyFreshness(collections, actorUrl);
|
|
279
|
+
await resetDeliveryStrikes(collections, actorUrl);
|
|
236
280
|
|
|
237
281
|
await enqueueActivity(collections, {
|
|
238
282
|
activityType: "Delete",
|
|
@@ -247,6 +291,7 @@ export function registerInboxListeners(inboxChain, options) {
|
|
|
247
291
|
const actorUrl = move.actorId?.href || "";
|
|
248
292
|
if (await isServerBlocked(actorUrl, collections)) return;
|
|
249
293
|
await touchKeyFreshness(collections, actorUrl);
|
|
294
|
+
await resetDeliveryStrikes(collections, actorUrl);
|
|
250
295
|
|
|
251
296
|
await enqueueActivity(collections, {
|
|
252
297
|
activityType: "Move",
|
|
@@ -260,6 +305,7 @@ export function registerInboxListeners(inboxChain, options) {
|
|
|
260
305
|
const actorUrl = update.actorId?.href || "";
|
|
261
306
|
if (await isServerBlocked(actorUrl, collections)) return;
|
|
262
307
|
await touchKeyFreshness(collections, actorUrl);
|
|
308
|
+
await resetDeliveryStrikes(collections, actorUrl);
|
|
263
309
|
|
|
264
310
|
await enqueueActivity(collections, {
|
|
265
311
|
activityType: "Update",
|
|
@@ -299,6 +345,7 @@ export function registerInboxListeners(inboxChain, options) {
|
|
|
299
345
|
const actorUrl = flag.actorId?.href || "";
|
|
300
346
|
if (await isServerBlocked(actorUrl, collections)) return;
|
|
301
347
|
await touchKeyFreshness(collections, actorUrl);
|
|
348
|
+
await resetDeliveryStrikes(collections, actorUrl);
|
|
302
349
|
|
|
303
350
|
await enqueueActivity(collections, {
|
|
304
351
|
activityType: "Flag",
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Outbox permanent failure handling.
|
|
3
|
+
* Cleans up dead followers when delivery permanently fails.
|
|
4
|
+
*
|
|
5
|
+
* - 410 Gone: Immediate full cleanup (actor is permanently gone)
|
|
6
|
+
* - 404: Strike system — 3 failures over 7+ days triggers full cleanup
|
|
7
|
+
*
|
|
8
|
+
* @module outbox-failure
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { logActivity } from "./activity-log.js";
|
|
12
|
+
|
|
13
|
+
const STRIKE_THRESHOLD = 3;
|
|
14
|
+
const STRIKE_WINDOW_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Clean up all data associated with an actor.
|
|
18
|
+
* Removes follower record, their timeline items, and their notifications.
|
|
19
|
+
*
|
|
20
|
+
* @param {object} collections - MongoDB collections
|
|
21
|
+
* @param {string} actorUrl - Actor URL to clean up
|
|
22
|
+
* @param {string} reason - Reason for cleanup (for logging)
|
|
23
|
+
*/
|
|
24
|
+
async function cleanupActor(collections, actorUrl, reason) {
|
|
25
|
+
const { ap_followers, ap_timeline, ap_notifications } = collections;
|
|
26
|
+
|
|
27
|
+
// Remove from followers
|
|
28
|
+
const deleted = await ap_followers.deleteOne({ actorUrl });
|
|
29
|
+
|
|
30
|
+
// Remove their timeline items
|
|
31
|
+
if (ap_timeline) {
|
|
32
|
+
await ap_timeline.deleteMany({ "author.url": actorUrl });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Remove their notifications
|
|
36
|
+
if (ap_notifications) {
|
|
37
|
+
await ap_notifications.deleteMany({ actorUrl });
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (deleted.deletedCount > 0) {
|
|
41
|
+
console.info(`[outbox-failure] Cleaned up actor ${actorUrl}: ${reason}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Handle permanent outbox delivery failure.
|
|
47
|
+
* Called by Fedify's setOutboxPermanentFailureHandler.
|
|
48
|
+
*
|
|
49
|
+
* @param {number} statusCode - HTTP status code (404, 410, etc.)
|
|
50
|
+
* @param {readonly URL[]} actorIds - Array of actor ID URLs
|
|
51
|
+
* @param {URL} inbox - The inbox URL that failed
|
|
52
|
+
* @param {object} collections - MongoDB collections
|
|
53
|
+
*/
|
|
54
|
+
export async function onOutboxPermanentFailure(statusCode, actorIds, inbox, collections) {
|
|
55
|
+
const inboxUrl = inbox?.href || String(inbox);
|
|
56
|
+
|
|
57
|
+
for (const actorId of actorIds) {
|
|
58
|
+
const actorUrl = actorId?.href || String(actorId);
|
|
59
|
+
|
|
60
|
+
if (statusCode === 410) {
|
|
61
|
+
// 410 Gone — immediate full cleanup
|
|
62
|
+
await cleanupActor(collections, actorUrl, `410 Gone from ${inboxUrl}`);
|
|
63
|
+
|
|
64
|
+
await logActivity(collections.ap_activities, {
|
|
65
|
+
direction: "outbound",
|
|
66
|
+
type: "DeliveryFailed:410",
|
|
67
|
+
actorUrl,
|
|
68
|
+
objectUrl: inboxUrl,
|
|
69
|
+
summary: `Permanent delivery failure (410 Gone) to ${inboxUrl} — actor cleaned up`,
|
|
70
|
+
}, {});
|
|
71
|
+
} else {
|
|
72
|
+
// 404 or other — strike system
|
|
73
|
+
const now = new Date();
|
|
74
|
+
const result = await collections.ap_followers.findOneAndUpdate(
|
|
75
|
+
{ actorUrl },
|
|
76
|
+
{
|
|
77
|
+
$inc: { deliveryFailures: 1 },
|
|
78
|
+
$setOnInsert: { firstFailureAt: now.toISOString() },
|
|
79
|
+
$set: { lastFailureAt: now.toISOString() },
|
|
80
|
+
},
|
|
81
|
+
{ returnDocument: "after" },
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
if (!result) {
|
|
85
|
+
// Not a follower — nothing to track or clean up
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const failures = result.deliveryFailures || 1;
|
|
90
|
+
const firstFailure = result.firstFailureAt
|
|
91
|
+
? new Date(result.firstFailureAt)
|
|
92
|
+
: now;
|
|
93
|
+
const windowElapsed = now.getTime() - firstFailure.getTime() >= STRIKE_WINDOW_MS;
|
|
94
|
+
|
|
95
|
+
if (failures >= STRIKE_THRESHOLD && windowElapsed) {
|
|
96
|
+
// Confirmed dead — full cleanup
|
|
97
|
+
await cleanupActor(
|
|
98
|
+
collections,
|
|
99
|
+
actorUrl,
|
|
100
|
+
`${failures} failures over ${Math.round((now.getTime() - firstFailure.getTime()) / 86400000)}d (HTTP ${statusCode})`,
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
await logActivity(collections.ap_activities, {
|
|
104
|
+
direction: "outbound",
|
|
105
|
+
type: `DeliveryFailed:${statusCode}:cleanup`,
|
|
106
|
+
actorUrl,
|
|
107
|
+
objectUrl: inboxUrl,
|
|
108
|
+
summary: `${failures} delivery failures over 7+ days — actor cleaned up`,
|
|
109
|
+
}, {});
|
|
110
|
+
} else {
|
|
111
|
+
// Strike recorded, not yet confirmed dead
|
|
112
|
+
await logActivity(collections.ap_activities, {
|
|
113
|
+
direction: "outbound",
|
|
114
|
+
type: `DeliveryFailed:${statusCode}:strike`,
|
|
115
|
+
actorUrl,
|
|
116
|
+
objectUrl: inboxUrl,
|
|
117
|
+
summary: `Delivery strike ${failures}/${STRIKE_THRESHOLD} for ${actorUrl} (HTTP ${statusCode})`,
|
|
118
|
+
}, {});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Reset delivery failure strikes for an actor.
|
|
126
|
+
* Called when we receive an inbound activity from an actor,
|
|
127
|
+
* proving they are alive despite previous delivery failures.
|
|
128
|
+
*
|
|
129
|
+
* @param {object} collections - MongoDB collections
|
|
130
|
+
* @param {string} actorUrl - Actor URL
|
|
131
|
+
*/
|
|
132
|
+
export async function resetDeliveryStrikes(collections, actorUrl) {
|
|
133
|
+
if (!actorUrl) return;
|
|
134
|
+
// Only update if the fields exist — avoid unnecessary writes
|
|
135
|
+
await collections.ap_followers.updateOne(
|
|
136
|
+
{ actorUrl, deliveryFailures: { $exists: true } },
|
|
137
|
+
{ $unset: { deliveryFailures: "", firstFailureAt: "", lastFailureAt: "" } },
|
|
138
|
+
);
|
|
139
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rmdes/indiekit-endpoint-activitypub",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.15.0",
|
|
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",
|