diff --git a/packages/relay/README.md b/packages/relay/README.md index e0402fdc..d75cbd05 100644 --- a/packages/relay/README.md +++ b/packages/relay/README.md @@ -13,6 +13,9 @@ This package provides ActivityPub relay functionality for the [Fedify] ecosystem, enabling the creation and management of relay servers that can forward activities between federated instances. +For comprehensive documentation on building and operating relay servers, +see the [*Relay server* section in the Fedify manual][manual]. + What is an ActivityPub relay? ------------------------------ @@ -33,7 +36,7 @@ This package supports two popular relay protocols used in the fediverse: ### Mastodon-style relay The Mastodon-style relay protocol uses LD signatures for activity -verification and follows the Public collection. This protocol is widely +verification and follows the Public collection. This protocol is widely supported by Mastodon and many other ActivityPub implementations. Key features: @@ -100,10 +103,12 @@ import { MemoryKvStore } from "@fedify/fedify"; const relay = createRelay("mastodon", { kv: new MemoryKvStore(), domain: "relay.example.com", - // Optional: Set a custom subscription handler to approve/reject subscriptions + // Required: Set a subscription handler to approve/reject subscriptions subscriptionHandler: async (ctx, actor) => { - // Implement your approval logic here - // Return true to approve, false to reject + // For an open relay, simply return true + // return true; + + // Or implement custom approval logic: const domain = new URL(actor.id!).hostname; const blockedDomains = ["spam.example", "blocked.example"]; return !blockedDomains.includes(domain); @@ -120,13 +125,24 @@ You can also create a LitePub-style relay by changing the type: const relay = createRelay("litepub", { kv: new MemoryKvStore(), domain: "relay.example.com", + subscriptionHandler: async (ctx, actor) => true, }); ~~~~ ### Subscription handling -By default, the relay automatically rejects all subscription requests. -You can customize this behavior by providing a subscription handler in the options: +The `subscriptionHandler` is required and determines whether to approve or reject +subscription requests. For an open relay that accepts all subscriptions: + +~~~~ typescript +const relay = createRelay("mastodon", { + kv: new MemoryKvStore(), + domain: "relay.example.com", + subscriptionHandler: async (ctx, actor) => true, // Accept all +}); +~~~~ + +You can also implement custom approval logic: ~~~~ typescript const relay = createRelay("mastodon", { @@ -156,6 +172,7 @@ const app = new Hono(); const relay = createRelay("mastodon", { kv: new MemoryKvStore(), domain: "relay.example.com", + subscriptionHandler: async (ctx, actor) => true, }); app.use("*", async (c) => { @@ -258,7 +275,7 @@ Configuration options for the relay: - `kv: KvStore` (required): Key–value store for persisting relay data - `domain?: string`: Relay's domain name (defaults to `"localhost"`) - `name?: string`: Relay's display name (defaults to `"ActivityPub Relay"`) - - `subscriptionHandler?: SubscriptionRequestHandler`: Custom handler for + - `subscriptionHandler: SubscriptionRequestHandler` (required): Handler for subscription approval/rejection - `documentLoaderFactory?: DocumentLoaderFactory`: Custom document loader factory @@ -296,3 +313,4 @@ type SubscriptionRequestHandler = ( [@fedify@hollo.social]: https://hollo.social/@fedify [Fedify]: https://fedify.dev/ [Fedify documentation on key–value stores]: https://fedify.dev/manual/kv +[manual]: https://fedify.dev/manual/relay diff --git a/packages/relay/src/factory.ts b/packages/relay/src/factory.ts index a513f57a..f644346f 100644 --- a/packages/relay/src/factory.ts +++ b/packages/relay/src/factory.ts @@ -19,6 +19,7 @@ import type { RelayOptions, RelayType } from "./types.ts"; * const relay = createRelay("mastodon", { * kv: new MemoryKvStore(), * domain: "relay.example.com", + * subscriptionHandler: async (ctx, actor) => true, * }); * ``` * diff --git a/packages/relay/src/litepub.test.ts b/packages/relay/src/litepub.test.ts index 91adb220..59ba8033 100644 --- a/packages/relay/src/litepub.test.ts +++ b/packages/relay/src/litepub.test.ts @@ -101,6 +101,7 @@ describe("LitePubRelay", () => { kv, domain: "relay.example.com", documentLoaderFactory: () => mockDocumentLoader, + subscriptionHandler: () => Promise.resolve(true), }); const request = new Request("https://relay.example.com/users/relay", { @@ -117,6 +118,7 @@ describe("LitePubRelay", () => { kv, domain: "relay.example.com", documentLoaderFactory: () => mockDocumentLoader, + subscriptionHandler: () => Promise.resolve(true), }); const request = new Request("https://relay.example.com/users/relay", { @@ -137,6 +139,7 @@ describe("LitePubRelay", () => { kv, domain: "relay.example.com", documentLoaderFactory: () => mockDocumentLoader, + subscriptionHandler: () => Promise.resolve(true), }); const request = new Request( @@ -156,6 +159,7 @@ describe("LitePubRelay", () => { kv, domain: "relay.example.com", documentLoaderFactory: () => mockDocumentLoader, + subscriptionHandler: () => Promise.resolve(true), }); const request = new Request( @@ -204,6 +208,7 @@ describe("LitePubRelay", () => { kv, domain: "relay.example.com", documentLoaderFactory: () => mockDocumentLoader, + subscriptionHandler: () => Promise.resolve(true), }); const request = new Request( @@ -229,6 +234,7 @@ describe("LitePubRelay", () => { kv, domain: "relay.example.com", documentLoaderFactory: () => mockDocumentLoader, + subscriptionHandler: () => Promise.resolve(true), }); const request = new Request("https://relay.example.com/users/relay", { @@ -534,6 +540,7 @@ describe("LitePubRelay", () => { domain: "relay.example.com", documentLoaderFactory: () => mockDocumentLoader, authenticatedDocumentLoaderFactory: () => mockDocumentLoader, + subscriptionHandler: () => Promise.resolve(true), }); const relayFollow = new Follow({ @@ -596,6 +603,7 @@ describe("LitePubRelay", () => { domain: "relay.example.com", documentLoaderFactory: () => mockDocumentLoader, authenticatedDocumentLoaderFactory: () => mockDocumentLoader, + subscriptionHandler: () => Promise.resolve(true), }); const originalFollow = new Follow({ @@ -654,6 +662,7 @@ describe("LitePubRelay", () => { domain: "relay.example.com", documentLoaderFactory: () => mockDocumentLoader, authenticatedDocumentLoaderFactory: () => mockDocumentLoader, + subscriptionHandler: () => Promise.resolve(true), }); const note = new Note({ @@ -697,6 +706,7 @@ describe("LitePubRelay", () => { domain: "relay.example.com", documentLoaderFactory: () => mockDocumentLoader, authenticatedDocumentLoaderFactory: () => mockDocumentLoader, + subscriptionHandler: () => Promise.resolve(true), }); const note = new Note({ @@ -740,6 +750,7 @@ describe("LitePubRelay", () => { domain: "relay.example.com", documentLoaderFactory: () => mockDocumentLoader, authenticatedDocumentLoaderFactory: () => mockDocumentLoader, + subscriptionHandler: () => Promise.resolve(true), }); const moveActivity = new Move({ @@ -779,6 +790,7 @@ describe("LitePubRelay", () => { domain: "relay.example.com", documentLoaderFactory: () => mockDocumentLoader, authenticatedDocumentLoaderFactory: () => mockDocumentLoader, + subscriptionHandler: () => Promise.resolve(true), }); const deleteActivity = new Delete({ @@ -817,6 +829,7 @@ describe("LitePubRelay", () => { domain: "relay.example.com", documentLoaderFactory: () => mockDocumentLoader, authenticatedDocumentLoaderFactory: () => mockDocumentLoader, + subscriptionHandler: () => Promise.resolve(true), }); const announceActivity = new Announce({ diff --git a/packages/relay/src/litepub.ts b/packages/relay/src/litepub.ts index ef44d7e7..8c512af0 100644 --- a/packages/relay/src/litepub.ts +++ b/packages/relay/src/litepub.ts @@ -74,10 +74,10 @@ export class LitePubRelay extends BaseRelay { ]); if (existingFollow?.state === "pending") return; - let approved = false; - if (this.options.subscriptionHandler) { - approved = await this.options.subscriptionHandler(ctx, follower); - } + const approved = await this.options.subscriptionHandler( + ctx, + follower, + ); if (approved) { // Litepub-specific: save with "pending" state diff --git a/packages/relay/src/mastodon.test.ts b/packages/relay/src/mastodon.test.ts index 1f138807..7b0bd97c 100644 --- a/packages/relay/src/mastodon.test.ts +++ b/packages/relay/src/mastodon.test.ts @@ -99,6 +99,7 @@ describe("MastodonRelay", () => { kv, domain: "relay.example.com", documentLoaderFactory: () => mockDocumentLoader, + subscriptionHandler: () => Promise.resolve(true), }); const request = new Request("https://relay.example.com/users/relay", { @@ -115,6 +116,7 @@ describe("MastodonRelay", () => { kv, domain: "relay.example.com", documentLoaderFactory: () => mockDocumentLoader, + subscriptionHandler: () => Promise.resolve(true), }); const request = new Request("https://relay.example.com/users/relay", { @@ -135,6 +137,7 @@ describe("MastodonRelay", () => { kv, domain: "relay.example.com", documentLoaderFactory: () => mockDocumentLoader, + subscriptionHandler: () => Promise.resolve(true), }); const request = new Request( @@ -154,6 +157,7 @@ describe("MastodonRelay", () => { kv, domain: "relay.example.com", documentLoaderFactory: () => mockDocumentLoader, + subscriptionHandler: () => Promise.resolve(true), }); const request = new Request( @@ -203,6 +207,7 @@ describe("MastodonRelay", () => { kv, domain: "relay.example.com", documentLoaderFactory: () => mockDocumentLoader, + subscriptionHandler: () => Promise.resolve(true), }); const request = new Request( @@ -275,6 +280,7 @@ describe("MastodonRelay", () => { kv, domain: "relay.example.com", documentLoaderFactory: () => mockDocumentLoader, + subscriptionHandler: () => Promise.resolve(true), }); const request = new Request("https://relay.example.com/users/relay", { @@ -463,6 +469,7 @@ describe("MastodonRelay", () => { domain: "relay.example.com", documentLoaderFactory: () => mockDocumentLoader, authenticatedDocumentLoaderFactory: () => mockDocumentLoader, + subscriptionHandler: () => Promise.resolve(true), }); const originalFollow = new Follow({ @@ -521,6 +528,7 @@ describe("MastodonRelay", () => { domain: "relay.example.com", documentLoaderFactory: () => mockDocumentLoader, authenticatedDocumentLoaderFactory: () => mockDocumentLoader, + subscriptionHandler: () => Promise.resolve(true), }); const note = new Note({ @@ -564,6 +572,7 @@ describe("MastodonRelay", () => { domain: "relay.example.com", documentLoaderFactory: () => mockDocumentLoader, authenticatedDocumentLoaderFactory: () => mockDocumentLoader, + subscriptionHandler: () => Promise.resolve(true), }); const deleteActivity = new Delete({ @@ -602,6 +611,7 @@ describe("MastodonRelay", () => { domain: "relay.example.com", documentLoaderFactory: () => mockDocumentLoader, authenticatedDocumentLoaderFactory: () => mockDocumentLoader, + subscriptionHandler: () => Promise.resolve(true), }); const note = new Note({ @@ -645,6 +655,7 @@ describe("MastodonRelay", () => { domain: "relay.example.com", documentLoaderFactory: () => mockDocumentLoader, authenticatedDocumentLoaderFactory: () => mockDocumentLoader, + subscriptionHandler: () => Promise.resolve(true), }); const moveActivity = new Move({ diff --git a/packages/relay/src/mastodon.ts b/packages/relay/src/mastodon.ts index c4d19d35..6af28ab4 100644 --- a/packages/relay/src/mastodon.ts +++ b/packages/relay/src/mastodon.ts @@ -52,10 +52,10 @@ export class MastodonRelay extends BaseRelay { const follower = await validateFollowActivity(ctx, follow); if (!follower || !follower.id) return; - let approved = false; - if (this.options.subscriptionHandler) { - approved = await this.options.subscriptionHandler(ctx, follower); - } + const approved = await this.options.subscriptionHandler( + ctx, + follower, + ); if (approved) { // Mastodon-specific: immediately add to followers list with accepted state diff --git a/packages/relay/src/types.ts b/packages/relay/src/types.ts index 0d411ede..8608f9e9 100644 --- a/packages/relay/src/types.ts +++ b/packages/relay/src/types.ts @@ -30,7 +30,7 @@ export interface RelayOptions { documentLoaderFactory?: DocumentLoaderFactory; authenticatedDocumentLoaderFactory?: AuthenticatedDocumentLoaderFactory; queue?: MessageQueue; - subscriptionHandler?: SubscriptionRequestHandler; + subscriptionHandler: SubscriptionRequestHandler; } export interface RelayFollower {