@rmdes/indiekit-endpoint-activitypub 1.0.20 → 1.0.21
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 +24 -0
- package/lib/activity-log.js +7 -3
- package/lib/federation-setup.js +74 -14
- package/lib/inbox-listeners.js +38 -2
- package/package.json +5 -3
package/index.js
CHANGED
|
@@ -41,6 +41,7 @@ const defaults = {
|
|
|
41
41
|
alsoKnownAs: "",
|
|
42
42
|
activityRetentionDays: 90,
|
|
43
43
|
storeRawActivities: false,
|
|
44
|
+
redisUrl: "",
|
|
44
45
|
};
|
|
45
46
|
|
|
46
47
|
export default class ActivityPubEndpoint {
|
|
@@ -617,6 +618,28 @@ export default class ActivityPubEndpoint {
|
|
|
617
618
|
);
|
|
618
619
|
}
|
|
619
620
|
|
|
621
|
+
// Performance indexes for inbox handlers and batch refollow
|
|
622
|
+
this._collections.ap_followers.createIndex(
|
|
623
|
+
{ actorUrl: 1 },
|
|
624
|
+
{ unique: true, background: true },
|
|
625
|
+
);
|
|
626
|
+
this._collections.ap_following.createIndex(
|
|
627
|
+
{ actorUrl: 1 },
|
|
628
|
+
{ unique: true, background: true },
|
|
629
|
+
);
|
|
630
|
+
this._collections.ap_following.createIndex(
|
|
631
|
+
{ source: 1 },
|
|
632
|
+
{ background: true },
|
|
633
|
+
);
|
|
634
|
+
this._collections.ap_activities.createIndex(
|
|
635
|
+
{ objectUrl: 1 },
|
|
636
|
+
{ background: true },
|
|
637
|
+
);
|
|
638
|
+
this._collections.ap_activities.createIndex(
|
|
639
|
+
{ type: 1, actorUrl: 1, objectUrl: 1 },
|
|
640
|
+
{ background: true },
|
|
641
|
+
);
|
|
642
|
+
|
|
620
643
|
// Seed actor profile from config on first run
|
|
621
644
|
this._seedProfile().catch((error) => {
|
|
622
645
|
console.warn("[ActivityPub] Profile seed failed:", error.message);
|
|
@@ -628,6 +651,7 @@ export default class ActivityPubEndpoint {
|
|
|
628
651
|
mountPath: this.options.mountPath,
|
|
629
652
|
handle: this.options.actor.handle,
|
|
630
653
|
storeRawActivities: this.options.storeRawActivities,
|
|
654
|
+
redisUrl: this.options.redisUrl,
|
|
631
655
|
});
|
|
632
656
|
|
|
633
657
|
this._federation = federation;
|
package/lib/activity-log.js
CHANGED
|
@@ -19,12 +19,16 @@
|
|
|
19
19
|
* @param {string} [record.content] - Content excerpt
|
|
20
20
|
* @param {string} record.summary - Human-readable summary
|
|
21
21
|
*/
|
|
22
|
-
export async function logActivity(collection, record) {
|
|
22
|
+
export async function logActivity(collection, record, options = {}) {
|
|
23
23
|
try {
|
|
24
|
-
|
|
24
|
+
const doc = {
|
|
25
25
|
...record,
|
|
26
26
|
receivedAt: new Date().toISOString(),
|
|
27
|
-
}
|
|
27
|
+
};
|
|
28
|
+
if (options.rawJson) {
|
|
29
|
+
doc.rawJson = options.rawJson;
|
|
30
|
+
}
|
|
31
|
+
await collection.insertOne(doc);
|
|
28
32
|
} catch (error) {
|
|
29
33
|
console.warn("[ActivityPub] Failed to log activity:", error.message);
|
|
30
34
|
}
|
package/lib/federation-setup.js
CHANGED
|
@@ -15,10 +15,14 @@ import {
|
|
|
15
15
|
Person,
|
|
16
16
|
PropertyValue,
|
|
17
17
|
createFederation,
|
|
18
|
+
exportJwk,
|
|
18
19
|
generateCryptoKeyPair,
|
|
20
|
+
importJwk,
|
|
19
21
|
importSpki,
|
|
20
22
|
} from "@fedify/fedify";
|
|
21
23
|
import { configure, getConsoleSink } from "@logtape/logtape";
|
|
24
|
+
import { RedisMessageQueue } from "@fedify/redis";
|
|
25
|
+
import Redis from "ioredis";
|
|
22
26
|
import { MongoKvStore } from "./kv-store.js";
|
|
23
27
|
import { registerInboxListeners } from "./inbox-listeners.js";
|
|
24
28
|
|
|
@@ -41,6 +45,7 @@ export function setupFederation(options) {
|
|
|
41
45
|
mountPath,
|
|
42
46
|
handle,
|
|
43
47
|
storeRawActivities = false,
|
|
48
|
+
redisUrl = "",
|
|
44
49
|
} = options;
|
|
45
50
|
|
|
46
51
|
// Configure LogTape for Fedify delivery logging (once per process)
|
|
@@ -64,9 +69,20 @@ export function setupFederation(options) {
|
|
|
64
69
|
});
|
|
65
70
|
}
|
|
66
71
|
|
|
72
|
+
let queue;
|
|
73
|
+
if (redisUrl) {
|
|
74
|
+
queue = new RedisMessageQueue(() => new Redis(redisUrl));
|
|
75
|
+
console.info("[ActivityPub] Using Redis message queue");
|
|
76
|
+
} else {
|
|
77
|
+
queue = new InProcessMessageQueue();
|
|
78
|
+
console.warn(
|
|
79
|
+
"[ActivityPub] Using in-process message queue (not recommended for production)",
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
67
83
|
const federation = createFederation({
|
|
68
84
|
kv: new MongoKvStore(collections.ap_kv),
|
|
69
|
-
queue
|
|
85
|
+
queue,
|
|
70
86
|
});
|
|
71
87
|
|
|
72
88
|
// --- Actor dispatcher ---
|
|
@@ -113,7 +129,7 @@ export function setupFederation(options) {
|
|
|
113
129
|
|
|
114
130
|
if (keyPairs.length > 0) {
|
|
115
131
|
personOptions.publicKey = keyPairs[0].cryptographicKey;
|
|
116
|
-
personOptions.
|
|
132
|
+
personOptions.assertionMethods = keyPairs.map((k) => k.multikey);
|
|
117
133
|
}
|
|
118
134
|
|
|
119
135
|
if (profile.attachments?.length > 0) {
|
|
@@ -141,26 +157,70 @@ export function setupFederation(options) {
|
|
|
141
157
|
|
|
142
158
|
const keyPairs = [];
|
|
143
159
|
|
|
144
|
-
//
|
|
145
|
-
const legacyKey = await collections.ap_keys.findOne({});
|
|
146
|
-
|
|
160
|
+
// --- Legacy RSA key pair (HTTP Signatures) ---
|
|
161
|
+
const legacyKey = await collections.ap_keys.findOne({ type: "rsa" });
|
|
162
|
+
// Fall back to old schema (no type field) for backward compat
|
|
163
|
+
const rsaDoc =
|
|
164
|
+
legacyKey ||
|
|
165
|
+
(await collections.ap_keys.findOne({
|
|
166
|
+
publicKeyPem: { $exists: true },
|
|
167
|
+
}));
|
|
168
|
+
|
|
169
|
+
if (rsaDoc?.publicKeyPem && rsaDoc?.privateKeyPem) {
|
|
147
170
|
try {
|
|
148
|
-
const publicKey = await importSpki(
|
|
149
|
-
const privateKey = await importPkcs8Pem(
|
|
171
|
+
const publicKey = await importSpki(rsaDoc.publicKeyPem);
|
|
172
|
+
const privateKey = await importPkcs8Pem(rsaDoc.privateKeyPem);
|
|
150
173
|
keyPairs.push({ publicKey, privateKey });
|
|
151
174
|
} catch {
|
|
175
|
+
console.warn("[ActivityPub] Could not import legacy RSA keys");
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// --- Ed25519 key pair (Object Integrity Proofs) ---
|
|
180
|
+
// Load from DB or generate + persist on first use
|
|
181
|
+
let ed25519Doc = await collections.ap_keys.findOne({
|
|
182
|
+
type: "ed25519",
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
if (ed25519Doc?.publicKeyJwk && ed25519Doc?.privateKeyJwk) {
|
|
186
|
+
try {
|
|
187
|
+
const publicKey = await importJwk(
|
|
188
|
+
ed25519Doc.publicKeyJwk,
|
|
189
|
+
"public",
|
|
190
|
+
);
|
|
191
|
+
const privateKey = await importJwk(
|
|
192
|
+
ed25519Doc.privateKeyJwk,
|
|
193
|
+
"private",
|
|
194
|
+
);
|
|
195
|
+
keyPairs.push({ publicKey, privateKey });
|
|
196
|
+
} catch (error) {
|
|
152
197
|
console.warn(
|
|
153
|
-
"[ActivityPub] Could not import
|
|
198
|
+
"[ActivityPub] Could not import Ed25519 keys, regenerating:",
|
|
199
|
+
error.message,
|
|
154
200
|
);
|
|
201
|
+
ed25519Doc = null; // Force regeneration below
|
|
155
202
|
}
|
|
156
203
|
}
|
|
157
204
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
205
|
+
if (!ed25519Doc) {
|
|
206
|
+
try {
|
|
207
|
+
const ed25519 = await generateCryptoKeyPair("Ed25519");
|
|
208
|
+
await collections.ap_keys.insertOne({
|
|
209
|
+
type: "ed25519",
|
|
210
|
+
publicKeyJwk: await exportJwk(ed25519.publicKey),
|
|
211
|
+
privateKeyJwk: await exportJwk(ed25519.privateKey),
|
|
212
|
+
createdAt: new Date().toISOString(),
|
|
213
|
+
});
|
|
214
|
+
keyPairs.push(ed25519);
|
|
215
|
+
console.info(
|
|
216
|
+
"[ActivityPub] Generated and persisted Ed25519 key pair",
|
|
217
|
+
);
|
|
218
|
+
} catch (error) {
|
|
219
|
+
console.warn(
|
|
220
|
+
"[ActivityPub] Could not generate Ed25519 key pair:",
|
|
221
|
+
error.message,
|
|
222
|
+
);
|
|
223
|
+
}
|
|
164
224
|
}
|
|
165
225
|
|
|
166
226
|
return keyPairs;
|
package/lib/inbox-listeners.js
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
Like,
|
|
17
17
|
Move,
|
|
18
18
|
Note,
|
|
19
|
+
Reject,
|
|
19
20
|
Remove,
|
|
20
21
|
Undo,
|
|
21
22
|
Update,
|
|
@@ -160,6 +161,37 @@ export function registerInboxListeners(inboxChain, options) {
|
|
|
160
161
|
});
|
|
161
162
|
}
|
|
162
163
|
})
|
|
164
|
+
.on(Reject, async (ctx, reject) => {
|
|
165
|
+
const actorObj = await reject.getActor();
|
|
166
|
+
const actorUrl = actorObj?.id?.href || "";
|
|
167
|
+
if (!actorUrl) return;
|
|
168
|
+
|
|
169
|
+
// Mark rejected follow in ap_following
|
|
170
|
+
const result = await collections.ap_following.findOneAndUpdate(
|
|
171
|
+
{
|
|
172
|
+
actorUrl,
|
|
173
|
+
source: { $in: ["refollow:sent", "microsub-reader"] },
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
$set: {
|
|
177
|
+
source: "rejected",
|
|
178
|
+
rejectedAt: new Date().toISOString(),
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
{ returnDocument: "after" },
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
if (result) {
|
|
185
|
+
const actorName = result.name || result.handle || actorUrl;
|
|
186
|
+
await logActivity(collections, storeRawActivities, {
|
|
187
|
+
direction: "inbound",
|
|
188
|
+
type: "Reject(Follow)",
|
|
189
|
+
actorUrl,
|
|
190
|
+
actorName,
|
|
191
|
+
summary: `${actorName} rejected our Follow`,
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
})
|
|
163
195
|
.on(Like, async (ctx, like) => {
|
|
164
196
|
const objectId = (await like.getObject())?.id?.href || "";
|
|
165
197
|
|
|
@@ -324,8 +356,12 @@ export function registerInboxListeners(inboxChain, options) {
|
|
|
324
356
|
* Wrapper around the shared utility that accepts the (collections, storeRaw, record) signature
|
|
325
357
|
* used throughout this file.
|
|
326
358
|
*/
|
|
327
|
-
async function logActivity(collections, storeRaw, record) {
|
|
328
|
-
await logActivityShared(
|
|
359
|
+
async function logActivity(collections, storeRaw, record, rawJson) {
|
|
360
|
+
await logActivityShared(
|
|
361
|
+
collections.ap_activities,
|
|
362
|
+
record,
|
|
363
|
+
storeRaw && rawJson ? { rawJson } : {},
|
|
364
|
+
);
|
|
329
365
|
}
|
|
330
366
|
|
|
331
367
|
// Cached ActivityPub channel ObjectId
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rmdes/indiekit-endpoint-activitypub",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.21",
|
|
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,10 +37,12 @@
|
|
|
37
37
|
"url": "https://github.com/rmdes/indiekit-endpoint-activitypub/issues"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@fedify/fedify": "^1.10.0",
|
|
41
40
|
"@fedify/express": "^1.9.0",
|
|
41
|
+
"@fedify/fedify": "^1.10.0",
|
|
42
|
+
"@fedify/redis": "^1.10.3",
|
|
42
43
|
"@js-temporal/polyfill": "^0.5.0",
|
|
43
|
-
"express": "^5.0.0"
|
|
44
|
+
"express": "^5.0.0",
|
|
45
|
+
"ioredis": "^5.9.3"
|
|
44
46
|
},
|
|
45
47
|
"peerDependencies": {
|
|
46
48
|
"@indiekit/error": "^1.0.0-beta.25",
|