| [162c7a7] | 1 | # Federation
|
|---|
| 2 |
|
|---|
| 3 | Klonkt is a small single-tenant fediverse site engine. Each site is one
|
|---|
| 4 | ActivityPub actor (a `Person`) that federates its posts, replies, boosts,
|
|---|
| 5 | likes, follows and polls, and that native or web apps can drive over
|
|---|
| 6 | ActivityPub Client-to-Server. This document describes what Klonkt speaks on the
|
|---|
| 7 | wire, following [FEP-67ff](https://codeberg.org/fediverse/fep/src/branch/main/fep/67ff/fep-67ff.md).
|
|---|
| 8 |
|
|---|
| 9 | ## Supported federation protocols and standards
|
|---|
| 10 |
|
|---|
| 11 | - [ActivityPub](https://www.w3.org/TR/activitypub/) Server-to-Server (S2S)
|
|---|
| 12 | - [ActivityPub](https://www.w3.org/TR/activitypub/) Client-to-Server (C2S), over OAuth 2.0
|
|---|
| 13 | - [WebFinger](https://webfinger.net/) ([RFC 7033](https://www.rfc-editor.org/rfc/rfc7033))
|
|---|
| 14 | - [HTTP Signatures](https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures) (draft-cavage), `rsa-sha256`
|
|---|
| 15 | - [NodeInfo](https://nodeinfo.diaspora.software/) 2.1
|
|---|
| [8501ee8] | 16 | - [OpenWebAuth](https://codeberg.org/fediverse/fep/src/branch/main/fep/61cf/fep-61cf.md) single sign-on, both roles (see "Authentication")
|
|---|
| [162c7a7] | 17 | - [OAuth 2.0](https://www.rfc-editor.org/rfc/rfc6749) public clients with PKCE ([RFC 7636](https://www.rfc-editor.org/rfc/rfc7636), S256 only)
|
|---|
| 18 | - [OAuth 2.0 Authorization Server Metadata](https://www.rfc-editor.org/rfc/rfc8414) ([RFC 8414](https://www.rfc-editor.org/rfc/rfc8414))
|
|---|
| 19 | - [OAuth 2.0 Dynamic Client Registration](https://www.rfc-editor.org/rfc/rfc7591) ([RFC 7591](https://www.rfc-editor.org/rfc/rfc7591))
|
|---|
| 20 |
|
|---|
| 21 | ## Supported FEPs
|
|---|
| 22 |
|
|---|
| 23 | - [FEP-67ff: FEDERATION.md](https://codeberg.org/fediverse/fep/src/branch/main/fep/67ff/fep-67ff.md) (FINAL): this file.
|
|---|
| 24 | - [FEP-f1d5: NodeInfo in Fediverse Software](https://codeberg.org/fediverse/fep/src/branch/main/fep/f1d5/fep-f1d5.md) (FINAL): `/.well-known/nodeinfo` links to a NodeInfo 2.1 document advertising software, version and the `activitypub` protocol.
|
|---|
| [a5515b4] | 25 | - [FEP-e232: Object Links](https://codeberg.org/fediverse/fep/src/branch/main/fep/e232/fep-e232.md) (FINAL): `Link` tags with `mediaType` `application/ld+json; profile="https://www.w3.org/ns/activitystreams"` for object references inside content.
|
|---|
| 26 | - [FEP-044f: Consent-respecting quote posts](https://codeberg.org/fediverse/fep/src/branch/main/fep/044f/fep-044f.md) (DRAFT): a quoted fediverse object carries real quote semantics — the `quote` property plus an FEP-e232 `Link` tag — and renders as a quote card rather than a bare link.
|
|---|
| 27 | - [FEP-9098: Custom emojis](https://codeberg.org/fediverse/fep/src/branch/main/fep/9098/fep-9098.md) (DRAFT): `:shortcode:` emoji tags on content and display names, preserved on the wire so connected apps can render them.
|
|---|
| 28 | - [FEP-c648: Blocked Collection](https://codeberg.org/fediverse/fep/src/branch/main/fep/c648/fep-c648.md) (DRAFT): inbound `Block` and `Undo(Block)` are honoured.
|
|---|
| 29 | - [FEP-7628: Move actor](https://codeberg.org/fediverse/fep/src/branch/main/fep/7628/fep-7628.md) (DRAFT): account migration — `alsoKnownAs`, `movedTo` and the `Move` activity, sent and received. See "Account migration".
|
|---|
| 30 | - [FEP-1580: Move Actor Objects with a `migration` Collection](https://codeberg.org/fediverse/fep/src/branch/main/fep/1580/fep-1580.md) (DRAFT): the objects a `Move` leaves behind, exposed as a `migration` collection alongside `moves`.
|
|---|
| [8501ee8] | 31 | - [FEP-61cf: The OpenWebAuth Protocol](https://codeberg.org/fediverse/fep/src/branch/main/fep/61cf/fep-61cf.md) (DRAFT): federated single sign-on, implemented in **both** roles. As a *target*, Klonkt lets a visitor from another server prove who they are and read follower-only posts without an account here. As a *home instance*, a site owner can sign in to another OpenWebAuth site using their own actor. See "Authentication".
|
|---|
| [a5515b4] | 32 | - [FEP-888d: Using `https://w3id.org/fep` as a base for FEP-specific namespaces](https://codeberg.org/fediverse/fep/src/branch/main/fep/888d/fep-888d.md) (DRAFT): the FEP-1580 terms are declared under `https://w3id.org/fep/1580/`, as that FEP registers them.
|
|---|
| 33 |
|
|---|
| 34 | Two further proposals are our own and are not (yet) part of the FEP index:
|
|---|
| 35 |
|
|---|
| 36 | - **FEP-633c: Guardians** — guardian-gated actors: wards, gated follows and
|
|---|
| 37 | replies, the call-in flow, and a multi-party handshake. Implemented here and
|
|---|
| 38 | submitted upstream as pull request
|
|---|
| 39 | [#889](https://codeberg.org/fediverse/fep/pulls/889); the vocabulary is served
|
|---|
| 40 | at [`https://ns.klonkt.com/shaer`](https://ns.klonkt.com/shaer).
|
|---|
| 41 | - **FEP-9876: enriched actor references** — collection members are bare URIs by
|
|---|
| 42 | default and embedded objects on request, opt-in through
|
|---|
| 43 | `Prefer: return=representation` ([RFC 7240](https://www.rfc-editor.org/rfc/rfc7240)).
|
|---|
| 44 | Implemented; not yet submitted.
|
|---|
| [162c7a7] | 45 |
|
|---|
| 46 | Beyond these, Klonkt aims for de-facto Mastodon compatibility (the
|
|---|
| 47 | `http://joinmastodon.org/ns#` extension terms below). See "Under consideration"
|
|---|
| 48 | for FEPs we track but do not yet implement.
|
|---|
| 49 |
|
|---|
| 50 | ## ActivityPub
|
|---|
| 51 |
|
|---|
| 52 | ### Actor
|
|---|
| 53 |
|
|---|
| 54 | Each site exposes a `Person` actor at `/ap/users/:slug` with `inbox`, `outbox`,
|
|---|
| 55 | `followers`, `following` and `featured` collections, and an RSA-2048 public key
|
|---|
| 56 | under the legacy `publicKey` / `publicKeyPem` field
|
|---|
| 57 | ([w3id security/v1](https://w3id.org/security/v1)). The actor advertises
|
|---|
| [8501ee8] | 58 | `discoverable`, `manuallyApprovesFollowers` (true for a ward, and for any site
|
|---|
| 59 | with owner approval switched on — see "Follow approval"),
|
|---|
| [162c7a7] | 60 | `featured`, and profile metadata as `schema:PropertyValue` links (including
|
|---|
| 61 | `rel="me"` verification links). Actor and object requests are
|
|---|
| 62 | content-negotiated: `application/activity+json` returns the AP document, other
|
|---|
| 63 | `Accept` values redirect to the human profile page.
|
|---|
| 64 |
|
|---|
| 65 | ### Activities sent
|
|---|
| 66 |
|
|---|
| 67 | `Create`, `Update`, `Delete` (as `Tombstone`), `Follow`, `Accept`, `Like`,
|
|---|
| 68 | `Announce`, `Undo` (of `Follow` / `Like` / `Announce`), `Add` / `Remove`
|
|---|
| [a5515b4] | 69 | (featured-pin sync), `Flag` (moderation reports), `Move` (account migration),
|
|---|
| 70 | and `Offer` (the FEP-633c guardianship handshake). Posts, replies, boosts,
|
|---|
| [162c7a7] | 71 | likes and follows are delivered to remote inboxes with a signed HTTP request and
|
|---|
| 72 | a retrying delivery queue.
|
|---|
| 73 |
|
|---|
| 74 | ### Activities received
|
|---|
| 75 |
|
|---|
| 76 | `Create`, `Update`, `Delete`, `Follow`, `Accept`, `Reject`, `Like`, `Announce`,
|
|---|
| [a5515b4] | 77 | `Undo`, `Flag`, `Block`, `Move` and `Offer`. Inbound follows are answered with
|
|---|
| 78 | `Accept` and backfilled with recent posts. Every inbound activity must carry a
|
|---|
| 79 | valid HTTP signature; unsigned or unverifiable requests are rejected.
|
|---|
| [162c7a7] | 80 |
|
|---|
| 81 | ### Object types
|
|---|
| 82 |
|
|---|
| 83 | - `Note`: posts and replies, including rich replies with formatting and media.
|
|---|
| 84 | - `Question`: polls, single (`oneOf`) or multiple (`anyOf`) choice, with
|
|---|
| 85 | `endTime`, `closed`, and Mastodon's `toot:votersCount`. Votes are received as a
|
|---|
| 86 | `Note` with a `name` matching an option and `inReplyTo` the question. Tallies
|
|---|
| 87 | are pushed to followers as `Update(Question)`.
|
|---|
| 88 | - `Image`, `Audio`, `Video`: media attachments with `mediaType`, `url` and `name`
|
|---|
| 89 | (alt text). Audio attachments may carry cover art in `icon`.
|
|---|
| 90 | - `Tombstone`: in `Delete` activities.
|
|---|
| 91 |
|
|---|
| 92 | ### Extensions and compatibility terms
|
|---|
| 93 |
|
|---|
| 94 | Every emitted object carries the full `@context`. Beyond AS2 core and
|
|---|
| 95 | security/v1, Klonkt declares and uses:
|
|---|
| 96 |
|
|---|
| 97 | - `as:sensitive` and `summary` for content warnings (blurred media, hidden text).
|
|---|
| 98 | - `as:Hashtag` tag objects (`#Tag`, linked to `/tag/:slug`).
|
|---|
| 99 | - `Mention` tag objects (`@user@host`, linked to the actor), resolved outbound
|
|---|
| 100 | via WebFinger.
|
|---|
| 101 | - `contentMap`: BCP-47 language maps on posts and replies (Mastodon language
|
|---|
| 102 | filter and translate).
|
|---|
| 103 | - `toot:discoverable`, `toot:featured`, `toot:votersCount`.
|
|---|
| 104 | - `schema:PropertyValue` / `schema:value` for profile metadata, and
|
|---|
| 105 | `schema:embedUrl` for player-card embeds.
|
|---|
| 106 |
|
|---|
| 107 | ### Collections
|
|---|
| 108 |
|
|---|
| 109 | - `followers` and `following` are count-only for the public. An authenticated
|
|---|
| 110 | request from the site owner (a C2S bearer token scoped to that site) returns the
|
|---|
| 111 | full list of actor URIs, so a connected app can build a contacts list.
|
|---|
| 112 | - `featured` lists pinned posts. Pin and unpin federate immediately as
|
|---|
| 113 | `Add` / `Remove`, serialized per site to keep Mastodon's pin order.
|
|---|
| 114 | - A note's `replies` collection is served, and inbound threads are crawled one
|
|---|
| 115 | level at a time (stale-while-revalidate, SSRF-guarded, budget-limited).
|
|---|
| 116 |
|
|---|
| [a5515b4] | 117 | ## Account migration
|
|---|
| 118 |
|
|---|
| 119 | Klonkt implements account moves in both directions, following
|
|---|
| 120 | [FEP-7628](https://codeberg.org/fediverse/fep/src/branch/main/fep/7628/fep-7628.md)
|
|---|
| 121 | for the actor and
|
|---|
| 122 | [FEP-1580](https://codeberg.org/fediverse/fep/src/branch/main/fep/1580/fep-1580.md)
|
|---|
| 123 | for the objects the actor leaves behind.
|
|---|
| 124 |
|
|---|
| 125 | - **Actor terms.** `alsoKnownAs` and `movedTo` are declared with the same term
|
|---|
| 126 | definitions Mastodon ships, so an existing implementation reads them without
|
|---|
| 127 | special-casing. `alsoKnownAs` is reserved for former identities of the same
|
|---|
| 128 | actor; a reference to an external register (a MusicBrainz artist, say) uses
|
|---|
| 129 | `schema:sameAs` instead, precisely so that a move cannot be confused by it.
|
|---|
| 130 | - **The `Move` activity** is both sent and received. FEP-7628 moves *followers*
|
|---|
| 131 | and says so explicitly; the objects are a separate problem, which is what
|
|---|
| 132 | FEP-1580 addresses.
|
|---|
| 133 | - **Object migration.** A migrated site exposes a `migration` collection and a
|
|---|
| 134 | `moves` collection, plus `migrationComplete`, `migratedFrom` and `migratedAt`.
|
|---|
| 135 | The terms live under `https://w3id.org/fep/1580/`, the namespace that FEP
|
|---|
| 136 | registers by way of FEP-888d. The FEP's own CURIE for the collection is
|
|---|
| 137 | `migration:migration`; Klonkt emits the JSON key `migration`, because that is
|
|---|
| 138 | what a consumer reads on.
|
|---|
| 139 | - **Importing.** An import from an export is not treated as a separate case: the
|
|---|
| 140 | same path ingests from a source actor.
|
|---|
| 141 |
|
|---|
| 142 | One gap is worth stating plainly: the `moves` collection carries **no integrity
|
|---|
| 143 | proof**. Without [FEP-8b32](https://codeberg.org/fediverse/fep/src/branch/main/fep/8b32/fep-8b32.md)
|
|---|
| 144 | there is no signature under it, so a consumer has to trust the serving host
|
|---|
| 145 | rather than the claim itself. This is tracked and not yet resolved.
|
|---|
| 146 |
|
|---|
| [162c7a7] | 147 | ## Client-to-Server (C2S)
|
|---|
| 148 |
|
|---|
| 149 | Native and web apps drive an account over ActivityPub C2S. This is not the
|
|---|
| 150 | Mastodon client API: Mastodon apps (Ivory and the like) are not supported here.
|
|---|
| 151 |
|
|---|
| 152 | - Discovery: the actor advertises its OAuth endpoints, and
|
|---|
| 153 | `/.well-known/oauth-authorization-server` (RFC 8414) returns the authorization,
|
|---|
| 154 | token and registration endpoints, `response_types=["code"]`,
|
|---|
| 155 | `grant_types=["authorization_code"]`, `code_challenge_methods=["S256"]`,
|
|---|
| 156 | `token_endpoint_auth_methods=["none"]` and `scopes_supported=["c2s"]`.
|
|---|
| 157 | - Registration: `POST /oauth/register` (RFC 7591), public clients only.
|
|---|
| 158 | - Authorization: PKCE authorization-code flow with a consent screen where the
|
|---|
| 159 | user picks which site the app may act for. Tokens are hashed at rest and bound
|
|---|
| 160 | to a single user and site.
|
|---|
| 161 | - Outbox: `POST /ap/users/:slug/outbox` with a bearer token accepts `Create`
|
|---|
| 162 | (a bare `Note` is wrapped in a `Create` per spec), `Like`, `Announce`,
|
|---|
| 163 | `Follow` and their `Undo`. Content is sanitized; a token cannot post for a
|
|---|
| 164 | different site. `Delete` and `Update` over C2S are not yet implemented.
|
|---|
| 165 |
|
|---|
| 166 | ## Authentication
|
|---|
| 167 |
|
|---|
| 168 | - S2S: HTTP Signatures (draft-cavage), `rsa-sha256`, over
|
|---|
| 169 | `(request-target) host date digest`, with a configurable clock-skew tolerance
|
|---|
| 170 | and reverse-proxy-aware host matching. Object Integrity Proofs
|
|---|
| 171 | (FEP-8b32) are not used.
|
|---|
| 172 | - C2S: OAuth 2.0 bearer tokens, public clients with PKCE (S256), scope `c2s`.
|
|---|
| [8501ee8] | 173 | - Visitors: OpenWebAuth (FEP-61cf), described below.
|
|---|
| 174 |
|
|---|
| 175 | ### OpenWebAuth (FEP-61cf)
|
|---|
| 176 |
|
|---|
| 177 | Klonkt implements both roles.
|
|---|
| 178 |
|
|---|
| 179 | **As a target instance.** A visitor enters their fediverse address; we WebFinger
|
|---|
| 180 | it for a `http://purl.org/openwebauth/v1#redirect` link (falling back to `/magic`
|
|---|
| 181 | on the same host, as Hubzilla and (streams) do) and send them there. Their server
|
|---|
| 182 | then makes a signed request to our token endpoint at `/owa/token`, which accepts
|
|---|
| 183 | both `GET` and `POST`. We verify the HTTP Signature with the same code path that
|
|---|
| 184 | verifies inbox deliveries — the key is pinned to the origin the actor document
|
|---|
| 185 | was fetched from, the signed `Date` must be recent, and a body requires a signed
|
|---|
| 186 | digest — then return a single-use token encrypted to the actor's public key
|
|---|
| 187 | (PKCS #1 v1.5, URL-safe Base64, unpadded). The visitor returns with `?owt=`, we
|
|---|
| 188 | redeem the token once and know who they are.
|
|---|
| 189 |
|
|---|
| 190 | Discovery for the other side: a WebFinger query for this server's root URL
|
|---|
| 191 | returns a `http://purl.org/openwebauth/v1` link pointing at `/owa/token`. An
|
|---|
| 192 | actor's WebFinger response carries the `#redirect` link pointing at `/magic`.
|
|---|
| 193 |
|
|---|
| 194 | **As a home instance.** `/magic` takes an OpenWebAuth request for a logged-in
|
|---|
| 195 | owner, discovers the target's token endpoint, requests a token over a signed
|
|---|
| 196 | request, decrypts it with the site's private key and returns the visitor with
|
|---|
| 197 | `?owt=`. On Klonkt the fediverse identity is the *site* actor, so an owner with
|
|---|
| 198 | several sites picks which one to present. There is a consent screen: FEP-61cf
|
|---|
| 199 | warns under "Information leakage" that OpenWebAuth hands a strong identity claim
|
|---|
| 200 | to any site that asks, so the detour through the home instance is where the user
|
|---|
| 201 | can decline.
|
|---|
| 202 |
|
|---|
| 203 | Notes for implementers:
|
|---|
| 204 |
|
|---|
| 205 | - The signature travels in `Authorization: Signature …`, as FEP-61cf requires,
|
|---|
| 206 | not in the `Signature` header the rest of the fediverse uses. Our token
|
|---|
| 207 | endpoint accepts either.
|
|---|
| 208 | - Signed requests we send also carry a signed `X-Open-Web-Auth` header with
|
|---|
| 209 | random content, per the FEP.
|
|---|
| 210 | - `?zid=` may start the flow but never establishes identity; only a redeemed
|
|---|
| 211 | `?owt=` does. `?owt=` is stripped from the URL after redemption.
|
|---|
| 212 | - Tokens are single-use and expire after three minutes; expired ones are swept
|
|---|
| 213 | on every issue and redemption.
|
|---|
| 214 | - A discovered endpoint must share the origin it was discovered for. If
|
|---|
| 215 | discovery fails, `/magic` returns an error rather than redirecting, so it
|
|---|
| 216 | cannot be used as an open redirector.
|
|---|
| 217 |
|
|---|
| 218 | What this unlocks: `fan_only` posts. That gate used to ask for a local account,
|
|---|
| 219 | which is the wrong question — it excluded exactly the followers it was meant to
|
|---|
| 220 | admit. It now asks whether the proven actor follows this site.
|
|---|
| 221 |
|
|---|
| 222 | ### Follow approval
|
|---|
| 223 |
|
|---|
| 224 | A site can require the owner to approve followers. With it on, the actor
|
|---|
| 225 | advertises `manuallyApprovesFollowers: true`, an inbound `Follow` is held
|
|---|
| 226 | pending instead of auto-accepted, and the owner accepts (sending `Accept` plus a
|
|---|
| 227 | backfill) or rejects (sending `Reject`). Ward actors always gate this way
|
|---|
| 228 | through their guardians, which takes precedence. A pending request is not a
|
|---|
| 229 | follower, and so does not open follower-only posts.
|
|---|
| [162c7a7] | 230 |
|
|---|
| 231 | ## Moderation and safety
|
|---|
| 232 |
|
|---|
| 233 | - Inbound `Flag` reports are stored for the site owner; the owner can send an
|
|---|
| 234 | outbound `Flag` to a remote actor's server.
|
|---|
| 235 | - The owner can remove an inbound reply from a thread; a tombstone prevents the
|
|---|
| 236 | thread crawler from re-fetching it.
|
|---|
| 237 | - Actor and domain blocks silently drop matching activities (no error
|
|---|
| 238 | disclosure) and purge existing content.
|
|---|
| 239 | - All outbound fetches are SSRF-guarded (private-range IP blocking on every
|
|---|
| 240 | redirect hop, per-request timeout).
|
|---|
| 241 |
|
|---|
| 242 | ## Under consideration (not yet implemented)
|
|---|
| 243 |
|
|---|
| 244 | Klonkt tracks the following proposals but does not implement them yet. Draft
|
|---|
| 245 | specs are marked; per project policy, drafts are only adopted deliberately and
|
|---|
| 246 | with a note in the changelog.
|
|---|
| 247 |
|
|---|
| 248 | - Reply control. FEP-5624 (per-object reply control) is WITHDRAWN as of
|
|---|
| 249 | 2025-06-24; its Mastodon terms `canReply` / `ApproveReply` / `RejectReply`
|
|---|
| 250 | remain in production use. The live successor discussion is in drafts FEP-171b
|
|---|
| 251 | (conversation containers), FEP-7458 (replies collection) and FEP-11dd (context
|
|---|
| 252 | ownership). Klonkt currently accepts all replies to its posts.
|
|---|
| 253 | - Search-indexing consent: FEP-5feb (DRAFT). No `indexable` flag is emitted yet.
|
|---|
| 254 | - Actor public keys as Multikey: FEP-521a (FINAL). Klonkt still uses the legacy
|
|---|
| 255 | `publicKey` representation.
|
|---|
| [a5515b4] | 256 | - Object Integrity Proofs: FEP-8b32 (DRAFT). Not used. This is also what leaves
|
|---|
| 257 | the `moves` collection unsigned; see "Account migration".
|
|---|
| [162c7a7] | 258 | - Followers collection synchronization: FEP-8fcf (FINAL). Not implemented.
|
|---|
| 259 |
|
|---|
| 260 | ## Additional documentation
|
|---|
| 261 |
|
|---|
| [5143ccf] | 262 | - Client-to-Server API for apps (the Shaer contract): `docs/shaer-c2s-api.md`.
|
|---|
| [162c7a7] | 263 | - Source code: `src/services/ActivityPubService.js` (core AP logic),
|
|---|
| 264 | `src/routes/activitypub.js` (S2S, WebFinger, NodeInfo),
|
|---|
| [8501ee8] | 265 | `src/routes/oauth.js` (C2S / OAuth),
|
|---|
| 266 | `src/services/OpenWebAuthService.js` and `src/routes/openwebauth.js`
|
|---|
| 267 | (OpenWebAuth, both roles).
|
|---|
| [162c7a7] | 268 | - Changelog: `CHANGELOG.md` (and `CHANGELOG.nl.md`, `CHANGELOG.de.md`).
|
|---|