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