Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 25 additions & 7 deletions packages/relay/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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?
------------------------------
Expand All @@ -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:
Expand Down Expand Up @@ -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);
Expand All @@ -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", {
Expand Down Expand Up @@ -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) => {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
1 change: 1 addition & 0 deletions packages/relay/src/factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
* });
* ```
*
Expand Down
13 changes: 13 additions & 0 deletions packages/relay/src/litepub.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", {
Expand All @@ -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", {
Expand All @@ -137,6 +139,7 @@ describe("LitePubRelay", () => {
kv,
domain: "relay.example.com",
documentLoaderFactory: () => mockDocumentLoader,
subscriptionHandler: () => Promise.resolve(true),
});

const request = new Request(
Expand All @@ -156,6 +159,7 @@ describe("LitePubRelay", () => {
kv,
domain: "relay.example.com",
documentLoaderFactory: () => mockDocumentLoader,
subscriptionHandler: () => Promise.resolve(true),
});

const request = new Request(
Expand Down Expand Up @@ -204,6 +208,7 @@ describe("LitePubRelay", () => {
kv,
domain: "relay.example.com",
documentLoaderFactory: () => mockDocumentLoader,
subscriptionHandler: () => Promise.resolve(true),
});

const request = new Request(
Expand All @@ -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", {
Expand Down Expand Up @@ -534,6 +540,7 @@ describe("LitePubRelay", () => {
domain: "relay.example.com",
documentLoaderFactory: () => mockDocumentLoader,
authenticatedDocumentLoaderFactory: () => mockDocumentLoader,
subscriptionHandler: () => Promise.resolve(true),
});

const relayFollow = new Follow({
Expand Down Expand Up @@ -596,6 +603,7 @@ describe("LitePubRelay", () => {
domain: "relay.example.com",
documentLoaderFactory: () => mockDocumentLoader,
authenticatedDocumentLoaderFactory: () => mockDocumentLoader,
subscriptionHandler: () => Promise.resolve(true),
});

const originalFollow = new Follow({
Expand Down Expand Up @@ -654,6 +662,7 @@ describe("LitePubRelay", () => {
domain: "relay.example.com",
documentLoaderFactory: () => mockDocumentLoader,
authenticatedDocumentLoaderFactory: () => mockDocumentLoader,
subscriptionHandler: () => Promise.resolve(true),
});

const note = new Note({
Expand Down Expand Up @@ -697,6 +706,7 @@ describe("LitePubRelay", () => {
domain: "relay.example.com",
documentLoaderFactory: () => mockDocumentLoader,
authenticatedDocumentLoaderFactory: () => mockDocumentLoader,
subscriptionHandler: () => Promise.resolve(true),
});

const note = new Note({
Expand Down Expand Up @@ -740,6 +750,7 @@ describe("LitePubRelay", () => {
domain: "relay.example.com",
documentLoaderFactory: () => mockDocumentLoader,
authenticatedDocumentLoaderFactory: () => mockDocumentLoader,
subscriptionHandler: () => Promise.resolve(true),
});

const moveActivity = new Move({
Expand Down Expand Up @@ -779,6 +790,7 @@ describe("LitePubRelay", () => {
domain: "relay.example.com",
documentLoaderFactory: () => mockDocumentLoader,
authenticatedDocumentLoaderFactory: () => mockDocumentLoader,
subscriptionHandler: () => Promise.resolve(true),
});

const deleteActivity = new Delete({
Expand Down Expand Up @@ -817,6 +829,7 @@ describe("LitePubRelay", () => {
domain: "relay.example.com",
documentLoaderFactory: () => mockDocumentLoader,
authenticatedDocumentLoaderFactory: () => mockDocumentLoader,
subscriptionHandler: () => Promise.resolve(true),
});

const announceActivity = new Announce({
Expand Down
8 changes: 4 additions & 4 deletions packages/relay/src/litepub.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 11 additions & 0 deletions packages/relay/src/mastodon.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", {
Expand All @@ -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", {
Expand All @@ -135,6 +137,7 @@ describe("MastodonRelay", () => {
kv,
domain: "relay.example.com",
documentLoaderFactory: () => mockDocumentLoader,
subscriptionHandler: () => Promise.resolve(true),
});

const request = new Request(
Expand All @@ -154,6 +157,7 @@ describe("MastodonRelay", () => {
kv,
domain: "relay.example.com",
documentLoaderFactory: () => mockDocumentLoader,
subscriptionHandler: () => Promise.resolve(true),
});

const request = new Request(
Expand Down Expand Up @@ -203,6 +207,7 @@ describe("MastodonRelay", () => {
kv,
domain: "relay.example.com",
documentLoaderFactory: () => mockDocumentLoader,
subscriptionHandler: () => Promise.resolve(true),
});

const request = new Request(
Expand Down Expand Up @@ -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", {
Expand Down Expand Up @@ -463,6 +469,7 @@ describe("MastodonRelay", () => {
domain: "relay.example.com",
documentLoaderFactory: () => mockDocumentLoader,
authenticatedDocumentLoaderFactory: () => mockDocumentLoader,
subscriptionHandler: () => Promise.resolve(true),
});

const originalFollow = new Follow({
Expand Down Expand Up @@ -521,6 +528,7 @@ describe("MastodonRelay", () => {
domain: "relay.example.com",
documentLoaderFactory: () => mockDocumentLoader,
authenticatedDocumentLoaderFactory: () => mockDocumentLoader,
subscriptionHandler: () => Promise.resolve(true),
});

const note = new Note({
Expand Down Expand Up @@ -564,6 +572,7 @@ describe("MastodonRelay", () => {
domain: "relay.example.com",
documentLoaderFactory: () => mockDocumentLoader,
authenticatedDocumentLoaderFactory: () => mockDocumentLoader,
subscriptionHandler: () => Promise.resolve(true),
});

const deleteActivity = new Delete({
Expand Down Expand Up @@ -602,6 +611,7 @@ describe("MastodonRelay", () => {
domain: "relay.example.com",
documentLoaderFactory: () => mockDocumentLoader,
authenticatedDocumentLoaderFactory: () => mockDocumentLoader,
subscriptionHandler: () => Promise.resolve(true),
});

const note = new Note({
Expand Down Expand Up @@ -645,6 +655,7 @@ describe("MastodonRelay", () => {
domain: "relay.example.com",
documentLoaderFactory: () => mockDocumentLoader,
authenticatedDocumentLoaderFactory: () => mockDocumentLoader,
subscriptionHandler: () => Promise.resolve(true),
});

const moveActivity = new Move({
Expand Down
8 changes: 4 additions & 4 deletions packages/relay/src/mastodon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion packages/relay/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export interface RelayOptions {
documentLoaderFactory?: DocumentLoaderFactory;
authenticatedDocumentLoaderFactory?: AuthenticatedDocumentLoaderFactory;
queue?: MessageQueue;
subscriptionHandler?: SubscriptionRequestHandler;
subscriptionHandler: SubscriptionRequestHandler;
}

export interface RelayFollower {
Expand Down