@fedify/relay 2.0.0-pr.479.1919 → 2.0.0-pr.490.2

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