@fedify/relay 2.4.0-dev.1794 → 2.4.0-dev.1805

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.
@@ -1,12 +1,11 @@
1
1
  import "temporal-polyfill";
2
2
  import "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
- import { t as isRelayFollowerData } from "./types-CZvP2qE2.js";
4
+ import { n as exportSpki, r as getDocumentLoader, t as isRelayFollowerData } from "./types-1nwAof4Z.js";
5
5
  import { MemoryKvStore, signRequest } from "@fedify/fedify";
6
6
  import { createRelay } from "@fedify/relay";
7
7
  import { Accept, Announce, Create, Delete, Follow, Move, Note, Person, Undo, Update } from "@fedify/vocab";
8
- import { exportSpki, getDocumentLoader } from "@fedify/vocab-runtime";
9
- import { ok, strictEqual } from "node:assert";
8
+ import { deepStrictEqual, ok, strictEqual } from "node:assert";
10
9
  import test, { describe } from "node:test";
11
10
  //#region src/litepub.test.ts
12
11
  const mockDocumentLoader = async (url) => {
@@ -288,7 +287,58 @@ describe("LitePubRelay", () => {
288
287
  await relay.fetch(request);
289
288
  strictEqual(await kv.get(["follower", "https://remote.example.com/users/alice"]), void 0);
290
289
  });
291
- test("ignores duplicate Follow activity from pending follower", async () => {
290
+ test("replaces malformed follower data on Follow", async () => {
291
+ const followerId = "https://remote.example.com/users/alice";
292
+ const malformedRows = [
293
+ { state: "pending" },
294
+ {
295
+ actor: null,
296
+ state: "pending"
297
+ },
298
+ {
299
+ actor: {},
300
+ state: "pending"
301
+ },
302
+ {
303
+ actor: await new Person({ id: new URL("https://remote.example.com/users/bob") }).toJsonLd(),
304
+ state: "pending"
305
+ }
306
+ ];
307
+ for (const malformedRow of malformedRows) {
308
+ const kv = new MemoryKvStore();
309
+ await kv.set(["follower", followerId], malformedRow);
310
+ let handlerCallCount = 0;
311
+ const relay = createRelay("litepub", {
312
+ kv,
313
+ origin: "https://relay.example.com",
314
+ documentLoaderFactory: () => mockDocumentLoader,
315
+ authenticatedDocumentLoaderFactory: () => mockDocumentLoader,
316
+ subscriptionHandler: () => {
317
+ handlerCallCount++;
318
+ return Promise.resolve(true);
319
+ }
320
+ });
321
+ strictEqual(await relay.getFollower(followerId), null);
322
+ const followActivity = new Follow({
323
+ id: new URL("https://remote.example.com/activities/follow/1"),
324
+ actor: new URL(followerId),
325
+ object: new URL("https://relay.example.com/users/relay")
326
+ });
327
+ let request = new Request("https://relay.example.com/inbox", {
328
+ method: "POST",
329
+ headers: { "Content-Type": "application/activity+json" },
330
+ body: JSON.stringify(await followActivity.toJsonLd({ contextLoader: mockDocumentLoader }))
331
+ });
332
+ request = await signRequest(request, rsaKeyPair.privateKey, rsaPublicKey.id);
333
+ await relay.fetch(request);
334
+ strictEqual(handlerCallCount, 1);
335
+ const follower = await relay.getFollower(followerId);
336
+ ok(follower);
337
+ strictEqual(follower.state, "pending");
338
+ strictEqual(follower.actor.id?.href, followerId);
339
+ }
340
+ });
341
+ for (const state of ["pending", "accepted"]) test(`ignores duplicate Follow activity from ${state} follower`, async () => {
292
342
  const kv = new MemoryKvStore();
293
343
  let handlerCallCount = 0;
294
344
  const relay = createRelay("litepub", {
@@ -308,7 +358,7 @@ describe("LitePubRelay", () => {
308
358
  });
309
359
  await kv.set(["follower", "https://remote.example.com/users/alice"], {
310
360
  actor: await follower.toJsonLd(),
311
- state: "pending"
361
+ state
312
362
  });
313
363
  const followActivity = new Follow({
314
364
  id: new URL("https://remote.example.com/activities/follow/1"),
@@ -323,6 +373,9 @@ describe("LitePubRelay", () => {
323
373
  request = await signRequest(request, rsaKeyPair.privateKey, rsaPublicKey.id);
324
374
  await relay.fetch(request);
325
375
  strictEqual(handlerCallCount, 0);
376
+ const followerData = await kv.get(["follower", "https://remote.example.com/users/alice"]);
377
+ ok(isRelayFollowerData(followerData));
378
+ strictEqual(followerData.state, state);
326
379
  });
327
380
  test("handles Accept activity completing reciprocal follow", async () => {
328
381
  const kv = new MemoryKvStore();
@@ -363,7 +416,54 @@ describe("LitePubRelay", () => {
363
416
  ok(isRelayFollowerData(followerData));
364
417
  strictEqual(followerData.state, "accepted");
365
418
  });
366
- test("handles Undo Follow activity", async () => {
419
+ test("ignores Accept activity for invalid follower data", async () => {
420
+ const followerId = "https://remote.example.com/users/alice";
421
+ const invalidRows = [
422
+ { state: "pending" },
423
+ {
424
+ actor: null,
425
+ state: "pending"
426
+ },
427
+ {
428
+ actor: {},
429
+ state: "pending"
430
+ },
431
+ {
432
+ actor: await new Person({ id: new URL("https://remote.example.com/users/bob") }).toJsonLd(),
433
+ state: "pending"
434
+ }
435
+ ];
436
+ for (const invalidRow of invalidRows) {
437
+ const kv = new MemoryKvStore();
438
+ await kv.set(["follower", followerId], invalidRow);
439
+ const relay = createRelay("litepub", {
440
+ kv,
441
+ origin: "https://relay.example.com",
442
+ documentLoaderFactory: () => mockDocumentLoader,
443
+ authenticatedDocumentLoaderFactory: () => mockDocumentLoader,
444
+ subscriptionHandler: () => Promise.resolve(true)
445
+ });
446
+ const relayFollow = new Follow({
447
+ id: new URL("https://relay.example.com/activities/follow/1"),
448
+ actor: new URL("https://relay.example.com/users/relay"),
449
+ object: new URL(followerId)
450
+ });
451
+ const acceptActivity = new Accept({
452
+ id: new URL("https://remote.example.com/activities/accept/1"),
453
+ actor: new URL(followerId),
454
+ object: relayFollow
455
+ });
456
+ let request = new Request("https://relay.example.com/inbox", {
457
+ method: "POST",
458
+ headers: { "Content-Type": "application/activity+json" },
459
+ body: JSON.stringify(await acceptActivity.toJsonLd({ contextLoader: mockDocumentLoader }))
460
+ });
461
+ request = await signRequest(request, rsaKeyPair.privateKey, rsaPublicKey.id);
462
+ await relay.fetch(request);
463
+ deepStrictEqual(await kv.get(["follower", followerId]), invalidRow);
464
+ }
465
+ });
466
+ for (const state of ["pending", "accepted"]) test(`handles Undo Follow activity for ${state} follower`, async () => {
367
467
  const kv = new MemoryKvStore();
368
468
  const followerId = "https://remote.example.com/users/alice";
369
469
  const follower = new Person({
@@ -373,7 +473,7 @@ describe("LitePubRelay", () => {
373
473
  });
374
474
  await kv.set(["follower", followerId], {
375
475
  actor: await follower.toJsonLd(),
376
- state: "accepted"
476
+ state
377
477
  });
378
478
  const relay = createRelay("litepub", {
379
479
  kv,
@@ -1,12 +1,11 @@
1
1
  import "temporal-polyfill";
2
2
  import "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
- import { t as isRelayFollowerData } from "./types-CZvP2qE2.js";
5
- import { MemoryKvStore, signRequest } from "@fedify/fedify";
4
+ import { n as exportSpki, r as getDocumentLoader, t as isRelayFollowerData } from "./types-1nwAof4Z.js";
5
+ import { MemoryKvStore, signJsonLd, signRequest } from "@fedify/fedify";
6
6
  import { createRelay } from "@fedify/relay";
7
- import { Create, Delete, Follow, Move, Note, Person, Undo, Update } from "@fedify/vocab";
8
- import { exportSpki, getDocumentLoader } from "@fedify/vocab-runtime";
9
- import { ok, strictEqual } from "node:assert";
7
+ import { Announce, Create, Delete, Follow, Move, Note, Person, Undo, Update } from "@fedify/vocab";
8
+ import { deepStrictEqual, ok, strictEqual } from "node:assert";
10
9
  import test, { describe } from "node:test";
11
10
  //#region src/mastodon.test.ts
12
11
  const mockDocumentLoader = async (url) => {
@@ -254,7 +253,9 @@ describe("MastodonRelay", () => {
254
253
  await relay.fetch(request);
255
254
  strictEqual(handlerCalled, true);
256
255
  ok(handlerActor);
257
- ok(await kv.get(["follower", "https://remote.example.com/users/alice"]));
256
+ const followerData = await kv.get(["follower", "https://remote.example.com/users/alice"]);
257
+ ok(isRelayFollowerData(followerData));
258
+ strictEqual(followerData.state, "accepted");
258
259
  });
259
260
  test("handles Follow activity with subscription rejection", async () => {
260
261
  const kv = new MemoryKvStore();
@@ -433,6 +434,57 @@ describe("MastodonRelay", () => {
433
434
  const response = await relay.fetch(request);
434
435
  ok(response.status === 200 || response.status === 202);
435
436
  });
437
+ test("handles Announce activity forwarding", async () => {
438
+ const kv = new MemoryKvStore();
439
+ const follower = new Person({
440
+ id: new URL("https://follower.example.com/users/bob"),
441
+ preferredUsername: "bob",
442
+ inbox: new URL("https://follower.example.com/users/bob/inbox")
443
+ });
444
+ await kv.set(["follower", follower.id.href], {
445
+ actor: await follower.toJsonLd(),
446
+ state: "accepted"
447
+ });
448
+ const relay = createRelay("mastodon", {
449
+ kv,
450
+ origin: "https://relay.example.com",
451
+ documentLoaderFactory: () => mockDocumentLoader,
452
+ authenticatedDocumentLoaderFactory: () => mockDocumentLoader,
453
+ subscriptionHandler: () => Promise.resolve(true)
454
+ });
455
+ const signedAnnounce = await signJsonLd(await new Announce({
456
+ id: new URL("https://remote.example.com/activities/announce/1"),
457
+ actor: new URL("https://remote.example.com/users/alice"),
458
+ object: new URL("https://remote.example.com/notes/1")
459
+ }).toJsonLd({ contextLoader: mockDocumentLoader }), rsaKeyPair.privateKey, rsaPublicKey.id, { contextLoader: mockDocumentLoader });
460
+ let request = new Request("https://relay.example.com/inbox", {
461
+ method: "POST",
462
+ headers: { "Content-Type": "application/activity+json" },
463
+ body: JSON.stringify(signedAnnounce)
464
+ });
465
+ request = await signRequest(request, rsaKeyPair.privateKey, rsaPublicKey.id);
466
+ const originalFetch = globalThis.fetch;
467
+ let deliveryMethod;
468
+ let deliveredActivity;
469
+ globalThis.fetch = (async (input, init) => {
470
+ const outboundRequest = input instanceof Request ? input : new Request(input, init);
471
+ if (outboundRequest.url === "https://follower.example.com/users/bob/inbox") {
472
+ deliveryMethod = outboundRequest.method;
473
+ deliveredActivity = await outboundRequest.json();
474
+ return new Response(null, { status: 202 });
475
+ }
476
+ return originalFetch(input, init);
477
+ });
478
+ try {
479
+ const response = await relay.fetch(request);
480
+ ok(response.status === 200 || response.status === 202, `Unexpected inbox response status: ${response.status}`);
481
+ } finally {
482
+ globalThis.fetch = originalFetch;
483
+ }
484
+ ok(deliveredActivity, "Expected Announce delivery to the follower inbox");
485
+ strictEqual(deliveryMethod, "POST");
486
+ deepStrictEqual(deliveredActivity, signedAnnounce);
487
+ });
436
488
  test("ignores Follow activity without required fields", async () => {
437
489
  const kv = new MemoryKvStore();
438
490
  const relay = createRelay("mastodon", {
package/dist/mod.cjs CHANGED
@@ -18,6 +18,28 @@ function isRelayFollowerData(value) {
18
18
  const obj = value;
19
19
  return "actor" in obj && "state" in obj && typeof obj.state === "string" && (obj.state === "pending" || obj.state === "accepted");
20
20
  }
21
+ /**
22
+ * Parses and semantically validates follower data from storage.
23
+ *
24
+ * @param actorId The actor ID used as the follower's storage key.
25
+ * @param value The stored follower data.
26
+ * @returns The parsed follower, or `null` if the row is invalid.
27
+ * @internal
28
+ */
29
+ async function parseRelayFollowerData(actorId, value) {
30
+ if (!isRelayFollowerData(value)) return null;
31
+ try {
32
+ const actor = await _fedify_vocab.Object.fromJsonLd(value.actor);
33
+ if (!(0, _fedify_vocab.isActor)(actor) || actor.id?.href !== actorId) return null;
34
+ return {
35
+ actorId,
36
+ actor,
37
+ state: value.state
38
+ };
39
+ } catch {
40
+ return null;
41
+ }
42
+ }
21
43
  //#endregion
22
44
  //#region src/builder.ts
23
45
  const relayBuilder = (0, _fedify_fedify.createFederationBuilder)();
@@ -78,12 +100,12 @@ relayBuilder.setActorDispatcher("/users/{identifier}", async (ctx, identifier) =
78
100
  });
79
101
  async function getFollowerActors(ctx) {
80
102
  const actors = [];
81
- for await (const { value } of ctx.data.kv.list(["follower"])) {
82
- if (!isRelayFollowerData(value)) continue;
83
- if (value.state !== "accepted") continue;
84
- const actor = await _fedify_vocab.Object.fromJsonLd(value.actor);
85
- if (!(0, _fedify_vocab.isActor)(actor)) continue;
86
- actors.push(actor);
103
+ for await (const { key, value } of ctx.data.kv.list(["follower"])) {
104
+ const actorId = key[1];
105
+ if (typeof actorId !== "string") continue;
106
+ const follower = await parseRelayFollowerData(actorId, value);
107
+ if (follower?.state !== "accepted") continue;
108
+ actors.push(follower.actor);
87
109
  }
88
110
  return actors;
89
111
  }
@@ -3658,6 +3680,64 @@ const Temporal$2 = /*@__PURE__*/ Object.defineProperties({}, {
3658
3680
  const Temporal$1 = NativeTemporal || Temporal$2;
3659
3681
  NativeTemporal && Date.prototype.toTemporalInstant;
3660
3682
  //#endregion
3683
+ //#region src/follow.ts
3684
+ /**
3685
+ * Validate Follow activity and return follower actor if valid.
3686
+ * This validation is common to both Mastodon and LitePub relay protocols.
3687
+ *
3688
+ * @param ctx The federation context
3689
+ * @param follow The Follow activity to validate
3690
+ * @returns The follower Actor if valid, null otherwise
3691
+ */
3692
+ async function validateFollowActivity(ctx, follow) {
3693
+ if (follow.id == null || follow.objectId == null) return null;
3694
+ const parsed = ctx.parseUri(follow.objectId);
3695
+ if (!(follow.objectId.href === "https://www.w3.org/ns/activitystreams#Public") && parsed?.type !== "actor") return null;
3696
+ const follower = await follow.getActor(ctx);
3697
+ if (follower == null || follower.id == null || follower.preferredUsername == null || follower.inboxId == null) return null;
3698
+ return follower;
3699
+ }
3700
+ /**
3701
+ * Send Accept or Reject response for a Follow activity.
3702
+ * This is common to both Mastodon and LitePub relay protocols.
3703
+ *
3704
+ * @param ctx The federation context
3705
+ * @param follow The Follow activity being responded to
3706
+ * @param follower The actor who sent the Follow
3707
+ * @param approved Whether the follow was approved
3708
+ */
3709
+ async function sendFollowResponse(ctx, follow, follower, approved) {
3710
+ const relayActorUri = ctx.getActorUri(RELAY_SERVER_ACTOR);
3711
+ const Activity = approved ? _fedify_vocab.Accept : _fedify_vocab.Reject;
3712
+ const action = approved ? "accepts" : "rejects";
3713
+ await ctx.sendActivity({ identifier: RELAY_SERVER_ACTOR }, follower, new Activity({
3714
+ id: new URL(`#${action}`, relayActorUri),
3715
+ actor: relayActorUri,
3716
+ object: follow
3717
+ }));
3718
+ }
3719
+ /**
3720
+ * Handle Undo activity for Follow.
3721
+ * This logic is identical for both Mastodon and LitePub relay protocols.
3722
+ *
3723
+ * @param ctx The federation context
3724
+ * @param undo The Undo activity to handle
3725
+ * @param logger The logger instance to use for warnings
3726
+ */
3727
+ async function handleUndoFollow(ctx, undo, logger) {
3728
+ const activity = await undo.getObject({
3729
+ crossOrigin: "trust",
3730
+ ...ctx
3731
+ });
3732
+ if (activity instanceof _fedify_vocab.Follow) {
3733
+ if (activity.id == null || activity.actorId == null) return;
3734
+ await ctx.data.kv.delete(["follower", activity.actorId.href]);
3735
+ } else logger.warn("Unsupported object type ({type}) for Undo activity: {object}", {
3736
+ type: activity?.constructor.name,
3737
+ object: activity
3738
+ });
3739
+ }
3740
+ //#endregion
3661
3741
  //#region src/base.ts
3662
3742
  /**
3663
3743
  * Abstract base class for relay implementations.
@@ -3677,25 +3757,6 @@ var BaseRelay = class {
3677
3757
  return await (await this.#getFederation()).fetch(request, { contextData: this.options });
3678
3758
  }
3679
3759
  /**
3680
- * Helper method to parse and validate follower data from storage.
3681
- * Deserializes JSON-LD actor data and validates it.
3682
- *
3683
- * @param actorId The actor ID of the follower
3684
- * @param data Raw data from KV store
3685
- * @returns RelayFollower object if valid, null otherwise
3686
- * @internal
3687
- */
3688
- async parseFollowerData(actorId, data) {
3689
- if (!isRelayFollowerData(data)) return null;
3690
- const actor = await _fedify_vocab.Object.fromJsonLd(data.actor);
3691
- if (!(0, _fedify_vocab.isActor)(actor)) return null;
3692
- return {
3693
- actorId,
3694
- actor,
3695
- state: data.state
3696
- };
3697
- }
3698
- /**
3699
3760
  * Lists all followers of the relay.
3700
3761
  *
3701
3762
  * @returns An async iterator of follower entries
@@ -3724,7 +3785,7 @@ var BaseRelay = class {
3724
3785
  for await (const entry of this.options.kv.list(["follower"])) {
3725
3786
  const actorId = entry.key[1];
3726
3787
  if (typeof actorId !== "string") continue;
3727
- const follower = await this.parseFollowerData(actorId, entry.value);
3788
+ const follower = await parseRelayFollowerData(actorId, entry.value);
3728
3789
  if (follower) yield follower;
3729
3790
  }
3730
3791
  }
@@ -3757,8 +3818,35 @@ var BaseRelay = class {
3757
3818
  * @since 2.0.0
3758
3819
  */
3759
3820
  async getFollower(actorId) {
3760
- const followerData = await this.options.kv.get(["follower", actorId]);
3761
- return await this.parseFollowerData(actorId, followerData);
3821
+ return await parseRelayFollowerData(actorId, await this.options.kv.get(["follower", actorId]));
3822
+ }
3823
+ shouldSkipFollow(_ctx, _follower) {
3824
+ return Promise.resolve(false);
3825
+ }
3826
+ afterFollowApproved(_ctx, _follower) {
3827
+ return Promise.resolve();
3828
+ }
3829
+ async #handleFollow(ctx, follow) {
3830
+ const follower = await validateFollowActivity(ctx, follow);
3831
+ if (follower?.id == null || await this.shouldSkipFollow(ctx, follower)) return;
3832
+ const approved = await this.options.subscriptionHandler(ctx, follower);
3833
+ if (approved) await ctx.data.kv.set(["follower", follower.id.href], {
3834
+ actor: await follower.toJsonLd(),
3835
+ state: this.initialFollowerState
3836
+ });
3837
+ await sendFollowResponse(ctx, follow, follower, approved);
3838
+ if (approved) await this.afterFollowApproved(ctx, follower);
3839
+ }
3840
+ async #relayActivity(ctx, activity) {
3841
+ const senderId = activity.actorId;
3842
+ const excludeBaseUris = senderId == null ? [] : [senderId];
3843
+ await this.deliverActivity(ctx, activity, excludeBaseUris);
3844
+ }
3845
+ setupInboxListeners() {
3846
+ if (this.federation == null) throw new Error("Federation must be initialized before inbox listeners");
3847
+ const listeners = this.federation.setInboxListeners("/users/{identifier}/inbox", "/inbox");
3848
+ listeners.on(_fedify_vocab.Follow, async (ctx, follow) => await this.#handleFollow(ctx, follow)).on(_fedify_vocab.Undo, async (ctx, undo) => await handleUndoFollow(ctx, undo, this.logger)).on(_fedify_vocab.Create, async (ctx, create) => await this.#relayActivity(ctx, create)).on(_fedify_vocab.Delete, async (ctx, deleteActivity) => await this.#relayActivity(ctx, deleteActivity)).on(_fedify_vocab.Move, async (ctx, move) => await this.#relayActivity(ctx, move)).on(_fedify_vocab.Update, async (ctx, update) => await this.#relayActivity(ctx, update)).on(_fedify_vocab.Announce, async (ctx, announce) => await this.#relayActivity(ctx, announce));
3849
+ return listeners;
3762
3850
  }
3763
3851
  async #getFederation() {
3764
3852
  if (this.federation == null) {
@@ -3778,64 +3866,6 @@ var BaseRelay = class {
3778
3866
  }
3779
3867
  };
3780
3868
  //#endregion
3781
- //#region src/follow.ts
3782
- /**
3783
- * Validate Follow activity and return follower actor if valid.
3784
- * This validation is common to both Mastodon and LitePub relay protocols.
3785
- *
3786
- * @param ctx The federation context
3787
- * @param follow The Follow activity to validate
3788
- * @returns The follower Actor if valid, null otherwise
3789
- */
3790
- async function validateFollowActivity(ctx, follow) {
3791
- if (follow.id == null || follow.objectId == null) return null;
3792
- const parsed = ctx.parseUri(follow.objectId);
3793
- if (!(follow.objectId.href === "https://www.w3.org/ns/activitystreams#Public") && parsed?.type !== "actor") return null;
3794
- const follower = await follow.getActor(ctx);
3795
- if (follower == null || follower.id == null || follower.preferredUsername == null || follower.inboxId == null) return null;
3796
- return follower;
3797
- }
3798
- /**
3799
- * Send Accept or Reject response for a Follow activity.
3800
- * This is common to both Mastodon and LitePub relay protocols.
3801
- *
3802
- * @param ctx The federation context
3803
- * @param follow The Follow activity being responded to
3804
- * @param follower The actor who sent the Follow
3805
- * @param approved Whether the follow was approved
3806
- */
3807
- async function sendFollowResponse(ctx, follow, follower, approved) {
3808
- const relayActorUri = ctx.getActorUri(RELAY_SERVER_ACTOR);
3809
- const Activity = approved ? _fedify_vocab.Accept : _fedify_vocab.Reject;
3810
- const action = approved ? "accepts" : "rejects";
3811
- await ctx.sendActivity({ identifier: RELAY_SERVER_ACTOR }, follower, new Activity({
3812
- id: new URL(`#${action}`, relayActorUri),
3813
- actor: relayActorUri,
3814
- object: follow
3815
- }));
3816
- }
3817
- /**
3818
- * Handle Undo activity for Follow.
3819
- * This logic is identical for both Mastodon and LitePub relay protocols.
3820
- *
3821
- * @param ctx The federation context
3822
- * @param undo The Undo activity to handle
3823
- * @param logger The logger instance to use for warnings
3824
- */
3825
- async function handleUndoFollow(ctx, undo, logger) {
3826
- const activity = await undo.getObject({
3827
- crossOrigin: "trust",
3828
- ...ctx
3829
- });
3830
- if (activity instanceof _fedify_vocab.Follow) {
3831
- if (activity.id == null || activity.actorId == null) return;
3832
- await ctx.data.kv.delete(["follower", activity.actorId.href]);
3833
- } else logger.warn("Unsupported object type ({type}) for Undo activity: {object}", {
3834
- type: activity?.constructor.name,
3835
- object: activity
3836
- });
3837
- }
3838
- //#endregion
3839
3869
  //#region src/litepub.ts
3840
3870
  const Temporal = Temporal$1;
3841
3871
  const logger$1 = (0, _logtape_logtape.getLogger)([
@@ -3851,9 +3881,23 @@ const logger$1 = (0, _logtape_logtape.getLogger)([
3851
3881
  * @since 2.0.0
3852
3882
  */
3853
3883
  var LitePubRelay = class extends BaseRelay {
3854
- async #announceToFollowers(ctx, activity) {
3855
- const sender = await activity.getActor(ctx);
3856
- const excludeBaseUris = sender?.id ? [new URL(sender.id)] : [];
3884
+ initialFollowerState = "pending";
3885
+ logger = logger$1;
3886
+ async shouldSkipFollow(ctx, follower) {
3887
+ if (follower.id == null) return true;
3888
+ const existingFollow = await ctx.data.kv.get(["follower", follower.id.href]);
3889
+ return await parseRelayFollowerData(follower.id.href, existingFollow) != null;
3890
+ }
3891
+ async afterFollowApproved(ctx, follower) {
3892
+ if (follower.id == null) return;
3893
+ const relayActorUri = ctx.getActorUri(RELAY_SERVER_ACTOR);
3894
+ await ctx.sendActivity({ identifier: RELAY_SERVER_ACTOR }, follower, new _fedify_vocab.Follow({
3895
+ actor: relayActorUri,
3896
+ object: follower.id,
3897
+ to: follower.id
3898
+ }));
3899
+ }
3900
+ async deliverActivity(ctx, activity, excludeBaseUris) {
3857
3901
  const announce = new _fedify_vocab.Announce({
3858
3902
  id: new URL(`/announce#${crypto.randomUUID()}`, ctx.origin),
3859
3903
  actor: ctx.getActorUri(RELAY_SERVER_ACTOR),
@@ -3867,25 +3911,7 @@ var LitePubRelay = class extends BaseRelay {
3867
3911
  });
3868
3912
  }
3869
3913
  setupInboxListeners() {
3870
- if (this.federation != null) this.federation.setInboxListeners("/users/{identifier}/inbox", "/inbox").on(_fedify_vocab.Follow, async (ctx, follow) => {
3871
- const follower = await validateFollowActivity(ctx, follow);
3872
- if (!follower || !follower.id) return;
3873
- if ((await ctx.data.kv.get(["follower", follower.id.href]))?.state === "pending") return;
3874
- const approved = await this.options.subscriptionHandler(ctx, follower);
3875
- if (approved) {
3876
- await ctx.data.kv.set(["follower", follower.id.href], {
3877
- actor: await follower.toJsonLd(),
3878
- state: "pending"
3879
- });
3880
- await sendFollowResponse(ctx, follow, follower, approved);
3881
- const relayActorUri = ctx.getActorUri(RELAY_SERVER_ACTOR);
3882
- await ctx.sendActivity({ identifier: RELAY_SERVER_ACTOR }, follower, new _fedify_vocab.Follow({
3883
- actor: relayActorUri,
3884
- object: follower.id,
3885
- to: follower.id
3886
- }));
3887
- } else await sendFollowResponse(ctx, follow, follower, approved);
3888
- }).on(_fedify_vocab.Accept, async (ctx, accept) => {
3914
+ return super.setupInboxListeners().on(_fedify_vocab.Accept, async (ctx, accept) => {
3889
3915
  const follow = await accept.getObject({
3890
3916
  crossOrigin: "trust",
3891
3917
  ...ctx
@@ -3893,18 +3919,19 @@ var LitePubRelay = class extends BaseRelay {
3893
3919
  if (!(follow instanceof _fedify_vocab.Follow)) return;
3894
3920
  const relayActorId = follow.actorId;
3895
3921
  if (relayActorId == null) return;
3896
- const followerActor = await accept.getActor();
3922
+ const followerActor = await accept.getActor(ctx);
3897
3923
  if (!(0, _fedify_vocab.isActor)(followerActor) || !followerActor.id) return;
3898
3924
  const parsed = ctx.parseUri(relayActorId);
3899
3925
  if (parsed == null || parsed.type !== "actor") return;
3900
3926
  const followerData = await ctx.data.kv.get(["follower", followerActor.id.href]);
3901
- if (followerData == null) return;
3927
+ if (!isRelayFollowerData(followerData)) return;
3928
+ if (await parseRelayFollowerData(followerActor.id.href, followerData) == null) return;
3902
3929
  const updatedFollowerData = {
3903
3930
  ...followerData,
3904
3931
  state: "accepted"
3905
3932
  };
3906
3933
  await ctx.data.kv.set(["follower", followerActor.id.href], updatedFollowerData);
3907
- }).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));
3934
+ });
3908
3935
  }
3909
3936
  };
3910
3937
  //#endregion
@@ -3922,27 +3949,15 @@ const logger = (0, _logtape_logtape.getLogger)([
3922
3949
  * @since 2.0.0
3923
3950
  */
3924
3951
  var MastodonRelay = class extends BaseRelay {
3925
- async #forwardToFollowers(ctx, activity) {
3926
- const sender = await activity.getActor(ctx);
3927
- const excludeBaseUris = sender?.id ? [new URL(sender.id)] : [];
3952
+ initialFollowerState = "accepted";
3953
+ logger = logger;
3954
+ async deliverActivity(ctx, _activity, excludeBaseUris) {
3928
3955
  await ctx.forwardActivity({ identifier: RELAY_SERVER_ACTOR }, "followers", {
3929
3956
  skipIfUnsigned: true,
3930
3957
  excludeBaseUris,
3931
3958
  preferSharedInbox: true
3932
3959
  });
3933
3960
  }
3934
- setupInboxListeners() {
3935
- if (this.federation != null) this.federation.setInboxListeners("/users/{identifier}/inbox", "/inbox").on(_fedify_vocab.Follow, async (ctx, follow) => {
3936
- const follower = await validateFollowActivity(ctx, follow);
3937
- if (!follower || !follower.id) return;
3938
- const approved = await this.options.subscriptionHandler(ctx, follower);
3939
- if (approved) await ctx.data.kv.set(["follower", follower.id.href], {
3940
- actor: await follower.toJsonLd(),
3941
- state: "accepted"
3942
- });
3943
- await sendFollowResponse(ctx, follow, follower, approved);
3944
- }).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));
3945
- }
3946
3961
  };
3947
3962
  //#endregion
3948
3963
  //#region src/factory.ts