Error handling:
thiserror enum (NotFound/BadRequest/Unauthorized/Forbidden/Internal)
eliminates 41 boilerplate .map_err() calls
signature failures return 401, not 500
Named types:
Keypair, LocalObject (with to/cc/bto/bcc addressing), Addressing
Readability:
descriptive names everywhere, small functions, breathing room
noisy comments removed, intent-explicit error handling (no let _ =)
types.rs per module separating data from behavior
File organization:
handlers/ module (actor, featured, followers, inbox, nodeinfo, outbox, webfinger)
actors/ split (mod.rs + person.rs + types.rs)
service/ split (builder, broadcast, collections, delivery, fetch, follow, lookup, types)
tests next to modules
Repository traits:
FollowRepository → 5 sub-traits (FollowerWriter/Reader, FollowingWriter/Reader, FollowMigration)
ActorRepository → 3 sub-traits (KeypairRepository, RemoteActorCache, AnnounceRepository)
BlocklistRepository → 2 sub-traits (DomainBlocklist, ActorBlocklist)
supertraits with blanket impls — existing consumers unchanged
FollowMigration has default no-op
delete dead get_following_outbox_url
Testing:
mock_repo! macro generates mock builders from compact specs
MockFollowRepo, MockActorRepo, MockBlocklistRepo, MockActivityRepo,
MockUserRepo, MockContentReader, MockObjectHandler, MockEventPublisher
all hand-written test stubs replaced
Flexibility:
UrlScheme trait — configurable URL patterns (DefaultUrlScheme = /users/{uuid})
on_unknown_activity hook for custom AP extensions
broadcast_raw_to_followers for arbitrary activity JSON
broadcast_create/broadcast_update (renamed from Note-centric names)
internal modules locked to pub(crate), clean public re-exports
actor_handler, followers_handler, following_handler re-exported for custom routers
Security:
SSRF: block IPv6-mapped private IPv4, TEST-NET, benchmarking, reserved ranges
verify_attributed_to rejects missing/array attributedTo
remove .expect() from outbox handler
Architecture:
handlers/followers.rs delegates to serialize_ordered_collection (no more UrlScheme bypass)
extract dispatch_sends, prepare_addressed_broadcast (eliminate duplication)
DbActor::object_id(), RemoteActor::from/from_ap_person/placeholder
send_activity unifies prepare+dispatch, deterministic_activity_id helper
pass-through wrappers grouped in lookup.rs
15 KiB
Changelog
[0.5.0] — 2026-07-25
Breaking changes
broadcast_create_noterenamed tobroadcast_create,broadcast_update_noterenamed tobroadcast_updateLocalObjectis now a named struct with addressing fields:to,cc,bto,bccApContentReader::get_local_objects_pagereturnsVec<LocalObject>instead ofVec<(Url, Value, DateTime)>Erroris now a thiserror enum withNotFound/BadRequest/Unauthorized/Forbidden/Internalvariants- Repository traits split into sub-traits —
FollowRepositoryis now a supertrait ofFollowerWriter,FollowerReader,FollowingWriter,FollowingReader,FollowMigration;ActorRepositoryis a supertrait ofKeypairRepository,RemoteActorCache,AnnounceRepository. Existing consumers implementing the supertrait continue to work unchanged. ActorRepository::save_local_actor_keypairnow takes aKeypairnamed struct instead of two stringsNoteTypere-export removed- Internal modules (
activities,actors,handlers,data,content,error,user,federation) are nowpub(crate) get_following_outbox_urlremoved (dead code)
New features
UrlSchemetrait +DefaultUrlScheme— configurable URL patterns via.url_scheme(Arc::new(MyScheme))on the builderbroadcast_raw_to_followers— send arbitrary AP activity JSON to all accepted followerson_unknown_activityhook onApObjectHandler— handle custom AP extensions (EmojiReact, Question, Flag, etc.)DbActor::object_id()convenience methodRemoteActor::from_ap_person(),RemoteActor::placeholder()constructors- Mock builder framework:
MockFollowRepoBuilder,MockActorRepoBuilder,MockBlocklistRepoBuilder,MockActivityRepoBuilder,MockUserRepoBuilder,MockContentReaderBuilder,MockObjectHandlerBuilder,MockEventPublisherBuilder Keypairnamed struct (was anonymous tuple)LocalObjectnamed struct with addressing (was anonymous tuple)FollowMigration::migrate_follower_actorhas a default no-op implementationactor_handler,followers_handler,following_handlerre-exported for custom router construction- Constants:
AP_CONTENT_TYPE,AP_CONTEXT,INBOX_BODY_LIMIT
Bug fixes / security
- Fix SSRF bypass via IPv6-mapped IPv4 addresses
- Block additional reserved IP ranges (TEST-NET, benchmarking, reserved, IPv6 documentation)
verify_attributed_tonow rejects missingattributedToand handles array form- Signature failures return 401 instead of 500
- Remove
.expect()panics from outbox handler - Unknown activity types accepted gracefully instead of returning 500
Internal improvements
- thiserror enum for
Errortype (41 boilerplate.map_errcalls eliminated) - File organization:
handlers/,actors/,service/withtypes.rsper module mock_repo!macro replaces 980 lines of hand-written mocks- Small focused functions and descriptive variable names throughout
[0.4.6] — 2026-07-16
New features
ActivityPubServiceBuilder::nodeinfo_services(inbound, outbound)— forward NodeInfo services config through the builderActivityPubServiceBuilder::nodeinfo_metadata(value)— forward arbitrary NodeInfo metadata through the builder
[0.4.5] — 2026-07-16
Bug fixes
- Remove invalid
$schemafield from NodeInfo 2.0 response — the field is not allowed by the NodeInfo 2.0 schema (additionalProperties: false) - Add missing required
servicesandmetadatafields to NodeInfo 2.0 response
New features
FederationData::with_nodeinfo_services(inbound, outbound)— configure NodeInfoservices.inbound/services.outboundFederationData::with_nodeinfo_metadata(value)— set arbitrary NodeInfometadata
[0.4.4] — 2026-07-11
Bug fixes
- Actor cache TTL now treats
fetched_at: Noneas always-stale instead of always-fresh. Consumers who never populatedfetched_atwere silently getting no cache invalidation.
Other improvements
- Structured tracing on key code paths: inbound activity dispatch (activity ID, source domain), outbound delivery (inbox count, retry attempts), actor cache lookups (hit/miss, staleness).
[0.4.3] — 2026-07-11
Bug fixes
- NodeInfo 2.0 response now includes the required
$schemafield (http://nodeinfo.diaspora.software/ns/schema/2.0#). Validators like browser.pub rejected the previous response for missing it.
[0.4.2] — 2026-07-10
New features
FederationEvent::OutboundFollowAccepted { local_user_id, remote_actor_url, outbox_url }— emitted when a remote actor accepts our outbound follow request. Consumers should callimport_remote_outbox(outbox_url, actor_url)to backfill the remote user's content. Without this event, outbound follow acceptance was silent and consumers had no hook to trigger backfill.
[0.4.1] — 2026-06-30
Bug fixes
unblock_actornow sends anUndo(Block)activity to the remote actor's inbox before removing them from the local blocklist, making unblocking AP-compliant.
[0.4.0] — 2026-05-30
Breaking changes
RemoteActor has a new required field fetched_at: Option<DateTime<Utc>> — set to Some(Utc::now()) when fetched from a remote instance, or None for locally-constructed actors. Consumers must add this column to their upsert_remote_actor / get_remote_actor implementations.
ApFederationConfig::new() signature changed — now takes an additional signing_actor: Option<&DbActor> parameter. Internal to consumers using ApFederationConfig directly; builder users are unaffected.
FederationData::new() takes an additional actor_cache_ttl: Duration parameter — only affects consumers constructing FederationData directly (e.g. tests).
New features
Signed fetch for authorized-fetch / Secure Mode — set .signed_fetch_actor_id(uuid) on the builder to sign all outgoing GET requests with that actor's keypair. Call service.signed_fetch(&url) to fetch any remote AP resource with signatures.
Actor cache TTL — fetched_at is now tracked on RemoteActor. Configure staleness via .actor_cache_ttl_secs(secs) (default: 24h). Use get_or_refresh_remote_actor(actor_url) for TTL-aware lookups that re-fetch stale actors from origin.
SSRF protection — all outgoing HTTP requests (federation fetches, WebFinger, backfill) now validate resolved IPs against private/reserved ranges (127/8, 10/8, 172.16/12, 192.168/16, 169.254/16, CGNAT 100.64/10, ::1, fc00::/7, fe80::/10). Debug mode bypasses this check.
Bug fixes
Inbound Block now persists to BlocklistRepository — BlockActivity::receive() calls add_blocked_actor() after removing follower/following relationships. Undo(Block) clears the record via remove_blocked_actor().
[0.3.1] — 2026-05-29
Breaking changes
RemoteActor has five new required fields — struct literals must include them:
| Field | Type | Description |
|---|---|---|
bio |
Option<String> |
Actor biography/summary |
banner_url |
Option<String> |
Banner/header image URL |
followers_url |
Option<String> |
AP followers collection URL |
following_url |
Option<String> |
AP following collection URL |
also_known_as |
Vec<String> |
Account aliases (for Move verification) |
These are populated automatically when k-ap fetches a remote actor (via from_json) and when the local follow() method constructs a RemoteActor from the fetched DbActor. Consuming applications only need to add the new fields to their upsert_remote_actor / get_remote_actor SQL and any custom RemoteActor construction sites.
[0.3.0] — 2026-05-28
Breaking changes
Builder API — the service builder no longer takes positional arguments. All repos are named setters:
// Before (0.2.x)
ActivityPubService::builder(repo, user_repo, handler, "https://example.com")
// After (0.3.0)
ActivityPubService::builder("https://example.com")
.activity_repo(db.clone())
.follow_repo(db.clone())
.actor_repo(db.clone())
.blocklist_repo(db.clone())
.user_repo(db.clone())
.content_reader(db.clone())
.object_handler(db.clone())
.build()
.await?
FederationRepository split into 4 focused traits — implement each independently or all on one struct:
| Old | New |
|---|---|
FederationRepository (34 methods) |
ActivityRepository (2) |
FollowRepository (18) |
|
ActorRepository (6) |
|
BlocklistRepository (8) |
ApObjectHandler split into read/write traits:
| Old | New |
|---|---|
ApObjectHandler::get_local_objects_for_user |
removed (dead code) |
ApObjectHandler::get_local_objects_page |
ApContentReader::get_local_objects_page |
ApObjectHandler::count_local_posts |
ApContentReader::count_local_posts |
ApObjectHandler (callbacks) |
ApObjectHandler (9 callbacks, unchanged) |
ApContentReader also has a new optional method with a default empty impl:
get_featured_objects(user_id) -> Vec<Url>— override to expose pinned posts
broadcast_create_note and broadcast_update_note — new mentioned_inboxes parameter:
// Before
service.broadcast_create_note(user_id, note_json, ApVisibility::Public).await?;
// After — pass inboxes of mentioned non-followers, or vec![] if none
service.broadcast_create_note(user_id, note_json, ApVisibility::Public, mentioned_inboxes).await?;
ApUser::also_known_as changed from Option<String> to Vec<String> — stores all aliases, not just the first.
LookedUpActor::also_known_as changed from Option<String> to Vec<String>.
backfill_outbox renamed to import_remote_outbox — clarifies direction: imports content FROM a remote server into your instance, distinct from run_backfill_for_follower which sends your content TO a new follower.
FollowRepository — two new required methods:
count_accepted_followers(user_id) -> usize— DB-side count, replaces loading all followers into memoryget_accepted_followers_page(user_id, offset, limit) -> Vec<RemoteActor>— DB-side paginated listing
ActorRepository — one new required method:
remove_announce(activity_id, actor_url)— called whenUndo(Announce)is received
New features
ApVisibility — controls to/cc addressing and delivery scope for Create/Update:
Public—to: [AS_PUBLIC], cc: [followers]FollowersOnly—to: [followers], cc: []Private— no delivery at all
Mention delivery — broadcast_create_note and broadcast_update_note accept mentioned_inboxes: Vec<Url>. Delivery goes to followers + mentioned non-followers, deduplicated.
ApUser::discoverable: bool — serialized as discoverable in actor JSON (was hard-coded true).
ApUser::actor_type: ApActorType — serialized as the AP actor type (was hard-coded Person).
ApUser::featured_url: Option<Url> — serialized as featured in actor JSON. The library serves GET /users/{id}/featured automatically.
GET /users/{id}/featured route — serves an OrderedCollection of pinned posts via ApContentReader::get_featured_objects. Default is an empty collection.
router() no longer registers GET /users/{id}, /followers, or /following — these paths need content negotiation (AP JSON vs UI JSON) which k-ap can't do. Your application owns those routes and calls actor_json, followers_collection_json, following_collection_json to produce the AP response. See README for the pattern.
FederationEvent::BackfillRequested — when an EventPublisher is configured, accept_follower publishes this event instead of spawning an in-process task. Process it by calling run_backfill_for_follower.
run_backfill_for_follower(owner_user_id, follower_inbox_url) — public method for workers processing BackfillRequested events.
ApObjectHandler::on_announce_removed — called when Undo(Announce) is received for a locally-authored object. Default is a no-op — override to update boost counts.
ApObjectHandler::on_announce_of_remote — called when a remote actor boosts a non-local object. Default is a no-op.
count_accepted_followers / get_accepted_followers_page — service methods now use DB-side queries via the new FollowRepository methods instead of loading all followers into memory.
Bug fixes
AddActivity used activity ID instead of object ID — on_create was receiving the Add activity's id instead of object["id"]. Now matches CreateActivity behaviour.
Undo(Announce) was silently ignored — now removes the announce record from ActorRepository and calls on_announce_removed. Announce counts no longer drift.
Move re-follows blocked the inbox handler — re-follow HTTP requests are now spawned in the background so the inbox handler returns immediately.
alsoKnownAs truncated to first alias — from_json now stores all aliases from the incoming actor JSON.
Move alsoKnownAs check now verifies all aliases — previously only checked the first one.
Block check before actor HTTP fetch — FollowActivity::receive now checks per-actor blocks before calling dereference(), preventing SSRF from blocked actors.
Other improvements
- 1 MB
DefaultBodyLimiton inbox routes — prevents memory exhaustion from oversized payloads - 4xx error responses now use generic messages — internal details are only logged, never sent to clients
- Actor JSON includes W3C security vocabulary in
@context— required for public key resolution by strict implementations - WebFinger response includes
aliasesfield - Outbox
lastlink added to rootOrderedCollection - Outbox count uses
count_local_posts()— no longer loads all objects to count them - Backfill uses cursor-based
get_local_objects_page— no longer loads all posts into memory - All inbound activities are deduplicated by activity
idviaActivityRepository - Mentions extracted from
tagarrays inCreate/Updateand dispatched viaon_mention - Cross-server
Announcedispatched viaon_announce_of_remoteinstead of silently dropped discoverable,actor_type,featured_url,manually_approves_followersall configurable per-userdelivery_max_attemptsanddelivery_initial_delay_secsconfigurable via builderMakefilewithcheck,fmt,clippy,test,fixtargets
[0.1.10] — 2024 (previous release)
Initial public extraction from thoughts and movies-diary.
FederationRepository,ApUserRepository,ApObjectHandler— single-struct trait interface- Inbound: Follow, Accept, Reject, Undo, Create, Update, Delete, Announce, Like, Add, Block, Move
- Outbound broadcasts: Create, Update, Delete, Announce, Like, Move, actor update
- WebFinger, NodeInfo 2.0, shared inbox, follower/following collections
- Signed WebFinger resolution for actor lookup
- Account migration (Move) with alsoKnownAs verification and re-follow