Skip to content

Commit 1661d5f

Browse files
sij411claude
andcommitted
Make subscriptionHandler required in relay package
The subscriptionHandler option is now required instead of optional. This prevents the confusing situation where relays appear to work but silently reject all subscriptions. Users must now explicitly define their subscription policy: - For open relays: `subscriptionHandler: async () => true` - For restricted relays: implement custom approval logic Breaking changes: - RelayOptions.subscriptionHandler is now required (not optional) - All relay creations must include a subscriptionHandler 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent 78cf04d commit 1661d5f

6 files changed

Lines changed: 58 additions & 16 deletions

File tree

packages/relay/README.md

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ This package provides ActivityPub relay functionality for the [Fedify]
1313
ecosystem, enabling the creation and management of relay servers that can
1414
forward activities between federated instances.
1515

16+
For comprehensive documentation on building and operating relay servers,
17+
see the [*Relay server* section in the Fedify manual][manual].
18+
1619

1720
What is an ActivityPub relay?
1821
------------------------------
@@ -33,7 +36,7 @@ This package supports two popular relay protocols used in the fediverse:
3336
### Mastodon-style relay
3437

3538
The Mastodon-style relay protocol uses LD signatures for activity
36-
verification and follows the Public collection. This protocol is widely
39+
verification and follows the Public collection. This protocol is widely
3740
supported by Mastodon and many other ActivityPub implementations.
3841

3942
Key features:
@@ -100,10 +103,12 @@ import { MemoryKvStore } from "@fedify/fedify";
100103
const relay = createRelay("mastodon", {
101104
kv: new MemoryKvStore(),
102105
domain: "relay.example.com",
103-
// Optional: Set a custom subscription handler to approve/reject subscriptions
106+
// Required: Set a subscription handler to approve/reject subscriptions
104107
subscriptionHandler: async (ctx, actor) => {
105-
// Implement your approval logic here
106-
// Return true to approve, false to reject
108+
// For an open relay, simply return true
109+
// return true;
110+
111+
// Or implement custom approval logic:
107112
const domain = new URL(actor.id!).hostname;
108113
const blockedDomains = ["spam.example", "blocked.example"];
109114
return !blockedDomains.includes(domain);
@@ -120,13 +125,24 @@ You can also create a LitePub-style relay by changing the type:
120125
const relay = createRelay("litepub", {
121126
kv: new MemoryKvStore(),
122127
domain: "relay.example.com",
128+
subscriptionHandler: async (ctx, actor) => true,
123129
});
124130
~~~~
125131

126132
### Subscription handling
127133

128-
By default, the relay automatically rejects all subscription requests.
129-
You can customize this behavior by providing a subscription handler in the options:
134+
The `subscriptionHandler` is required and determines whether to approve or reject
135+
subscription requests. For an open relay that accepts all subscriptions:
136+
137+
~~~~ typescript
138+
const relay = createRelay("mastodon", {
139+
kv: new MemoryKvStore(),
140+
domain: "relay.example.com",
141+
subscriptionHandler: async (ctx, actor) => true, // Accept all
142+
});
143+
~~~~
144+
145+
You can also implement custom approval logic:
130146

131147
~~~~ typescript
132148
const relay = createRelay("mastodon", {
@@ -156,6 +172,7 @@ const app = new Hono();
156172
const relay = createRelay("mastodon", {
157173
kv: new MemoryKvStore(),
158174
domain: "relay.example.com",
175+
subscriptionHandler: async (ctx, actor) => true,
159176
});
160177

161178
app.use("*", async (c) => {
@@ -258,7 +275,7 @@ Configuration options for the relay:
258275
- `kv: KvStore` (required): Keyvalue store for persisting relay data
259276
- `domain?: string`: Relay's domain name (defaults to `"localhost"`)
260277
- `name?: string`: Relay's display name (defaults to `"ActivityPub Relay"`)
261-
- `subscriptionHandler?: SubscriptionRequestHandler`: Custom handler for
278+
- `subscriptionHandler: SubscriptionRequestHandler` (required): Handler for
262279
subscription approval/rejection
263280
- `documentLoaderFactory?: DocumentLoaderFactory`: Custom document loader
264281
factory
@@ -296,3 +313,4 @@ type SubscriptionRequestHandler = (
296313
[@fedify@hollo.social]: https://hollo.social/@fedify
297314
[Fedify]: https://fedify.dev/
298315
[Fedify documentation on key–value stores]: https://fedify.dev/manual/kv
316+
[manual]: https://fedify.dev/manual/relay

packages/relay/src/litepub.test.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ describe("LitePubRelay", () => {
101101
kv,
102102
domain: "relay.example.com",
103103
documentLoaderFactory: () => mockDocumentLoader,
104+
subscriptionHandler: () => Promise.resolve(true),
104105
});
105106

106107
const request = new Request("https://relay.example.com/users/relay", {
@@ -117,6 +118,7 @@ describe("LitePubRelay", () => {
117118
kv,
118119
domain: "relay.example.com",
119120
documentLoaderFactory: () => mockDocumentLoader,
121+
subscriptionHandler: () => Promise.resolve(true),
120122
});
121123

122124
const request = new Request("https://relay.example.com/users/relay", {
@@ -137,6 +139,7 @@ describe("LitePubRelay", () => {
137139
kv,
138140
domain: "relay.example.com",
139141
documentLoaderFactory: () => mockDocumentLoader,
142+
subscriptionHandler: () => Promise.resolve(true),
140143
});
141144

142145
const request = new Request(
@@ -156,6 +159,7 @@ describe("LitePubRelay", () => {
156159
kv,
157160
domain: "relay.example.com",
158161
documentLoaderFactory: () => mockDocumentLoader,
162+
subscriptionHandler: () => Promise.resolve(true),
159163
});
160164

161165
const request = new Request(
@@ -204,6 +208,7 @@ describe("LitePubRelay", () => {
204208
kv,
205209
domain: "relay.example.com",
206210
documentLoaderFactory: () => mockDocumentLoader,
211+
subscriptionHandler: () => Promise.resolve(true),
207212
});
208213

209214
const request = new Request(
@@ -229,6 +234,7 @@ describe("LitePubRelay", () => {
229234
kv,
230235
domain: "relay.example.com",
231236
documentLoaderFactory: () => mockDocumentLoader,
237+
subscriptionHandler: () => Promise.resolve(true),
232238
});
233239

234240
const request = new Request("https://relay.example.com/users/relay", {
@@ -534,6 +540,7 @@ describe("LitePubRelay", () => {
534540
domain: "relay.example.com",
535541
documentLoaderFactory: () => mockDocumentLoader,
536542
authenticatedDocumentLoaderFactory: () => mockDocumentLoader,
543+
subscriptionHandler: () => Promise.resolve(true),
537544
});
538545

539546
const relayFollow = new Follow({
@@ -596,6 +603,7 @@ describe("LitePubRelay", () => {
596603
domain: "relay.example.com",
597604
documentLoaderFactory: () => mockDocumentLoader,
598605
authenticatedDocumentLoaderFactory: () => mockDocumentLoader,
606+
subscriptionHandler: () => Promise.resolve(true),
599607
});
600608

601609
const originalFollow = new Follow({
@@ -654,6 +662,7 @@ describe("LitePubRelay", () => {
654662
domain: "relay.example.com",
655663
documentLoaderFactory: () => mockDocumentLoader,
656664
authenticatedDocumentLoaderFactory: () => mockDocumentLoader,
665+
subscriptionHandler: () => Promise.resolve(true),
657666
});
658667

659668
const note = new Note({
@@ -697,6 +706,7 @@ describe("LitePubRelay", () => {
697706
domain: "relay.example.com",
698707
documentLoaderFactory: () => mockDocumentLoader,
699708
authenticatedDocumentLoaderFactory: () => mockDocumentLoader,
709+
subscriptionHandler: () => Promise.resolve(true),
700710
});
701711

702712
const note = new Note({
@@ -740,6 +750,7 @@ describe("LitePubRelay", () => {
740750
domain: "relay.example.com",
741751
documentLoaderFactory: () => mockDocumentLoader,
742752
authenticatedDocumentLoaderFactory: () => mockDocumentLoader,
753+
subscriptionHandler: () => Promise.resolve(true),
743754
});
744755

745756
const moveActivity = new Move({
@@ -779,6 +790,7 @@ describe("LitePubRelay", () => {
779790
domain: "relay.example.com",
780791
documentLoaderFactory: () => mockDocumentLoader,
781792
authenticatedDocumentLoaderFactory: () => mockDocumentLoader,
793+
subscriptionHandler: () => Promise.resolve(true),
782794
});
783795

784796
const deleteActivity = new Delete({
@@ -817,6 +829,7 @@ describe("LitePubRelay", () => {
817829
domain: "relay.example.com",
818830
documentLoaderFactory: () => mockDocumentLoader,
819831
authenticatedDocumentLoaderFactory: () => mockDocumentLoader,
832+
subscriptionHandler: () => Promise.resolve(true),
820833
});
821834

822835
const announceActivity = new Announce({

packages/relay/src/litepub.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -74,10 +74,10 @@ export class LitePubRelay extends BaseRelay {
7474
]);
7575
if (existingFollow?.state === "pending") return;
7676

77-
let approved = false;
78-
if (this.options.subscriptionHandler) {
79-
approved = await this.options.subscriptionHandler(ctx, follower);
80-
}
77+
const approved = await this.options.subscriptionHandler(
78+
ctx,
79+
follower,
80+
);
8181

8282
if (approved) {
8383
// Litepub-specific: save with "pending" state

packages/relay/src/mastodon.test.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ describe("MastodonRelay", () => {
9999
kv,
100100
domain: "relay.example.com",
101101
documentLoaderFactory: () => mockDocumentLoader,
102+
subscriptionHandler: () => Promise.resolve(true),
102103
});
103104

104105
const request = new Request("https://relay.example.com/users/relay", {
@@ -115,6 +116,7 @@ describe("MastodonRelay", () => {
115116
kv,
116117
domain: "relay.example.com",
117118
documentLoaderFactory: () => mockDocumentLoader,
119+
subscriptionHandler: () => Promise.resolve(true),
118120
});
119121

120122
const request = new Request("https://relay.example.com/users/relay", {
@@ -135,6 +137,7 @@ describe("MastodonRelay", () => {
135137
kv,
136138
domain: "relay.example.com",
137139
documentLoaderFactory: () => mockDocumentLoader,
140+
subscriptionHandler: () => Promise.resolve(true),
138141
});
139142

140143
const request = new Request(
@@ -154,6 +157,7 @@ describe("MastodonRelay", () => {
154157
kv,
155158
domain: "relay.example.com",
156159
documentLoaderFactory: () => mockDocumentLoader,
160+
subscriptionHandler: () => Promise.resolve(true),
157161
});
158162

159163
const request = new Request(
@@ -203,6 +207,7 @@ describe("MastodonRelay", () => {
203207
kv,
204208
domain: "relay.example.com",
205209
documentLoaderFactory: () => mockDocumentLoader,
210+
subscriptionHandler: () => Promise.resolve(true),
206211
});
207212

208213
const request = new Request(
@@ -275,6 +280,7 @@ describe("MastodonRelay", () => {
275280
kv,
276281
domain: "relay.example.com",
277282
documentLoaderFactory: () => mockDocumentLoader,
283+
subscriptionHandler: () => Promise.resolve(true),
278284
});
279285

280286
const request = new Request("https://relay.example.com/users/relay", {
@@ -463,6 +469,7 @@ describe("MastodonRelay", () => {
463469
domain: "relay.example.com",
464470
documentLoaderFactory: () => mockDocumentLoader,
465471
authenticatedDocumentLoaderFactory: () => mockDocumentLoader,
472+
subscriptionHandler: () => Promise.resolve(true),
466473
});
467474

468475
const originalFollow = new Follow({
@@ -521,6 +528,7 @@ describe("MastodonRelay", () => {
521528
domain: "relay.example.com",
522529
documentLoaderFactory: () => mockDocumentLoader,
523530
authenticatedDocumentLoaderFactory: () => mockDocumentLoader,
531+
subscriptionHandler: () => Promise.resolve(true),
524532
});
525533

526534
const note = new Note({
@@ -564,6 +572,7 @@ describe("MastodonRelay", () => {
564572
domain: "relay.example.com",
565573
documentLoaderFactory: () => mockDocumentLoader,
566574
authenticatedDocumentLoaderFactory: () => mockDocumentLoader,
575+
subscriptionHandler: () => Promise.resolve(true),
567576
});
568577

569578
const deleteActivity = new Delete({
@@ -602,6 +611,7 @@ describe("MastodonRelay", () => {
602611
domain: "relay.example.com",
603612
documentLoaderFactory: () => mockDocumentLoader,
604613
authenticatedDocumentLoaderFactory: () => mockDocumentLoader,
614+
subscriptionHandler: () => Promise.resolve(true),
605615
});
606616

607617
const note = new Note({
@@ -645,6 +655,7 @@ describe("MastodonRelay", () => {
645655
domain: "relay.example.com",
646656
documentLoaderFactory: () => mockDocumentLoader,
647657
authenticatedDocumentLoaderFactory: () => mockDocumentLoader,
658+
subscriptionHandler: () => Promise.resolve(true),
648659
});
649660

650661
const moveActivity = new Move({

packages/relay/src/mastodon.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,10 +52,10 @@ export class MastodonRelay extends BaseRelay {
5252
const follower = await validateFollowActivity(ctx, follow);
5353
if (!follower || !follower.id) return;
5454

55-
let approved = false;
56-
if (this.options.subscriptionHandler) {
57-
approved = await this.options.subscriptionHandler(ctx, follower);
58-
}
55+
const approved = await this.options.subscriptionHandler(
56+
ctx,
57+
follower,
58+
);
5959

6060
if (approved) {
6161
// Mastodon-specific: immediately add to followers list with accepted state

packages/relay/src/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ export interface RelayOptions {
3030
documentLoaderFactory?: DocumentLoaderFactory;
3131
authenticatedDocumentLoaderFactory?: AuthenticatedDocumentLoaderFactory;
3232
queue?: MessageQueue;
33-
subscriptionHandler?: SubscriptionRequestHandler;
33+
subscriptionHandler: SubscriptionRequestHandler;
3434
}
3535

3636
export interface RelayFollower {

0 commit comments

Comments
 (0)