@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 +7 -3
- package/lib/batch-refollow.js +1 -1
- package/lib/controllers/compose.js +1 -1
- package/lib/controllers/interactions-boost.js +2 -2
- package/lib/controllers/interactions-like.js +2 -2
- package/lib/controllers/moderation.js +2 -2
- package/lib/controllers/post-detail.js +1 -1
- package/lib/controllers/resolve.js +1 -1
- package/lib/federation-bridge.js +20 -5
- package/lib/federation-setup.js +86 -17
- package/lib/inbox-listeners.js +1 -1
- package/lib/jf2-to-as2.js +1 -1
- package/lib/kv-store.js +21 -0
- package/lib/timeline-store.js +1 -1
- package/package.json +4 -4
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;
|
package/lib/batch-refollow.js
CHANGED
|
@@ -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";
|
package/lib/federation-bridge.js
CHANGED
|
@@ -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
|
}
|
package/lib/federation-setup.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
292
|
-
|
|
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
|
-
|
|
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.
|
package/lib/inbox-listeners.js
CHANGED
|
@@ -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
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
|
}
|
package/lib/timeline-store.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rmdes/indiekit-endpoint-activitypub",
|
|
3
|
-
"version": "
|
|
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/
|
|
41
|
-
"@fedify/fedify": "^
|
|
42
|
-
"@fedify/redis": "^
|
|
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",
|