@rmdes/indiekit-endpoint-activitypub 1.1.17 → 1.1.18

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/README.md CHANGED
@@ -93,6 +93,7 @@ export default {
93
93
  | `redisUrl` | string | `""` | Redis connection URL for delivery queue |
94
94
  | `parallelWorkers` | number | `5` | Number of parallel delivery workers (requires Redis) |
95
95
  | `actorType` | string | `"Person"` | Actor type: `Person`, `Service`, `Organization`, or `Group` |
96
+ | `logLevel` | string | `"warning"` | Fedify log level: `"debug"`, `"info"`, `"warning"`, `"error"`, `"fatal"` |
96
97
  | `timelineRetention` | number | `1000` | Maximum timeline items to keep (0 = unlimited) |
97
98
 
98
99
  ### Redis (Recommended for Production)
package/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import express from "express";
2
2
 
3
- import { setupFederation } from "./lib/federation-setup.js";
3
+ import { setupFederation, buildPersonActor } from "./lib/federation-setup.js";
4
4
  import {
5
5
  createFedifyMiddleware,
6
6
  } from "./lib/federation-bridge.js";
@@ -82,6 +82,7 @@ const defaults = {
82
82
  redisUrl: "",
83
83
  parallelWorkers: 5,
84
84
  actorType: "Person",
85
+ logLevel: "warning",
85
86
  timelineRetention: 1000,
86
87
  notificationRetentionDays: 30,
87
88
  };
@@ -689,9 +690,15 @@ export default class ActivityPubEndpoint {
689
690
  { handle, publicationUrl: this._publicationUrl },
690
691
  );
691
692
 
692
- // Retrieve the full actor from the dispatcher (same object remote
693
- // servers will get when they re-fetch the actor URL)
694
- const actor = await ctx.getActor(handle);
693
+ // Build the full actor object (same as what the dispatcher serves).
694
+ // Note: ctx.getActor() only exists on RequestContext, not the base
695
+ // Context returned by createContext(), so we use the shared helper.
696
+ const actor = await buildPersonActor(
697
+ ctx,
698
+ handle,
699
+ this._collections,
700
+ this.options.actorType,
701
+ );
695
702
  if (!actor) {
696
703
  console.warn("[ActivityPub] broadcastActorUpdate: could not build actor");
697
704
  return;
@@ -891,6 +898,7 @@ export default class ActivityPubEndpoint {
891
898
  publicationUrl: this._publicationUrl,
892
899
  parallelWorkers: this.options.parallelWorkers,
893
900
  actorType: this.options.actorType,
901
+ logLevel: this.options.logLevel,
894
902
  });
895
903
 
896
904
  this._federation = federation;
@@ -60,6 +60,7 @@ export function setupFederation(options) {
60
60
  publicationUrl = "",
61
61
  parallelWorkers = 5,
62
62
  actorType = "Person",
63
+ logLevel = "warning",
63
64
  } = options;
64
65
 
65
66
  // Map config string to Fedify actor class
@@ -67,6 +68,9 @@ export function setupFederation(options) {
67
68
  const ActorClass = actorTypeMap[actorType] || Person;
68
69
 
69
70
  // Configure LogTape for Fedify delivery logging (once per process)
71
+ // Valid levels: "debug" | "info" | "warning" | "error" | "fatal"
72
+ const validLevels = ["debug", "info", "warning", "error", "fatal"];
73
+ const resolvedLevel = validLevels.includes(logLevel) ? logLevel : "warning";
70
74
  if (!_logtapeConfigured) {
71
75
  _logtapeConfigured = true;
72
76
  configure({
@@ -79,7 +83,7 @@ export function setupFederation(options) {
79
83
  // All Fedify logs — federation, vocab, delivery, HTTP signatures
80
84
  category: ["fedify"],
81
85
  sinks: ["console"],
82
- lowestLevel: "info",
86
+ lowestLevel: resolvedLevel,
83
87
  },
84
88
  ],
85
89
  }).catch((error) => {
@@ -138,74 +142,7 @@ export function setupFederation(options) {
138
142
 
139
143
  if (identifier !== handle) return null;
140
144
 
141
- const profile = await getProfile(collections);
142
- const keyPairs = await ctx.getActorKeyPairs(identifier);
143
-
144
- const personOptions = {
145
- id: ctx.getActorUri(identifier),
146
- preferredUsername: identifier,
147
- name: profile.name || identifier,
148
- url: profile.url ? new URL(profile.url) : null,
149
- inbox: ctx.getInboxUri(identifier),
150
- outbox: ctx.getOutboxUri(identifier),
151
- followers: ctx.getFollowersUri(identifier),
152
- following: ctx.getFollowingUri(identifier),
153
- liked: ctx.getLikedUri(identifier),
154
- featured: ctx.getFeaturedUri(identifier),
155
- featuredTags: ctx.getFeaturedTagsUri(identifier),
156
- endpoints: new Endpoints({ sharedInbox: ctx.getInboxUri() }),
157
- manuallyApprovesFollowers:
158
- profile.manuallyApprovesFollowers || false,
159
- };
160
-
161
- if (profile.summary) {
162
- personOptions.summary = profile.summary;
163
- }
164
-
165
- if (profile.icon) {
166
- personOptions.icon = new Image({
167
- url: new URL(profile.icon),
168
- mediaType: guessImageMediaType(profile.icon),
169
- });
170
- }
171
-
172
- if (profile.image) {
173
- personOptions.image = new Image({
174
- url: new URL(profile.image),
175
- mediaType: guessImageMediaType(profile.image),
176
- });
177
- }
178
-
179
- if (keyPairs.length > 0) {
180
- personOptions.publicKey = keyPairs[0].cryptographicKey;
181
- personOptions.assertionMethods = keyPairs.map((k) => k.multikey);
182
- }
183
-
184
- if (profile.attachments?.length > 0) {
185
- personOptions.attachments = profile.attachments.map(
186
- (att) =>
187
- new PropertyValue({
188
- name: att.name,
189
- value: formatAttachmentValue(att.value),
190
- }),
191
- );
192
- }
193
-
194
- if (profile.alsoKnownAs?.length > 0) {
195
- personOptions.alsoKnownAs = profile.alsoKnownAs.map(
196
- (u) => new URL(u),
197
- );
198
- }
199
-
200
- if (profile.createdAt) {
201
- personOptions.published = Temporal.Instant.from(profile.createdAt);
202
- }
203
-
204
- // Actor type from profile overrides config default
205
- const profileActorType = profile.actorType || actorType;
206
- const ResolvedActorClass = actorTypeMap[profileActorType] || ActorClass;
207
-
208
- return new ResolvedActorClass(personOptions);
145
+ return buildPersonActor(ctx, identifier, collections, actorType);
209
146
  },
210
147
  )
211
148
  .mapHandle((_ctx, username) => {
@@ -674,6 +611,90 @@ async function getProfile(collections) {
674
611
  return doc || {};
675
612
  }
676
613
 
614
+ /**
615
+ * Build the Person/Service/Organization actor object from the stored profile.
616
+ * Used by both the actor dispatcher (for serving the actor to federation
617
+ * requests) and broadcastActorUpdate() (for sending Update activities).
618
+ *
619
+ * @param {object} ctx - Fedify context (base Context or RequestContext)
620
+ * @param {string} identifier - Actor handle (e.g. "rick")
621
+ * @param {object} collections - MongoDB collections
622
+ * @param {string} [defaultActorType="Person"] - Fallback actor type
623
+ * @returns {Promise<import("@fedify/fedify").Actor|null>}
624
+ */
625
+ export async function buildPersonActor(
626
+ ctx,
627
+ identifier,
628
+ collections,
629
+ defaultActorType = "Person",
630
+ ) {
631
+ const actorTypeMap = { Person, Service, Application, Organization, Group };
632
+ const profile = await getProfile(collections);
633
+ const keyPairs = await ctx.getActorKeyPairs(identifier);
634
+
635
+ const personOptions = {
636
+ id: ctx.getActorUri(identifier),
637
+ preferredUsername: identifier,
638
+ name: profile.name || identifier,
639
+ url: profile.url ? new URL(profile.url) : null,
640
+ inbox: ctx.getInboxUri(identifier),
641
+ outbox: ctx.getOutboxUri(identifier),
642
+ followers: ctx.getFollowersUri(identifier),
643
+ following: ctx.getFollowingUri(identifier),
644
+ liked: ctx.getLikedUri(identifier),
645
+ featured: ctx.getFeaturedUri(identifier),
646
+ featuredTags: ctx.getFeaturedTagsUri(identifier),
647
+ endpoints: new Endpoints({ sharedInbox: ctx.getInboxUri() }),
648
+ manuallyApprovesFollowers: profile.manuallyApprovesFollowers || false,
649
+ };
650
+
651
+ if (profile.summary) {
652
+ personOptions.summary = profile.summary;
653
+ }
654
+
655
+ if (profile.icon) {
656
+ personOptions.icon = new Image({
657
+ url: new URL(profile.icon),
658
+ mediaType: guessImageMediaType(profile.icon),
659
+ });
660
+ }
661
+
662
+ if (profile.image) {
663
+ personOptions.image = new Image({
664
+ url: new URL(profile.image),
665
+ mediaType: guessImageMediaType(profile.image),
666
+ });
667
+ }
668
+
669
+ if (keyPairs.length > 0) {
670
+ personOptions.publicKey = keyPairs[0].cryptographicKey;
671
+ personOptions.assertionMethods = keyPairs.map((k) => k.multikey);
672
+ }
673
+
674
+ if (profile.attachments?.length > 0) {
675
+ personOptions.attachments = profile.attachments.map(
676
+ (att) =>
677
+ new PropertyValue({
678
+ name: att.name,
679
+ value: formatAttachmentValue(att.value),
680
+ }),
681
+ );
682
+ }
683
+
684
+ if (profile.alsoKnownAs?.length > 0) {
685
+ personOptions.alsoKnownAs = profile.alsoKnownAs.map((u) => new URL(u));
686
+ }
687
+
688
+ if (profile.createdAt) {
689
+ personOptions.published = Temporal.Instant.from(profile.createdAt);
690
+ }
691
+
692
+ const profileActorType = profile.actorType || defaultActorType;
693
+ const ResolvedActorClass = actorTypeMap[profileActorType] || Person;
694
+
695
+ return new ResolvedActorClass(personOptions);
696
+ }
697
+
677
698
  /**
678
699
  * Import a PKCS#8 PEM private key using Web Crypto API.
679
700
  * Fedify's importPem only handles PKCS#1, but Node.js crypto generates PKCS#8.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rmdes/indiekit-endpoint-activitypub",
3
- "version": "1.1.17",
3
+ "version": "1.1.18",
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",