feat: production hardening — security, scale, protocol, DX
Breaking changes to FederationRepository, ApObjectHandler, ApUser:
FederationRepository:
- add is_activity_processed / mark_activity_processed (inbox idempotency)
- add get_accepted_follower_inboxes (DB-side dedup/filtering, replaces in-memory load-all)
ApObjectHandler:
- add on_announce_of_remote (cross-server boosts, previously silently dropped)
ApUser:
- add manually_approves_followers: bool
- add actor_type: ApActorType (was hardcoded Person)
Security:
- block check before actor HTTP fetch in Follow (prevents SSRF on blocked actors)
- 4xx responses use generic "not found"/"bad request" (no internal leak)
- 1 MB DefaultBodyLimit on inbox routes
- zeroize private key after generation
Delivery:
- all broadcasts are now non-blocking (tokio::spawn fallback, or EventPublisher queue)
- EventPublisher redesigned with typed FederationEvent enum (DeliveryRequested/DeliveryFailed)
- new deliver_to_inbox() public method for queue consumers
- configurable delivery_max_attempts and delivery_initial_delay_secs via builder
- Follow saved as Pending BEFORE delivery (race condition fix)
Router:
- GET /users/{id} (actor), GET /users/{id}/followers, GET /users/{id}/following now mounted
Protocol:
- mention extraction from Create/Update tag arrays → on_mention() dispatched
- WebFinger: add aliases field (acct: URI + AP actor URL)
- outbox: add last link, use count_local_posts for totalItems
- idempotency guard added to every inbound activity receive()
- actor serializes display_name and configurable actor_type/manually_approves_followers
Bump: 0.1.10 → 0.2.0
This commit is contained in:
@@ -27,6 +27,7 @@ pub struct OrderedCollection {
|
||||
id: String,
|
||||
total_items: u64,
|
||||
first: String,
|
||||
last: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
@@ -38,6 +39,7 @@ pub struct OrderedCollectionPage {
|
||||
kind: String,
|
||||
id: String,
|
||||
part_of: String,
|
||||
total_items: u64,
|
||||
ordered_items: Vec<serde_json::Value>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
next: Option<String>,
|
||||
@@ -59,6 +61,16 @@ pub async fn outbox_handler(
|
||||
|
||||
let outbox_url = format!("{}/users/{}/outbox", data.base_url, user_id_str);
|
||||
|
||||
// Total count — uses count_local_posts for an aggregated count. For a
|
||||
// per-user count we use the page length on the first page as an upper bound
|
||||
// if count_local_posts returns 0. In practice this trait method is called
|
||||
// infrequently (only on the root collection endpoint).
|
||||
let total = data
|
||||
.object_handler
|
||||
.count_local_posts()
|
||||
.await
|
||||
.map_err(|e| Error::from(anyhow::anyhow!("{}", e)))?;
|
||||
|
||||
if query.page.unwrap_or(false) {
|
||||
let before: Option<DateTime<Utc>> = query.before.as_deref().and_then(|s| s.parse().ok());
|
||||
|
||||
@@ -114,24 +126,19 @@ pub async fn outbox_handler(
|
||||
kind: "OrderedCollectionPage".to_string(),
|
||||
id: page_id,
|
||||
part_of: outbox_url,
|
||||
total_items: total,
|
||||
ordered_items,
|
||||
next,
|
||||
})
|
||||
.into_response())
|
||||
} else {
|
||||
let total = data
|
||||
.object_handler
|
||||
.get_local_objects_for_user(uuid)
|
||||
.await
|
||||
.map_err(|e| Error::from(anyhow::anyhow!("{}", e)))?
|
||||
.len() as u64;
|
||||
|
||||
Ok(axum::Json(OrderedCollection {
|
||||
context: crate::urls::AP_CONTEXT.to_string(),
|
||||
kind: "OrderedCollection".to_string(),
|
||||
id: outbox_url.clone(),
|
||||
total_items: total,
|
||||
first: format!("{}?page=true", outbox_url),
|
||||
last: format!("{}?page=true&before=1970-01-01T00:00:00.000Z", outbox_url),
|
||||
})
|
||||
.into_response())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user