@fedify/relay 2.0.0-dev.12 → 2.0.0-dev.158

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;
@@ -22,181 +25,295 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
22
25
 
23
26
  //#endregion
24
27
  const __fedify_fedify = __toESM(require("@fedify/fedify"));
25
- const __fedify_fedify_vocab = __toESM(require("@fedify/fedify/vocab"));
28
+ const __fedify_vocab = __toESM(require("@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 valid RelayFollowerData from KV store.
35
+ * Validates the storage format (JSON-LD), not the deserialized Actor instance.
33
36
  *
34
- * @since 2.0.0
37
+ * @param value The value to check
38
+ * @returns true if the value is a RelayFollowerData
39
+ * @internal
35
40
  */
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];
104
- });
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 };
41
+ function isRelayFollowerData(value) {
42
+ if (!value || typeof value !== "object") return false;
43
+ const obj = value;
44
+ return "actor" in obj && "state" in obj && typeof obj.state === "string" && (obj.state === "pending" || obj.state === "accepted");
45
+ }
46
+
47
+ //#endregion
48
+ //#region src/builder.ts
49
+ const relayBuilder = (0, __fedify_fedify.createFederationBuilder)();
50
+ relayBuilder.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => {
51
+ if (identifier !== RELAY_SERVER_ACTOR) return null;
52
+ const keys = await ctx.getActorKeyPairs(identifier);
53
+ return new __fedify_vocab.Application({
54
+ id: ctx.getActorUri(identifier),
55
+ preferredUsername: identifier,
56
+ name: ctx.data.name ?? "ActivityPub Relay",
57
+ inbox: ctx.getInboxUri(),
58
+ followers: ctx.getFollowersUri(identifier),
59
+ following: ctx.getFollowingUri(identifier),
60
+ url: ctx.getActorUri(identifier),
61
+ publicKey: keys[0].cryptographicKey,
62
+ assertionMethods: keys.map((k) => k.multikey)
63
+ });
64
+ }).setKeyPairsDispatcher(async (ctx, identifier) => {
65
+ if (identifier !== RELAY_SERVER_ACTOR) return [];
66
+ const rsaPairJson = await ctx.data.kv.get([
67
+ "keypair",
68
+ "rsa",
69
+ identifier
70
+ ]);
71
+ const ed25519PairJson = await ctx.data.kv.get([
72
+ "keypair",
73
+ "ed25519",
74
+ identifier
75
+ ]);
76
+ if (rsaPairJson == null || ed25519PairJson == null) {
77
+ const rsaPair$1 = await (0, __fedify_fedify.generateCryptoKeyPair)("RSASSA-PKCS1-v1_5");
78
+ const ed25519Pair$1 = await (0, __fedify_fedify.generateCryptoKeyPair)("Ed25519");
79
+ await ctx.data.kv.set([
80
+ "keypair",
81
+ "rsa",
82
+ identifier
83
+ ], {
84
+ privateKey: await (0, __fedify_fedify.exportJwk)(rsaPair$1.privateKey),
85
+ publicKey: await (0, __fedify_fedify.exportJwk)(rsaPair$1.publicKey)
116
86
  });
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 follower = await follow.getActor(ctx);
124
- if (follower == null || follower.id == null || follower.preferredUsername == null || follower.inboxId == null) return;
125
- let approved = false;
126
- if (this.#subscriptionHandler) approved = await this.#subscriptionHandler(ctx, follower);
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 follower.toJsonLd());
132
- await ctx.sendActivity({ identifier: RELAY_SERVER_ACTOR }, follower, 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 }, follower, 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
- });
87
+ await ctx.data.kv.set([
88
+ "keypair",
89
+ "ed25519",
90
+ identifier
91
+ ], {
92
+ privateKey: await (0, __fedify_fedify.exportJwk)(ed25519Pair$1.privateKey),
93
+ publicKey: await (0, __fedify_fedify.exportJwk)(ed25519Pair$1.publicKey)
187
94
  });
95
+ return [rsaPair$1, ed25519Pair$1];
96
+ }
97
+ const rsaPair = {
98
+ privateKey: await (0, __fedify_fedify.importJwk)(rsaPairJson.privateKey, "private"),
99
+ publicKey: await (0, __fedify_fedify.importJwk)(rsaPairJson.publicKey, "public")
100
+ };
101
+ const ed25519Pair = {
102
+ privateKey: await (0, __fedify_fedify.importJwk)(ed25519PairJson.privateKey, "private"),
103
+ publicKey: await (0, __fedify_fedify.importJwk)(ed25519PairJson.publicKey, "public")
104
+ };
105
+ return [rsaPair, ed25519Pair];
106
+ });
107
+ async function getFollowerActors(ctx) {
108
+ const actors = [];
109
+ for await (const { value } of ctx.data.kv.list(["follower"])) {
110
+ if (!isRelayFollowerData(value)) continue;
111
+ if (value.state !== "accepted") continue;
112
+ const actor = await __fedify_vocab.Object.fromJsonLd(value.actor);
113
+ if (!(0, __fedify_vocab.isActor)(actor)) continue;
114
+ actors.push(actor);
115
+ }
116
+ return actors;
117
+ }
118
+ async function dispatchRelayActors(ctx, identifier) {
119
+ if (identifier !== RELAY_SERVER_ACTOR) return null;
120
+ const actors = await getFollowerActors(ctx);
121
+ return { items: actors };
122
+ }
123
+ relayBuilder.setFollowersDispatcher("/users/{identifier}/followers", dispatchRelayActors);
124
+ relayBuilder.setFollowingDispatcher("/users/{identifier}/following", dispatchRelayActors);
125
+
126
+ //#endregion
127
+ //#region src/base.ts
128
+ /**
129
+ * Abstract base class for relay implementations.
130
+ * Provides common infrastructure for both Mastodon and LitePub relays.
131
+ *
132
+ * @internal
133
+ */
134
+ var BaseRelay = class {
135
+ federationBuilder;
136
+ options;
137
+ federation;
138
+ constructor(options, relayBuilder$1) {
139
+ this.options = options;
140
+ this.federationBuilder = relayBuilder$1;
141
+ }
142
+ async fetch(request) {
143
+ return await (await this.#getFederation()).fetch(request, { contextData: this.options });
144
+ }
145
+ /**
146
+ * Helper method to parse and validate follower data from storage.
147
+ * Deserializes JSON-LD actor data and validates it.
148
+ *
149
+ * @param actorId The actor ID of the follower
150
+ * @param data Raw data from KV store
151
+ * @returns RelayFollower object if valid, null otherwise
152
+ * @internal
153
+ */
154
+ async parseFollowerData(actorId, data) {
155
+ if (!isRelayFollowerData(data)) return null;
156
+ const actor = await __fedify_vocab.Object.fromJsonLd(data.actor);
157
+ if (!(0, __fedify_vocab.isActor)(actor)) return null;
158
+ return {
159
+ actorId,
160
+ actor,
161
+ state: data.state
162
+ };
188
163
  }
189
- get domain() {
190
- return this.#options.domain || "localhost";
164
+ /**
165
+ * Lists all followers of the relay.
166
+ *
167
+ * @returns An async iterator of follower entries
168
+ *
169
+ * @example
170
+ * ```ts
171
+ * import { createRelay } from "@fedify/relay";
172
+ * import { MemoryKvStore } from "@fedify/fedify";
173
+ *
174
+ * const relay = createRelay("mastodon", {
175
+ * kv: new MemoryKvStore(),
176
+ * origin: "https://relay.example.com",
177
+ * subscriptionHandler: async (ctx, actor) => true,
178
+ * });
179
+ *
180
+ * for await (const follower of relay.listFollowers()) {
181
+ * console.log(`Follower: ${follower.actorId}`);
182
+ * console.log(`State: ${follower.state}`);
183
+ * console.log(`Actor: ${follower.actor.name}`);
184
+ * }
185
+ * ```
186
+ *
187
+ * @since 2.0.0
188
+ */
189
+ async *listFollowers() {
190
+ for await (const entry of this.options.kv.list(["follower"])) {
191
+ const actorId = entry.key[1];
192
+ if (typeof actorId !== "string") continue;
193
+ const follower = await this.parseFollowerData(actorId, entry.value);
194
+ if (follower) yield follower;
195
+ }
191
196
  }
192
- fetch(request) {
193
- return this.#federation.fetch(request, { contextData: void 0 });
197
+ /**
198
+ * Gets a specific follower by actor ID.
199
+ *
200
+ * @param actorId The actor ID (URL) of the follower to retrieve
201
+ * @returns The follower entry if found, null otherwise
202
+ *
203
+ * @example
204
+ * ```ts
205
+ * import { createRelay } from "@fedify/relay";
206
+ * import { MemoryKvStore } from "@fedify/fedify";
207
+ *
208
+ * const relay = createRelay("mastodon", {
209
+ * kv: new MemoryKvStore(),
210
+ * origin: "https://relay.example.com",
211
+ * subscriptionHandler: async (ctx, actor) => true,
212
+ * });
213
+ *
214
+ * const follower = await relay.getFollower(
215
+ * "https://mastodon.example.com/users/alice"
216
+ * );
217
+ * if (follower) {
218
+ * console.log(`State: ${follower.state}`);
219
+ * console.log(`Actor: ${follower.actor.preferredUsername}`);
220
+ * }
221
+ * ```
222
+ *
223
+ * @since 2.0.0
224
+ */
225
+ async getFollower(actorId) {
226
+ const followerData = await this.options.kv.get(["follower", actorId]);
227
+ return await this.parseFollowerData(actorId, followerData);
194
228
  }
195
- setSubscriptionHandler(handler) {
196
- this.#subscriptionHandler = handler;
197
- return this;
229
+ async #getFederation() {
230
+ if (this.federation == null) {
231
+ this.federation = await this.federationBuilder.build(this.options);
232
+ this.setupInboxListeners();
233
+ }
234
+ return this.federation;
235
+ }
236
+ async #createContext() {
237
+ const context = (await this.#getFederation()).createContext(new URL(this.options.origin), this.options);
238
+ return context;
239
+ }
240
+ async getActorUri() {
241
+ const context = await this.#createContext();
242
+ return context.getActorUri(RELAY_SERVER_ACTOR);
243
+ }
244
+ async getSharedInboxUri() {
245
+ const context = await this.#createContext();
246
+ return context.getInboxUri();
198
247
  }
199
248
  };
249
+
250
+ //#endregion
251
+ //#region src/follow.ts
252
+ /**
253
+ * Validate Follow activity and return follower actor if valid.
254
+ * This validation is common to both Mastodon and LitePub relay protocols.
255
+ *
256
+ * @param ctx The federation context
257
+ * @param follow The Follow activity to validate
258
+ * @returns The follower Actor if valid, null otherwise
259
+ */
260
+ async function validateFollowActivity(ctx, follow) {
261
+ if (follow.id == null || follow.objectId == null) return null;
262
+ const parsed = ctx.parseUri(follow.objectId);
263
+ const isPublicFollow = follow.objectId.href === "https://www.w3.org/ns/activitystreams#Public";
264
+ if (!isPublicFollow && parsed?.type !== "actor") return null;
265
+ const follower = await follow.getActor(ctx);
266
+ if (follower == null || follower.id == null || follower.preferredUsername == null || follower.inboxId == null) return null;
267
+ return follower;
268
+ }
269
+ /**
270
+ * Send Accept or Reject response for a Follow activity.
271
+ * This is common to both Mastodon and LitePub relay protocols.
272
+ *
273
+ * @param ctx The federation context
274
+ * @param follow The Follow activity being responded to
275
+ * @param follower The actor who sent the Follow
276
+ * @param approved Whether the follow was approved
277
+ */
278
+ async function sendFollowResponse(ctx, follow, follower, approved) {
279
+ const relayActorUri = ctx.getActorUri(RELAY_SERVER_ACTOR);
280
+ const Activity = approved ? __fedify_vocab.Accept : __fedify_vocab.Reject;
281
+ const action = approved ? "accepts" : "rejects";
282
+ await ctx.sendActivity({ identifier: RELAY_SERVER_ACTOR }, follower, new Activity({
283
+ id: new URL(`#${action}`, relayActorUri),
284
+ actor: relayActorUri,
285
+ object: follow
286
+ }));
287
+ }
288
+ /**
289
+ * Handle Undo activity for Follow.
290
+ * This logic is identical for both Mastodon and LitePub relay protocols.
291
+ *
292
+ * @param ctx The federation context
293
+ * @param undo The Undo activity to handle
294
+ * @param logger The logger instance to use for warnings
295
+ */
296
+ async function handleUndoFollow(ctx, undo, logger$2) {
297
+ const activity = await undo.getObject({
298
+ crossOrigin: "trust",
299
+ ...ctx
300
+ });
301
+ if (activity instanceof __fedify_vocab.Follow) {
302
+ if (activity.id == null || activity.actorId == null) return;
303
+ await ctx.data.kv.delete(["follower", activity.actorId.href]);
304
+ } else logger$2.warn("Unsupported object type ({type}) for Undo activity: {object}", {
305
+ type: activity?.constructor.name,
306
+ object: activity
307
+ });
308
+ }
309
+
310
+ //#endregion
311
+ //#region src/litepub.ts
312
+ const logger$1 = (0, __logtape_logtape.getLogger)([
313
+ "fedify",
314
+ "relay",
315
+ "litepub"
316
+ ]);
200
317
  /**
201
318
  * A LitePub-compatible ActivityPub relay implementation.
202
319
  * This relay follows LitePub's relay protocol and extensions for
@@ -204,251 +321,133 @@ var MastodonRelay = class {
204
321
  *
205
322
  * @since 2.0.0
206
323
  */
207
- var LitePubRelay = class {
208
- #federation;
209
- #options;
210
- #subscriptionHandler;
211
- constructor(options) {
212
- this.#options = options;
213
- this.#federation = options.federation ?? (0, __fedify_fedify.createFederation)({
214
- kv: options.kv,
215
- queue: options.queue,
216
- documentLoaderFactory: options.documentLoaderFactory,
217
- authenticatedDocumentLoaderFactory: options.authenticatedDocumentLoaderFactory
324
+ var LitePubRelay = class extends BaseRelay {
325
+ async #announceToFollowers(ctx, activity) {
326
+ const sender = await activity.getActor(ctx);
327
+ const excludeBaseUris = sender?.id ? [new URL(sender.id)] : [];
328
+ const announce = new __fedify_vocab.Announce({
329
+ id: new URL(`/announce#${crypto.randomUUID()}`, ctx.origin),
330
+ actor: ctx.getActorUri(RELAY_SERVER_ACTOR),
331
+ object: activity.objectId,
332
+ to: __fedify_vocab.PUBLIC_COLLECTION,
333
+ published: Temporal.Now.instant()
218
334
  });
219
- this.#federation.setActorDispatcher("/users/{identifier}", async (ctx, identifier) => {
220
- if (identifier !== RELAY_SERVER_ACTOR) return null;
221
- const keys = await ctx.getActorKeyPairs(identifier);
222
- return new __fedify_fedify_vocab.Application({
223
- id: ctx.getActorUri(identifier),
224
- preferredUsername: identifier,
225
- name: "ActivityPub Relay",
226
- summary: "LitePub-compatible ActivityPub relay server",
227
- inbox: ctx.getInboxUri(),
228
- followers: ctx.getFollowersUri(identifier),
229
- following: ctx.getFollowingUri(identifier),
230
- url: ctx.getActorUri(identifier),
231
- publicKey: keys[0].cryptographicKey,
232
- assertionMethods: keys.map((k) => k.multikey)
233
- });
234
- }).setKeyPairsDispatcher(async (_ctx, identifier) => {
235
- if (identifier !== RELAY_SERVER_ACTOR) return [];
236
- const rsaPairJson = await options.kv.get([
237
- "keypair",
238
- "rsa",
239
- identifier
240
- ]);
241
- const ed25519PairJson = await options.kv.get([
242
- "keypair",
243
- "ed25519",
244
- identifier
245
- ]);
246
- if (rsaPairJson == null || ed25519PairJson == null) {
247
- const rsaPair$1 = await (0, __fedify_fedify.generateCryptoKeyPair)("RSASSA-PKCS1-v1_5");
248
- const ed25519Pair$1 = await (0, __fedify_fedify.generateCryptoKeyPair)("Ed25519");
249
- await options.kv.set([
250
- "keypair",
251
- "rsa",
252
- identifier
253
- ], {
254
- privateKey: await (0, __fedify_fedify.exportJwk)(rsaPair$1.privateKey),
255
- publicKey: await (0, __fedify_fedify.exportJwk)(rsaPair$1.publicKey)
256
- });
257
- await options.kv.set([
258
- "keypair",
259
- "ed25519",
260
- identifier
261
- ], {
262
- privateKey: await (0, __fedify_fedify.exportJwk)(ed25519Pair$1.privateKey),
263
- publicKey: await (0, __fedify_fedify.exportJwk)(ed25519Pair$1.publicKey)
264
- });
265
- return [rsaPair$1, ed25519Pair$1];
266
- }
267
- const rsaPair = {
268
- privateKey: await (0, __fedify_fedify.importJwk)(rsaPairJson.privateKey, "private"),
269
- publicKey: await (0, __fedify_fedify.importJwk)(rsaPairJson.publicKey, "public")
270
- };
271
- const ed25519Pair = {
272
- privateKey: await (0, __fedify_fedify.importJwk)(ed25519PairJson.privateKey, "private"),
273
- publicKey: await (0, __fedify_fedify.importJwk)(ed25519PairJson.publicKey, "public")
274
- };
275
- return [rsaPair, ed25519Pair];
335
+ await ctx.sendActivity({ identifier: RELAY_SERVER_ACTOR }, "followers", announce, {
336
+ excludeBaseUris,
337
+ preferSharedInbox: true
276
338
  });
277
- this.#federation.setFollowingDispatcher("/users/{identifier}/following", async (_ctx, identifier) => {
278
- if (identifier !== RELAY_SERVER_ACTOR) return null;
279
- const followers = await options.kv.get(["followers"]) ?? [];
280
- const actors = [];
281
- for (const followerId of followers) {
282
- const follower = await options.kv.get(["follower", followerId]);
283
- if (!follower) continue;
284
- const actor = await __fedify_fedify_vocab.Object.fromJsonLd(follower.actor);
285
- if (!(0, __fedify_fedify_vocab.isActor)(actor)) continue;
286
- actors.push(actor);
287
- }
288
- return { items: actors };
289
- });
290
- this.#federation.setFollowersDispatcher("/users/{identifier}/followers", async (_ctx, identifier) => {
291
- if (identifier !== RELAY_SERVER_ACTOR) return null;
292
- const followers = await options.kv.get(["followers"]) ?? [];
293
- const actors = [];
294
- for (const followerId of followers) {
295
- const follower = await options.kv.get(["follower", followerId]);
296
- if (!follower) continue;
297
- const actor = await __fedify_fedify_vocab.Object.fromJsonLd(follower.actor);
298
- if (!(0, __fedify_fedify_vocab.isActor)(actor)) continue;
299
- actors.push(actor);
300
- }
301
- return { items: actors };
302
- });
303
- this.#federation.setInboxListeners("/users/{identifier}/inbox", "/inbox").on(__fedify_fedify_vocab.Follow, async (ctx, follow) => {
304
- if (follow.id == null || follow.objectId == null) return;
305
- const parsed = ctx.parseUri(follow.objectId);
306
- const isPublicFollow = follow.objectId.href === "https://www.w3.org/ns/activitystreams#Public";
307
- if (!isPublicFollow && parsed?.type !== "actor") return;
308
- const relayActorUri = ctx.getActorUri(RELAY_SERVER_ACTOR);
309
- const follower = await follow.getActor(ctx);
310
- if (follower == null || follower.id == null || follower.preferredUsername == null || follower.inboxId == null) return;
311
- const existingFollow = await options.kv.get(["follower", follower.id.href]);
339
+ }
340
+ setupInboxListeners() {
341
+ if (this.federation != null) this.federation.setInboxListeners("/users/{identifier}/inbox", "/inbox").on(__fedify_vocab.Follow, async (ctx, follow) => {
342
+ const follower = await validateFollowActivity(ctx, follow);
343
+ if (!follower || !follower.id) return;
344
+ const existingFollow = await ctx.data.kv.get(["follower", follower.id.href]);
312
345
  if (existingFollow?.state === "pending") return;
313
- let subscriptionApproved = false;
314
- if (this.#subscriptionHandler) subscriptionApproved = await this.#subscriptionHandler(ctx, follower);
315
- if (subscriptionApproved) {
316
- await options.kv.set(["follower", follower.id.href], {
317
- "actor": await follower.toJsonLd(),
318
- "state": "pending"
346
+ const approved = await this.options.subscriptionHandler(ctx, follower);
347
+ if (approved) {
348
+ await ctx.data.kv.set(["follower", follower.id.href], {
349
+ actor: await follower.toJsonLd(),
350
+ state: "pending"
319
351
  });
320
- await ctx.sendActivity({ identifier: RELAY_SERVER_ACTOR }, follower, new __fedify_fedify_vocab.Accept({
321
- id: new URL(`#accepts`, relayActorUri),
322
- actor: relayActorUri,
323
- object: follow
324
- }));
325
- await ctx.sendActivity({ identifier: RELAY_SERVER_ACTOR }, follower, new __fedify_fedify_vocab.Follow({
352
+ await sendFollowResponse(ctx, follow, follower, approved);
353
+ const relayActorUri = ctx.getActorUri(RELAY_SERVER_ACTOR);
354
+ await ctx.sendActivity({ identifier: RELAY_SERVER_ACTOR }, follower, new __fedify_vocab.Follow({
326
355
  actor: relayActorUri,
327
356
  object: follower.id,
328
357
  to: follower.id
329
358
  }));
330
- } else await ctx.sendActivity({ identifier: RELAY_SERVER_ACTOR }, follower, new __fedify_fedify_vocab.Reject({
331
- id: new URL(`#rejects`, relayActorUri),
332
- actor: relayActorUri,
333
- object: follow
334
- }));
335
- }).on(__fedify_fedify_vocab.Accept, async (ctx, accept) => {
359
+ } else await sendFollowResponse(ctx, follow, follower, approved);
360
+ }).on(__fedify_vocab.Accept, async (ctx, accept) => {
336
361
  const follow = await accept.getObject({
337
362
  crossOrigin: "trust",
338
363
  ...ctx
339
364
  });
340
- if (!(follow instanceof __fedify_fedify_vocab.Follow)) return;
341
- const follower = follow.actorId;
342
- if (follower == null) return;
343
- const following = await accept.getActor();
344
- if (!(0, __fedify_fedify_vocab.isActor)(following) || !following.id) return;
345
- const parsed = ctx.parseUri(follower);
365
+ if (!(follow instanceof __fedify_vocab.Follow)) return;
366
+ const relayActorId = follow.actorId;
367
+ if (relayActorId == null) return;
368
+ const followerActor = await accept.getActor();
369
+ if (!(0, __fedify_vocab.isActor)(followerActor) || !followerActor.id) return;
370
+ const parsed = ctx.parseUri(relayActorId);
346
371
  if (parsed == null || parsed.type !== "actor") return;
347
- const followerData = await options.kv.get(["follower", following.id.href]);
372
+ const followerData = await ctx.data.kv.get(["follower", followerActor.id.href]);
348
373
  if (followerData == null) return;
349
374
  const updatedFollowerData = {
350
375
  ...followerData,
351
376
  state: "accepted"
352
377
  };
353
- await options.kv.set(["follower", following.id.href], updatedFollowerData);
354
- const followers = await options.kv.get(["followers"]) ?? [];
355
- followers.push(following.id.href);
356
- await options.kv.set(["followers"], followers);
357
- }).on(__fedify_fedify_vocab.Undo, async (ctx, undo) => {
358
- const activity = await undo.getObject({
359
- crossOrigin: "trust",
360
- ...ctx
361
- });
362
- if (activity instanceof __fedify_fedify_vocab.Follow) {
363
- if (activity.id == null || activity.actorId == null) return;
364
- const followers = await options.kv.get(["followers"]) ?? [];
365
- const updatedFollowers = followers.filter((id) => id !== activity.actorId?.href);
366
- await options.kv.set(["followers"], updatedFollowers);
367
- options.kv.delete(["follower", activity.actorId?.href]);
368
- } else console.warn("Unsupported object type ({type}) for Undo activity: {object}", {
369
- type: activity?.constructor.name,
370
- object: activity
371
- });
372
- }).on(__fedify_fedify_vocab.Create, async (ctx, create) => {
373
- const sender = await create.getActor(ctx);
374
- const excludeBaseUris = sender?.id ? [new URL(sender.id)] : [];
375
- const announce = new __fedify_fedify_vocab.Announce({
376
- id: new URL(`/announce#${crypto.randomUUID()}`, ctx.origin),
377
- actor: ctx.getActorUri(RELAY_SERVER_ACTOR),
378
- object: create.objectId,
379
- to: __fedify_fedify_vocab.PUBLIC_COLLECTION,
380
- published: Temporal.Now.instant()
381
- });
382
- await ctx.sendActivity({ identifier: RELAY_SERVER_ACTOR }, "followers", announce, {
383
- excludeBaseUris,
384
- preferSharedInbox: true
385
- });
386
- }).on(__fedify_fedify_vocab.Update, async (ctx, update) => {
387
- const sender = await update.getActor(ctx);
388
- const excludeBaseUris = sender?.id ? [new URL(sender.id)] : [];
389
- const announce = new __fedify_fedify_vocab.Announce({
390
- id: new URL(`/announce#${crypto.randomUUID()}`, ctx.origin),
391
- actor: ctx.getActorUri(RELAY_SERVER_ACTOR),
392
- object: update.objectId,
393
- to: __fedify_fedify_vocab.PUBLIC_COLLECTION
394
- });
395
- await ctx.sendActivity({ identifier: RELAY_SERVER_ACTOR }, "followers", announce, {
396
- excludeBaseUris,
397
- preferSharedInbox: true
398
- });
399
- }).on(__fedify_fedify_vocab.Move, async (ctx, move) => {
400
- const sender = await move.getActor(ctx);
401
- const excludeBaseUris = sender?.id ? [new URL(sender.id)] : [];
402
- const announce = new __fedify_fedify_vocab.Announce({
403
- id: new URL(`/announce#${crypto.randomUUID()}`, ctx.origin),
404
- actor: ctx.getActorUri(RELAY_SERVER_ACTOR),
405
- object: move.objectId,
406
- to: __fedify_fedify_vocab.PUBLIC_COLLECTION
407
- });
408
- await ctx.sendActivity({ identifier: RELAY_SERVER_ACTOR }, "followers", announce, {
409
- excludeBaseUris,
410
- preferSharedInbox: true
411
- });
412
- }).on(__fedify_fedify_vocab.Delete, async (ctx, deleteActivity) => {
413
- const sender = await deleteActivity.getActor(ctx);
414
- const excludeBaseUris = sender?.id ? [new URL(sender.id)] : [];
415
- const announce = new __fedify_fedify_vocab.Announce({
416
- id: new URL(`/announce#${crypto.randomUUID()}`, ctx.origin),
417
- actor: ctx.getActorUri(RELAY_SERVER_ACTOR),
418
- object: deleteActivity.objectId,
419
- to: __fedify_fedify_vocab.PUBLIC_COLLECTION
420
- });
421
- await ctx.sendActivity({ identifier: RELAY_SERVER_ACTOR }, "followers", announce, {
422
- excludeBaseUris,
423
- preferSharedInbox: true
424
- });
425
- }).on(__fedify_fedify_vocab.Announce, async (ctx, announceActivity) => {
426
- const sender = await announceActivity.getActor(ctx);
427
- const excludeBaseUris = sender?.id ? [new URL(sender.id)] : [];
428
- const announce = new __fedify_fedify_vocab.Announce({
429
- id: new URL(`/announce#${crypto.randomUUID()}`, ctx.origin),
430
- actor: ctx.getActorUri(RELAY_SERVER_ACTOR),
431
- object: announceActivity.objectId,
432
- to: __fedify_fedify_vocab.PUBLIC_COLLECTION
433
- });
434
- await ctx.sendActivity({ identifier: RELAY_SERVER_ACTOR }, "followers", announce, {
435
- excludeBaseUris,
436
- preferSharedInbox: true
437
- });
438
- });
378
+ await ctx.data.kv.set(["follower", followerActor.id.href], updatedFollowerData);
379
+ }).on(__fedify_vocab.Undo, async (ctx, undo) => await handleUndoFollow(ctx, undo, logger$1)).on(__fedify_vocab.Create, async (ctx, create) => await this.#announceToFollowers(ctx, create)).on(__fedify_vocab.Update, async (ctx, update) => await this.#announceToFollowers(ctx, update)).on(__fedify_vocab.Move, async (ctx, move) => await this.#announceToFollowers(ctx, move)).on(__fedify_vocab.Delete, async (ctx, deleteActivity) => await this.#announceToFollowers(ctx, deleteActivity)).on(__fedify_vocab.Announce, async (ctx, announce) => await this.#announceToFollowers(ctx, announce));
439
380
  }
440
- get domain() {
441
- return this.#options.domain || "localhost";
442
- }
443
- fetch(request) {
444
- return this.#federation.fetch(request, { contextData: void 0 });
381
+ };
382
+
383
+ //#endregion
384
+ //#region src/mastodon.ts
385
+ const logger = (0, __logtape_logtape.getLogger)([
386
+ "fedify",
387
+ "relay",
388
+ "mastodon"
389
+ ]);
390
+ /**
391
+ * A Mastodon-compatible ActivityPub relay implementation.
392
+ * This relay follows Mastodon's relay protocol for compatibility
393
+ * with Mastodon instances.
394
+ *
395
+ * @since 2.0.0
396
+ */
397
+ var MastodonRelay = class extends BaseRelay {
398
+ async #forwardToFollowers(ctx, activity) {
399
+ const sender = await activity.getActor(ctx);
400
+ const excludeBaseUris = sender?.id ? [new URL(sender.id)] : [];
401
+ await ctx.forwardActivity({ identifier: RELAY_SERVER_ACTOR }, "followers", {
402
+ skipIfUnsigned: true,
403
+ excludeBaseUris,
404
+ preferSharedInbox: true
405
+ });
445
406
  }
446
- setSubscriptionHandler(handler) {
447
- this.#subscriptionHandler = handler;
448
- return this;
407
+ setupInboxListeners() {
408
+ if (this.federation != null) this.federation.setInboxListeners("/users/{identifier}/inbox", "/inbox").on(__fedify_vocab.Follow, async (ctx, follow) => {
409
+ const follower = await validateFollowActivity(ctx, follow);
410
+ if (!follower || !follower.id) return;
411
+ const approved = await this.options.subscriptionHandler(ctx, follower);
412
+ if (approved) await ctx.data.kv.set(["follower", follower.id.href], {
413
+ actor: await follower.toJsonLd(),
414
+ state: "accepted"
415
+ });
416
+ await sendFollowResponse(ctx, follow, follower, approved);
417
+ }).on(__fedify_vocab.Undo, async (ctx, undo) => await handleUndoFollow(ctx, undo, logger)).on(__fedify_vocab.Create, async (ctx, create) => await this.#forwardToFollowers(ctx, create)).on(__fedify_vocab.Delete, async (ctx, deleteActivity) => await this.#forwardToFollowers(ctx, deleteActivity)).on(__fedify_vocab.Move, async (ctx, move) => await this.#forwardToFollowers(ctx, move)).on(__fedify_vocab.Update, async (ctx, update) => await this.#forwardToFollowers(ctx, update)).on(__fedify_vocab.Announce, async (ctx, announce) => await this.#forwardToFollowers(ctx, announce));
449
418
  }
450
419
  };
451
420
 
452
421
  //#endregion
453
- exports.LitePubRelay = LitePubRelay;
454
- exports.MastodonRelay = MastodonRelay;
422
+ //#region src/factory.ts
423
+ /**
424
+ * Factory function to create a relay instance.
425
+ *
426
+ * @param type The type of relay to create ("mastodon" or "litepub")
427
+ * @param options Configuration options for the relay
428
+ * @returns A relay instance
429
+ *
430
+ * @example
431
+ * ```ts
432
+ * import { createRelay } from "@fedify/relay";
433
+ * import { MemoryKvStore } from "@fedify/fedify";
434
+ *
435
+ * const relay = createRelay("mastodon", {
436
+ * kv: new MemoryKvStore(),
437
+ * origin: "https://relay.example.com",
438
+ * subscriptionHandler: async (ctx, actor) => true,
439
+ * });
440
+ * ```
441
+ *
442
+ * @since 2.0.0
443
+ */
444
+ function createRelay(type, options) {
445
+ switch (type) {
446
+ case "mastodon": return new MastodonRelay(options, relayBuilder);
447
+ case "litepub": return new LitePubRelay(options, relayBuilder);
448
+ }
449
+ }
450
+
451
+ //#endregion
452
+ exports.RELAY_SERVER_ACTOR = RELAY_SERVER_ACTOR;
453
+ exports.createRelay = createRelay;