@fedify/relay 2.0.0-dev.1908 → 2.0.0-dev.85

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/dist/mod.cjs CHANGED
@@ -1,3 +1,6 @@
1
+
2
+ const { Temporal } = require("@js-temporal/polyfill");
3
+
1
4
  //#region rolldown:runtime
2
5
  var __create = Object.create;
3
6
  var __defProp = Object.defineProperty;
@@ -23,180 +26,194 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
23
26
  //#endregion
24
27
  const __fedify_fedify = __toESM(require("@fedify/fedify"));
25
28
  const __fedify_fedify_vocab = __toESM(require("@fedify/fedify/vocab"));
29
+ const __logtape_logtape = __toESM(require("@logtape/logtape"));
26
30
 
27
- //#region src/relay.ts
31
+ //#region src/types.ts
28
32
  const RELAY_SERVER_ACTOR = "relay";
29
33
  /**
30
- * A Mastodon-compatible ActivityPub relay implementation.
31
- * This relay follows Mastodon's relay protocol for maximum compatibility
32
- * with Mastodon instances.
34
+ * Type predicate to check if a value is a valid RelayFollower.
35
+ * Provides both runtime validation and compile-time type narrowing.
33
36
  *
34
- * @since 2.0.0
37
+ * @param value The value to check
38
+ * @returns true if the value is a RelayFollower
35
39
  */
36
- var MastodonRelay = class {
37
- #federation;
38
- #options;
39
- #subscriptionHandler;
40
- constructor(options) {
41
- this.#options = options;
42
- this.#federation = options.federation ?? (0, __fedify_fedify.createFederation)({
43
- kv: options.kv,
44
- queue: options.queue,
45
- documentLoaderFactory: options.documentLoaderFactory,
46
- authenticatedDocumentLoaderFactory: options.authenticatedDocumentLoaderFactory
47
- });
48
- this.#federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => {
49
- if (identifier !== RELAY_SERVER_ACTOR) return null;
50
- const keys = await ctx.getActorKeyPairs(identifier);
51
- return new __fedify_fedify_vocab.Service({
52
- id: ctx.getActorUri(identifier),
53
- preferredUsername: identifier,
54
- name: "ActivityPub Relay",
55
- summary: "Mastodon-compatible ActivityPub relay server",
56
- inbox: ctx.getInboxUri(),
57
- followers: ctx.getFollowersUri(identifier),
58
- url: ctx.getActorUri(identifier),
59
- publicKey: keys[0].cryptographicKey,
60
- assertionMethods: keys.map((k) => k.multikey)
61
- });
62
- }).setKeyPairsDispatcher(async (_ctx, identifier) => {
63
- if (identifier !== RELAY_SERVER_ACTOR) return [];
64
- const rsaPairJson = await options.kv.get([
65
- "keypair",
66
- "rsa",
67
- identifier
68
- ]);
69
- const ed25519PairJson = await options.kv.get([
70
- "keypair",
71
- "ed25519",
72
- identifier
73
- ]);
74
- if (rsaPairJson == null || ed25519PairJson == null) {
75
- const rsaPair$1 = await (0, __fedify_fedify.generateCryptoKeyPair)("RSASSA-PKCS1-v1_5");
76
- const ed25519Pair$1 = await (0, __fedify_fedify.generateCryptoKeyPair)("Ed25519");
77
- await options.kv.set([
78
- "keypair",
79
- "rsa",
80
- identifier
81
- ], {
82
- privateKey: await (0, __fedify_fedify.exportJwk)(rsaPair$1.privateKey),
83
- publicKey: await (0, __fedify_fedify.exportJwk)(rsaPair$1.publicKey)
84
- });
85
- await options.kv.set([
86
- "keypair",
87
- "ed25519",
88
- identifier
89
- ], {
90
- privateKey: await (0, __fedify_fedify.exportJwk)(ed25519Pair$1.privateKey),
91
- publicKey: await (0, __fedify_fedify.exportJwk)(ed25519Pair$1.publicKey)
92
- });
93
- return [rsaPair$1, ed25519Pair$1];
94
- }
95
- const rsaPair = {
96
- privateKey: await (0, __fedify_fedify.importJwk)(rsaPairJson.privateKey, "private"),
97
- publicKey: await (0, __fedify_fedify.importJwk)(rsaPairJson.publicKey, "public")
98
- };
99
- const ed25519Pair = {
100
- privateKey: await (0, __fedify_fedify.importJwk)(ed25519PairJson.privateKey, "private"),
101
- publicKey: await (0, __fedify_fedify.importJwk)(ed25519PairJson.publicKey, "public")
102
- };
103
- return [rsaPair, ed25519Pair];
40
+ function isRelayFollower(value) {
41
+ if (!value || typeof value !== "object") return false;
42
+ const obj = value;
43
+ return "actor" in obj && "state" in obj && typeof obj.state === "string" && (obj.state === "pending" || obj.state === "accepted");
44
+ }
45
+
46
+ //#endregion
47
+ //#region src/builder.ts
48
+ const relayBuilder = (0, __fedify_fedify.createFederationBuilder)();
49
+ relayBuilder.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => {
50
+ if (identifier !== RELAY_SERVER_ACTOR) return null;
51
+ const keys = await ctx.getActorKeyPairs(identifier);
52
+ return new __fedify_fedify_vocab.Application({
53
+ id: ctx.getActorUri(identifier),
54
+ preferredUsername: identifier,
55
+ name: ctx.data.name ?? "ActivityPub Relay",
56
+ inbox: ctx.getInboxUri(),
57
+ followers: ctx.getFollowersUri(identifier),
58
+ following: ctx.getFollowingUri(identifier),
59
+ url: ctx.getActorUri(identifier),
60
+ publicKey: keys[0].cryptographicKey,
61
+ assertionMethods: keys.map((k) => k.multikey)
62
+ });
63
+ }).setKeyPairsDispatcher(async (ctx, identifier) => {
64
+ if (identifier !== RELAY_SERVER_ACTOR) return [];
65
+ const rsaPairJson = await ctx.data.kv.get([
66
+ "keypair",
67
+ "rsa",
68
+ identifier
69
+ ]);
70
+ const ed25519PairJson = await ctx.data.kv.get([
71
+ "keypair",
72
+ "ed25519",
73
+ identifier
74
+ ]);
75
+ if (rsaPairJson == null || ed25519PairJson == null) {
76
+ const rsaPair$1 = await (0, __fedify_fedify.generateCryptoKeyPair)("RSASSA-PKCS1-v1_5");
77
+ const ed25519Pair$1 = await (0, __fedify_fedify.generateCryptoKeyPair)("Ed25519");
78
+ await ctx.data.kv.set([
79
+ "keypair",
80
+ "rsa",
81
+ identifier
82
+ ], {
83
+ privateKey: await (0, __fedify_fedify.exportJwk)(rsaPair$1.privateKey),
84
+ publicKey: await (0, __fedify_fedify.exportJwk)(rsaPair$1.publicKey)
104
85
  });
105
- this.#federation.setFollowersDispatcher("/users/{identifier}/followers", async (_ctx, identifier) => {
106
- if (identifier !== RELAY_SERVER_ACTOR) return null;
107
- const activityIds = await options.kv.get(["followers"]) ?? [];
108
- const actors = [];
109
- for (const activityId of activityIds) {
110
- const actorJson = await options.kv.get(["follower", activityId]);
111
- const actor = await __fedify_fedify_vocab.Object.fromJsonLd(actorJson);
112
- if (!(0, __fedify_fedify_vocab.isActor)(actor)) continue;
113
- actors.push(actor);
114
- }
115
- return { items: actors };
116
- });
117
- this.#federation.setInboxListeners("/users/{identifier}/inbox", "/inbox").on(__fedify_fedify_vocab.Follow, async (ctx, follow) => {
118
- if (follow.id == null || follow.objectId == null) return;
119
- const parsed = ctx.parseUri(follow.objectId);
120
- const isPublicFollow = follow.objectId.href === "https://www.w3.org/ns/activitystreams#Public";
121
- if (!isPublicFollow && parsed?.type !== "actor") return;
122
- const relayActorUri = ctx.getActorUri(RELAY_SERVER_ACTOR);
123
- const recipient = await follow.getActor(ctx);
124
- if (recipient == null || recipient.id == null || recipient.preferredUsername == null || recipient.inboxId == null) return;
125
- let approved = false;
126
- if (this.#subscriptionHandler) approved = await this.#subscriptionHandler(ctx, recipient);
127
- if (approved) {
128
- const followers = await options.kv.get(["followers"]) ?? [];
129
- followers.push(follow.id.href);
130
- await options.kv.set(["followers"], followers);
131
- await options.kv.set(["follower", follow.id.href], await recipient.toJsonLd());
132
- await ctx.sendActivity({ identifier: RELAY_SERVER_ACTOR }, recipient, new __fedify_fedify_vocab.Accept({
133
- id: new URL(`#accepts`, relayActorUri),
134
- actor: relayActorUri,
135
- object: follow
136
- }));
137
- } else await ctx.sendActivity({ identifier: RELAY_SERVER_ACTOR }, recipient, new __fedify_fedify_vocab.Reject({
138
- id: new URL(`#rejects`, relayActorUri),
139
- actor: relayActorUri,
140
- object: follow
141
- }));
142
- }).on(__fedify_fedify_vocab.Undo, async (ctx, undo) => {
143
- const activity = await undo.getObject(ctx);
144
- if (activity instanceof __fedify_fedify_vocab.Follow) {
145
- if (activity.id == null || activity.actorId == null) return;
146
- const activityId = activity.id.href;
147
- const followers = await options.kv.get(["followers"]) ?? [];
148
- const updatedFollowers = followers.filter((id) => id !== activityId);
149
- await options.kv.set(["followers"], updatedFollowers);
150
- options.kv.delete(["follower", activityId]);
151
- } else console.warn("Unsupported object type ({type}) for Undo activity: {object}", {
152
- type: activity?.constructor.name,
153
- object: activity
154
- });
155
- }).on(__fedify_fedify_vocab.Create, async (ctx, create) => {
156
- const sender = await create.getActor(ctx);
157
- const excludeBaseUris = sender?.id ? [new URL(sender.id)] : [];
158
- await ctx.forwardActivity({ identifier: RELAY_SERVER_ACTOR }, "followers", {
159
- skipIfUnsigned: true,
160
- excludeBaseUris,
161
- preferSharedInbox: true
162
- });
163
- }).on(__fedify_fedify_vocab.Delete, async (ctx, deleteActivity) => {
164
- const sender = await deleteActivity.getActor(ctx);
165
- const excludeBaseUris = sender?.id ? [new URL(sender.id)] : [];
166
- await ctx.forwardActivity({ identifier: RELAY_SERVER_ACTOR }, "followers", {
167
- skipIfUnsigned: true,
168
- excludeBaseUris,
169
- preferSharedInbox: true
170
- });
171
- }).on(__fedify_fedify_vocab.Move, async (ctx, deleteActivity) => {
172
- const sender = await deleteActivity.getActor(ctx);
173
- const excludeBaseUris = sender?.id ? [new URL(sender.id)] : [];
174
- await ctx.forwardActivity({ identifier: RELAY_SERVER_ACTOR }, "followers", {
175
- skipIfUnsigned: true,
176
- excludeBaseUris,
177
- preferSharedInbox: true
178
- });
179
- }).on(__fedify_fedify_vocab.Update, async (ctx, deleteActivity) => {
180
- const sender = await deleteActivity.getActor(ctx);
181
- const excludeBaseUris = sender?.id ? [new URL(sender.id)] : [];
182
- await ctx.forwardActivity({ identifier: RELAY_SERVER_ACTOR }, "followers", {
183
- skipIfUnsigned: true,
184
- excludeBaseUris,
185
- preferSharedInbox: true
186
- });
86
+ await ctx.data.kv.set([
87
+ "keypair",
88
+ "ed25519",
89
+ identifier
90
+ ], {
91
+ privateKey: await (0, __fedify_fedify.exportJwk)(ed25519Pair$1.privateKey),
92
+ publicKey: await (0, __fedify_fedify.exportJwk)(ed25519Pair$1.publicKey)
187
93
  });
94
+ return [rsaPair$1, ed25519Pair$1];
188
95
  }
189
- get domain() {
190
- return this.#options.domain || "localhost";
96
+ const rsaPair = {
97
+ privateKey: await (0, __fedify_fedify.importJwk)(rsaPairJson.privateKey, "private"),
98
+ publicKey: await (0, __fedify_fedify.importJwk)(rsaPairJson.publicKey, "public")
99
+ };
100
+ const ed25519Pair = {
101
+ privateKey: await (0, __fedify_fedify.importJwk)(ed25519PairJson.privateKey, "private"),
102
+ publicKey: await (0, __fedify_fedify.importJwk)(ed25519PairJson.publicKey, "public")
103
+ };
104
+ return [rsaPair, ed25519Pair];
105
+ });
106
+ async function getFollowerActors(ctx) {
107
+ const actors = [];
108
+ for await (const { value } of ctx.data.kv.list(["follower"])) {
109
+ if (!isRelayFollower(value)) continue;
110
+ if (value.state !== "accepted") continue;
111
+ const actor = await __fedify_fedify_vocab.Object.fromJsonLd(value.actor);
112
+ if (!(0, __fedify_fedify_vocab.isActor)(actor)) continue;
113
+ actors.push(actor);
191
114
  }
192
- fetch(request) {
193
- return this.#federation.fetch(request, { contextData: void 0 });
115
+ return actors;
116
+ }
117
+ async function dispatchRelayActors(ctx, identifier) {
118
+ if (identifier !== RELAY_SERVER_ACTOR) return null;
119
+ const actors = await getFollowerActors(ctx);
120
+ return { items: actors };
121
+ }
122
+ relayBuilder.setFollowersDispatcher("/users/{identifier}/followers", dispatchRelayActors);
123
+ relayBuilder.setFollowingDispatcher("/users/{identifier}/following", dispatchRelayActors);
124
+
125
+ //#endregion
126
+ //#region src/base.ts
127
+ /**
128
+ * Abstract base class for relay implementations.
129
+ * Provides common infrastructure for both Mastodon and LitePub relays.
130
+ *
131
+ * @since 2.0.0
132
+ */
133
+ var BaseRelay = class {
134
+ federationBuilder;
135
+ options;
136
+ federation;
137
+ constructor(options, relayBuilder$1) {
138
+ this.options = options;
139
+ this.federationBuilder = relayBuilder$1;
194
140
  }
195
- setSubscriptionHandler(handler) {
196
- this.#subscriptionHandler = handler;
197
- return this;
141
+ async fetch(request) {
142
+ if (this.federation == null) {
143
+ this.federation = await this.federationBuilder.build(this.options);
144
+ this.setupInboxListeners();
145
+ }
146
+ return await this.federation.fetch(request, { contextData: this.options });
198
147
  }
199
148
  };
149
+
150
+ //#endregion
151
+ //#region src/follow.ts
152
+ /**
153
+ * Validate Follow activity and return follower actor if valid.
154
+ * This validation is common to both Mastodon and LitePub relay protocols.
155
+ *
156
+ * @param ctx The federation context
157
+ * @param follow The Follow activity to validate
158
+ * @returns The follower Actor if valid, null otherwise
159
+ */
160
+ async function validateFollowActivity(ctx, follow) {
161
+ if (follow.id == null || follow.objectId == null) return null;
162
+ const parsed = ctx.parseUri(follow.objectId);
163
+ const isPublicFollow = follow.objectId.href === "https://www.w3.org/ns/activitystreams#Public";
164
+ if (!isPublicFollow && parsed?.type !== "actor") return null;
165
+ const follower = await follow.getActor(ctx);
166
+ if (follower == null || follower.id == null || follower.preferredUsername == null || follower.inboxId == null) return null;
167
+ return follower;
168
+ }
169
+ /**
170
+ * Send Accept or Reject response for a Follow activity.
171
+ * This is common to both Mastodon and LitePub relay protocols.
172
+ *
173
+ * @param ctx The federation context
174
+ * @param follow The Follow activity being responded to
175
+ * @param follower The actor who sent the Follow
176
+ * @param approved Whether the follow was approved
177
+ */
178
+ async function sendFollowResponse(ctx, follow, follower, approved) {
179
+ const relayActorUri = ctx.getActorUri(RELAY_SERVER_ACTOR);
180
+ const Activity = approved ? __fedify_fedify.Accept : __fedify_fedify.Reject;
181
+ const action = approved ? "accepts" : "rejects";
182
+ await ctx.sendActivity({ identifier: RELAY_SERVER_ACTOR }, follower, new Activity({
183
+ id: new URL(`#${action}`, relayActorUri),
184
+ actor: relayActorUri,
185
+ object: follow
186
+ }));
187
+ }
188
+ /**
189
+ * Handle Undo activity for Follow.
190
+ * This logic is identical for both Mastodon and LitePub relay protocols.
191
+ *
192
+ * @param ctx The federation context
193
+ * @param undo The Undo activity to handle
194
+ * @param logger The logger instance to use for warnings
195
+ */
196
+ async function handleUndoFollow(ctx, undo, logger$2) {
197
+ const activity = await undo.getObject({
198
+ crossOrigin: "trust",
199
+ ...ctx
200
+ });
201
+ if (activity instanceof __fedify_fedify.Follow) {
202
+ if (activity.id == null || activity.actorId == null) return;
203
+ await ctx.data.kv.delete(["follower", activity.actorId.href]);
204
+ } else logger$2.warn("Unsupported object type ({type}) for Undo activity: {object}", {
205
+ type: activity?.constructor.name,
206
+ object: activity
207
+ });
208
+ }
209
+
210
+ //#endregion
211
+ //#region src/litepub.ts
212
+ const logger$1 = (0, __logtape_logtape.getLogger)([
213
+ "fedify",
214
+ "relay",
215
+ "litepub"
216
+ ]);
200
217
  /**
201
218
  * A LitePub-compatible ActivityPub relay implementation.
202
219
  * This relay follows LitePub's relay protocol and extensions for
@@ -204,26 +221,137 @@ var MastodonRelay = class {
204
221
  *
205
222
  * @since 2.0.0
206
223
  */
207
- var LitePubRelay = class {
208
- #federation;
209
- #options;
210
- #subscriptionHandler;
211
- constructor(options) {
212
- this.#options = options;
213
- this.#federation = (0, __fedify_fedify.createFederation)({ kv: options.kv });
224
+ var LitePubRelay = class extends BaseRelay {
225
+ async #announceToFollowers(ctx, activity) {
226
+ const sender = await activity.getActor(ctx);
227
+ const excludeBaseUris = sender?.id ? [new URL(sender.id)] : [];
228
+ const announce = new __fedify_fedify.Announce({
229
+ id: new URL(`/announce#${crypto.randomUUID()}`, ctx.origin),
230
+ actor: ctx.getActorUri(RELAY_SERVER_ACTOR),
231
+ object: activity.objectId,
232
+ to: __fedify_fedify.PUBLIC_COLLECTION,
233
+ published: Temporal.Now.instant()
234
+ });
235
+ await ctx.sendActivity({ identifier: RELAY_SERVER_ACTOR }, "followers", announce, {
236
+ excludeBaseUris,
237
+ preferSharedInbox: true
238
+ });
214
239
  }
215
- get domain() {
216
- return this.#options.domain || "localhost";
240
+ setupInboxListeners() {
241
+ if (this.federation != null) this.federation.setInboxListeners("/users/{identifier}/inbox", "/inbox").on(__fedify_fedify.Follow, async (ctx, follow) => {
242
+ const follower = await validateFollowActivity(ctx, follow);
243
+ if (!follower || !follower.id) return;
244
+ const existingFollow = await ctx.data.kv.get(["follower", follower.id.href]);
245
+ if (existingFollow?.state === "pending") return;
246
+ const approved = await this.options.subscriptionHandler(ctx, follower);
247
+ if (approved) {
248
+ await ctx.data.kv.set(["follower", follower.id.href], {
249
+ actor: await follower.toJsonLd(),
250
+ state: "pending"
251
+ });
252
+ await sendFollowResponse(ctx, follow, follower, approved);
253
+ const relayActorUri = ctx.getActorUri(RELAY_SERVER_ACTOR);
254
+ await ctx.sendActivity({ identifier: RELAY_SERVER_ACTOR }, follower, new __fedify_fedify.Follow({
255
+ actor: relayActorUri,
256
+ object: follower.id,
257
+ to: follower.id
258
+ }));
259
+ } else await sendFollowResponse(ctx, follow, follower, approved);
260
+ }).on(__fedify_fedify.Accept, async (ctx, accept) => {
261
+ const follow = await accept.getObject({
262
+ crossOrigin: "trust",
263
+ ...ctx
264
+ });
265
+ if (!(follow instanceof __fedify_fedify.Follow)) return;
266
+ const relayActorId = follow.actorId;
267
+ if (relayActorId == null) return;
268
+ const followerActor = await accept.getActor();
269
+ if (!(0, __fedify_fedify.isActor)(followerActor) || !followerActor.id) return;
270
+ const parsed = ctx.parseUri(relayActorId);
271
+ if (parsed == null || parsed.type !== "actor") return;
272
+ const followerData = await ctx.data.kv.get(["follower", followerActor.id.href]);
273
+ if (followerData == null) return;
274
+ const updatedFollowerData = {
275
+ ...followerData,
276
+ state: "accepted"
277
+ };
278
+ await ctx.data.kv.set(["follower", followerActor.id.href], updatedFollowerData);
279
+ }).on(__fedify_fedify.Undo, async (ctx, undo) => await handleUndoFollow(ctx, undo, logger$1)).on(__fedify_fedify.Create, async (ctx, create) => await this.#announceToFollowers(ctx, create)).on(__fedify_fedify.Update, async (ctx, update) => await this.#announceToFollowers(ctx, update)).on(__fedify_fedify.Move, async (ctx, move) => await this.#announceToFollowers(ctx, move)).on(__fedify_fedify.Delete, async (ctx, deleteActivity) => await this.#announceToFollowers(ctx, deleteActivity)).on(__fedify_fedify.Announce, async (ctx, announce) => await this.#announceToFollowers(ctx, announce));
217
280
  }
218
- fetch(request) {
219
- return this.#federation.fetch(request, { contextData: void 0 });
281
+ };
282
+
283
+ //#endregion
284
+ //#region src/mastodon.ts
285
+ const logger = (0, __logtape_logtape.getLogger)([
286
+ "fedify",
287
+ "relay",
288
+ "mastodon"
289
+ ]);
290
+ /**
291
+ * A Mastodon-compatible ActivityPub relay implementation.
292
+ * This relay follows Mastodon's relay protocol for compatibility
293
+ * with Mastodon instances.
294
+ *
295
+ * @since 2.0.0
296
+ */
297
+ var MastodonRelay = class extends BaseRelay {
298
+ async #forwardToFollowers(ctx, activity) {
299
+ const sender = await activity.getActor(ctx);
300
+ const excludeBaseUris = sender?.id ? [new URL(sender.id)] : [];
301
+ await ctx.forwardActivity({ identifier: RELAY_SERVER_ACTOR }, "followers", {
302
+ skipIfUnsigned: true,
303
+ excludeBaseUris,
304
+ preferSharedInbox: true
305
+ });
220
306
  }
221
- setSubscriptionHandler(handler) {
222
- this.#subscriptionHandler = handler;
223
- return this;
307
+ setupInboxListeners() {
308
+ if (this.federation != null) this.federation.setInboxListeners("/users/{identifier}/inbox", "/inbox").on(__fedify_fedify.Follow, async (ctx, follow) => {
309
+ const follower = await validateFollowActivity(ctx, follow);
310
+ if (!follower || !follower.id) return;
311
+ const approved = await this.options.subscriptionHandler(ctx, follower);
312
+ if (approved) await ctx.data.kv.set(["follower", follower.id.href], {
313
+ actor: await follower.toJsonLd(),
314
+ state: "accepted"
315
+ });
316
+ await sendFollowResponse(ctx, follow, follower, approved);
317
+ }).on(__fedify_fedify.Undo, async (ctx, undo) => await handleUndoFollow(ctx, undo, logger)).on(__fedify_fedify.Create, async (ctx, create) => await this.#forwardToFollowers(ctx, create)).on(__fedify_fedify.Delete, async (ctx, deleteActivity) => await this.#forwardToFollowers(ctx, deleteActivity)).on(__fedify_fedify.Move, async (ctx, move) => await this.#forwardToFollowers(ctx, move)).on(__fedify_fedify.Update, async (ctx, update) => await this.#forwardToFollowers(ctx, update)).on(__fedify_fedify.Announce, async (ctx, announce) => await this.#forwardToFollowers(ctx, announce));
224
318
  }
225
319
  };
226
320
 
321
+ //#endregion
322
+ //#region src/factory.ts
323
+ /**
324
+ * Factory function to create a relay instance.
325
+ *
326
+ * @param type The type of relay to create ("mastodon" or "litepub")
327
+ * @param options Configuration options for the relay
328
+ * @returns A relay instance
329
+ *
330
+ * @example
331
+ * ```ts
332
+ * import { createRelay } from "@fedify/relay";
333
+ * import { MemoryKvStore } from "@fedify/fedify";
334
+ *
335
+ * const relay = createRelay("mastodon", {
336
+ * kv: new MemoryKvStore(),
337
+ * domain: "relay.example.com",
338
+ * subscriptionHandler: async (ctx, actor) => true,
339
+ * });
340
+ * ```
341
+ *
342
+ * @since 2.0.0
343
+ */
344
+ function createRelay(type, options) {
345
+ switch (type) {
346
+ case "mastodon": return new MastodonRelay(options, relayBuilder);
347
+ case "litepub": return new LitePubRelay(options, relayBuilder);
348
+ }
349
+ }
350
+
227
351
  //#endregion
228
352
  exports.LitePubRelay = LitePubRelay;
229
- exports.MastodonRelay = MastodonRelay;
353
+ exports.MastodonRelay = MastodonRelay;
354
+ exports.RELAY_SERVER_ACTOR = RELAY_SERVER_ACTOR;
355
+ exports.createRelay = createRelay;
356
+ exports.isRelayFollower = isRelayFollower;
357
+ exports.relayBuilder = relayBuilder;
package/dist/mod.d.cts CHANGED
@@ -1,46 +1,90 @@
1
- import { Context, Federation, KvStore, MessageQueue } from "@fedify/fedify";
1
+ import { Context, Federation, FederationBuilder, KvStore, MessageQueue } from "@fedify/fedify";
2
2
  import { Actor } from "@fedify/fedify/vocab";
3
3
  import { AuthenticatedDocumentLoaderFactory, DocumentLoaderFactory } from "@fedify/vocab-runtime";
4
4
 
5
- //#region src/relay.d.ts
6
-
5
+ //#region src/types.d.ts
6
+ declare const RELAY_SERVER_ACTOR = "relay";
7
+ /**
8
+ * Supported relay types.
9
+ */
10
+ type RelayType = "mastodon" | "litepub";
7
11
  /**
8
12
  * Handler for subscription requests (Follow/Undo activities).
9
13
  */
10
- type SubscriptionRequestHandler = (ctx: Context<void>, clientActor: Actor) => Promise<boolean>;
14
+ type SubscriptionRequestHandler = (ctx: Context<RelayOptions>, clientActor: Actor) => Promise<boolean>;
11
15
  /**
12
16
  * Configuration options for the ActivityPub relay.
13
17
  */
14
18
  interface RelayOptions {
15
19
  kv: KvStore;
16
20
  domain?: string;
21
+ name?: string;
17
22
  documentLoaderFactory?: DocumentLoaderFactory;
18
23
  authenticatedDocumentLoaderFactory?: AuthenticatedDocumentLoaderFactory;
19
- federation?: Federation<void>;
20
24
  queue?: MessageQueue;
25
+ subscriptionHandler: SubscriptionRequestHandler;
26
+ }
27
+ interface RelayFollower {
28
+ readonly actor: unknown;
29
+ readonly state: "pending" | "accepted";
21
30
  }
22
31
  /**
23
- * Base interface for ActivityPub relay implementations.
32
+ * Type predicate to check if a value is a valid RelayFollower.
33
+ * Provides both runtime validation and compile-time type narrowing.
34
+ *
35
+ * @param value The value to check
36
+ * @returns true if the value is a RelayFollower
24
37
  */
25
- interface Relay {
26
- readonly domain: string;
27
- fetch(request: Request): Promise<Response>;
28
- setSubscriptionHandler(handler: SubscriptionRequestHandler): this;
29
- }
38
+ declare function isRelayFollower(value: unknown): value is RelayFollower;
39
+ //#endregion
40
+ //#region src/builder.d.ts
41
+ declare const relayBuilder: FederationBuilder<RelayOptions>;
42
+ //#endregion
43
+ //#region src/base.d.ts
30
44
  /**
31
- * A Mastodon-compatible ActivityPub relay implementation.
32
- * This relay follows Mastodon's relay protocol for maximum compatibility
33
- * with Mastodon instances.
45
+ * Abstract base class for relay implementations.
46
+ * Provides common infrastructure for both Mastodon and LitePub relays.
34
47
  *
35
48
  * @since 2.0.0
36
49
  */
37
- declare class MastodonRelay implements Relay {
38
- #private;
39
- constructor(options: RelayOptions);
40
- get domain(): string;
50
+ declare abstract class BaseRelay {
51
+ protected federationBuilder: FederationBuilder<RelayOptions>;
52
+ protected options: RelayOptions;
53
+ protected federation?: Federation<RelayOptions>;
54
+ constructor(options: RelayOptions, relayBuilder: FederationBuilder<RelayOptions>);
41
55
  fetch(request: Request): Promise<Response>;
42
- setSubscriptionHandler(handler: SubscriptionRequestHandler): this;
56
+ /**
57
+ * Set up inbox listeners for handling ActivityPub activities.
58
+ * Each relay type implements this method with protocol-specific logic.
59
+ */
60
+ protected abstract setupInboxListeners(): void;
43
61
  }
62
+ //#endregion
63
+ //#region src/factory.d.ts
64
+ /**
65
+ * Factory function to create a relay instance.
66
+ *
67
+ * @param type The type of relay to create ("mastodon" or "litepub")
68
+ * @param options Configuration options for the relay
69
+ * @returns A relay instance
70
+ *
71
+ * @example
72
+ * ```ts
73
+ * import { createRelay } from "@fedify/relay";
74
+ * import { MemoryKvStore } from "@fedify/fedify";
75
+ *
76
+ * const relay = createRelay("mastodon", {
77
+ * kv: new MemoryKvStore(),
78
+ * domain: "relay.example.com",
79
+ * subscriptionHandler: async (ctx, actor) => true,
80
+ * });
81
+ * ```
82
+ *
83
+ * @since 2.0.0
84
+ */
85
+ declare function createRelay(type: RelayType, options: RelayOptions): BaseRelay;
86
+ //#endregion
87
+ //#region src/litepub.d.ts
44
88
  /**
45
89
  * A LitePub-compatible ActivityPub relay implementation.
46
90
  * This relay follows LitePub's relay protocol and extensions for
@@ -48,12 +92,22 @@ declare class MastodonRelay implements Relay {
48
92
  *
49
93
  * @since 2.0.0
50
94
  */
51
- declare class LitePubRelay implements Relay {
95
+ declare class LitePubRelay extends BaseRelay {
52
96
  #private;
53
- constructor(options: RelayOptions);
54
- get domain(): string;
55
- fetch(request: Request): Promise<Response>;
56
- setSubscriptionHandler(handler: SubscriptionRequestHandler): this;
97
+ protected setupInboxListeners(): void;
98
+ }
99
+ //#endregion
100
+ //#region src/mastodon.d.ts
101
+ /**
102
+ * A Mastodon-compatible ActivityPub relay implementation.
103
+ * This relay follows Mastodon's relay protocol for compatibility
104
+ * with Mastodon instances.
105
+ *
106
+ * @since 2.0.0
107
+ */
108
+ declare class MastodonRelay extends BaseRelay {
109
+ #private;
110
+ protected setupInboxListeners(): void;
57
111
  }
58
112
  //#endregion
59
- export { LitePubRelay, MastodonRelay, Relay, RelayOptions };
113
+ export { LitePubRelay, MastodonRelay, RELAY_SERVER_ACTOR, RelayFollower, RelayOptions, RelayType, SubscriptionRequestHandler, createRelay, isRelayFollower, relayBuilder };