Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
a8a6666
refactor(relay): add relay factory pattern with federation builder
sij411 Nov 22, 2025
a5660ac
refactor(relay): add create factory function for mastodon and litepub
sij411 Nov 22, 2025
d1f1652
refactor(relay): add mastodon rela y and its test
sij411 Nov 22, 2025
e9a9009
refactor(relay): add litepub relay and its test
sij411 Nov 22, 2025
b292911
refactor(relay): delete old relay test
sij411 Nov 22, 2025
8058130
fix(relay): add temporal polyfill
sij411 Nov 22, 2025
c25ebf5
refactor(relay): fix import
sij411 Nov 26, 2025
bccda7c
fix(relay): add changed data model when mastodon save follower to kv
sij411 Nov 26, 2025
9112d4d
refactor(relay): change type name from LitePubFollower to RelayFollower
sij411 Nov 26, 2025
955508a
refactor(relay): address PR #484 review comments
sij411 Dec 12, 2025
b2e020e
Add changes in the document
sij411 Dec 12, 2025
f14078c
Add missing published field in Activity types
sij411 Dec 12, 2025
518628e
refactor(relay): add relay factory pattern with federation builder
sij411 Nov 22, 2025
10155f9
refactor(relay): add create factory function for mastodon and litepub
sij411 Nov 22, 2025
900ea8d
refactor(relay): add mastodon rela y and its test
sij411 Nov 22, 2025
9301739
refactor(relay): add litepub relay and its test
sij411 Nov 22, 2025
079cb1a
refactor(relay): delete old relay test
sij411 Nov 22, 2025
6a3f9dc
fix(relay): add temporal polyfill
sij411 Nov 22, 2025
efa92db
refactor(relay): fix import
sij411 Nov 26, 2025
8283800
fix(relay): add changed data model when mastodon save follower to kv
sij411 Nov 26, 2025
5cd2a6d
refactor(relay): change type name from LitePubFollower to RelayFollower
sij411 Nov 26, 2025
5fc79c4
refactor(relay): sync with upstream
sij411 Dec 19, 2025
6349c99
Add changes in the document
sij411 Dec 12, 2025
69e0525
Add missing published field in Activity types
sij411 Dec 12, 2025
fb6fefa
Merge branch 'feat/relay' of https://github.com/sij411/fedify into fe…
sij411 Dec 19, 2025
99a396d
refactor(relay): clarify variable names in Accept handler
sij411 Dec 19, 2025
e3437bc
chores(relay): sync with upstream branch
sij411 Dec 23, 2025
80858b6
Address PR #490 review feedback - simple fixes
sij411 Dec 23, 2025
14807f8
Convert duplicated logic in both LitePub and Mastodon into helper fun…
sij411 Dec 23, 2025
01894b1
Add forward activity helper function
sij411 Dec 23, 2025
f63fd71
Add Announce inbox listener and async on methods
sij411 Dec 23, 2025
564f5b4
Add helper functions of announce activities
sij411 Dec 23, 2025
c8a9c76
Implement abstract BaseRelay Class
sij411 Dec 23, 2025
3abbf2d
Extract dispatchRelayActors helper function
sij411 Dec 23, 2025
de8a11a
Remove Relay interface
sij411 Dec 23, 2025
f748672
Delete unnecessary comments
sij411 Dec 23, 2025
264f3fa
Extract relay types and constants to types.ts
sij411 Dec 24, 2025
3ea4cf7
Extract shared FederationBuilder and dispatchers
sij411 Dec 24, 2025
b5e1cad
Extract follow activity handling helpers
sij411 Dec 24, 2025
0f98be5
Add BaseRelay abstract class
sij411 Dec 24, 2025
71e50f6
Refactor relay implementations to extend BaseRelay
sij411 Dec 24, 2025
83c066e
Add factory function and update module exports
sij411 Dec 24, 2025
35656a5
Update README.md and comments based on structure changes
sij411 Dec 24, 2025
ac2ada3
Update email in pacakge.json
sij411 Dec 24, 2025
c611431
Merge branch 'next' into feat/relay
sij411 Dec 24, 2025
2c53cbe
Fix deno.lock
sij411 Dec 24, 2025
d6b127f
Fix deno.lock
sij411 Dec 24, 2025
46b317c
Keep BaseRelay only for internal
sij411 Dec 25, 2025
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
5 changes: 4 additions & 1 deletion CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ To be released.
### @fedify/relay

- Created ActivityPub relay integration as the *@fedify/relay* package.
[[#359], [#459], [#471] by Jiwon Kwon]
[[#359], [#459], [#471], [#490] by Jiwon Kwon]

- Added `Relay` interface defining the common contract for relay
implementations.
Expand All @@ -149,10 +149,13 @@ To be released.
- Added `SubscriptionRequestHandler` type for custom subscription approval
logic.
- Added `RelayOptions` interface for relay configuration.
- Added `RelayType` type alias to document the type-safe parameter
- Added `createRelay()` factory function as a key public API

[#359]: https://github.com/fedify-dev/fedify/issues/359
[#459]: https://github.com/fedify-dev/fedify/pull/459
[#471]: https://github.com/fedify-dev/fedify/pull/471
[#490]: https://github.com/fedify-dev/fedify/pull/490

### @fedify/vocab-tools

Expand Down
332 changes: 199 additions & 133 deletions deno.lock

Large diffs are not rendered by default.

117 changes: 79 additions & 38 deletions packages/relay/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,16 @@ Key features:

### LitePub-style relay

*LitePub relay support is planned for a future release.*

The LitePub-style relay protocol uses bidirectional following relationships
and wraps activities in `Announce` activities for distribution.

Key features:

- Reciprocal following between relay and subscribers
- Activities wrapped in `Announce` for distribution
- Two-phase subscription (pending → accepted)
- Enhanced federation capabilities


Installation
------------
Expand Down Expand Up @@ -83,43 +88,56 @@ bun add @fedify/relay
Usage
-----

### Creating a Mastodon-style relay
### Creating a relay

Here's a simple example of creating a Mastodon-compatible relay server:
Here's a simple example of creating a relay server using the factory function:

~~~~ typescript
import { MastodonRelay } from "@fedify/relay";
import { createRelay } from "@fedify/relay";
import { MemoryKvStore } from "@fedify/fedify";

const relay = new MastodonRelay({
// Create a Mastodon-style relay
const relay = createRelay("mastodon", {
kv: new MemoryKvStore(),
domain: "relay.example.com",
});

// Optional: Set a custom subscription handler to approve/reject subscriptions
relay.setSubscriptionHandler(async (ctx, actor) => {
// Implement your approval logic here
// Return true to approve, false to reject
const domain = new URL(actor.id!).hostname;
const blockedDomains = ["spam.example", "blocked.example"];
return !blockedDomains.includes(domain);
// Optional: Set a custom subscription handler to approve/reject subscriptions
subscriptionHandler: async (ctx, actor) => {
// Implement your approval logic here
// Return true to approve, false to reject
const domain = new URL(actor.id!).hostname;
const blockedDomains = ["spam.example", "blocked.example"];
return !blockedDomains.includes(domain);
},
});

// Serve the relay
Deno.serve((request) => relay.fetch(request));
~~~~

You can also create a LitePub-style relay by changing the type:

~~~~ typescript
const relay = createRelay("litepub", {
kv: new MemoryKvStore(),
domain: "relay.example.com",
});
~~~~

### Subscription handling

By default, the relay automatically rejects all subscription requests.
You can customize this behavior by setting a subscription handler:
You can customize this behavior by providing a subscription handler in the options:

~~~~ typescript
relay.setSubscriptionHandler(async (ctx, actor) => {
// Example: Only allow subscriptions from specific domains
const domain = new URL(actor.id!).hostname;
const allowedDomains = ["mastodon.social", "fosstodon.org"];
return allowedDomains.includes(domain);
const relay = createRelay("mastodon", {
kv: new MemoryKvStore(),
domain: "relay.example.com",
subscriptionHandler: async (ctx, actor) => {
// Example: Only allow subscriptions from specific domains
const domain = new URL(actor.id!).hostname;
const allowedDomains = ["mastodon.social", "fosstodon.org"];
return allowedDomains.includes(domain);
},
});
~~~~

Expand All @@ -131,11 +149,11 @@ example with Hono:

~~~~ typescript
import { Hono } from "hono";
import { MastodonRelay } from "@fedify/relay";
import { createRelay } from "@fedify/relay";
import { MemoryKvStore } from "@fedify/fedify";

const app = new Hono();
const relay = new MastodonRelay({
const relay = createRelay("mastodon", {
kv: new MemoryKvStore(),
domain: "relay.example.com",
});
Expand Down Expand Up @@ -191,38 +209,61 @@ details.
API reference
-------------

### `MastodonRelay`

A Mastodon-compatible ActivityPub relay implementation.
### `createRelay()`

#### Constructor
Factory function to create a relay instance.

~~~~ typescript
new MastodonRelay(options: RelayOptions)
function createRelay(
type: "mastodon" | "litepub",
options: RelayOptions
): BaseRelay
~~~~

#### Properties
**Parameters:**

- `type`: The type of relay to create (`"mastodon"` or `"litepub"`)
- `options`: Configuration options for the relay

- `domain`: The relay's domain name (read-only)
**Returns:** A relay instance (`MastodonRelay` or `LitePubRelay`)

### `BaseRelay`

Abstract base class for relay implementations.

#### Methods

- `fetch(request: Request): Promise<Response>`: Handle incoming HTTP requests
- `setSubscriptionHandler(handler: SubscriptionRequestHandler): this`:
Set a custom handler for subscription approval/rejection

### `MastodonRelay`

A Mastodon-compatible ActivityPub relay implementation that extends `BaseRelay`.

- Uses direct activity forwarding
- Immediate subscription approval
- Compatible with standard ActivityPub implementations

### `LitePubRelay`

A LitePub-compatible ActivityPub relay implementation that extends `BaseRelay`.

- Uses bidirectional following
- Activities wrapped in `Announce`
- Two-phase subscription (pending → accepted)

### `RelayOptions`

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
subscription approval/rejection
- `documentLoaderFactory?: DocumentLoaderFactory`: Custom document loader
factory
- `authenticatedDocumentLoaderFactory?: AuthenticatedDocumentLoaderFactory`:
Custom authenticated document loader factory
- `federation?: Federation<void>`: Custom Federation instance (for advanced
use cases)
- `queue?: MessageQueue`: Message queue for background activity processing

### `SubscriptionRequestHandler`
Expand All @@ -231,17 +272,17 @@ A function that determines whether to approve a subscription request:

~~~~ typescript
type SubscriptionRequestHandler = (
ctx: Context<void>,
ctx: Context<RelayOptions>,
clientActor: Actor,
) => Promise<boolean>
~~~~

Parameters:
**Parameters:**

- `ctx`: The Fedify context object
- `ctx`: The Fedify context object with relay options
- `clientActor`: The actor requesting to subscribe

Returns:
**Returns:**

- `true` to approve the subscription
- `false` to reject the subscription
Expand Down
9 changes: 6 additions & 3 deletions packages/relay/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
],
"author": {
"name": "Jiwon Kwon",
"email": "jiwonkwon@duck.com"
"email": "work@kwonjiwon.org"
},
"homepage": "https://fedify.dev/",
"repository": {
Expand Down Expand Up @@ -47,6 +47,10 @@
"dist/",
"package.json"
],
"dependencies": {
"@js-temporal/polyfill": "catalog:",
"@logtape/logtape": "catalog:"
},
"peerDependencies": {
"@fedify/fedify": "workspace:^"
},
Expand All @@ -61,7 +65,6 @@
"prepack": "deno task codegen && tsdown",
"prepublish": "deno task codegen && tsdown",
"test": "deno task codegen && tsdown && node --test",
"test:bun": "deno task codegen && tsdown && bun test --timeout 60000",
"test:cfworkers": "deno task codegen && wrangler deploy --dry-run --outdir src/cfworkers && node --import=tsx src/cfworkers/client.ts"
"test:bun": "deno task codegen && tsdown && bun test --timeout 60000"
}
}
39 changes: 39 additions & 0 deletions packages/relay/src/base.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import type { Federation, FederationBuilder } from "@fedify/fedify";
import type { RelayOptions } from "./types.ts";

/**
* Abstract base class for relay implementations.
* Provides common infrastructure for both Mastodon and LitePub relays.
*
* @since 2.0.0
*/
export abstract class BaseRelay {
protected federationBuilder: FederationBuilder<RelayOptions>;
protected options: RelayOptions;
protected federation?: Federation<RelayOptions>;

constructor(
options: RelayOptions,
relayBuilder: FederationBuilder<RelayOptions>,
) {
this.options = options;
this.federationBuilder = relayBuilder;
}

async fetch(request: Request): Promise<Response> {
if (this.federation == null) {
this.federation = await this.federationBuilder.build(this.options);
this.setupInboxListeners();
}

return await this.federation.fetch(request, {
contextData: this.options,
});
}

/**
* Set up inbox listeners for handling ActivityPub activities.
* Each relay type implements this method with protocol-specific logic.
*/
protected abstract setupInboxListeners(): void;
}
Loading
Loading