@rmdes/indiekit-endpoint-activitypub 1.1.20 → 2.0.1

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
@@ -86,6 +86,8 @@ const defaults = {
86
86
  logLevel: "warning",
87
87
  timelineRetention: 1000,
88
88
  notificationRetentionDays: 30,
89
+ debugDashboard: false,
90
+ debugPassword: "",
89
91
  };
90
92
 
91
93
  export default class ActivityPubEndpoint {
@@ -505,7 +507,7 @@ export default class ActivityPubEndpoint {
505
507
  }
506
508
 
507
509
  try {
508
- const { Follow } = await import("@fedify/fedify");
510
+ const { Follow } = await import("@fedify/fedify/vocab");
509
511
  const handle = this.options.actor.handle;
510
512
  const ctx = this._federation.createContext(
511
513
  new URL(this._publicationUrl),
@@ -607,7 +609,7 @@ export default class ActivityPubEndpoint {
607
609
  }
608
610
 
609
611
  try {
610
- const { Follow, Undo } = await import("@fedify/fedify");
612
+ const { Follow, Undo } = await import("@fedify/fedify/vocab");
611
613
  const handle = this.options.actor.handle;
612
614
  const ctx = this._federation.createContext(
613
615
  new URL(this._publicationUrl),
@@ -692,7 +694,7 @@ export default class ActivityPubEndpoint {
692
694
  if (!this._federation) return;
693
695
 
694
696
  try {
695
- const { Update } = await import("@fedify/fedify");
697
+ const { Update } = await import("@fedify/fedify/vocab");
696
698
  const handle = this.options.actor.handle;
697
699
  const ctx = this._federation.createContext(
698
700
  new URL(this._publicationUrl),
@@ -967,6 +969,8 @@ export default class ActivityPubEndpoint {
967
969
  parallelWorkers: this.options.parallelWorkers,
968
970
  actorType: this.options.actorType,
969
971
  logLevel: this.options.logLevel,
972
+ debugDashboard: this.options.debugDashboard,
973
+ debugPassword: this.options.debugPassword,
970
974
  });
971
975
 
972
976
  this._federation = federation;
@@ -11,7 +11,7 @@
11
11
  * import → refollow:sent → refollow:failed (after MAX_RETRIES)
12
12
  */
13
13
 
14
- import { Follow } from "@fedify/fedify";
14
+ import { Follow } from "@fedify/fedify/vocab";
15
15
  import { logActivity } from "./activity-log.js";
16
16
 
17
17
  const BATCH_SIZE = 10;
@@ -188,7 +188,7 @@ export function submitComposeController(mountPath, plugin) {
188
188
  });
189
189
  }
190
190
 
191
- const { Create, Note } = await import("@fedify/fedify");
191
+ const { Create, Note } = await import("@fedify/fedify/vocab");
192
192
  const handle = plugin.options.actor.handle;
193
193
  const ctx = plugin._federation.createContext(
194
194
  new URL(plugin._publicationUrl),
@@ -34,7 +34,7 @@ export function boostController(mountPath, plugin) {
34
34
  });
35
35
  }
36
36
 
37
- const { Announce } = await import("@fedify/fedify");
37
+ const { Announce } = await import("@fedify/fedify/vocab");
38
38
  const handle = plugin.options.actor.handle;
39
39
  const ctx = plugin._federation.createContext(
40
40
  new URL(plugin._publicationUrl),
@@ -168,7 +168,7 @@ export function unboostController(mountPath, plugin) {
168
168
  });
169
169
  }
170
170
 
171
- const { Announce, Undo } = await import("@fedify/fedify");
171
+ const { Announce, Undo } = await import("@fedify/fedify/vocab");
172
172
  const handle = plugin.options.actor.handle;
173
173
  const ctx = plugin._federation.createContext(
174
174
  new URL(plugin._publicationUrl),
@@ -36,7 +36,7 @@ export function likeController(mountPath, plugin) {
36
36
  });
37
37
  }
38
38
 
39
- const { Like } = await import("@fedify/fedify");
39
+ const { Like } = await import("@fedify/fedify/vocab");
40
40
  const handle = plugin.options.actor.handle;
41
41
  const ctx = plugin._federation.createContext(
42
42
  new URL(plugin._publicationUrl),
@@ -191,7 +191,7 @@ export function unlikeController(mountPath, plugin) {
191
191
  });
192
192
  }
193
193
 
194
- const { Like, Undo } = await import("@fedify/fedify");
194
+ const { Like, Undo } = await import("@fedify/fedify/vocab");
195
195
  const handle = plugin.options.actor.handle;
196
196
  const ctx = plugin._federation.createContext(
197
197
  new URL(plugin._publicationUrl),
@@ -144,7 +144,7 @@ export function blockController(mountPath, plugin) {
144
144
  // Send Block activity via federation
145
145
  if (plugin._federation) {
146
146
  try {
147
- const { Block } = await import("@fedify/fedify");
147
+ const { Block } = await import("@fedify/fedify/vocab");
148
148
  const handle = plugin.options.actor.handle;
149
149
  const ctx = plugin._federation.createContext(
150
150
  new URL(plugin._publicationUrl),
@@ -223,7 +223,7 @@ export function unblockController(mountPath, plugin) {
223
223
  // Send Undo(Block) via federation
224
224
  if (plugin._federation) {
225
225
  try {
226
- const { Block, Undo } = await import("@fedify/fedify");
226
+ const { Block, Undo } = await import("@fedify/fedify/vocab");
227
227
  const handle = plugin.options.actor.handle;
228
228
  const ctx = plugin._federation.createContext(
229
229
  new URL(plugin._publicationUrl),
@@ -1,5 +1,5 @@
1
1
  // Post detail controller — view individual AP posts/notes/articles
2
- import { Article, Note, Person, Service, Application } from "@fedify/fedify";
2
+ import { Article, Note, Person, Service, Application } from "@fedify/fedify/vocab";
3
3
  import { getToken } from "../csrf.js";
4
4
  import { extractObjectData } from "../timeline-store.js";
5
5
  import { getCached, setCache } from "../lookup-cache.js";
@@ -10,7 +10,7 @@ import {
10
10
  Application,
11
11
  Organization,
12
12
  Group,
13
- } from "@fedify/fedify";
13
+ } from "@fedify/fedify/vocab";
14
14
 
15
15
  /**
16
16
  * GET /admin/reader/resolve?q=<url-or-handle>
@@ -28,14 +28,29 @@ export function fromExpressRequest(req) {
28
28
  }
29
29
  }
30
30
 
31
+ let body;
32
+ if (req.method === "GET" || req.method === "HEAD") {
33
+ body = undefined;
34
+ } else if (!req.readable && req.body) {
35
+ // Express body parser already consumed the stream — reconstruct
36
+ // so downstream handlers (e.g. @fedify/debugger login) can read it.
37
+ const ct = req.headers["content-type"] || "";
38
+ if (ct.includes("application/json")) {
39
+ body = JSON.stringify(req.body);
40
+ } else if (ct.includes("application/x-www-form-urlencoded")) {
41
+ body = new URLSearchParams(req.body).toString();
42
+ } else {
43
+ body = undefined;
44
+ }
45
+ } else {
46
+ body = Readable.toWeb(req);
47
+ }
48
+
31
49
  return new Request(url, {
32
50
  method: req.method,
33
51
  headers,
34
52
  duplex: "half",
35
- body:
36
- req.method === "GET" || req.method === "HEAD"
37
- ? undefined
38
- : Readable.toWeb(req),
53
+ body,
39
54
  });
40
55
  }
41
56
 
@@ -52,7 +67,7 @@ async function sendFedifyResponse(res, response, request) {
52
67
  res.setHeader(key, value);
53
68
  });
54
69
 
55
- if (!response.body) {
70
+ if (!response.body || response.bodyUsed) {
56
71
  res.end();
57
72
  return;
58
73
  }
@@ -9,6 +9,16 @@
9
9
  import { AsyncLocalStorage } from "node:async_hooks";
10
10
  import { createRequire } from "node:module";
11
11
  import { Temporal } from "@js-temporal/polyfill";
12
+ import {
13
+ createFederation,
14
+ InProcessMessageQueue,
15
+ ParallelMessageQueue,
16
+ } from "@fedify/fedify";
17
+ import {
18
+ exportJwk,
19
+ generateCryptoKeyPair,
20
+ importJwk,
21
+ } from "@fedify/fedify/sig";
12
22
  import {
13
23
  Application,
14
24
  Article,
@@ -17,21 +27,15 @@ import {
17
27
  Group,
18
28
  Hashtag,
19
29
  Image,
20
- InProcessMessageQueue,
21
30
  Note,
22
31
  Organization,
23
- ParallelMessageQueue,
24
32
  Person,
25
33
  PropertyValue,
26
34
  Service,
27
- createFederation,
28
- exportJwk,
29
- generateCryptoKeyPair,
30
- importJwk,
31
- importSpki,
32
- } from "@fedify/fedify";
35
+ } from "@fedify/fedify/vocab";
33
36
  import { configure, getConsoleSink } from "@logtape/logtape";
34
37
  import { RedisMessageQueue } from "@fedify/redis";
38
+ import { createFederationDebugger } from "@fedify/debugger";
35
39
  import Redis from "ioredis";
36
40
  import { MongoKvStore } from "./kv-store.js";
37
41
  import { registerInboxListeners } from "./inbox-listeners.js";
@@ -61,17 +65,21 @@ export function setupFederation(options) {
61
65
  parallelWorkers = 5,
62
66
  actorType = "Person",
63
67
  logLevel = "warning",
68
+ debugDashboard = false,
69
+ debugPassword = "",
64
70
  } = options;
65
71
 
66
72
  // Map config string to Fedify actor class
67
73
  const actorTypeMap = { Person, Service, Application, Organization, Group };
68
74
  const ActorClass = actorTypeMap[actorType] || Person;
69
75
 
70
- // Configure LogTape for Fedify delivery logging (once per process)
76
+ // Configure LogTape for Fedify delivery logging (once per process).
77
+ // When the debug dashboard is enabled, skip this — the debugger
78
+ // auto-configures LogTape with per-trace log collection + OpenTelemetry.
71
79
  // Valid levels: "debug" | "info" | "warning" | "error" | "fatal"
72
80
  const validLevels = ["debug", "info", "warning", "error", "fatal"];
73
81
  const resolvedLevel = validLevels.includes(logLevel) ? logLevel : "warning";
74
- if (!_logtapeConfigured) {
82
+ if (!debugDashboard && !_logtapeConfigured) {
75
83
  _logtapeConfigured = true;
76
84
  configure({
77
85
  contextLocalStorage: new AsyncLocalStorage(),
@@ -186,7 +194,7 @@ export function setupFederation(options) {
186
194
 
187
195
  if (rsaDoc?.publicKeyPem && rsaDoc?.privateKeyPem) {
188
196
  try {
189
- const publicKey = await importSpki(rsaDoc.publicKeyPem);
197
+ const publicKey = await importSpkiPem(rsaDoc.publicKeyPem);
190
198
  const privateKey = await importPkcs8Pem(rsaDoc.privateKeyPem);
191
199
  keyPairs.push({ publicKey, privateKey });
192
200
  } catch {
@@ -284,13 +292,13 @@ export function setupFederation(options) {
284
292
  setupObjectDispatchers(federation, mountPath, handle, collections, publicationUrl);
285
293
 
286
294
  // --- NodeInfo ---
287
- let softwareVersion = { major: 1, minor: 0, patch: 0 };
295
+ // Fedify 2.0: software.version is now a plain string (was SemVer object)
296
+ let softwareVersion = "1.0.0";
288
297
  try {
289
298
  const require = createRequire(import.meta.url);
290
299
  const pkg = require("@indiekit/indiekit/package.json");
291
- const [major, minor, patch] = pkg.version.split(/[.-]/).map(Number);
292
- if (!Number.isNaN(major)) softwareVersion = { major, minor: minor || 0, patch: patch || 0 };
293
- } catch { /* fallback to 1.0.0 */ }
300
+ if (pkg.version) softwareVersion = pkg.version;
301
+ } catch { /* fallback to "1.0.0" */ }
294
302
 
295
303
  federation.setNodeInfoDispatcher("/nodeinfo/2.1", async () => {
296
304
  const postsCount = collections.posts
@@ -311,15 +319,57 @@ export function setupFederation(options) {
311
319
  };
312
320
  });
313
321
 
322
+ // Handle permanent delivery failures (Fedify 2.0).
323
+ // Fires when a remote inbox returns 404/410 — the server is gone.
324
+ // Log it and let the admin see which followers are unreachable.
325
+ federation.setOutboxPermanentFailureHandler(async (_ctx, values) => {
326
+ const { inbox, error, actorIds } = values;
327
+ const inboxUrl = inbox?.href || String(inbox);
328
+ const actors = actorIds?.map((id) => id?.href || String(id)) || [];
329
+ console.warn(
330
+ `[ActivityPub] Permanent delivery failure to ${inboxUrl}: ${error?.message || "unknown"}` +
331
+ (actors.length ? ` (actors: ${actors.join(", ")})` : ""),
332
+ );
333
+ collections.ap_activities.insertOne({
334
+ direction: "outbound",
335
+ type: "DeliveryFailed",
336
+ actorUrl: publicationUrl,
337
+ objectUrl: inboxUrl,
338
+ summary: `Permanent delivery failure to ${inboxUrl}: ${error?.message || "unknown"}`,
339
+ affectedActors: actors,
340
+ receivedAt: new Date().toISOString(),
341
+ }).catch(() => {});
342
+ });
343
+
344
+ // Wrap with debug dashboard if enabled. The debugger proxies the
345
+ // Federation object and intercepts requests at {mountPath}/__debug__/,
346
+ // serving a live dashboard showing traces, activities, signature
347
+ // verification, and correlated logs. It auto-configures OpenTelemetry
348
+ // tracing and LogTape per-trace log collection.
349
+ let activeFederation = federation;
350
+ if (debugDashboard) {
351
+ const debugOptions = {
352
+ path: `${mountPath}/__debug__`,
353
+ };
354
+ if (debugPassword) {
355
+ debugOptions.auth = { type: "password", password: debugPassword };
356
+ }
357
+ activeFederation = createFederationDebugger(federation, debugOptions);
358
+ console.info(
359
+ `[ActivityPub] Debug dashboard enabled at ${mountPath}/__debug__/` +
360
+ (debugPassword ? " (password-protected)" : " (WARNING: no password set)"),
361
+ );
362
+ }
363
+
314
364
  // Start the message queue for outbound activity delivery.
315
365
  // Without this, ctx.sendActivity() enqueues delivery tasks but the
316
366
  // InProcessMessageQueue never processes them — activities are never
317
367
  // actually POSTed to follower inboxes.
318
- federation.startQueue().catch((error) => {
368
+ activeFederation.startQueue().catch((error) => {
319
369
  console.error("[ActivityPub] Failed to start delivery queue:", error.message);
320
370
  });
321
371
 
322
- return { federation };
372
+ return { federation: activeFederation };
323
373
  }
324
374
 
325
375
  // --- Collection setup helpers ---
@@ -695,6 +745,25 @@ export async function buildPersonActor(
695
745
  return new ResolvedActorClass(personOptions);
696
746
  }
697
747
 
748
+ /**
749
+ * Import an SPKI PEM public key using Web Crypto API.
750
+ * Replaces Fedify 1.x's importSpki() which was removed in 2.0.
751
+ */
752
+ async function importSpkiPem(pem) {
753
+ const lines = pem
754
+ .replace("-----BEGIN PUBLIC KEY-----", "")
755
+ .replace("-----END PUBLIC KEY-----", "")
756
+ .replace(/\s/g, "");
757
+ const der = Uint8Array.from(atob(lines), (c) => c.charCodeAt(0));
758
+ return crypto.subtle.importKey(
759
+ "spki",
760
+ der,
761
+ { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" },
762
+ true,
763
+ ["verify"],
764
+ );
765
+ }
766
+
698
767
  /**
699
768
  * Import a PKCS#8 PEM private key using Web Crypto API.
700
769
  * Fedify's importPem only handles PKCS#1, but Node.js crypto generates PKCS#8.
@@ -21,7 +21,7 @@ import {
21
21
  Remove,
22
22
  Undo,
23
23
  Update,
24
- } from "@fedify/fedify";
24
+ } from "@fedify/fedify/vocab";
25
25
 
26
26
  import { logActivity as logActivityShared } from "./activity-log.js";
27
27
  import { sanitizeContent, extractActorInfo, extractObjectData } from "./timeline-store.js";
package/lib/jf2-to-as2.js CHANGED
@@ -18,7 +18,7 @@ import {
18
18
  Mention,
19
19
  Note,
20
20
  Video,
21
- } from "@fedify/fedify";
21
+ } from "@fedify/fedify/vocab";
22
22
 
23
23
  // ---------------------------------------------------------------------------
24
24
  // Plain JSON-LD (content negotiation on individual post URLs)
package/lib/kv-store.js CHANGED
@@ -52,4 +52,25 @@ export class MongoKvStore {
52
52
  async delete(key) {
53
53
  await this.collection.deleteOne({ _id: this._serializeKey(key) });
54
54
  }
55
+
56
+ /**
57
+ * List all entries whose key starts with the given prefix.
58
+ * Required by Fedify 2.0's KvStore interface.
59
+ *
60
+ * @param {string[]} [prefix=[]]
61
+ * @returns {AsyncIterable<{ key: string[], value: unknown }>}
62
+ */
63
+ async *list(prefix = []) {
64
+ const prefixStr = this._serializeKey(prefix);
65
+ const filter = prefixStr
66
+ ? { _id: { $regex: `^${prefixStr.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}` } }
67
+ : {};
68
+ const cursor = this.collection.find(filter);
69
+ for await (const doc of cursor) {
70
+ yield {
71
+ key: doc._id.split("/"),
72
+ value: doc.value,
73
+ };
74
+ }
75
+ }
55
76
  }
@@ -3,7 +3,7 @@
3
3
  * @module timeline-store
4
4
  */
5
5
 
6
- import { Article } from "@fedify/fedify";
6
+ import { Article } from "@fedify/fedify/vocab";
7
7
  import sanitizeHtml from "sanitize-html";
8
8
 
9
9
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rmdes/indiekit-endpoint-activitypub",
3
- "version": "1.1.20",
3
+ "version": "2.0.1",
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",
@@ -37,9 +37,9 @@
37
37
  "url": "https://github.com/rmdes/indiekit-endpoint-activitypub/issues"
38
38
  },
39
39
  "dependencies": {
40
- "@fedify/express": "^1.10.3",
41
- "@fedify/fedify": "^1.10.3",
42
- "@fedify/redis": "^1.10.3",
40
+ "@fedify/debugger": "^2.0.0",
41
+ "@fedify/fedify": "^2.0.0",
42
+ "@fedify/redis": "^2.0.0",
43
43
  "@js-temporal/polyfill": "^0.5.0",
44
44
  "express": "^5.0.0",
45
45
  "ioredis": "^5.9.3",