Skip to content

Commit 00c7443

Browse files
committed
Merge pull request #459 from sij411/feat/relay
Implemet Mastodon Relay protocol for `@fedify/relay`
2 parents bca8ab8 + cb8c689 commit 00c7443

File tree

12 files changed

+7671
-11147
lines changed

12 files changed

+7671
-11147
lines changed

CHANGES.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,22 @@ To be released.
124124
[#457]: https://github.com/fedify-dev/fedify/pull/457
125125
[#458]: https://github.com/fedify-dev/fedify/pull/458
126126

127+
### @fedify/relay
128+
129+
- Created ActivityPub relay integration as the *@fedify/relay* package.
130+
[[#359], [#459] by Jiwon Kwon]
131+
132+
- Added `Relay` interface defining the common contract for relay
133+
implementations.
134+
- Added `MastodonRelay` class implementing Mastodon-compatible relay
135+
protocol with:
136+
- Added `SubscriptionRequestHandler` type for custom subscription approval
137+
logic.
138+
- Added `RelayOptions` interface for relay configuration.
139+
140+
[#359]: https://github.com/fedify-dev/fedify/issues/359
141+
[#459]: https://github.com/fedify-dev/fedify/pull/459
142+
127143
### @fedify/vocab-tools
128144

129145
- Created Activity Vocabulary code generator as the *@fedify/vocab-tools*

deno.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"./packages/sveltekit",
1616
"./packages/testing",
1717
"./packages/vocab-runtime",
18+
"./packages/relay",
1819
"./packages/vocab-tools",
1920
"./examples/blog",
2021
"./examples/hono-sample"

deno.lock

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/relay/README.md

Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
1+
<!-- deno-fmt-ignore-file -->
2+
3+
@fedify/relay: ActivityPub relay for Fedify
4+
============================================
5+
6+
[![JSR][JSR badge]][JSR]
7+
[![npm][npm badge]][npm]
8+
[![Follow @fedify@hollo.social][@fedify@hollo.social badge]][@fedify@hollo.social]
9+
10+
*This package is available since Fedify 2.0.0.*
11+
12+
This package provides ActivityPub relay functionality for the [Fedify]
13+
ecosystem, enabling the creation and management of relay servers that can
14+
forward activities between federated instances.
15+
16+
17+
What is an ActivityPub relay?
18+
------------------------------
19+
20+
ActivityPub relays are infrastructure components that help small instances
21+
participate effectively in the federated social network by acting as
22+
intermediary servers that distribute public content without requiring
23+
individual actor-following relationships. When an instance subscribes to
24+
a relay, all public posts from that instance are forwarded to all other
25+
subscribed instances, creating a shared pool of federated content.
26+
27+
28+
Relay protocols
29+
---------------
30+
31+
This package supports two popular relay protocols used in the fediverse:
32+
33+
### Mastodon-style relay
34+
35+
The Mastodon-style relay protocol uses LD signatures for activity
36+
verification and follows the Public collection. This protocol is widely
37+
supported by Mastodon and many other ActivityPub implementations.
38+
39+
Key features:
40+
41+
- Direct activity relaying with proper content types (`Create`, `Update`,
42+
`Delete`, `Move`)
43+
- LD signature verification and generation
44+
- Follows the ActivityPub Public collection
45+
- Simple subscription mechanism via `Follow` activities
46+
47+
### LitePub-style relay
48+
49+
*LitePub relay support is planned for a future release.*
50+
51+
The LitePub-style relay protocol uses bidirectional following relationships
52+
and wraps activities in `Announce` activities for distribution.
53+
54+
55+
Installation
56+
------------
57+
58+
::: code-group
59+
60+
~~~~ sh [Deno]
61+
deno add jsr:@fedify/relay
62+
~~~~
63+
64+
~~~~ sh [npm]
65+
npm add @fedify/relay
66+
~~~~
67+
68+
~~~~ sh [pnpm]
69+
pnpm add @fedify/relay
70+
~~~~
71+
72+
~~~~ sh [Yarn]
73+
yarn add @fedify/relay
74+
~~~~
75+
76+
~~~~ sh [Bun]
77+
bun add @fedify/relay
78+
~~~~
79+
80+
:::
81+
82+
83+
Usage
84+
-----
85+
86+
### Creating a Mastodon-style relay
87+
88+
Here's a simple example of creating a Mastodon-compatible relay server:
89+
90+
~~~~ typescript
91+
import { MastodonRelay } from "@fedify/relay";
92+
import { MemoryKvStore } from "@fedify/fedify";
93+
94+
const relay = new MastodonRelay({
95+
kv: new MemoryKvStore(),
96+
domain: "relay.example.com",
97+
});
98+
99+
// Optional: Set a custom subscription handler to approve/reject subscriptions
100+
relay.setSubscriptionHandler(async (ctx, actor) => {
101+
// Implement your approval logic here
102+
// Return true to approve, false to reject
103+
const domain = new URL(actor.id!).hostname;
104+
const blockedDomains = ["spam.example", "blocked.example"];
105+
return !blockedDomains.includes(domain);
106+
});
107+
108+
// Serve the relay
109+
Deno.serve((request) => relay.fetch(request));
110+
~~~~
111+
112+
### Subscription handling
113+
114+
By default, the relay automatically rejects all subscription requests.
115+
You can customize this behavior by setting a subscription handler:
116+
117+
~~~~ typescript
118+
relay.setSubscriptionHandler(async (ctx, actor) => {
119+
// Example: Only allow subscriptions from specific domains
120+
const domain = new URL(actor.id!).hostname;
121+
const allowedDomains = ["mastodon.social", "fosstodon.org"];
122+
return allowedDomains.includes(domain);
123+
});
124+
~~~~
125+
126+
### Integration with web frameworks
127+
128+
The relay's `fetch()` method returns a standard `Response` object, making it
129+
compatible with any web framework that supports the Fetch API. Here's an
130+
example with Hono:
131+
132+
~~~~ typescript
133+
import { Hono } from "hono";
134+
import { MastodonRelay } from "@fedify/relay";
135+
import { MemoryKvStore } from "@fedify/fedify";
136+
137+
const app = new Hono();
138+
const relay = new MastodonRelay({
139+
kv: new MemoryKvStore(),
140+
domain: "relay.example.com",
141+
});
142+
143+
app.use("*", async (c) => {
144+
return await relay.fetch(c.req.raw);
145+
});
146+
147+
export default app;
148+
~~~~
149+
150+
151+
How it works
152+
------------
153+
154+
The relay operates by:
155+
156+
1. **Actor registration**: The relay presents itself as a Service actor at
157+
`/users/relay`
158+
2. **Subscription**: Instances subscribe to the relay by sending a `Follow`
159+
activity
160+
3. **Approval**: The relay's subscription handler determines whether to
161+
approve the subscription (responds with `Accept` or `Reject`)
162+
4. **Forwarding**: When a subscribed instance sends activities (`Create`,
163+
`Update`, `Delete`, `Move`) to the relay's inbox, the relay forwards them
164+
to all other subscribed instances
165+
5. **Unsubscription**: Instances can unsubscribe by sending an `Undo` activity
166+
wrapping their original `Follow` activity
167+
168+
169+
Storage requirements
170+
--------------------
171+
172+
The relay requires a key–value store to persist:
173+
174+
- Subscriber list and their Follow activity IDs
175+
- Subscriber actor information
176+
- Relay's cryptographic key pairs (RSA and Ed25519)
177+
178+
Any `KvStore` implementation from Fedify can be used, including:
179+
180+
- `MemoryKvStore` (for development/testing)
181+
- `DenoKvStore` (Deno KV)
182+
- `RedisKvStore` (Redis)
183+
- `PostgresKvStore` (PostgreSQL)
184+
- `SqliteKvStore` (SQLite)
185+
186+
For production use, choose a persistent storage backend like Redis or
187+
PostgreSQL. See the [Fedify documentation on key–value stores] for more
188+
details.
189+
190+
191+
API reference
192+
-------------
193+
194+
### `MastodonRelay`
195+
196+
A Mastodon-compatible ActivityPub relay implementation.
197+
198+
#### Constructor
199+
200+
~~~~ typescript
201+
new MastodonRelay(options: RelayOptions)
202+
~~~~
203+
204+
#### Properties
205+
206+
- `domain`: The relay's domain name (read-only)
207+
208+
#### Methods
209+
210+
- `fetch(request: Request): Promise<Response>`: Handle incoming HTTP requests
211+
- `setSubscriptionHandler(handler: SubscriptionRequestHandler): this`:
212+
Set a custom handler for subscription approval/rejection
213+
214+
### `RelayOptions`
215+
216+
Configuration options for the relay:
217+
218+
- `kv: KvStore` (required): Key–value store for persisting relay data
219+
- `domain?: string`: Relay's domain name (defaults to `"localhost"`)
220+
- `documentLoaderFactory?: DocumentLoaderFactory`: Custom document loader
221+
factory
222+
- `authenticatedDocumentLoaderFactory?: AuthenticatedDocumentLoaderFactory`:
223+
Custom authenticated document loader factory
224+
- `federation?: Federation<void>`: Custom Federation instance (for advanced
225+
use cases)
226+
- `queue?: MessageQueue`: Message queue for background activity processing
227+
228+
### `SubscriptionRequestHandler`
229+
230+
A function that determines whether to approve a subscription request:
231+
232+
~~~~ typescript
233+
type SubscriptionRequestHandler = (
234+
ctx: Context<void>,
235+
clientActor: Actor,
236+
) => Promise<boolean>
237+
~~~~
238+
239+
Parameters:
240+
241+
- `ctx`: The Fedify context object
242+
- `clientActor`: The actor requesting to subscribe
243+
244+
Returns:
245+
246+
- `true` to approve the subscription
247+
- `false` to reject the subscription
248+
249+
250+
[JSR]: https://jsr.io/@fedify/relay
251+
[JSR badge]: https://jsr.io/badges/@fedify/relay
252+
[npm]: https://www.npmjs.com/package/@fedify/relay
253+
[npm badge]: https://img.shields.io/npm/v/@fedify/relay?logo=npm
254+
[@fedify@hollo.social badge]: https://fedi-badge.deno.dev/@fedify@hollo.social/followers.svg
255+
[@fedify@hollo.social]: https://hollo.social/@fedify
256+
[Fedify]: https://fedify.dev/
257+
[Fedify documentation on keyvalue stores]: https://fedify.dev/manual/kv

packages/relay/deno.json

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
{
2+
"name": "@fedify/relay",
3+
"version": "2.0.0",
4+
"license": "MIT",
5+
"exports": {},
6+
"imports": {
7+
"@std/assert": "jsr:@std/assert@^1.0.13"
8+
},
9+
"exports": {
10+
".": "./src/mod.ts"
11+
},
12+
"exclude": [
13+
"dist/",
14+
"node_modules/"
15+
],
16+
"tasks": {
17+
"codegen": "deno task -f @fedify/fedify codegen",
18+
"check": {
19+
"command": "deno task codegen && deno fmt --check && deno lint && deno check src/**/*.ts",
20+
"dependencies": [
21+
"codegen"
22+
]
23+
},
24+
"run": {
25+
"command": "deno run --allow-all src/mod.ts",
26+
"dependencies": [
27+
"codegen"
28+
]
29+
},
30+
"runi": "deno run --allow-all src/mod.ts",
31+
"pack": {
32+
"command": "deno run -A scripts/pack.ts",
33+
"dependencies": [
34+
"codegen"
35+
]
36+
}
37+
},
38+
"fmt": {
39+
"exclude": [
40+
"src/init/templates/**"
41+
]
42+
},
43+
"lint": {
44+
"exclude": [
45+
"src/init/templates/**"
46+
]
47+
}
48+
}

0 commit comments

Comments
 (0)