@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 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;
@@ -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
- await collection.insertOne({
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
  }
@@ -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: new InProcessMessageQueue(),
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.assertionMethod = keyPairs[0].multikey;
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
- // Import legacy RSA key pair (for HTTP Signatures compatibility)
145
- const legacyKey = await collections.ap_keys.findOne({});
146
- if (legacyKey?.publicKeyPem && legacyKey?.privateKeyPem) {
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(legacyKey.publicKeyPem);
149
- const privateKey = await importPkcs8Pem(legacyKey.privateKeyPem);
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 legacy RSA keys",
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
- // Generate Ed25519 key pair (for Object Integrity Proofs)
159
- try {
160
- const ed25519 = await generateCryptoKeyPair("Ed25519");
161
- keyPairs.push(ed25519);
162
- } catch (error) {
163
- console.warn("[ActivityPub] Could not generate Ed25519 key pair:", error.message);
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;
@@ -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(collections.ap_activities, record);
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.20",
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",