From cbb51460362eba2c9ccf5d639f9d25899bad64a4 Mon Sep 17 00:00:00 2001 From: Jiwon Kwon Date: Tue, 30 Dec 2025 22:25:17 +0900 Subject: [PATCH] Add relay manual --- docs/.vitepress/config.mts | 2 + docs/manual/relay.md | 368 +++++++++++++++++++++++++++++++++++++ 2 files changed, 370 insertions(+) create mode 100644 docs/manual/relay.md diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index b4414692..46642a62 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -75,6 +75,7 @@ const MANUAL = { { text: "Key–value store", link: "/manual/kv.md" }, { text: "Message queue", link: "/manual/mq.md" }, { text: "Integration", link: "/manual/integration.md" }, + { text: "Relay", link: "/manual/relay.md" }, { text: "Testing", link: "/manual/test.md" }, { text: "Linting", link: "/manual/lint.md" }, { text: "Logging", link: "/manual/log.md" }, @@ -98,6 +99,7 @@ const REFERENCES = { { text: "@fedify/koa", link: "https://jsr.io/@fedify/koa/doc" }, { text: "@fedify/postgres", link: "https://jsr.io/@fedify/postgres/doc" }, { text: "@fedify/redis", link: "https://jsr.io/@fedify/redis/doc" }, + { text: "@fedify/relay", link: "https://jsr.io/@fedify/relay/doc" }, { text: "@fedify/sqlite", link: "https://jsr.io/@fedify/sqlite/doc" }, { text: "@fedify/sveltekit", link: "https://jsr.io/@fedify/sveltekit/doc" }, { text: "@fedify/testing", link: "https://jsr.io/@fedify/testing/doc" }, diff --git a/docs/manual/relay.md b/docs/manual/relay.md new file mode 100644 index 00000000..5e448059 --- /dev/null +++ b/docs/manual/relay.md @@ -0,0 +1,368 @@ +--- +description: >- + Fedify provides a ready-to-use relay server implementation for building + ActivityPub relay infrastructure. +--- + +Relay +============ + +*This API is available since Fedify 2.0.0.* + +Fedify provides the `@fedify/relay` package for building [ActivityPub relay +servers]—services that forward activities between instances without requiring +individual actor-following relationships. + +[ActivityPub relay servers]: https://fediverse.party/en/miscellaneous/#relays + + +Setting up a relay server +------------------------- + +First, install the `@fedify/relay` package. + +::: code-group + +~~~~ sh [Deno] +deno add @fedify/relay +~~~~ + +~~~~ sh [Node.js] +npm add @fedify/relay +~~~~ + +~~~~ sh [Bun] +bun add @fedify/relay +~~~~ + +::: + +Then create a relay using the `createRelay()` function. + +~~~~ typescript twoslash +import { createRelay } from "@fedify/relay"; +import { MemoryKvStore } from "@fedify/fedify"; + +const relay = createRelay("mastodon", { + kv: new MemoryKvStore(), + domain: "relay.example.com", + name: "My ActivityPub Relay", + subscriptionHandler: async (ctx, actor) => { + // Approve all subscriptions + return true; + }, +}); + +Deno.serve((request) => relay.fetch(request)); +~~~~ + +> [!WARNING] +> `MemoryKvStore` is for development only. For production, use a persistent +> store like `RedisKvStore` from [`@fedify/redis`], `PostgresKvStore` from +> [`@fedify/postgres`], or `DenoKvStore` from [`@fedify/denokv`]. +> +> See the [*Key-value store* section](./kv.md) for details. + +[`@fedify/redis`]: https://github.com/fedify-dev/fedify/tree/main/packages/redis +[`@fedify/postgres`]: https://github.com/fedify-dev/fedify/tree/main/packages/postgres +[`@fedify/denokv`]: https://github.com/fedify-dev/fedify/tree/main/packages/denokv + + +Configuration options +--------------------- + +`kv` (required) +: A [`KvStore`](./kv.md) for storing subscriber information and cryptographic + keys. + +`domain` +: The domain name where the relay is hosted. Defaults to `"localhost"`. + +`name` +: Display name for the relay actor. Defaults to `"ActivityPub Relay"`. + +`queue` +: A [`MessageQueue`](./mq.md) for background activity processing. Recommended + for production: + + ~~~~ typescript twoslash + import { createRelay } from "@fedify/relay"; + import { MemoryKvStore, InProcessMessageQueue } from "@fedify/fedify"; + // ---cut-before--- + const relay = createRelay("mastodon", { + kv: new MemoryKvStore(), + domain: "relay.example.com", + queue: new InProcessMessageQueue(), + subscriptionHandler: async (ctx, actor) => true, + }); + ~~~~ + + > [!NOTE] + > For production, use [`RedisMessageQueue`] or [`PostgresMessageQueue`]. + +[`RedisMessageQueue`]: https://jsr.io/@fedify/redis/doc/mq/~/RedisMessageQueue +[`PostgresMessageQueue`]: https://jsr.io/@fedify/postgres/doc/mq/~/PostgresMessageQueue + +`subscriptionHandler` (required) +: Callback to approve or reject subscription requests. See + [*Handling subscriptions*](#handling-subscriptions). To create an open relay that accepts all subscriptions: + + ~~~~ typescript + subscriptionHandler: async (ctx, actor) => true + ~~~~ + +`documentLoaderFactory` +: A factory function for creating a document loader to fetch remote + ActivityPub objects. See [*Getting a `Federation` + object*](./federation.md#documentloaderfactory). + +`authenticatedDocumentLoaderFactory` + : A factory function for creating an authenticated document loader. + See [`authenticatedDocumentLoaderFactory`](./federation.md#authenticateddocumentloaderfactory). + + +Relay types +----------- + +The first parameter to `createRelay()` specifies the relay protocol: + +| Feature | `"mastodon"` | `"litepub"` | +|---------|--------------|-------------| +| Activity forwarding | Direct | Wrapped in `Announce` | +| Following relationship | One-way | Bidirectional | +| Subscription state | Immediate `"accepted"` | `"pending"` → `"accepted"` | +| Compatibility | Broad (most implementations) | LitePub-aware servers | + +> [!TIP] +> Use `"mastodon"` for broader compatibility. Switch to `"litepub"` only if +> you need its specific features. + +### Mastodon-style relay + +Activities are forwarded directly to subscribers. Instances follow the relay, +but the relay doesn't follow back. + +~~~~ typescript twoslash +import { createRelay } from "@fedify/relay"; +import { MemoryKvStore } from "@fedify/fedify"; +// ---cut-before--- +const relay = createRelay("mastodon", { + kv: new MemoryKvStore(), + domain: "relay.example.com", + subscriptionHandler: async (ctx, actor) => true, +}); +~~~~ + +Forwards `Create`, `Update`, `Delete`, `Move`, and `Announce` activities. + +### LitePub-style relay + +The relay server follows back instances that subscribe to it. Forwarded +activities are wrapped in `Announce` objects. + +~~~~ typescript twoslash +import { createRelay } from "@fedify/relay"; +import { MemoryKvStore } from "@fedify/fedify"; +// ---cut-before--- +const relay = createRelay("litepub", { + kv: new MemoryKvStore(), + domain: "relay.example.com", + subscriptionHandler: async (ctx, actor) => true, +}); +~~~~ + + +Handling subscriptions +---------------------- + +The `subscriptionHandler` is required and determines whether to approve or +reject subscription requests. For an open relay that accepts all subscriptions: + +~~~~ typescript twoslash +import { createRelay } from "@fedify/relay"; +import { MemoryKvStore } from "@fedify/fedify"; +// ---cut-before--- +const relay = createRelay("mastodon", { + kv: new MemoryKvStore(), + domain: "relay.example.com", + subscriptionHandler: async (ctx, actor) => true, // Accept all +}); +~~~~ + +To implement approval logic with blocklists: + +~~~~ typescript twoslash +import { createRelay } from "@fedify/relay"; +import { MemoryKvStore } from "@fedify/fedify"; +// ---cut-before--- +const blockedDomains = ["spam.example", "blocked.example"]; + +const relay = createRelay("mastodon", { + kv: new MemoryKvStore(), + domain: "relay.example.com", + subscriptionHandler: async (ctx, actor) => { + const domain = new URL(actor.id!).hostname; + if (blockedDomains.includes(domain)) { + return false; // Reject + } + return true; // Approve + }, +}); +~~~~ + +The handler receives: + + - `ctx`: The `Context` object + - `actor`: The `Actor` requesting subscription + +Return `true` to approve or `false` to reject. Rejected requests receive a +`Reject` activity. + + +Managing followers +------------------ + +Follower data is stored in the [`KvStore`](./kv.md) with keys following the +pattern `["follower", actorId]`. Each entry contains: + + - `actor`: The actor's JSON-LD data + - `state`: Either `"pending"` or `"accepted"` + +### Querying followers + +~~~~ typescript twoslash +import type { KvStore } from "@fedify/fedify"; +const kv = null as unknown as KvStore; +// ---cut-before--- +import type { RelayFollower } from "@fedify/relay"; + +for await (const entry of kv.list(["follower"])) { + console.log(`Follower: ${entry.value.actor["@id"]}`); + console.log(`State: ${entry.value.state}`); +} +~~~~ + +> [!NOTE] +> The `~KvStore.list()` method requires a `KvStore` implementation that +> supports listing by prefix (Redis, PostgreSQL, SQLite, Deno KV all support +> this). + +### Validating follower objects + +~~~~ typescript twoslash +import type { KvStore } from "@fedify/fedify"; +const kv = null as unknown as KvStore; +// ---cut-before--- +import { isRelayFollower } from "@fedify/relay"; + +for await (const entry of kv.list(["follower"])) { + if (isRelayFollower(entry.value)) { + console.log(`Valid follower in state: ${entry.value.state}`); + } +} +~~~~ + + +Storage requirements +-------------------- + +### Follower data + +Stored with keys `["follower", actorId]`. Actor objects typically range from +1–10 KB. For 1,000 subscribers, expect 1–10 MB of storage. + +### Cryptographic keys + +Two key pairs are generated and stored: + +| Key | Purpose | +|-----|---------| +| `["keypair", "rsa", "relay"]` | HTTP Signatures | +| `["keypair", "ed25519", "relay"]` | Linked Data Signatures, Object Integrity Proofs | + +> [!NOTE] +> These keys are critical for the relay's identity. Back up your `KvStore` +> regularly. + + +Security considerations +----------------------- + +### Signature verification + +The relay automatically verifies incoming activities using: + + - [HTTP Signatures] + - [Linked Data Signatures] + - [Object Integrity Proofs] + +Invalid signatures are silently ignored. Enable [logging](./log.md) for the +`["fedify", "sig"]` category to debug verification failures. + +[HTTP Signatures]: https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures-12 +[Linked Data Signatures]: https://web.archive.org/web/20170923124140/https://w3c-dvcg.github.io/ld-signatures/ +[Object Integrity Proofs]: https://w3id.org/fep/8b32 + +### Subscription abuse + +Protect against abuse by: + + 1. Implementing a `subscriptionHandler` to validate requests + 2. Maintaining a blocklist + 3. Rate limiting at the infrastructure level + 4. Monitoring activity volumes + +### Content moderation + +> [!WARNING] +> Running a relay makes you responsible for forwarded content. Establish clear +> policies and vet subscribing instances. + +### Privacy + +The relay has access to all activities that pass through it. Do not store or +log activity content beyond operational needs. + +> [!CAUTION] +> Never forward non-public activities. The relay is designed only for public +> content distribution. + + +Monitoring +---------- + +### Logging + +Enable relay-specific logging: + +~~~~ typescript twoslash +import { configure, getConsoleSink } from "@logtape/logtape"; + +await configure({ + sinks: { console: getConsoleSink() }, + loggers: [ + { category: ["fedify"], level: "info", sinks: ["console"] }, + ], +}); +~~~~ + +Key log categories: + +| Category | Description | +|----------|-------------| +| `["fedify", "federation", "inbox"]` | Incoming activities | +| `["fedify", "federation", "outbox"]` | Outgoing activities | +| `["fedify", "sig"]` | Signature verification | + +### OpenTelemetry + +The relay supports [OpenTelemetry](./opentelemetry.md) tracing. Key spans: + +| Span | Description | +|------|-------------| +| `activitypub.inbox` | Receiving activities | +| `activitypub.send_activity` | Forwarding activities | +| `activitypub.dispatch_inbox_listener` | Processing inbox events | + + +