@rmdes/indiekit-endpoint-activitypub 3.5.0 → 3.5.3

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);
@@ -174,7 +174,7 @@ router.get("/.well-known/oauth-authorization-server", (req, res) => {
174
174
 
175
175
  router.get("/oauth/authorize", async (req, res, next) => {
176
176
  try {
177
- const {
177
+ let {
178
178
  client_id,
179
179
  redirect_uri,
180
180
  response_type,
@@ -184,6 +184,21 @@ router.get("/oauth/authorize", async (req, res, next) => {
184
184
  force_login,
185
185
  } = req.query;
186
186
 
187
+ // Restore OAuth params from session after login redirect.
188
+ // Indiekit's login flow doesn't re-encode the redirect param, so query
189
+ // params with & are stripped during the /session/login → /session/auth
190
+ // round-trip. We store them in the session before redirecting.
191
+ if (!response_type && req.session?.pendingOAuth) {
192
+ const p = req.session.pendingOAuth;
193
+ delete req.session.pendingOAuth;
194
+ client_id = p.client_id;
195
+ redirect_uri = p.redirect_uri;
196
+ response_type = p.response_type;
197
+ scope = p.scope;
198
+ code_challenge = p.code_challenge;
199
+ code_challenge_method = p.code_challenge_method;
200
+ }
201
+
187
202
  if (response_type !== "code") {
188
203
  return res.status(400).json({
189
204
  error: "unsupported_response_type",
@@ -219,11 +234,14 @@ router.get("/oauth/authorize", async (req, res, next) => {
219
234
  // Check if user is logged in via IndieAuth session
220
235
  const session = req.session;
221
236
  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}`;
224
- return res.redirect(
225
- `/auth?redirect=${encodeURIComponent(returnUrl)}`,
226
- );
237
+ // Store OAuth params in session they won't survive Indiekit's
238
+ // login redirect chain due to a re-encoding bug in indieauth.js.
239
+ req.session.pendingOAuth = {
240
+ client_id, redirect_uri, response_type, scope,
241
+ code_challenge, code_challenge_method,
242
+ };
243
+ // Redirect to Indiekit's login page with a simple return path.
244
+ return res.redirect("/session/login?redirect=/oauth/authorize");
227
245
  }
228
246
 
229
247
  // Render simple authorization page
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.3",
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",