Skip to content

Promote the experimental architecture to main - #44

Open
sij411 wants to merge 31 commits into
mainfrom
feat/experiment/architecture
Open

Promote the experimental architecture to main#44
sij411 wants to merge 31 commits into
mainfrom
feat/experiment/architecture

Conversation

@sij411

@sij411 sij411 commented Aug 8, 2026

Copy link
Copy Markdown
Member

Summary

Promote the architecture developed on feat/experiment/architecture to the
main development line.

  • Replace the legacy stateful feder-core implementation with portable,
    stateless protocol transition functions and capability traits.
  • Replace feder-runtime-server with the public feder-server runtime.
  • Preserve HTTP signature verification, signed delivery, SSRF protections,
    SQLite persistence, followers, Follow/Accept/Undo handling, and Note delivery.
  • Add outbound Follow orchestration and shared inbox handling.
  • Migrate the single-user server to the new runtime.
  • Return 202 Accepted for unsupported nested Accept and Undo activities.

Compatibility

This intentionally replaces the previous public core and runtime APIs.
Consumers of feder-runtime-server must migrate to feder-server.

The existing SQLite follower, signing-key, and object tables remain compatible.
The new runtime adds storage for outbound Follow relationships.

The legacy /healthz route is intentionally not carried forward.

Migration notes

This change replaces the experimental legacy API rather than preserving
backward compatibility.

  • Replace feder-runtime-server dependencies and imports with feder-server.
  • Replace the stateful FederCore workflow with stateless protocol functions
    and runtime-provided capability traits.
  • Construct the standard runtime using FederServer, an ActorDispatcher,
    and storage implementations.
  • Existing SQLite follower, object, and signing-key data remains compatible.
    A new table stores outbound Follow relationships.
  • The legacy /healthz endpoint is not included.

Validation

  • mise run check
  • mise run test
  • Manually verified the experimental branch against current main

This PR is assisted by Codex: gpt-sol-5.6.

Summary by CodeRabbit

  • New Features

    • Added ActivityPub follow, note creation, undo validation, actor lookup, inbox handling, WebFinger, follower, and object endpoints.
    • Added SQLite-backed persistence for notes, followers, pending follows, and actor keys.
    • Added signed activity delivery with configurable outbound address and inbox authentication policies.
    • Added a single-user server example using the new server runtime.
  • Documentation

    • Updated project guidance and examples to describe the portable core and server responsibilities.
  • Bug Fixes

    • Improved protection against unsafe outbound destinations, including additional private IPv6 ranges.

sij411 added 30 commits July 30, 2026 17:00
Assisted-by: Codex-gpt-5.6-sol
Add new minimal core and runtime architecture
Add actor endpoint with generics
Add negotiation features

Assisted-by: Codex:gpt-5.6-sol
Assisted-by: Codex:gpt-5.6-sol
Port RSA actor key generation and persisted PEM validation into
ref-feder-core. Redact private key material from debug output and keep
actor dispatch as a minimal capability called directly by the server
runtime.

Assisted-by: Codex:gpt-5.6-sol
Parse and validate local acct resources, resolve actors through the actor
dispatcher, and return JRD discovery responses from the experimental server
router.

Assisted-by: Codex:gpt-5.6-sol
Introduce a stateless receive_follow transition in ref-feder-core. Validate
that the Follow actor matches the resolved remote actor and that its object
targets the selected local actor, then return the follower facts and Accept
delivery through a transient FollowOutcome without retaining protocol state.

Extend the portable key module with SHA-256 digest generation and
draft-Cavage RSA-SHA256 signing and verification primitives.

Add a personal inbox endpoint to ref-feder-runtime-server. Validate
ActivityPub content types, body size, request host and date, body digests,
signature headers, remote key ownership, and actor identity before invoking
the core Follow transition. Persist the follower before sending the generated
Accept and return 202 Accepted for successfully handled or irrelevant
activities.

Represent local actor access and runtime services through FederServer<A, S>,
while placing Arc only at the Axum router boundary for concurrent request
sharing. Keep signed authentication as the default and temporarily retain an
explicit insecure development policy until the reference example can issue
signed Follow requests.

Update the reference server example with bounded adapters for remote
resolution, follower storage, and Accept sending. Document and exercise the
actor, WebFinger, and personal inbox endpoints without accumulating an
in-memory activity history.

Shared inbox and Undo handling remain out of scope for this change.

Assisted-by: Codex:gpt-5.6-sol
Define ServerStorage as the application-owned persistence boundary for
follower relationships and per-actor signing keys.

Allow runtimes to persist Follow outcomes and retrieve the appropriate
ActorKeyPair without retaining protocol state inside the core.

Assisted-by: Codex:gpt-5.6-sol
Port protected remote actor and key resolution to the reference server
runtime. Reject private and special-use destinations by default, disable
redirects and proxies, and enforce request timeouts and response size limits.

Give FederServer concrete resolver and activity sender components. Verify
signed inbox requests, persist Follow relationships through ServerStorage,
load per-actor signing keys, and deliver signed Accept activities.

Migrate the reference actor-server example to ServerStorage and the fallible
server constructor. Use the bundled test key pair and a local recipient inbox
to demonstrate signed Accept delivery without generating keys at startup.

Assisted-by: Codex:gpt-5.6-sol
Reject the deprecated fec0::/10 site-local range when outbound networking uses
the PublicOnly policy, closing an SSRF path through literal URLs and DNS
resolution.

Assisted-by: Codex:gpt-5.6-sol
Extend ServerStorage with an idempotent follower-removal operation for the
upcoming Undo Follow transition.

Update the reference actor-server storage adapter to remove its retained
follower only when both sides of the relationship match.

Assisted-by: Codex:gpt-5.6-sol
Validate that an Undo actor owns its embedded Follow and that the Follow
targets the local actor.

Return a transient follower-removal outcome without retaining protocol state
or performing storage operations inside core.

Assisted-by: Codex:gpt-5.6-sol
Dispatch Undo activities through the pure core transition and remove
validated follower relationships through ServerStorage.

Update the reference actor server example with idempotent follower removal
and a documented Follow-to-Undo flow.

Assisted-by: Codex:gpt-5.6-sol
Add canonical actor ID lookup to ActorDispatcher so shared inbox activities
can be routed without assuming an application URL structure.

Route Follow and Undo Follow activities from the shared inbox through the
existing authentication, core transition, storage, and delivery flow.

Update the reference actor server to advertise and demonstrate the shared
inbox endpoint.

Assisted-by: Codex:gpt-5.6-sol
Add a follower-listing capability to ServerStorage and expose each local
actor's followers as an ActivityStreams OrderedCollection.

Advertise the collection from the example actor and demonstrate how Follow
and Undo requests update its contents.

Assisted-by: Codex:gpt-5.6-sol
Add a pure core operation that constructs an outbound Follow activity and its
transient pending relationship.

Extend ServerStorage with pending Follow persistence and add
FederServer::follow_actor to resolve the remote actor, persist intent, load
the local signing key, and deliver the signed activity.

Allow applications to retain shared FederServer state alongside the router,
and update the reference example with bounded pending storage and an
end-to-end outbound Follow demonstration.

Assisted-by: Codex:gpt-5.6-sol
Add a pure core transition that validates inbound Accept activities against
the authenticated remote actor and the stored pending Follow relationship.

Extend ServerStorage with pending Follow lookup and compare-and-set
confirmation capabilities.

Route linked and embedded Accept Follow activities through personal and shared
inboxes, and update the reference example with a bounded pending-to-accepted
state transition.

Assisted-by: Codex:gpt-5.6-sol
Add a pure core operation that constructs a local Note and its corresponding
Create activity from runtime-provided IDs, addressing, and content.

Introduce a dedicated NoteStore capability and add FederServer::create_note
to load the local actor and persist only the durable Note without retaining
protocol history.

Update the reference example with bounded Note storage and a local creation
flow while leaving Create delivery for the next migration step.

Assisted-by: Codex:gpt-5.6-sol
Derive transient delivery intents from Note addressing in core without
retaining recipient or activity history.

Add follower delivery and shared storage error capabilities, then expand
followers, resolve direct actors, deduplicate inboxes, and deliver signed
Create activities after Note persistence.

Update the reference example to demonstrate signed Create delivery to a
stored follower.

Assisted-by: Codex:gpt-5.6-sol
Extend NoteStore with typed Note loading and add a pure core check for public
ActivityStreams addressing.

Expose canonical local post paths through the reference server runtime with
ActivityPub content negotiation while hiding missing and non-public Notes.

Update the reference example to load and serve its bounded persisted Note.

Assisted-by: Codex:gpt-5.6-sol
Require FederServer users to provide the authoritative WebFinger handle
host and validate acct resources against it.

Stop trusting the client-controlled Host header when deciding whether an
account belongs to the local deployment, and update the reference actor
server with its configured handle host.

Assisted-by: Codex:gpt-5.6-sol
Preserve the inbox endpoint selected during request routing and use its
authority when verifying signed requests.

Personal inbox requests continue to use the actor inbox, while shared inbox
requests use the actor's advertised sharedInbox endpoint. Treat requests for
actors without a shared inbox as accepted no-ops.

Assisted-by: Codex:gpt-5.6-sol
Restore the http-signatures feature boundary around actor keys, digest
generation, and draft-Cavage signing and verification.

Keep RSA, Base64, and zeroization out of the portable core dependency tree
unless explicitly requested. Enable the feature from the server runtime and
reference actor example.

Assisted-by: Codex:gpt-5.6-sol
  Replace complete stored Actor values in follower fan-out with the minimal
  addressing facts required for delivery: actor ID, inbox, and optional shared
  inbox.

  Update Note delivery and the reference example to use the compact target,
  keeping the storage contract compatible with the existing SQLite follower
  schema and avoiding unnecessary actor retention.

  Assisted-by: Codex:gpt-5.6-sol
Provide file-backed and in-memory SQLite storage implementing the new server,
Note, and follower-delivery storage capabilities.

Preserve the existing followers, actor keys, and objects schema while adding
persistent outbound Follow state. Synchronize connection access for shared
Axum state and use an immediate transaction for exact pending-Follow
confirmation.

Add focused coverage for follower delivery facts, Notes, pending Follow
transitions, actor keys, and Send and Sync compatibility.

Assisted-by: Codex:gpt-5.6-sol
Add a SQLite-backed load-or-generate operation for actor signing identities.

Reuse existing actor keys without invoking the random number generator.
When a key is absent, generate it outside the database lock and insert it
without replacing an identity established by a concurrent provisioner.

Add focused coverage that verifies initial provisioning and stable reuse.

Assisted-by: Codex:gpt-5.6-sol
  Replace the original RuntimeConfig-based example with the new FederServer,
  actor dispatcher, and built-in SQLite storage adapter.

  Provision the actor signing key through SQLite, publish its public key on the
  actor document, and preserve the signing identity across server restarts.
  Allow the database path to be configured through FEDER_DATABASE.

  Update the example documentation with actor and WebFinger requests.

  Assisted-by: Codex:gpt-5.6-sol
Move protocol transition tests into ref-feder-core/tests, covering Follow,
Accept, Undo, Note construction, actor keys, digests, and HTTP signatures.

Move the SQLite adapter tests out of the implementation module and into the
ref-feder-runtime-server integration test suite.

Add runtime coverage for actor and object endpoints, WebFinger, follower
collections, actor resolution, outbound delivery, signed personal and shared
inboxes, outbound Follow, incoming Undo, and Note persistence and delivery.

Keep test keys and shared test infrastructure local to each crate so the
reference crates can be tested independently.

Assisted-by: Codex:gpt-5.6-sol
Replace the previous stateful feder-core implementation with the portable
protocol transition functions and capability traits developed in the
reference crate.

Publish the standard operating system runtime as feder-server and update its
dependencies, imports, tests, and documentation to use the final feder-core
package.

Remove the superseded feder-runtime-server, temporary ref-* crates, and the
redundant custom-storage example. Retain the SQLite single-user server as a
non-publishable end-to-end example.

Restrict release version stamping to publishable workspace packages and update
the project documentation to describe the stateless core and server runtime
boundaries.

 Assisted-by: Codex:gpt-5.6-sol
Return 202 Accepted for Accept and Undo activities wrapping unsupported
activity types before attempting Follow-specific deserialization.

Assisted-by: Codex:gpt-5.6-sol
@coderabbitai

coderabbitai Bot commented Aug 8, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The project replaces the stateful feder-core implementation with stateless protocol primitives and storage traits. It renames the runtime crate to feder-server, adds generic ActivityPub server workflows, SQLite persistence, authenticated inbox handling, endpoint integration, and an updated single-user example.

Changes

Stateless federation architecture

Layer / File(s) Summary
Stateless core contracts and protocol primitives
crates/feder-core/src/*, crates/feder-core/tests/*, README.md, CONTRIBUTING.md
feder-core now provides actor dispatch, storage traits, note creation, follow validation, undo validation, and transient outcomes.
FederServer state, storage, and delivery
crates/feder-server/src/lib.rs, crates/feder-server/src/storage/*, crates/feder-server/src/send.rs, crates/feder-server/src/config.rs
feder-server adds generic server state, SQLite persistence, signing-key management, outbound address policies, activity delivery, and routed endpoints.
HTTP endpoints and inbox processing
crates/feder-server/src/{actor,followers,object,webfinger,inbox}.rs, crates/feder-server/tests/cases/*
Handlers use actor dispatch and server storage. Direct and shared inboxes validate signed requests and process Follow, Accept, and Undo activities.
Follow and note workflows
crates/feder-server/src/{follow,note}.rs, crates/feder-server/tests/cases/operation.rs, crates/feder-server/tests/cases/send.rs
Server workflows persist relationships and notes, resolve recipients, load keys, and deliver signed activities.
Example and project wiring
Cargo.toml, examples/single-user-server/*, mise.toml, cspell.json
Workspace references use feder-server. The example uses SQLite and direct core/server construction. Publishing metadata excludes unpublished packages.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

  • fedify-dev/feder#17: Introduced the stateful core implementation that this change replaces.
  • fedify-dev/feder#28: Added the earlier runtime-server structure that becomes feder-server.
  • fedify-dev/feder#38: Covered SQLite follower persistence that is moved into the new storage implementation.

Suggested labels: enhancement

Suggested reviewers: dahlia

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 45.34% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the primary change: replacing the experimental architecture with the promoted stateless core and feder-server architecture.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/experiment/architecture

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🧹 Nitpick comments (13)
crates/feder-server/src/inbox.rs (4)

103-119: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Log the silent 202 branches in shared_inbox.

Three separate conditions return 202 Accepted and drop the activity: no routable target, no local actor for the target, and no advertised sharedInbox endpoint. The third condition indicates a server misconfiguration, not a remote error. Without a log line, an operator cannot distinguish a dropped activity from a processed one.

Add a debug or warning log for each branch before returning.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/feder-server/src/inbox.rs` around lines 103 - 119, Add a debug or
warning log immediately before each 202 Accepted return in shared_inbox: when
shared_inbox_target yields no routable target, when get_actor_by_id finds no
local actor, and when the actor has no advertised shared_inbox endpoint. Include
enough context to distinguish the three dropped-activity cases, treating the
missing endpoint as a server configuration issue.

51-59: 🔒 Security & Privacy | 🔵 Trivial

Track removal of AllowUnsignedInsecureDev.

The variant disables all inbox authentication and is part of the public API. The default is RequireSigned, so the exposure needs an explicit opt-in. The FIXME records the plan to remove the variant after the reference example signs its Follow requests.

Do you want me to open an issue to track the removal and the example update?

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/feder-server/src/inbox.rs` around lines 51 - 59, Track the planned
removal of the public InboxAuthPolicy::AllowUnsignedInsecureDev variant and the
corresponding reference-example update by opening an issue. Record that the
example must send signed Follow requests before removing the variant, while
preserving RequireSigned as the default until then.

469-474: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Reuse a core helper instead of duplicating follow_reference_id.

This function repeats the private follow_reference_id in crates/feder-core/src/follow.rs lines 132-137. The same duplication exists for the Reference<Actor> variant. Exporting one small accessor from feder-core, for example an inherent id() on Reference<T> where T has an id, removes both copies and keeps the two crates aligned if Reference gains a variant.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/feder-server/src/inbox.rs` around lines 469 - 474, Remove the local
follow_reference_id helper in inbox.rs and reuse a shared accessor from
feder-core for both Reference<Follow> and Reference<Actor>. Expose an inherent
id() method on Reference<T> in the core implementation, constrained to
referenced types with an id, then update callers to use it so both crates share
one variant-aware implementation.

315-391: 🔒 Security & Privacy | 🔵 Trivial

Consider a replay guard for signed inbox requests.

The date window allows a captured request to be replayed for up to 65 minutes. The digest binds the body, so a replay repeats the identical activity. Follow, Undo, and Accept handling is idempotent today, so the current impact is low. If the inbox later handles non-idempotent activities, a short-lived cache of seen signature values, or of activity IDs, prevents duplicate processing.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/feder-server/src/inbox.rs` around lines 315 - 391, Extend
verify_signed_request with a short-lived replay guard keyed by each accepted
signature value (or activity ID), rejecting entries already seen within the
allowed date window while preserving normal validation and idempotent handling.
Ensure the guard is concurrency-safe, bounded/expiring, and only records
requests after signature verification succeeds.
crates/feder-server/tests/cases/inbox.rs (1)

336-363: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a test for the Accept confirmation path.

The suite covers Follow, Undo, and unsupported nested activities. It does not cover a valid Accept that confirms a pending outbound Follow. That path in crates/feder-server/src/inbox.rs lines 187-231 loads the pending relationship, validates it through receive_accept_follow, and calls confirm_pending_follow. It is the newest logic in the handler and has no server-level test.

Add one test that stores a pending Follow, posts a signed Accept to the personal inbox, and asserts the relationship became confirmed. Add a second test that posts the same Accept to the shared inbox, to cover the shared_inbox_target lookup at lines 445-456.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/feder-server/tests/cases/inbox.rs` around lines 336 - 363, Add
server-level tests for the valid Accept confirmation flow: create and persist a
pending outbound Follow, post its signed Accept to the recipient’s personal
inbox, and assert the relationship is confirmed; add a second test posting the
same Accept to the shared inbox to exercise shared_inbox_target lookup. Reuse
existing Follow, signing, inbox, and relationship assertion helpers, anchoring
the tests near ignores_accept_and_undo_of_unsupported_activities.
crates/feder-core/tests/follow.rs (1)

113-142: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add coverage for the remaining AcceptFollowError variants.

The tests assert WrongActor and WrongFollow only. WrongLocalActor, WrongFollowActor, and WrongFollowObject stay untested. Those branches guard cross-actor confirmation of a pending Follow, so a regression there would be silent.

🧪 Suggested additional cases
#[test]
fn rejects_accept_for_another_local_actor() {
    let local = actor("https://local.example/users/alice");
    let remote = actor("https://remote.example/users/bob");
    let pending = PendingFollow {
        local_actor: iri("https://local.example/users/carol"),
        remote_actor: remote.clone(),
        follow_activity: iri("https://local.example/activities/follow/1"),
    };
    let accept = Accept::new(
        iri("https://remote.example/activities/accept/1"),
        Reference::id(remote.id.clone()),
        Reference::id(pending.follow_activity.clone()),
    );

    assert_eq!(
        receive_accept_follow(&local, &remote, &pending, accept),
        Err(AcceptFollowError::WrongLocalActor)
    );
}

#[test]
fn rejects_accept_whose_embedded_follow_has_wrong_endpoints() {
    let local = actor("https://local.example/users/alice");
    let remote = actor("https://remote.example/users/bob");
    let other = actor("https://local.example/users/mallory");
    let pending = PendingFollow {
        local_actor: local.id.clone(),
        remote_actor: remote.clone(),
        follow_activity: iri("https://local.example/activities/follow/1"),
    };
    let follow = Follow::new(
        pending.follow_activity.clone(),
        Reference::id(other.id.clone()),
        Reference::id(remote.id.clone()),
    );
    let accept = Accept::new(
        iri("https://remote.example/activities/accept/1"),
        Reference::id(remote.id.clone()),
        Reference::object(follow),
    );

    assert_eq!(
        receive_accept_follow(&local, &remote, &pending, accept),
        Err(AcceptFollowError::WrongFollowActor)
    );
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/feder-core/tests/follow.rs` around lines 113 - 142, Extend the
receive_accept_follow test coverage with cases for WrongLocalActor,
WrongFollowActor, and WrongFollowObject. Add assertions using mismatched pending
local actors and embedded Follow objects with incorrect actor and object
endpoints, ensuring each case returns its corresponding AcceptFollowError
variant.
crates/feder-core/tests/undo.rs (1)

29-70: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Add a case for a mismatched embedded Follow actor.

The three cases cover LinkedFollow, the outer-actor WrongActor branch, and WrongObject. They do not cover the second WrongActor branch in crates/feder-core/src/undo.rs at lines 42-44, where undo.actor matches remote_actor.id but the embedded follow.actor does not.

That branch carries the authorization guarantee. It stops a verified signer from undoing a Follow that another actor created. crates/feder-server/src/inbox.rs maps the result to 401.

💚 Proposed additional case
+    let wrong_follow_actor = Undo::new(
+        iri("https://remote.example/activities/undo/4"),
+        Reference::id(remote.id.clone()),
+        Reference::object(Follow::new(
+            iri("https://remote.example/activities/follow/4"),
+            Reference::id(other.id.clone()),
+            Reference::id(local.id.clone()),
+        )),
+    );
+
     assert_eq!(
         receive_undo_follow(&local, &remote, linked),
         Err(UndoFollowError::LinkedFollow)
     );
+    assert_eq!(
+        receive_undo_follow(&local, &remote, wrong_follow_actor),
+        Err(UndoFollowError::WrongActor)
+    );

Change Reference::id(other.id) at line 41 to Reference::id(other.id.clone()) so other remains usable.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/feder-core/tests/undo.rs` around lines 29 - 70, Add a test case in
rejects_linked_follow_and_wrong_actor_or_object for an Undo whose outer actor
matches remote.id but whose embedded Follow actor is other.id, and assert
receive_undo_follow returns UndoFollowError::WrongActor. Clone other.id where
needed so the existing wrong_actor fixture remains usable.
crates/feder-server/src/lib.rs (1)

73-104: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove the duplicated constructor body.

new and with_outbound_address_policy build the same value. Only the policy differs. Let new delegate to with_outbound_address_policy. This keeps future field additions in one place.

Also consider the naming. with_inbox_auth_policy on Line 107 is a consuming builder method, but with_outbound_address_policy is a constructor with a different shape. A name such as with_config or a builder method that mirrors with_inbox_auth_policy would make the public API consistent.

♻️ Proposed change
     pub fn new(actors: A, storage: S, handle_host: impl Into<String>) -> Result<Self, Error> {
-        let policy = OutboundAddressPolicy::PublicOnly;
-        let resolver = ActorResolver::new(policy)?;
-        let sender = ActivitySender::new(policy)?;
-        Ok(Self {
-            actors,
-            storage,
-            handle_host: handle_host.into(),
-            resolver,
-            sender,
-            inbox_auth_policy: InboxAuthPolicy::RequireSigned,
-        })
+        Self::with_outbound_address_policy(
+            actors,
+            storage,
+            handle_host,
+            OutboundAddressPolicy::PublicOnly,
+        )
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/feder-server/src/lib.rs` around lines 73 - 104, Remove the duplicated
construction logic by making new delegate to with_outbound_address_policy with
OutboundAddressPolicy::PublicOnly, preserving the existing default behavior and
centralizing field initialization. Rename with_outbound_address_policy to a
configuration-oriented or consuming-builder name consistent with
with_inbox_auth_policy, and update its callers accordingly.
crates/feder-server/Cargo.toml (1)

12-12: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Avoid an exact version pin for axum in a library crate.

= 0.8 resolves to exactly 0.8.0. Cargo excludes every later patch release, so the crate cannot receive 0.8.x bug and security fixes. An exact pin in a published library also creates conflicts for downstream users who need a newer patch. Use a caret requirement instead.

♻️ Proposed change
-axum = "=0.8"
+axum = "0.8"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/feder-server/Cargo.toml` at line 12, Update the axum dependency
declaration in the crate manifest from an exact version requirement to a
caret-compatible 0.8 requirement, allowing later 0.8.x patch releases while
preserving the current minor-version constraint.
crates/feder-server/src/follow.rs (1)

73-83: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Generic error variants drop the underlying cause in both workflow error types. FollowActorError and CreateNoteError carry the dispatcher and storage errors by value without #[source], while the non-generic variants in the same enums use #[source]. Callers that walk the error chain lose the real reason for a dispatcher or storage failure.

  • crates/feder-server/src/follow.rs#L73-L83: add #[source] to ActorDispatcher(A) and Storage(S), and add A: std::error::Error + 'static and S: std::error::Error + 'static bounds to FollowActorError.
  • crates/feder-server/src/note.rs#L141-L148: apply the same #[source] attributes and generic bounds to ActorDispatcher(A) and Storage(S) in CreateNoteError.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/feder-server/src/follow.rs` around lines 73 - 83, Update
FollowActorError in crates/feder-server/src/follow.rs lines 73-83 by marking
ActorDispatcher(A) and Storage(S) as #[source] and adding A: std::error::Error +
'static and S: std::error::Error + 'static bounds. Apply the same #[source]
attributes and generic bounds to ActorDispatcher(A) and Storage(S) in
CreateNoteError at crates/feder-server/src/note.rs lines 141-148, preserving the
existing non-generic error behavior.
crates/feder-server/src/send.rs (1)

57-66: 🔒 Security & Privacy | 🔵 Trivial | 💤 Low value

The Reference::Id branch skips the key consistency check.

The Reference::Object branch verifies the owner and the PEM against key_pair. The Reference::Id branch accepts the key id without any check. An id-only reference carries no PEM, so a local comparison is not possible. The result is that ActorKeyMismatch only protects the embedded-key case.

If the actor document is always expected to embed the key, reject Reference::Id with MissingActorKey instead. If both forms are supported, document the reduced guarantee at the method level.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/feder-server/src/send.rs` around lines 57 - 66, Update the key
selection logic around local_actor.public_key so Reference::Id cannot bypass key
consistency validation. Prefer rejecting id-only references with
SendError::MissingActorKey if embedded keys are required; otherwise document the
reduced validation guarantee at the enclosing method and preserve support for
both reference forms.
crates/feder-server/src/note.rs (1)

69-86: 🚀 Performance & Scalability | 🔵 Trivial | 🏗️ Heavy lift

Delivery failures after the first one are discarded without a trace.

The loop keeps only first_send_error. Every later failure is dropped. There is no log statement. If delivery to 50 of 100 follower inboxes fails, the caller sees one error and cannot identify the affected inboxes.

The loop is also sequential. Each inbox waits for the previous request, and build_client sets a request timeout. For a large follower collection the total time grows linearly with the follower count on the caller's task.

Two suggestions for the operator-facing path:

  • Log each delivery failure with the inbox, or return the full list of failures instead of one error.
  • Consider bounded concurrency for the fan-out, for example a FuturesUnordered with a concurrency limit, or hand the deliveries to a background queue so the caller does not block.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/feder-server/src/note.rs` around lines 69 - 86, Update the inbox
delivery loop around send_activity to record every delivery failure with its
inbox through the existing logging facility, while preserving the
first_send_error result behavior. Replace the sequential fan-out with bounded
concurrency, such as a concurrency-limited FuturesUnordered flow, so large
follower collections do not block linearly on the caller task. Preserve the
existing ActorResolver error precedence when no delivery error occurs.
crates/feder-server/tests/cases/operation.rs (1)

82-100: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert pending Follow persistence.

This test never loads outbound_follows. It passes if follow_actor sends the Follow without storing the pending relationship.

Assert that server.storage().load_pending_follow(&follow_id) returns the expected relationship. If delivery ordering is required, block the remote response and inspect storage after the handler receives the request but before it releases the response.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/feder-server/tests/cases/operation.rs` around lines 82 - 100, Extend
outbound_follow_is_persisted_before_signed_delivery to load the pending
relationship with server.storage().load_pending_follow(&follow_id) after
follow_actor completes, and assert it matches the expected local and remote
actors. If persistence must be verified before delivery completes, hold the
remote response, inspect storage after receiving the request, then release the
response.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/feder-core/src/note.rs`:
- Around line 81-104: Add a shared public-address predicate that recognizes both
PUBLIC_COLLECTION and the compact "as:Public" alias, then use it in
is_public_note and note_recipients. Ensure alias recipients are skipped as
public addressing rather than reported as NoteRecipient::Actor targets.

In `@crates/feder-server/src/follow.rs`:
- Around line 56-60: Update follow_actor’s inbox selection to always use
remote_actor.inbox for the targeted Follow, removing the shared_inbox fallback
and any related endpoint preference. Preserve the existing delivery flow while
ensuring no follower endpoint recipients are used.

In `@crates/feder-server/src/inbox.rs`:
- Around line 226-229: Update the confirmation flow in the inbox handler around
confirm_pending_follow to bind its returned bool and detect false. Log an
appropriate diagnostic when the exact pending relationship was not confirmed,
while preserving the existing internal-server-error handling for returned
errors.
- Around line 601-611: The signature parsing and verification flow around
parse_signature_header and verify_signed_request must accept an omitted
algorithm and algorithm="hs2019" for RSA signatures. Treat either case as the
existing RSA-SHA256 verification path while preserving current handling for
explicit rsa-sha256 and rejecting unsupported algorithms.

In `@crates/feder-server/src/lib.rs`:
- Around line 87-93: Add rustdoc to the public constructor
with_outbound_address_policy describing that
OutboundAddressPolicy::AllowPrivateAddress disables SSRF protection and should
be limited to development or tests; replace or supplement the non-rustdoc marker
so published crate consumers see the warning. If the project’s feature structure
supports it, gate this constructor behind an insecure-dev cargo feature to
prevent access in production builds.

In `@crates/feder-server/src/storage/sqlite.rs`:
- Around line 260-264: Update the SQLite upsert for follow activities so a
conflict on follow_activity_id never changes an existing accepted relationship
back to pending. Replace the unconditional state update with DO NOTHING, or
condition updates so they apply only when the stored state is already pending,
while preserving the current insert behavior.

In `@examples/single-user-server/src/main.rs`:
- Around line 98-104: Update the example server construction using
FederServer::with_outbound_address_policy to use
OutboundAddressPolicy::PublicOnly and the default InboxAuthPolicy::RequireSigned
instead of permitting private addresses or unsigned insecure development
requests. Keep private-address access and unsigned local-federation behavior
available only through explicit opt-in configuration.

---

Nitpick comments:
In `@crates/feder-core/tests/follow.rs`:
- Around line 113-142: Extend the receive_accept_follow test coverage with cases
for WrongLocalActor, WrongFollowActor, and WrongFollowObject. Add assertions
using mismatched pending local actors and embedded Follow objects with incorrect
actor and object endpoints, ensuring each case returns its corresponding
AcceptFollowError variant.

In `@crates/feder-core/tests/undo.rs`:
- Around line 29-70: Add a test case in
rejects_linked_follow_and_wrong_actor_or_object for an Undo whose outer actor
matches remote.id but whose embedded Follow actor is other.id, and assert
receive_undo_follow returns UndoFollowError::WrongActor. Clone other.id where
needed so the existing wrong_actor fixture remains usable.

In `@crates/feder-server/Cargo.toml`:
- Line 12: Update the axum dependency declaration in the crate manifest from an
exact version requirement to a caret-compatible 0.8 requirement, allowing later
0.8.x patch releases while preserving the current minor-version constraint.

In `@crates/feder-server/src/follow.rs`:
- Around line 73-83: Update FollowActorError in
crates/feder-server/src/follow.rs lines 73-83 by marking ActorDispatcher(A) and
Storage(S) as #[source] and adding A: std::error::Error + 'static and S:
std::error::Error + 'static bounds. Apply the same #[source] attributes and
generic bounds to ActorDispatcher(A) and Storage(S) in CreateNoteError at
crates/feder-server/src/note.rs lines 141-148, preserving the existing
non-generic error behavior.

In `@crates/feder-server/src/inbox.rs`:
- Around line 103-119: Add a debug or warning log immediately before each 202
Accepted return in shared_inbox: when shared_inbox_target yields no routable
target, when get_actor_by_id finds no local actor, and when the actor has no
advertised shared_inbox endpoint. Include enough context to distinguish the
three dropped-activity cases, treating the missing endpoint as a server
configuration issue.
- Around line 51-59: Track the planned removal of the public
InboxAuthPolicy::AllowUnsignedInsecureDev variant and the corresponding
reference-example update by opening an issue. Record that the example must send
signed Follow requests before removing the variant, while preserving
RequireSigned as the default until then.
- Around line 469-474: Remove the local follow_reference_id helper in inbox.rs
and reuse a shared accessor from feder-core for both Reference<Follow> and
Reference<Actor>. Expose an inherent id() method on Reference<T> in the core
implementation, constrained to referenced types with an id, then update callers
to use it so both crates share one variant-aware implementation.
- Around line 315-391: Extend verify_signed_request with a short-lived replay
guard keyed by each accepted signature value (or activity ID), rejecting entries
already seen within the allowed date window while preserving normal validation
and idempotent handling. Ensure the guard is concurrency-safe, bounded/expiring,
and only records requests after signature verification succeeds.

In `@crates/feder-server/src/lib.rs`:
- Around line 73-104: Remove the duplicated construction logic by making new
delegate to with_outbound_address_policy with OutboundAddressPolicy::PublicOnly,
preserving the existing default behavior and centralizing field initialization.
Rename with_outbound_address_policy to a configuration-oriented or
consuming-builder name consistent with with_inbox_auth_policy, and update its
callers accordingly.

In `@crates/feder-server/src/note.rs`:
- Around line 69-86: Update the inbox delivery loop around send_activity to
record every delivery failure with its inbox through the existing logging
facility, while preserving the first_send_error result behavior. Replace the
sequential fan-out with bounded concurrency, such as a concurrency-limited
FuturesUnordered flow, so large follower collections do not block linearly on
the caller task. Preserve the existing ActorResolver error precedence when no
delivery error occurs.

In `@crates/feder-server/src/send.rs`:
- Around line 57-66: Update the key selection logic around
local_actor.public_key so Reference::Id cannot bypass key consistency
validation. Prefer rejecting id-only references with SendError::MissingActorKey
if embedded keys are required; otherwise document the reduced validation
guarantee at the enclosing method and preserve support for both reference forms.

In `@crates/feder-server/tests/cases/inbox.rs`:
- Around line 336-363: Add server-level tests for the valid Accept confirmation
flow: create and persist a pending outbound Follow, post its signed Accept to
the recipient’s personal inbox, and assert the relationship is confirmed; add a
second test posting the same Accept to the shared inbox to exercise
shared_inbox_target lookup. Reuse existing Follow, signing, inbox, and
relationship assertion helpers, anchoring the tests near
ignores_accept_and_undo_of_unsupported_activities.

In `@crates/feder-server/tests/cases/operation.rs`:
- Around line 82-100: Extend outbound_follow_is_persisted_before_signed_delivery
to load the pending relationship with
server.storage().load_pending_follow(&follow_id) after follow_actor completes,
and assert it matches the expected local and remote actors. If persistence must
be verified before delivery completes, hold the remote response, inspect storage
after receiving the request, then release the response.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 3ed53852-d143-46bd-b143-33f6adea91b9

📥 Commits

Reviewing files that changed from the base of the PR and between c7833b3 and c53c89a.

⛔ Files ignored due to path filters (3)
  • Cargo.lock is excluded by !**/*.lock
  • crates/feder-server/tests/fixtures/rsa-private-key.pem is excluded by !**/*.pem
  • crates/feder-server/tests/fixtures/rsa-public-key.pem is excluded by !**/*.pem
📒 Files selected for processing (63)
  • CONTRIBUTING.md
  • Cargo.toml
  • README.md
  • crates/feder-core/Cargo.toml
  • crates/feder-core/src/follow.rs
  • crates/feder-core/src/key.rs
  • crates/feder-core/src/lib.rs
  • crates/feder-core/src/note.rs
  • crates/feder-core/src/storage.rs
  • crates/feder-core/src/undo.rs
  • crates/feder-core/tests/common/mod.rs
  • crates/feder-core/tests/follow.rs
  • crates/feder-core/tests/key.rs
  • crates/feder-core/tests/note.rs
  • crates/feder-core/tests/undo.rs
  • crates/feder-runtime-server/README.md
  • crates/feder-runtime-server/src/app.rs
  • crates/feder-runtime-server/src/config.rs
  • crates/feder-runtime-server/src/error.rs
  • crates/feder-runtime-server/src/inbox.rs
  • crates/feder-runtime-server/src/lib.rs
  • crates/feder-runtime-server/src/operation.rs
  • crates/feder-runtime-server/src/storage/mod.rs
  • crates/feder-runtime-server/src/storage/sqlite.rs
  • crates/feder-runtime-server/tests/cases/actor.rs
  • crates/feder-runtime-server/tests/cases/app.rs
  • crates/feder-runtime-server/tests/cases/followers.rs
  • crates/feder-runtime-server/tests/cases/inbox.rs
  • crates/feder-runtime-server/tests/cases/object.rs
  • crates/feder-runtime-server/tests/cases/operation.rs
  • crates/feder-runtime-server/tests/cases/send.rs
  • crates/feder-runtime-server/tests/cases/webfinger.rs
  • crates/feder-runtime-server/tests/common/mod.rs
  • crates/feder-server/Cargo.toml
  • crates/feder-server/src/actor.rs
  • crates/feder-server/src/config.rs
  • crates/feder-server/src/follow.rs
  • crates/feder-server/src/followers.rs
  • crates/feder-server/src/inbox.rs
  • crates/feder-server/src/lib.rs
  • crates/feder-server/src/negotiation.rs
  • crates/feder-server/src/note.rs
  • crates/feder-server/src/object.rs
  • crates/feder-server/src/send.rs
  • crates/feder-server/src/storage/mod.rs
  • crates/feder-server/src/storage/sqlite.rs
  • crates/feder-server/src/url.rs
  • crates/feder-server/src/webfinger.rs
  • crates/feder-server/tests/cases/actor.rs
  • crates/feder-server/tests/cases/followers.rs
  • crates/feder-server/tests/cases/inbox.rs
  • crates/feder-server/tests/cases/object.rs
  • crates/feder-server/tests/cases/operation.rs
  • crates/feder-server/tests/cases/send.rs
  • crates/feder-server/tests/cases/webfinger.rs
  • crates/feder-server/tests/common/mod.rs
  • crates/feder-server/tests/runtime.rs
  • crates/feder-server/tests/storage.rs
  • cspell.json
  • examples/single-user-server/Cargo.toml
  • examples/single-user-server/README.md
  • examples/single-user-server/src/main.rs
  • mise.toml
💤 Files with no reviewable changes (19)
  • crates/feder-runtime-server/tests/cases/webfinger.rs
  • crates/feder-runtime-server/src/lib.rs
  • crates/feder-runtime-server/tests/cases/actor.rs
  • crates/feder-runtime-server/src/inbox.rs
  • crates/feder-server/src/negotiation.rs
  • crates/feder-runtime-server/tests/cases/operation.rs
  • crates/feder-runtime-server/tests/cases/inbox.rs
  • crates/feder-runtime-server/src/app.rs
  • crates/feder-runtime-server/README.md
  • crates/feder-runtime-server/tests/cases/send.rs
  • crates/feder-runtime-server/src/operation.rs
  • crates/feder-runtime-server/src/config.rs
  • crates/feder-runtime-server/src/error.rs
  • crates/feder-runtime-server/src/storage/mod.rs
  • crates/feder-runtime-server/tests/cases/object.rs
  • crates/feder-runtime-server/tests/cases/followers.rs
  • crates/feder-runtime-server/src/storage/sqlite.rs
  • crates/feder-runtime-server/tests/cases/app.rs
  • crates/feder-runtime-server/tests/common/mod.rs

Comment on lines +81 to +104
#[must_use]
pub fn is_public_note(note: &Note) -> bool {
note.to
.iter()
.chain(note.cc.iter())
.any(|recipient| recipient.as_str() == PUBLIC_COLLECTION)
}

fn note_recipients(local_actor: &Actor, note: &Note) -> Vec<NoteRecipient> {
let mut recipients = Vec::new();

for address in note.to.iter().chain(note.cc.iter()) {
let recipient = if address.as_str() == PUBLIC_COLLECTION || address == &local_actor.id {
continue;
} else if local_actor.followers.as_ref() == Some(address) {
NoteRecipient::Followers(local_actor.id.clone())
} else {
NoteRecipient::Actor(address.clone())
};

if !recipients.contains(&recipient) {
recipients.push(recipient);
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Recognize the as:Public alias for public addressing.

is_public_note and note_recipients compare addresses only against the full IRI https://www.w3.org/ns/activitystreams#Public. ActivityStreams also defines the compact alias as:Public, which parses as a valid IRI and can appear in to or cc.

Two effects follow. is_public_note reports false, so crates/feder-server/src/object.rs returns 404 for a note the application addressed publicly. note_recipients also emits NoteRecipient::Actor("as:Public"), which the server then passes to the actor resolver as a delivery target.

Add a shared predicate so both functions treat the alias as public.

🐛 Proposed fix to recognize the public alias
+const PUBLIC_COLLECTION_ALIAS: &str = "as:Public";
+
+fn is_public_address(address: &Iri) -> bool {
+    matches!(address.as_str(), PUBLIC_COLLECTION | PUBLIC_COLLECTION_ALIAS)
+}
+
 #[must_use]
 pub fn is_public_note(note: &Note) -> bool {
     note.to
         .iter()
         .chain(note.cc.iter())
-        .any(|recipient| recipient.as_str() == PUBLIC_COLLECTION)
+        .any(is_public_address)
 }
 
 fn note_recipients(local_actor: &Actor, note: &Note) -> Vec<NoteRecipient> {
     let mut recipients = Vec::new();
 
     for address in note.to.iter().chain(note.cc.iter()) {
-        let recipient = if address.as_str() == PUBLIC_COLLECTION || address == &local_actor.id {
+        let recipient = if is_public_address(address) || address == &local_actor.id {
             continue;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
#[must_use]
pub fn is_public_note(note: &Note) -> bool {
note.to
.iter()
.chain(note.cc.iter())
.any(|recipient| recipient.as_str() == PUBLIC_COLLECTION)
}
fn note_recipients(local_actor: &Actor, note: &Note) -> Vec<NoteRecipient> {
let mut recipients = Vec::new();
for address in note.to.iter().chain(note.cc.iter()) {
let recipient = if address.as_str() == PUBLIC_COLLECTION || address == &local_actor.id {
continue;
} else if local_actor.followers.as_ref() == Some(address) {
NoteRecipient::Followers(local_actor.id.clone())
} else {
NoteRecipient::Actor(address.clone())
};
if !recipients.contains(&recipient) {
recipients.push(recipient);
}
}
const PUBLIC_COLLECTION_ALIAS: &str = "as:Public";
fn is_public_address(address: &Iri) -> bool {
matches!(address.as_str(), PUBLIC_COLLECTION | PUBLIC_COLLECTION_ALIAS)
}
#[must_use]
pub fn is_public_note(note: &Note) -> bool {
note.to
.iter()
.chain(note.cc.iter())
.any(is_public_address)
}
fn note_recipients(local_actor: &Actor, note: &Note) -> Vec<NoteRecipient> {
let mut recipients = Vec::new();
for address in note.to.iter().chain(note.cc.iter()) {
let recipient = if is_public_address(address) || address == &local_actor.id {
continue;
} else if local_actor.followers.as_ref() == Some(address) {
NoteRecipient::Followers(local_actor.id.clone())
} else {
NoteRecipient::Actor(address.clone())
};
if !recipients.contains(&recipient) {
recipients.push(recipient);
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/feder-core/src/note.rs` around lines 81 - 104, Add a shared
public-address predicate that recognizes both PUBLIC_COLLECTION and the compact
"as:Public" alias, then use it in is_public_note and note_recipients. Ensure
alias recipients are skipped as public addressing rather than reported as
NoteRecipient::Actor targets.

Comment on lines +56 to +60
let inbox = remote_actor
.endpoints
.as_ref()
.and_then(|endpoints| endpoints.shared_inbox.as_ref())
.unwrap_or(&remote_actor.inbox);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

ActivityPub sharedInbox only for public or followers addressed activities, personal inbox for directly addressed Follow

💡 Result:

The ActivityPub specification defines the sharedInbox as an optional endpoint designed for efficient, wide delivery of activities to multiple users on a single server [1][2]. Its intended use cases are limited to specific scenarios rather than direct, personal messaging [1][2]. SharedInbox Usage The sharedInbox is primarily used for two categories of activities: 1. Publicly addressed activities (those addressed to the Public special collection) [1][2]. 2. Activities sent to an actor's followers [1][2]. When an originating server sends these types of activities, it may choose to deliver a single copy of the activity to a recipient server's sharedInbox endpoint, rather than delivering individual copies to the personal inboxes of every follower on that server [1][2]. The receiving server is then responsible for distributing that activity internally to the appropriate users [1][2]. Personal Inbox for Direct Addressing Activities that are directly addressed to a specific actor (such as a direct message or a targeted notification) should be delivered to that actor's personal inbox endpoint [3][4]. Sending directly addressed activities to a sharedInbox is generally considered improper, as the sharedInbox is intended for optimized, wide-scale delivery (broadcasting to many) rather than point-to-point delivery [5][6]. Key Technical Considerations - Actor Model vs. Efficiency: The sharedInbox is an optimization that deviates from the pure Actor model of ActivityPub, as it shifts the responsibility of target identification from the sender to the receiver [5][6]. - Addressing Requirements: Even when using a sharedInbox for efficiency, the origin server remains obligated to deliver the activity to any intended recipients (specified in to, cc, etc.) who do not share that sharedInbox or would otherwise not receive the message [1][2]. - Privacy and Filtering: Because the sharedInbox is a shared endpoint, receiving servers must implement internal routing to ensure that private or followers-only content is only distributed to the authorized recipients [3][6]. The specification warns that sharedInbox endpoints should not be used to bypass these privacy constraints [3]. In practice, the sharedInbox is often criticized for its implementation complexity and the potential to act as a firehose, leading many developers to suggest that it should be reserved strictly for its intended optimizations—public posts and follower-only distribution—while personal interactions must remain directed to individual actor inboxes [7][6].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Files:"
git ls-files | rg 'crates/feder-server/src/(follow|note)\.rs|crates/feder-server/src/.+\.rs' | sed -n '1,120p'

echo
echo "follow outline:"
ast-grep outline crates/feder-server/src/follow.rs --view mini || true

echo
echo "follow.rs lines 1-140:"
cat -n crates/feder-server/src/follow.rs | sed -n '1,140p'

echo
echo "note relevant lines:"
cat -n crates/feder-server/src/note.rs | sed -n '80,125p'

echo
echo "Endpoint/shared_inbox definitions/usages:"
rg -n "struct .*Endpoint|shared_inbox|sharedInbox|followers|Follow|followers_url" crates/feder-server src crates -g '*.rs' | sed -n '1,220p'

Repository: fedify-dev/feder

Length of output: 26601


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Behavioral/static check: identify all ActivityPub delivery targets generated by Follow actor creation
# and whether the shared inbox is selected through Endpoint priority.
python3 - <<'PY'
from pathlib import Path
p = Path('crates/feder-server/src/follow.rs')
text = p.read_text()
for needle in [
    "remote_actor.endpoints.as_ref().and_then(|endpoints| endpoints.shared_inbox.as_ref()).unwrap_or(&remote_actor.inbox)",
    "follow_request.addresses.push(remote_actor.inbox.clone())",
    "follow_request.to.push(remote_actor.id.clone())",
]:
    print(f"contains {needle!r}: {needle in text}")

print("\nfollow.rs occurrences:")
for i,line in enumerate(text.splitlines(),1):
    if 'endpoints' in line or 'shared_inbox' in line or 'follow_request.addresses' in line or 'follow_request.to' in line:
        print(f"{i}: {line}")
PY

Repository: fedify-dev/feder

Length of output: 553


Deliver the Follow to the remote actor’s personal inbox.

follow_actor() prefers endpoints.shared_inbox for every Follow, and no recipients are collected from follower endpoints. ActivityPub shared inbox use is for public/follower-addressed activities; a targeted Follow should be sent to the actor’s personal inbox.

🐛 Proposed fix
-        let inbox = remote_actor
-            .endpoints
-            .as_ref()
-            .and_then(|endpoints| endpoints.shared_inbox.as_ref())
-            .unwrap_or(&remote_actor.inbox);
+        let inbox = &remote_actor.inbox;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let inbox = remote_actor
.endpoints
.as_ref()
.and_then(|endpoints| endpoints.shared_inbox.as_ref())
.unwrap_or(&remote_actor.inbox);
let inbox = &remote_actor.inbox;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/feder-server/src/follow.rs` around lines 56 - 60, Update
follow_actor’s inbox selection to always use remote_actor.inbox for the targeted
Follow, removing the shared_inbox fallback and any related endpoint preference.
Preserve the existing delivery flow while ensuring no follower endpoint
recipients are used.

Comment on lines +226 to +229
server
.storage()
.confirm_pending_follow(&pending)
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Use the bool returned by confirm_pending_follow.

confirm_pending_follow returns false when the exact relationship is no longer pending. That happens if a duplicate Accept, or a concurrent Undo, changed the row between load_pending_follow at line 208 and this call. The code discards the value, so a lost confirmation produces the same 202 Accepted as a successful one and leaves no trace.

Bind the result and log the false case.

♻️ Proposed change
-            server
-                .storage()
-                .confirm_pending_follow(&pending)
-                .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
+            let confirmed = server
+                .storage()
+                .confirm_pending_follow(&pending)
+                .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
+            if !confirmed {
+                tracing::debug!(
+                    follow_activity = %pending.follow_activity,
+                    "Accept did not confirm a pending Follow; the relationship changed concurrently"
+                );
+            }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
server
.storage()
.confirm_pending_follow(&pending)
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
let confirmed = server
.storage()
.confirm_pending_follow(&pending)
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
if !confirmed {
tracing::debug!(
follow_activity = %pending.follow_activity,
"Accept did not confirm a pending Follow; the relationship changed concurrently"
);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/feder-server/src/inbox.rs` around lines 226 - 229, Update the
confirmation flow in the inbox handler around confirm_pending_follow to bind its
returned bool and detect false. Log an appropriate diagnostic when the exact
pending relationship was not confirmed, while preserving the existing
internal-server-error handling for returned errors.

Comment on lines +601 to +611
let key_id = parameters.remove("keyid")?;
let algorithm = parameters.remove("algorithm")?;
let signed_headers = parameters
.remove("headers")?
.split_ascii_whitespace()
.map(str::to_ascii_lowercase)
.collect::<Vec<_>>();
let signature = parameters.remove("signature")?;
if key_id.is_empty() || signed_headers.is_empty() || signature.is_empty() {
return None;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

HTTP signatures draft-cavage hs2019 algorithm parameter deprecated ActivityPub Mastodon

💡 Result:

The hs2019 algorithm parameter, associated with the draft-cavage-http-signatures specification, is a deprecated, functionally vestigial placeholder that is not part of the finalized HTTP Message Signatures standard, RFC 9421 [1][2][3]. In the original draft-cavage-http-signatures (often referred to as cavage-12), hs2019 was introduced as a generic identifier that deferred the actual algorithm choice to metadata associated with the keyId [1][4]. When the IETF work on HTTP Message Signatures evolved, this approach was superseded. The final standard, RFC 9421, removed the need for such a placeholder because the new specification allows for more flexible, explicit algorithm negotiation that defaults to context-specific behavior, rendering hs2019 obsolete [3][5]. Regarding ActivityPub and Mastodon: 1. Status in Mastodon: Mastodon historically relied on draft-cavage-http-signatures for its security model [6][7]. While Mastodon has included support for hs2019 (often treating it as a fallback or equating it to rsa-sha256) to maintain compatibility with older implementations, it remains tethered to the cavage-12 draft [1][8][9]. Mastodon has been working on implementing support for RFC 9421 (the modern standard), but as of 2026, it maintains cavage-12 for broad compatibility with the wider Fediverse, which has been slow to migrate [10][11]. 2. Fediverse Ecosystem: cavage-12 remains the most widely supported version of HTTP Signatures across the Fediverse [1]. Because many projects have not yet implemented RFC 9421, the hs2019 placeholder is still frequently encountered in the wild [1][8]. However, it is explicitly not recommended for new implementations, which should aim to support the standardized RFC 9421 [1][2]. In summary, hs2019 is a legacy artifact from the drafting phase of HTTP signatures [3]. While it persists in the Fediverse due to the ubiquity of cavage-12 support, it has no place in a modern, standards-compliant implementation [1][2].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Files matching inbox/signature:"
fd -a 'inbox\.rs$' . || true

echo
echo "Relevant function outlines:"
ast-grep outline crates/feder-server/src/inbox.rs --match parse_signature_header --view expanded || true
ast-grep outline crates/feder-server/src/inbox.rs --match verify_signed_request --view expanded || true

echo
echo "Line counts:"
wc -l crates/feder-server/src/inbox.rs

echo
echo "Relevant source slices:"
sed -n '270,350p' crates/feder-server/src/inbox.rs | cat -n
sed -n '540,650p' crates/feder-server/src/inbox.rs | cat -n

echo
echo "Search for algorithm handling in repository:"
rg -n "algorithm|rsa-sha256|hs2019|created|expires|signature" crates/feder-server/src/inbox.rs crates -g '*.rs' | head -n 200

Repository: fedify-dev/feder

Length of output: 14975


🌐 Web query:

draft-cavage-http-signatures Signature Input String keyid headers algorithm signature optional parameters

💡 Result:

The draft-cavage-http-signatures specification (now superseded by RFC 9421) defined a method for signing HTTP messages by constructing a canonicalized "signing string" and including parameters in the Signature header [1][2]. Key Signature Parameters: keyId (Required): A string identifier for the key material that the verifier uses to validate the signature [3][4]. signature (Required): The base 64 encoded digital signature [1]. It is created by signing the canonicalized "signing string" using the algorithm specified and the key identified by keyId [1][5]. algorithm (Recommended): Specifies the signature string construction mechanism and the cryptographic algorithm (e.g., rsa-sha256) [1][3]. Implementers are encouraged to derive the specific algorithm from the key metadata associated with the keyId rather than relying solely on this field [1][4]. headers (Optional): A lowercased, quoted list of HTTP header fields to be included in the signature [1][6]. If omitted, implementations act as if it were set to (created) [1][4]. The order of headers in this list is critical because it dictates the concatenation order used in the signing string construction [1][6]. Additional Optional Parameters: created: A Unix timestamp integer indicating when the signature was created [6]. expires: A Unix timestamp integer indicating when the signature ceases to be valid [6]. It is important to note that this draft has been replaced by RFC 9421, which introduced the Signature-Input header to separate signature metadata from the signature bytes themselves [2][7][8].

Citations:


🌐 Web query:

draft-cavage-http-signatures hs2019 algorithm parameter generic placeholder required optional

💡 Result:

In the context of the draft-cavage-http-signatures specification (specifically later versions like -11 and -12), the hs2019 algorithm identifier was introduced as an active, recommended mechanism [1][2][3]. Its primary purpose was to indicate that the digital signature algorithm should be derived from the key metadata associated with the keyId, rather than being explicitly defined by the algorithm parameter itself [1][4][5]. Regarding your query on the algorithm parameter and generic placeholder status: 1. Required vs. Optional: In the draft-cavage-http-signatures specification, the algorithm parameter is generally used to specify the signature string construction mechanism [1][3]. While the specification notes it as RECOMMENDED, the introduction of hs2019 was specifically designed to handle cases where the actual cryptographic algorithm is determined by external key metadata [1][5]. 2. Generic Placeholder: hs2019 functioned as a "pseudo-algorithm" or placeholder [6][7]. By using hs2019, an implementation signaled that it would ignore a specific hardcoded algorithm in the signature header and instead determine the correct signing algorithm (such as RSASSA-PSS, ECDSA, or Ed25519) from the associated key metadata [1][4][8]. This was an intentional design to avoid the security risks associated with previous implementations that relied solely on the algorithm parameter for determining the signature type [1][2]. 3. Evolution and Current Status: The draft-cavage-http-signatures work was eventually superseded by the IETF's HTTP Message Signatures work (published as RFC 9421) [9]. In the successor specification, the explicit requirement for an algorithm parameter was removed, making the behavior previously offered by hs2019 (deriving the algorithm from context or key metadata) the new default standard [6]. Consequently, hs2019 was removed as an explicit identifier in later versions of the formal HTTP Message Signatures specification [6].

Citations:


Accept missing or hs2019 in algorithm.

verify_signed_request requires algorithm="rsa-sha256" at line 327, and parse_signature_header rejects a missing parameter at line 602. Later draft-cavage makes algorithm recommended, deprecates concrete algorithm tokens, and uses hs2019 to indicate the key determines the algorithm. Accept a missing algorithm as unspecified, or map algorithm="hs2019" to the same RSA-SHA256 verification already used for "rsa-sha256", so valid RSA signatures are not rejected.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/feder-server/src/inbox.rs` around lines 601 - 611, The signature
parsing and verification flow around parse_signature_header and
verify_signed_request must accept an omitted algorithm and algorithm="hs2019"
for RSA signatures. Treat either case as the existing RSA-SHA256 verification
path while preserving current handling for explicit rsa-sha256 and rejecting
unsupported algorithms.

Comment on lines +87 to +93
// for development
pub fn with_outbound_address_policy(
actors: A,
storage: S,
handle_host: impl Into<String>,
policy: OutboundAddressPolicy,
) -> Result<Self, Error> {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Document the security effect of this public constructor.

with_outbound_address_policy accepts OutboundAddressPolicy::AllowPrivateAddress, which disables the SSRF protection described in config.rs Line 22. The only marker is the bare comment // for development. That comment does not appear in rustdoc, so consumers of the published crate see an unmarked constructor next to new.

Add a doc comment that states the risk. If the constructor is intended only for development and tests, also consider placing it behind a cargo feature such as insecure-dev so production builds cannot reach it.

🛡️ Proposed change
-    // for development
+    /// Creates a server with an explicit outbound address policy.
+    ///
+    /// # Security
+    ///
+    /// [`OutboundAddressPolicy::AllowPrivateAddress`] disables SSRF
+    /// protection for outbound requests. Use it only in development and
+    /// tests. Use [`FederServer::new`] in production.
     pub fn with_outbound_address_policy(
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// for development
pub fn with_outbound_address_policy(
actors: A,
storage: S,
handle_host: impl Into<String>,
policy: OutboundAddressPolicy,
) -> Result<Self, Error> {
/// Creates a server with an explicit outbound address policy.
///
/// # Security
///
/// [`OutboundAddressPolicy::AllowPrivateAddress`] disables SSRF
/// protection for outbound requests. Use it only in development and
/// tests. Use [`FederServer::new`] in production.
pub fn with_outbound_address_policy(
actors: A,
storage: S,
handle_host: impl Into<String>,
policy: OutboundAddressPolicy,
) -> Result<Self, Error> {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/feder-server/src/lib.rs` around lines 87 - 93, Add rustdoc to the
public constructor with_outbound_address_policy describing that
OutboundAddressPolicy::AllowPrivateAddress disables SSRF protection and should
be limited to development or tests; replace or supplement the non-rustdoc marker
so published crate consumers see the warning. If the project’s feature structure
supports it, gate this constructor behind an insecure-dev cargo feature to
prevent access in production builds.

Comment on lines +260 to +264
ON CONFLICT(follow_activity_id) DO UPDATE SET
local_actor_id = excluded.local_actor_id,
remote_actor_json = excluded.remote_actor_json,
state = 'pending'
"#,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Do not reset an accepted Follow to pending.

A retry with the same follow_activity_id enters this conflict branch. Line 263 changes an already accepted relationship back to pending. This permits later Accept processing to treat the old Follow as pending again.

Preserve an existing record on conflict. Use DO NOTHING, or update fields only when the stored state is already pending.

Proposed fix
-            ON CONFLICT(follow_activity_id) DO UPDATE SET
-                local_actor_id = excluded.local_actor_id,
-                remote_actor_json = excluded.remote_actor_json,
-                state = 'pending'
+            ON CONFLICT(follow_activity_id) DO NOTHING
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
ON CONFLICT(follow_activity_id) DO UPDATE SET
local_actor_id = excluded.local_actor_id,
remote_actor_json = excluded.remote_actor_json,
state = 'pending'
"#,
ON CONFLICT(follow_activity_id) DO NOTHING
"#,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/feder-server/src/storage/sqlite.rs` around lines 260 - 264, Update the
SQLite upsert for follow activities so a conflict on follow_activity_id never
changes an existing accepted relationship back to pending. Replace the
unconditional state update with DO NOTHING, or condition updates so they apply
only when the stored state is already pending, while preserving the current
insert behavior.

Comment on lines +98 to +104
let server = FederServer::with_outbound_address_policy(
dispatcher,
storage,
HANDLE_HOST,
OutboundAddressPolicy::AllowPrivateAddress,
)?
.with_inbox_auth_policy(InboxAuthPolicy::AllowUnsignedInsecureDev);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C 3 \
  'AllowPrivateAddress|AllowUnsignedInsecureDev|PublicOnly|RequireSigned' \
  examples/single-user-server/src/main.rs \
  crates/feder-server/src/config.rs \
  crates/feder-server/src/inbox.rs

Repository: fedify-dev/feder

Length of output: 2562


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== single-user-server main outline =="
ast-grep outline examples/single-user-server/src/main.rs || true

echo "== relevant single-user-server main =="
cat -n examples/single-user-server/src/main.rs | sed -n '1,140p'

echo "== config.rs =="
cat -n crates/feder-server/src/config.rs | sed -n '1,220p'

echo "== inbox auth policy usage =="
cat -n crates/feder-server/src/inbox.rs | sed -n '150,210p'

echo "== outbound address policy usages =="
rg -n -C 4 'outbound_address_policy|OutboundAddressPolicy|verify_signed_request|allow_unsigned' crates/feder-server examples -g '*.rs'

Repository: fedify-dev/feder

Length of output: 29055


Use secure defaults for the example server.

examples/single-user-server/src/main.rs:102-104 disables SSRF protection and HTTP signature verification. An unsigned Follow with a caller-controlled actor IRI can then be resolved through private network addresses when the default policy accepts unsigned local-federation activities.

Use OutboundAddressPolicy::PublicOnly, apply the default InboxAuthPolicy::RequireSigned, and make private-address local-federation settings an explicit opt-in.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/single-user-server/src/main.rs` around lines 98 - 104, Update the
example server construction using FederServer::with_outbound_address_policy to
use OutboundAddressPolicy::PublicOnly and the default
InboxAuthPolicy::RequireSigned instead of permitting private addresses or
unsigned insecure development requests. Keep private-address access and unsigned
local-federation behavior available only through explicit opt-in configuration.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant