@rmdes/indiekit-endpoint-activitypub 3.5.0 → 3.5.2

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/index.js CHANGED
@@ -224,6 +224,14 @@ export default class ActivityPubEndpoint {
224
224
  // Skip Fedify for admin UI routes — they're handled by the
225
225
  // authenticated `routes` getter, not the federation layer.
226
226
  if (req.path.startsWith("/admin")) return next();
227
+
228
+ // Diagnostic: log inbox POSTs to detect federation stalls
229
+ if (req.method === "POST" && req.path.includes("inbox")) {
230
+ const ua = req.get("user-agent") || "unknown";
231
+ const bodyParsed = req.body !== undefined && Object.keys(req.body || {}).length > 0;
232
+ console.info(`[federation-diag] POST ${req.path} from=${ua.slice(0, 60)} bodyParsed=${bodyParsed} readable=${req.readable}`);
233
+ }
234
+
227
235
  return self._fedifyMiddleware(req, res, next);
228
236
  });
229
237
 
@@ -46,12 +46,31 @@ export function registerInboxListeners(inboxChain, options) {
46
46
 
47
47
  const getAuthLoader = (ctx) => ctx.getDocumentLoader({ identifier: handle });
48
48
 
49
+ // Diagnostic: track listener invocations to detect federation stalls
50
+ const _diag = { count: 0, lastType: "", lastActor: "", lastAt: 0 };
51
+ const _diagInterval = setInterval(() => {
52
+ if (_diag.count > 0) {
53
+ console.info(`[inbox-diag] ${_diag.count} activities received in last 5min (last: ${_diag.lastType} from ${_diag.lastActor})`);
54
+ _diag.count = 0;
55
+ }
56
+ }, 5 * 60 * 1000);
57
+ // Prevent timer from keeping the process alive
58
+ _diagInterval.unref?.();
59
+
60
+ const _diagTrack = (type, actorUrl) => {
61
+ _diag.count++;
62
+ _diag.lastType = type;
63
+ _diag.lastActor = actorUrl?.split("/").pop() || "?";
64
+ _diag.lastAt = Date.now();
65
+ };
66
+
49
67
  inboxChain
50
68
  // ── Follow ──────────────────────────────────────────────────────
51
69
  // Synchronous: Accept/Reject + follower storage (federation requirement)
52
70
  // Async: notification + activity log
53
71
  .on(Follow, async (ctx, follow) => {
54
72
  const actorUrl = follow.actorId?.href || "";
73
+ _diagTrack("Follow", actorUrl);
55
74
  if (await isServerBlocked(actorUrl, collections)) return;
56
75
  await touchKeyFreshness(collections, actorUrl);
57
76
  await resetDeliveryStrikes(collections, actorUrl);
@@ -226,6 +245,7 @@ export function registerInboxListeners(inboxChain, options) {
226
245
  // ── Announce ────────────────────────────────────────────────────
227
246
  .on(Announce, async (ctx, announce) => {
228
247
  const actorUrl = announce.actorId?.href || "";
248
+ _diagTrack("Announce", actorUrl);
229
249
  if (await isServerBlocked(actorUrl, collections)) return;
230
250
  await touchKeyFreshness(collections, actorUrl);
231
251
  await resetDeliveryStrikes(collections, actorUrl);
@@ -241,6 +261,7 @@ export function registerInboxListeners(inboxChain, options) {
241
261
  // ── Create ──────────────────────────────────────────────────────
242
262
  .on(Create, async (ctx, create) => {
243
263
  const actorUrl = create.actorId?.href || "";
264
+ _diagTrack("Create", actorUrl);
244
265
  if (await isServerBlocked(actorUrl, collections)) return;
245
266
  await touchKeyFreshness(collections, actorUrl);
246
267
  await resetDeliveryStrikes(collections, actorUrl);
@@ -291,6 +312,7 @@ export function registerInboxListeners(inboxChain, options) {
291
312
  // ── Delete ──────────────────────────────────────────────────────
292
313
  .on(Delete, async (ctx, del) => {
293
314
  const actorUrl = del.actorId?.href || "";
315
+ _diagTrack("Delete", actorUrl);
294
316
  if (await isServerBlocked(actorUrl, collections)) return;
295
317
  await touchKeyFreshness(collections, actorUrl);
296
318
  await resetDeliveryStrikes(collections, actorUrl);
@@ -320,6 +342,7 @@ export function registerInboxListeners(inboxChain, options) {
320
342
  // ── Update ──────────────────────────────────────────────────────
321
343
  .on(Update, async (ctx, update) => {
322
344
  const actorUrl = update.actorId?.href || "";
345
+ _diagTrack("Update", actorUrl);
323
346
  if (await isServerBlocked(actorUrl, collections)) return;
324
347
  await touchKeyFreshness(collections, actorUrl);
325
348
  await resetDeliveryStrikes(collections, actorUrl);
@@ -83,11 +83,32 @@ export async function enqueueActivity(collections, { activityType, actorUrl, obj
83
83
  * @returns {NodeJS.Timeout} Interval ID (for cleanup)
84
84
  */
85
85
  export function startInboxProcessor(collections, getCtx, handle) {
86
+ // Diagnostic: detect stuck processing items and log queue health
87
+ let _diagProcessed = 0;
88
+ const _diagInterval = setInterval(async () => {
89
+ try {
90
+ const stuck = await collections.ap_inbox_queue?.countDocuments({ status: "processing" }) || 0;
91
+ const pending = await collections.ap_inbox_queue?.countDocuments({ status: "pending" }) || 0;
92
+ if (stuck > 0 || _diagProcessed > 0 || pending > 10) {
93
+ console.info(`[inbox-queue-diag] processed=${_diagProcessed}/5min pending=${pending} stuck_processing=${stuck}`);
94
+ }
95
+ _diagProcessed = 0;
96
+ } catch { /* ignore */ }
97
+ }, 5 * 60 * 1000);
98
+ _diagInterval.unref?.();
99
+
86
100
  const intervalId = setInterval(async () => {
87
101
  try {
88
102
  const ctx = getCtx();
89
103
  if (ctx) {
104
+ const before = Date.now();
90
105
  await processNextItem(collections, ctx, handle);
106
+ const elapsed = Date.now() - before;
107
+ if (elapsed > 0) _diagProcessed++;
108
+ // Warn if a single item takes too long (potential hang)
109
+ if (elapsed > 30_000) {
110
+ console.warn(`[inbox-queue-diag] slow item: ${elapsed}ms`);
111
+ }
91
112
  }
92
113
  } catch (error) {
93
114
  console.error("[inbox-queue] Processor error:", error.message);
@@ -219,10 +219,11 @@ router.get("/oauth/authorize", async (req, res, next) => {
219
219
  // Check if user is logged in via IndieAuth session
220
220
  const session = req.session;
221
221
  if (!session?.access_token && !force_login) {
222
- // Not logged in — redirect to Indiekit login, then back here
223
- const returnUrl = `${req.protocol}://${req.get("host")}${req.originalUrl}`;
222
+ // Not logged in — redirect to Indiekit's login page, then back here.
223
+ // Indiekit uses /session/login?redirect=<path> (see indieauth.js authenticate()).
224
+ // The redirect value must be a local path (validated by regex in indieauth.js).
224
225
  return res.redirect(
225
- `/auth?redirect=${encodeURIComponent(returnUrl)}`,
226
+ `/session/login?redirect=${encodeURIComponent(req.originalUrl)}`,
226
227
  );
227
228
  }
228
229
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rmdes/indiekit-endpoint-activitypub",
3
- "version": "3.5.0",
3
+ "version": "3.5.2",
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",