update CLI et documentations

EBO <eric.bouhana@softalys.com> committé le 2026-05-15 13:57

49e01a66811d1c725990177566cee671b47c4893

1 parent(s)

3 fichiers modifiés +958 -4
M docs/SERVICES_REFERENCE.md
+315 -1
@@ -29,6 +29,7 @@
29 29 18. [Middleware (CSRF, auth, rate limit)](#18-middleware-csrf-auth-rate-limit)
30 30 19. [Utilitaires](#19-utilitaires)
31 31 20. [Patterns communs](#20-patterns-communs)
32 +21. [Fédération ActivityPub (features `fediverse*`)](#21-federation-activitypub-features-fediverse)
32 33
33 34 ---
34 35
@@ -209,6 +210,16 @@ async fn handler(user: AuthUser) -> impl IntoResponse {
209 210 | `exp` | `i64` | Timestamp d'expiration |
210 211 | `iat` | `i64` | Timestamp d'emission |
211 212 | `iss` | `String` | Issuer |
213 +| `identity_kind` | `IdentityKind` | `Local` (user en DB) ou `Remote` (identité fédérée). Sérialisé `"local"` / `"remote"`. **RFC 0007.** |
214 +| `remote_id` | `Option<Uuid>` | Si `identity_kind == Remote` : ID de la row `remote_identities`. Sinon `None`. |
215 +
216 +**Enum `IdentityKind`** (module `rustwarden_core::identity`, exporté via prelude) :
217 +
218 +```rust
219 +pub enum IdentityKind { Local, Remote }
220 +```
221 +
222 +Permet au middleware d'auth de différencier une session humaine locale d'une session AP-native (un user d'une instance distante connecté via le flow `/fediverse/auth/*`). Voir aussi [`Identity`](#21-federation-activitypub-features-fediverse) — abstraction unifiant les deux types pour les services qui n'ont pas besoin de la distinction.
212 223
213 224 ---
214 225
@@ -499,7 +510,9 @@ pub enum Principal {
499 510
500 511 **Modele `resource_share::Model`** :
501 512 - `id: Uuid`, `resource_id: Uuid`, `permission_level: String`, `shared_by_user_id: Uuid`, `created_at`
502 -- `shared_with_user_id: Option<Uuid>` et `shared_with_team_id: Option<Uuid>` — exactement un des deux est `Some`.
513 +- `shared_with_user_id: Option<Uuid>`, `shared_with_team_id: Option<Uuid>`, `shared_with_remote_identity_id: Option<Uuid>` — **exactement un des trois** est `Some` (contrainte CHECK + 3 partial unique indexes). La 3ᵉ colonne est ajoutée par la migration `m20260510_000014` (RFC 0008) et reste à `None` sur tout déploiement sans fédération.
514 +
515 +> **Cross-instance** : `Principal` n'est **pas** étendu — un partage avec une identité fédérée passe par l'écriture directe d'une row `resource_share { shared_with_remote_identity_id: Some(remote_id) }` (RFC 0009). L'API helper côté framework n'est pas encore figée ; pour l'instant les apps consommatrices écrivent via SeaORM en attendant une RFC dédiée. Le `effective_permission_for_user` reste user-only — les remote shares se résolvent côté handler fédération.
503 516
504 517 **Exemple d'utilisation dans un projet consommateur** :
505 518
@@ -544,12 +557,18 @@ Groupes d'utilisateurs avec membership, owner, role par membre. Un `Principal::T
544 557 | `find_by_id` | `(db, team_id) → Result<Option<team::Model>>` | Recherche par ID. |
545 558 | `find_by_slug` | `(db, slug: &TeamSlug) → Result<Option<team::Model>>` | Recherche par slug. |
546 559 | `add_member` | `(db, team_id, user_id, role: TeamRole, hooks) → Result<team_member::Model>` | **Idempotent** : update le role si l'edge existe, insere sinon. Cable `on_team_member_added`. |
560 +| `add_member_identity` | `(db, team_id, identity: Identity, role: TeamRole, hooks) → Result<team_member::Model>` | **RFC 0009** — variante Identity-aware. Pour `Identity::Local` délègue à `add_member` ; pour `Identity::Remote(remote_id)` insère une row avec `user_id = NULL` et `remote_identity_id = Some(remote_id)`. L'unicité est garantie par `team_members_remote_unique` (partial index, migration `m20260510_000015`). |
561 +| `teams_of_identity` | `(db, identity: Identity) → Result<Vec<team::Model>>` | **RFC 0009** — variante Identity-aware de `teams_of_user`. |
547 562 | `remove_member` | `(db, team_id, user_id, hooks) → Result<()>` | Supprime un membre. **Refuse de retirer l'owner** (`AppError::Validation`) — utiliser `delete` pour drop la team. Cable `on_team_member_removed`. |
548 563 | `list_members` | `(db, team_id) → Result<Vec<team_member::Model>>` | Liste les membres, tri desc par `joined_at`. |
549 564 | `teams_of_user` | `(db, user_id) → Result<Vec<team::Model>>` | Les teams dont le user est membre (inner join). |
550 565 | `team_ids_of_user` | `(db, user_id) → Result<Vec<Uuid>>` | Juste les IDs — helper perf pour la resolution de permission. |
551 566 | `delete` | `(db, team_id, hooks) → Result<()>` | Hard-delete la team. Cascade DB sur `team_members` + `resource_shares` avec ce team comme principal. Cable `on_team_deleted`. |
552 567
568 +> **PK relâchée pour la fédération** : depuis la migration `m20260510_000016`, la PK historique `(team_id, user_id)` est remplacée par une PK synthétique `id: Uuid`, et `team_members.user_id` devient `NULLABLE` (avec `pgcrypto` activé). Indispensable pour porter une identité fédérée (`Identity::Remote`) sans utilisateur local. Les apps qui requêtent directement `team_members` doivent tolérer `user_id IS NULL`.
569 +
570 +> **Invitations cross-instance** : voir [§21.6](#216-fediverseinviteservice) — `FediverseInviteService` gère le flow d'invitation `Invite{Membership}` → `Accept` / `Reject` avec les 6 endpoints `/api/v1/teams/{id}/invites/*` et `/api/v1/fediverse/invites/*`.
571 +
553 572 ### `TeamSlug` (value object)
554 573
555 574 **Module** : `rustwarden_core::types::team_slug`
@@ -932,6 +951,8 @@ Installation : `RustwardenApp::builder().hooks(Arc::new(MyHooks)).build()?`.
932 951
933 952 > **Integration consommateur** : voir [DEVELOPER.md §12 — Hooks](../DEVELOPER.md#hooks--intercepter-les-evenements) pour l'exemple complet d'un `AuditHooks` implementant les 4 hooks teams.
934 953
954 +> **Hook fédération distinct** : `RustwardenHooks` couvre les événements du framework cœur. Les événements fédération (signatures invalides, replay, SSRF, abus inbox, challenges d'auth, décisions d'invitation) passent par un trait dédié [`FediverseEventObserver`](#218-fediverseeventobserver) — voir §21.8. Les deux traits coexistent sans interférence : un même `Arc<MyObserver>` peut implémenter les deux si vous voulez un sink unique.
955 +
935 956 ---
936 957
937 958 ## 18. Middleware (CSRF, auth, rate limit)
@@ -1098,3 +1119,296 @@ let layout = LayoutContext::from_session(&session, "Dashboard").await;
1098 1119 ```
1099 1120
1100 1121 Cela force la reconnexion sur tous les appareils apres un changement de mot de passe.
1122 +
1123 +---
1124 +
1125 +## 21. Fédération ActivityPub (features `fediverse*`)
1126 +
1127 +> **Statut** : `experimental-fediverse` — l'API publique peut casser sans bump majeur jusqu'à promotion stable (cf. [`fediverse-consumer-guide.md`](fediverse-consumer-guide.md#6-statut-experimental-fediverse--regles-de-versioning)). RFCs [0001-0025](rfcs/README.md).
1128 +>
1129 +> Pour le **câblage** (`main.rs`, features Cargo, migrations, env vars), voir [`fediverse-consumer-guide.md`](fediverse-consumer-guide.md). Cette section couvre uniquement les **APIs publiques** des services exposés.
1130 +
1131 +### Périmètre & crates
1132 +
1133 +| Crate | Feature app | Rôle |
1134 +|---|---|---|
1135 +| `rustwarden-fediverse` | `fediverse` | Protocole ActivityPub headless (services, routes, signatures). Aucune UI. |
1136 +| `rustwarden-fediverse-ui` | `fediverse-ui` (⇒ `fediverse`) | UI HTML de référence (handlers admin/user, templates Askama, dashboard card). |
1137 +| `rustwarden-fediverse-guard` | `fediverse-guard` (⇒ `fediverse`) | Détection d'abus runtime, ban manager, observer (RFC 0028). |
1138 +
1139 +Aucun re-export via `rustwarden_core::prelude`. Importez explicitement depuis `rustwarden_fediverse::*`.
1140 +
1141 +### 21.1 `FediverseConfig`
1142 +
1143 +**Module** : `rustwarden_fediverse::config`
1144 +
1145 +Configuration immuable de la fédération, chargée au démarrage. **RFC 0001.**
1146 +
1147 +| Méthode | Signature | Description |
1148 +|---|---|---|
1149 +| `from_env` | `() → Result<Self, FediverseConfigError>` | Charge depuis l'environnement. **Refuse les placeholders** (`example.com`, `localhost.localdomain`, `your.tld`) et exige HTTPS sauf si `FEDIVERSE_DEBUG_LOCAL_FEDERATION=true`. |
1150 +| `domain` | `(&self) → &str` | Domaine canonique de l'instance. |
1151 +| `base_url` | `(&self) → &Url` | URL de base (`https://{domain}/`). |
1152 +| `user_agent` | `(&self) → &str` | UA envoyé sur les requêtes sortantes. |
1153 +| `authorized_fetch` | `(&self) → bool` | Mode authorized-fetch (récupération signée d'acteurs distants). |
1154 +| `outbound_workers` | `(&self) → usize` | Nombre de workers outbound (réservé). |
1155 +| `outbound_max_attempts` | `(&self) → u32` | Default 8 tentatives avant `Dead`. |
1156 +| `outbound_request_timeout` | `(&self) → Duration` | Default 30 s par tentative. |
1157 +| `fetch_actor_cache_ttl` | `(&self) → Duration` | Default 24 h sur `remote_identities`. |
1158 +| `auth_challenge_ttl` | `(&self) → Duration` | Default 5 min sur un challenge AP. |
1159 +| `debug_local_federation` | `(&self) → bool` | **Ne jamais `true` en prod** — autorise HTTP et désactive certains contrôles. |
1160 +
1161 +Variables d'environnement : voir [`fediverse.md` §3](fediverse.md#3-variables-denvironnement).
1162 +
1163 +### 21.2 `FediverseTrustService`
1164 +
1165 +**Module** : `rustwarden_fediverse::services::fediverse_trust_service`
1166 +
1167 +Allowlist + blocklist + mode de confiance, persistés en DB (`fediverse_trusted_domains`, `fediverse_blocked_domains`, `app_settings`). **RFC 0004.**
1168 +
1169 +| Méthode | Signature | Description |
1170 +|---|---|---|
1171 +| `new` | `(db) → Self` | Construit le service. |
1172 +| `is_enabled` | `(&self) → Result<bool>` | Lit `app_settings.fediverse_enabled`. |
1173 +| `trust_mode` | `(&self) → Result<TrustMode>` | `Allowlist` (défaut, zero-trust) ou `OpenWithBlocklist` (Mastodon-like). |
1174 +| `interop_scope` | `(&self) → Result<InteropScope>` | Périmètre interopérable (auth-only / auth+invites / full). |
1175 +| `is_domain_allowed` | `(&self, domain: &str) → Result<bool>` | Décide si un domaine peut fédérer selon le mode courant. |
1176 +| `assert_domain_allowed` | `(&self, domain: &str) → Result<()>` | Variante errorante. À utiliser dans les handlers entrants. |
1177 +| `add_trusted_domain` | `(&self, domain: &str, label: Option<&str>, scope: Option<InteropScope>) → Result<()>` | Ajoute à l'allowlist. |
1178 +| `remove_trusted_domain` | `(&self, domain: &str) → Result<bool>` | Retire de l'allowlist (retourne `true` si supprimé). |
1179 +| `list_trusted_domains` | `(&self) → Result<Vec<TrustedDomain>>` | Liste complète. |
1180 +| `add_blocked_domain` | `(&self, domain: &str, reason: &str) → Result<()>` | Ajoute à la blocklist. |
1181 +| `remove_blocked_domain` | `(&self, domain: &str) → Result<bool>` | Retire de la blocklist. |
1182 +| `list_blocked_domains` | `(&self) → Result<Vec<BlockedDomain>>` | Liste complète. |
1183 +
1184 +### 21.3 `ActorKeyService`
1185 +
1186 +**Module** : `rustwarden_fediverse::services::actor_key_service`
1187 +
1188 +Génération / lecture / persistence des clés RSA-2048 d'acteur local, chiffrées avec `ENCRYPTION_KEY` (AES-GCM). **RFC 0005.**
1189 +
1190 +| Méthode | Signature | Description |
1191 +|---|---|---|
1192 +| `generate` | `() → Result<ActorKeyMaterial>` | Génère une paire RSA-2048 (PEM public + PEM privé chiffré). ~100 ms CPU. |
1193 +| `public_key_for_user` | `(db, user_id: Uuid) → Result<Option<String>>` | Retourne le PEM public si déjà persisté, sinon `None`. |
1194 +| `ensure_keys` | `(db, user_id: Uuid) → Result<ActorKeyMaterial>` | Idempotent : génère + persiste si absent, retourne l'existant sinon. Appelé à la première requête `/users/{u}` ou `webfinger`. |
1195 +
1196 +**Sécurité** : sans `ENCRYPTION_KEY` (64 hex), `ensure_keys` refuse de générer. Aucun écriture en clair de clé privée en DB.
1197 +
1198 +### 21.4 `FediverseOutboundService` + worker
1199 +
1200 +**Module** : `rustwarden_fediverse::services::fediverse_outbound_service`
1201 +
1202 +File d'activités sortantes avec retry exponentiel (5 min → 30 min → 2 h → 6 h → 24 h → 48 h, cap 8). **RFC 0010, 0011, 0019.**
1203 +
1204 +| Méthode | Signature | Description |
1205 +|---|---|---|
1206 +| `FediverseOutboundService::enqueue` | `(activity: serde_json::Value, target_inbox: &str, target_domain: &str) → Result<Uuid>` | Insère une row `Pending` dans `fediverse_outbound_queue`. Retourne l'`activity_id`. |
1207 +| `FediverseOutboundService::drain_one<T: OutboundDelivery>` | `(&self, transport: &T) → Result<Option<Outcome>>` | Pop la plus ancienne row prête à livrer, tente avec `transport`, met à jour `Delivered` / `Pending` (avec `next_retry_at`) / `Dead`. Utilisée par le worker. |
1208 +| `OutboundStatus::next_retry_offset` | `(attempts: i32) → Duration` | Pure function. Expose le ladder de retry pour les tests / l'UI. |
1209 +| `OutboundWorker::new` | `(db, transport: T, poll_interval: Duration) → Self` | Construit le worker (T implémente `OutboundDelivery`). |
1210 +| `OutboundWorker::run` | `(self) → impl Future<Output = ()>` | Boucle infinie ; à lancer dans `tokio::spawn`. |
1211 +
1212 +**Trait `OutboundDelivery`** :
1213 +
1214 +```rust
1215 +#[async_trait]
1216 +pub trait OutboundDelivery: Send + Sync {
1217 + async fn deliver(&self, item: &fediverse_outbound_queue::Model) -> Result<(), AppError>;
1218 +}
1219 +```
1220 +
1221 +Deux implémentations livrées :
1222 +
1223 +| Implémentation | Module | Usage |
1224 +|---|---|---|
1225 +| `NoopDelivery` | `services::fediverse_outbound_service` | Logue + return Ok. Tests / dev. |
1226 +| `HttpSignedDelivery` | `http_signatures` | Signe RSA-SHA256 draft-cavage avec la clé privée du local actor, POST réel sur `target_inbox`. **À utiliser en prod.** |
1227 +
1228 +**Observabilité** : 4 événements `tracing` sous target `rustwarden::fediverse::outbound` : `delivered` / `deferred` / `dead` / `drain_failed`. Champs : `activity_id`, `target_inbox`, `target_domain`, `attempts`, `max_attempts`, `last_error`, `next_retry_at` (RFC 0019).
1229 +
1230 +### 21.5 `FediverseSignalService`
1231 +
1232 +**Module** : `rustwarden_fediverse::services::fediverse_signal_service`
1233 +
1234 +Application des signaux lifecycle entrants sur l'état local (cache `remote_identities`, memberships, JWT blacklist). **RFC 0010.**
1235 +
1236 +| Méthode | Signature | Description |
1237 +|---|---|---|
1238 +| `apply_update_actor` | `(db, actor: UpdatedActor) → Result<remote_identity::Model>` | Upsert dans `remote_identities` (insert si inconnu, update sinon). Retourne la row finale. |
1239 +| `apply_delete_actor` | `(db, ap_id: &str) → Result<DeleteOutcome>` | Marque `disabled_at = NOW()`. `DeleteOutcome.rows_affected` indique 0 ou 1. |
1240 +| `apply_delete_actor_with_iterator` | `(db, ap_id, iter: &dyn ActiveTokenIterator) → Result<DeleteOutcome>` | Variante qui blackliste **en cascade** tous les JWT remote portant ce `remote_id` (RFC 0018). Le consommateur injecte un `ActiveTokenIterator` (cf. `rustwarden_core::services::jwt_blacklist_service`) pour énumérer les JWT actifs. |
1241 +| `apply_revoke_membership` | `(db, team_id, remote_identity_id) → Result<bool>` | Drop la row `team_members` correspondante. Retourne `true` si une row a été supprimée. |
1242 +
1243 +Les trois `apply_*` sont idempotents et sûrs en concurrence : un même signal rejoué ne provoque pas d'effet de bord supplémentaire.
1244 +
1245 +### 21.6 `FediverseInviteService`
1246 +
1247 +**Module** : `rustwarden_fediverse::services::fediverse_invite_service`
1248 +
1249 +State machine des invitations cross-instance (équipes locales → identité fédérée). **RFC 0009, 0014.**
1250 +
1251 +| Méthode | Signature | Description |
1252 +|---|---|---|
1253 +| `invite_remote` | `(db, team_id, invited_handle: &str, role: TeamRole, invited_by_user_id: Uuid) → Result<fediverse_pending_invite::Model>` | Résout `@user@domain`, crée une row `fediverse_pending_invites` (`Pending`), prépare l'`Invite{Membership}` AP à enqueue vers l'inbox de l'identité distante. |
1254 +| `accept` | `(db, invite_id, remote_identity_id) → Result<team_member::Model>` | L'identité distante accepte. Bascule `Accepted`, insère la row `team_members` avec `remote_identity_id`, déclenche `on_team_member_added`. |
1255 +| `reject` | `(db, invite_id) → Result<fediverse_pending_invite::Model>` | Bascule `Rejected`. Transition autorisée seulement depuis `Pending`. |
1256 +| `revoke` | `(db, invite_id) → Result<fediverse_pending_invite::Model>` | Bascule `Revoked` (annulation par l'équipe inviteuse). |
1257 +| `list_pending_for_team` | `(db, team_id) → Result<Vec<fediverse_pending_invite::Model>>` | Vue admin équipe. |
1258 +| `list_pending_for_handle` | `(db, handle: &str) → Result<Vec<fediverse_pending_invite::Model>>` | Vue user (sa boîte d'invitations entrantes). |
1259 +
1260 +Endpoints HTTP correspondants : 6 routes sous `/api/v1/teams/{id}/invites/*` et `/api/v1/fediverse/invites/*` montées par `router_with_auth` (cf. §21.13).
1261 +
1262 +### 21.7 `FediverseAuthService`
1263 +
1264 +**Module** : `rustwarden_fediverse::services::fediverse_auth_service`
1265 +
1266 +State machine du flow d'authentification AP-native : un user d'une instance distante se logue chez vous. **RFC 0006, 0013, 0025.**
1267 +
1268 +| Méthode | Signature | Description |
1269 +|---|---|---|
1270 +| `initiate` | `(db, request: InitiateRequest, ttl: Duration) → Result<InitiatedChallenge>` | Démarre un challenge sortant : résout l'acteur dérivé du `handle`, génère un nonce, persiste `fediverse_auth_challenges` (TTL fourni). Retourne le challenge prêt à diffuser. |
1271 +| `record_decision` | `(db, challenge_id, decision: ChallengeDecision) → Result<fediverse_auth_challenge::Model>` | L'IdP (acteur du user) consent ou refuse. Bascule l'état local ; l'émission de l'`AuthApproval` / `AuthDenial` AP est faite par le handler appelant. |
1272 +| `persist_inbound_request` | `(db, body: &serde_json::Value, idp_domain: &str) → Result<fediverse_auth_challenge::Model>` | Réception d'un `AuthRequest` AP signé. Parse le payload, vérifie la cible, persiste pour décision du user local via consent UI. |
1273 +| `record_inbound_decision` | `(db, body: &serde_json::Value) → Result<()>` | Réception d'une réponse AP (`AuthApproval` / `AuthDenial`) signée du back-channel (RFC 0025). Met à jour le challenge correspondant. |
1274 +| `status` | `(db, challenge_id) → Result<fediverse_auth_challenge::Model>` | Poll pour le client SPA : retourne la row courante (état + métadonnées). |
1275 +| `consume` | `(db, config: &AuthConfig, challenge_id) → Result<ConsumedSession>` | Échange un challenge approuvé contre un JWT remote (`identity_kind=Remote`, `remote_id` peuplé). Marque le challenge `Consumed` (one-shot). |
1276 +
1277 +Endpoints HTTP : `/api/v1/auth/login/fediverse/initiate`, `/fediverse/auth/inbox`, `/api/v1/fediverse/auth/consent`, `/api/v1/auth/login/fediverse/status`, `/api/v1/auth/login/fediverse/consume`.
1278 +
1279 +### 21.8 `FediverseEventObserver`
1280 +
1281 +**Module** : `rustwarden_fediverse::observer`
1282 +
1283 +Trait d'observation des événements fédération. Hook unique côté framework, branché par `axum::Extension` sur le routeur. **Distinct de `RustwardenHooks`** (qui couvre le cœur).
1284 +
1285 +```rust
1286 +#[async_trait]
1287 +pub trait FediverseEventObserver: Send + Sync {
1288 + async fn on_signature_failure(
1289 + &self, _domain: &str, _actor_uri: Option<&str>, _reason: &str,
1290 + ) {}
1291 + async fn on_replay_attempt(
1292 + &self, _domain: &str, _actor_uri: Option<&str>, _details: serde_json::Value,
1293 + ) {}
1294 + async fn on_fetch_ssrf(&self, _target_host: &str, _attempted_ip: Option<&str>) {}
1295 + async fn on_inbox_abuse(
1296 + &self, _domain: &str, _actor_uri: Option<&str>, _reason: &str,
1297 + ) {}
1298 + async fn on_auth_challenge(&self, _domain: &str, _handle: &str) {}
1299 + async fn on_invite_action(&self, _domain: &str, _decision: &str) {}
1300 +}
1301 +```
1302 +
1303 +Toutes les méthodes ont un default `{}` — overridez celles que vous écoutez. Deux implémentations livrées :
1304 +
1305 +| Impl | Module | Usage |
1306 +|---|---|---|
1307 +| `NoopFediverseObserver` | `rustwarden_fediverse::observer` | Zero-cost no-op. Default si la feature `fediverse-guard` n'est pas activée. |
1308 +| `GuardObserver` | `rustwarden_fediverse_guard` | Détection d'abus runtime + ban manager (RFC 0028). |
1309 +
1310 +Installation : `fediverse_router.layer(axum::Extension(observer))` (cf. [`fediverse-consumer-guide.md` §4](fediverse-consumer-guide.md#4-cabler-la-federation-dans-votre-main)).
1311 +
1312 +### 21.9 `LocalUserResolver`
1313 +
1314 +**Module** : `rustwarden_fediverse::resolver`
1315 +
1316 +Trait de résolution `@handle` → `UserSummary` côté local, consommé par WebFinger / acteur / NodeInfo. **RFC 0002.**
1317 +
1318 +```rust
1319 +#[async_trait]
1320 +pub trait LocalUserResolver: Send + Sync {
1321 + async fn find_local_user(&self, username: &str) -> Result<Option<UserSummary>, AppError>;
1322 + async fn count_users_total(&self) -> Result<u64, AppError>;
1323 + async fn count_users_active_since(&self, days: u32) -> Result<u64, AppError>;
1324 + async fn count_local_posts(&self) -> Result<u64, AppError>;
1325 +}
1326 +```
1327 +
1328 +Struct `UserSummary` (champs publics) : `id: Uuid`, `username: String`, `display_name: Option<String>`, `bio: Option<String>`, `ap_public_key_pem: Option<String>`.
1329 +
1330 +Implémentation par défaut : `SeaOrmLocalUserResolver::new(db)`. Si votre app a un mapping custom (ex. acteur = team), implémentez le trait et injectez-le à `router_with_auth`. Les méthodes `count_*` alimentent NodeInfo (`/.well-known/nodeinfo`).
1331 +
1332 +### 21.10 `SignatureVerifier`
1333 +
1334 +**Module** : `rustwarden_fediverse::services::fediverse_signal_service`
1335 +
1336 +Trait de vérification de signatures HTTP entrantes (inbox + auth-back-channel). **RFC 0015, 0016, 0017.**
1337 +
1338 +```rust
1339 +#[async_trait]
1340 +pub trait SignatureVerifier: Send + Sync {
1341 + async fn verify(&self, ctx: &VerifyCtx<'_>) -> Result<(), AppError>;
1342 +}
1343 +```
1344 +
1345 +Deux implémentations livrées :
1346 +
1347 +| Impl | Module | Mode |
1348 +|---|---|---|
1349 +| `HttpSigVerifier` | `rustwarden_fediverse::http_signatures` | **Production**. RSA-SHA256 draft-cavage, fenêtre de skew Date 5 min (ANSSI F-CRYPTO-1), résolution auto du PEM via `remote_identities` (RFC 0017). |
1350 +| `AlwaysAcceptVerifier` | `services::fediverse_signal_service` | **Testbed uniquement**. Vérifie que l'acteur résout, ne valide pas la signature. À gater derrière `FEDIVERSE_DEBUG_LOCAL_FEDERATION=true`. |
1351 +
1352 +Constructeurs :
1353 +
1354 +```rust
1355 +HttpSigVerifier::with_resolver(db) // prod
1356 +AlwaysAcceptVerifier::with_resolver(db) // testbed
1357 +AlwaysAcceptVerifier::new() // tests unitaires
1358 +```
1359 +
1360 +Les deux exposent `.with_observer(Arc<dyn FediverseEventObserver>)` pour notifier en cas d'échec.
1361 +
1362 +### 21.11 `RemoteIdentityRevocationChecker`
1363 +
1364 +**Module** : `rustwarden_core::middleware::revocation`
1365 +
1366 +Trait consommé par le middleware d'auth pour rejeter un JWT remote dont la `remote_identity` a été disabled. **RFC 0018.**
1367 +
1368 +```rust
1369 +#[async_trait]
1370 +pub trait RemoteIdentityRevocationChecker: Send + Sync {
1371 + async fn is_revoked(&self, remote_identity_id: Uuid) -> bool;
1372 +}
1373 +```
1374 +
1375 +Implémentation livrée : `rustwarden_fediverse::services::revocation::SeaOrmRemoteIdentityRevocationChecker::new(db)`. Lit `remote_identities.disabled_at IS NOT NULL`.
1376 +
1377 +Installation : `builder.register_revocation_checker(Arc::new(...))`. **Sans cet appel**, un `Delete{Actor}` reçu ne révoque pas les JWT remote actifs — faille de sécurité.
1378 +
1379 +### 21.12 `Identity` / `IdentityKind`
1380 +
1381 +**Module** : `rustwarden_core::identity`
1382 +
1383 +```rust
1384 +pub enum IdentityKind { Local, Remote }
1385 +pub enum Identity {
1386 + Local(Uuid), // user_id
1387 + Remote(Uuid), // remote_identity_id
1388 +}
1389 +```
1390 +
1391 +Abstraction utilisée par les services qui acceptent indifféremment un user local ou une identité fédérée (`TeamService::add_member_identity`, `teams_of_identity` — cf. §9). Le champ `Claims.identity_kind` côté JWT (§4) permet aux handlers de matcher dynamiquement.
1392 +
1393 +### 21.13 Routes prêtes à monter
1394 +
1395 +**Module** : `rustwarden_fediverse::routes`
1396 +
1397 +Deux constructeurs de routeur. Tous deux retournent `Router<DatabaseConnection>` mergeable via `builder.merge_routes(...)`.
1398 +
1399 +| Fonction | Signature | Routes montées |
1400 +|---|---|---|
1401 +| `router` | `(config: FediverseConfig, db: DatabaseConnection) → Router<DatabaseConnection>` | **Minimum** : `/.well-known/webfinger`, `/.well-known/nodeinfo`, `/.well-known/host-meta`, `/nodeinfo/2.0`, `/users/{username}`, `/inbox`. Pas d'auth, pas d'invites. |
1402 +| `router_with_auth` | `(config, resolver: Arc<dyn LocalUserResolver>, verifier: Arc<dyn SignatureVerifier>, auth_config: Arc<AuthConfig>) → Router<DatabaseConnection>` | **Complet** : `router(..)` + 5 endpoints `/api/v1/auth/login/fediverse/*` + `/fediverse/auth/inbox` + `/api/v1/fediverse/auth/consent` + 6 endpoints invites + `/fediverse/consent` + `/fediverse/invite`. |
1403 +
1404 +Côté `rustwarden-fediverse-ui` : `default_routes()` retourne le routeur UI complet (admin trust/blocks/outbound + écrans user consent/login), et `dashboard_card()` la `DashboardCardDTO` injectable via `builder.add_dashboard_card(..)`.
1405 +
1406 +### 21.14 Patterns d'intégration
1407 +
1408 +Pour le câblage complet du `main.rs`, les variables d'environnement, les migrations et la check-list d'adoption, voir [`fediverse-consumer-guide.md`](fediverse-consumer-guide.md). Récap des bonnes pratiques côté code :
1409 +
1410 +- **Toujours brancher un `RemoteIdentityRevocationChecker`** si vous activez `fediverse` — sans lui, `Delete{Actor}` n'invalide rien.
1411 +- **Toujours utiliser `HttpSigVerifier` en prod** — `AlwaysAcceptVerifier` ne doit être atteint qu'avec `FEDIVERSE_DEBUG_LOCAL_FEDERATION=true` (guard explicite dans `main`).
1412 +- **Toujours `tokio::spawn(OutboundWorker::run())`** au démarrage, avec un `HttpSignedDelivery` injecté — `NoopDelivery` ne fait que logger.
1413 +- **Toujours s'abonner au target tracing `rustwarden::fediverse::outbound`** — sinon vous découvrirez les `dead` par les clients qui râlent.
1414 +- **Étendre via les 5 traits** (`LocalUserResolver`, `SignatureVerifier`, `FediverseEventObserver`, `RemoteIdentityRevocationChecker`, `OutboundDelivery`) plutôt que forker. Les anti-patterns sont listés dans [`fediverse-consumer-guide.md` §10](fediverse-consumer-guide.md#10-anti-patterns-specifiques-federation).
A docs/fediverse-consumer-guide.md
+428 -0
@@ -0,0 +1,428 @@
1 +# Fédération — Guide d'intégration pour les consommateurs du framework
2 +
3 +> **Public visé** : développeurs d'applications qui consomment le
4 +> framework Rustwarden et veulent activer la fédération ActivityPub
5 +> (SSO inter-instances + invitations cross-instance d'équipe).
6 +>
7 +> **Hors périmètre** : déploiement / runtime opérationnel (variables
8 +> d'env, supervision, troubleshooting). Voir [`fediverse.md`](fediverse.md)
9 +> et [`fediverse-operations.md`](fediverse-operations.md).
10 +
11 +Ce document décrit comment **exploiter**, depuis une application
12 +consommatrice, les fonctionnalités de fédération livrées entre le
13 +9 et le 14 mai 2026 (plans [`2026-05-09`](plans/2026-05-09-fediverse-identity-federation.md)
14 +à [`2026-05-14`](plans/2026-05-14-fediverse-security-and-guard.md),
15 +RFCs [`0001`](rfcs/0001-fediverse-config-and-builder.md) à
16 +[`0025`](rfcs/0025-fediverse-auth-back-channel.md)). Il s'inscrit dans
17 +le [processus d'évolution Rustwarden](RUSTWARDEN_EVOLUTION_PROCESS.md)
18 +(§11 — guide consommateur).
19 +
20 +---
21 +
22 +## 1. Périmètre fonctionnel livré
23 +
24 +| Capacité | RFCs | Crate exposant l'API |
25 +|---|---|---|
26 +| Configuration + builder | [0001](rfcs/0001-fediverse-config-and-builder.md) | `rustwarden-fediverse` |
27 +| WebFinger + Actor + NodeInfo | [0002](rfcs/0002-webfinger-actor-nodeinfo.md) | `rustwarden-fediverse` |
28 +| Cache acteurs distants | [0003](rfcs/0003-remote-identity-store.md) | `rustwarden-fediverse` |
29 +| Modèle de confiance (allowlist / blocklist) | [0004](rfcs/0004-fediverse-trust-model.md) | `rustwarden-fediverse` (`services::fediverse_trust_service`) |
30 +| Clés RSA d'acteur chiffrées | [0005](rfcs/0005-actor-key-storage.md) | `rustwarden-fediverse` (`services::actor_key_service`) |
31 +| Auth AP-native (challenge / consent / back-channel) | [0006](rfcs/0006-ap-native-auth-flow.md), [0013](rfcs/0013-fediverse-auth-flow-routes.md), [0025](rfcs/0025-fediverse-auth-back-channel.md) | `rustwarden-fediverse` (`auth_routes`) |
32 +| `Claims.identity_kind` + `remote_id` | [0007](rfcs/0007-jwt-claims-identity-kind.md) | `rustwarden-core` (`auth`) |
33 +| Abstraction `Identity` + sibling FKs | [0008](rfcs/0008-identity-abstraction.md) | `rustwarden-core` |
34 +| Équipes / partages cross-instance | [0009](rfcs/0009-team-resource-cross-instance.md), [0014](rfcs/0014-fediverse-invite-routes.md) | `rustwarden-fediverse` (`invite_routes`) |
35 +| Signaux lifecycle + retry/backoff | [0010](rfcs/0010-fediverse-lifecycle-signals.md), [0012](rfcs/0012-fediverse-inbox-route.md) | `rustwarden-fediverse` (`inbox`) |
36 +| Worker outbound + métriques | [0011](rfcs/0011-fediverse-outbound-worker.md), [0019](rfcs/0019-fediverse-outbound-metrics.md) | `rustwarden-fediverse` (`services::fediverse_outbound_service`) |
37 +| Signatures HTTP draft-cavage RSA-SHA256 | [0015](rfcs/0015-fediverse-http-signatures.md), [0016](rfcs/0016-fediverse-signed-delivery-and-verifier-widening.md), [0017](rfcs/0017-fediverse-auto-resolve-public-key.md) | `rustwarden-fediverse` (`http_signatures`) |
38 +| Tests interopérabilité wiremock | [0020](rfcs/0020-fediverse-interop-tests.md) | — (tests) |
39 +| UI admin foundations + parcours utilisateur + ops | [0021](rfcs/0021-fediverse-ui-admin-foundations.md), [0023](rfcs/0023-fediverse-ui-user-auth.md), [0024](rfcs/0024-fediverse-ui-ops-slice-c.md) | `rustwarden-fediverse-ui` |
40 +| Smoke tests anti-régression | [0022](rfcs/0022-smoke-tests-regression.md) | — (tests) |
41 +
42 +L'ensemble est **expérimental** (cf. §6). L'API publique peut casser
43 +sans bump majeur jusqu'à promotion stable.
44 +
45 +---
46 +
47 +## 2. Décider si vous activez la fédération
48 +
49 +Activez la fédération si **au moins** un de ces besoins existe côté
50 +app :
51 +
52 +- **SSO entre instances** Rustwarden distinctes (un user d'instance A
53 + doit pouvoir s'authentifier sur instance B sans recréer de compte).
54 +- **Invitations cross-instance** dans une équipe (inviter
55 + `@alice@autre-instance.org` dans une équipe locale).
56 +- **Annonce d'un acteur ActivityPub** consommable par Mastodon-likes
57 + (découverte WebFinger).
58 +
59 +Si vous n'avez **aucun** de ces besoins : n'activez rien. La
60 +fonctionnalité ajoute 4 tables, 1 worker tokio, ~2000 LOC compilées et
61 +une surface réseau (`/inbox`, `/users/{u}`, `/.well-known/*`). Les
62 +modules `auth`, `oauth`, `team`, etc. de `rustwarden-core` restent
63 +parfaitement utilisables sans elle.
64 +
65 +---
66 +
67 +## 3. Choisir vos crates et vos features
68 +
69 +Trois niveaux d'opt-in, **cumulatifs** (chaque niveau implique le
70 +précédent).
71 +
72 +| Feature app | Implique | Ce que ça apporte | Quand l'activer |
73 +|---|---|---|---|
74 +| `fediverse` | crate `rustwarden-fediverse` | Protocole ActivityPub headless : routes `/.well-known/*`, `/users/{u}`, `/inbox`, `/fediverse/auth/*`, `/api/v1/teams/{id}/invites/*`. **Aucune UI HTML.** | API-only / SPA séparée |
75 +| `fediverse-ui` | `fediverse` + crate `rustwarden-fediverse-ui` | UI HTML de référence : pages admin trust / blocks / outbound / invites, écrans utilisateur consent / login, carte de dashboard injectée via `add_dashboard_card`. | App SSR qui veut une UI prête à l'emploi |
76 +| `fediverse-guard` | `fediverse` + crate `rustwarden-fediverse-guard` | Détection d'abus temps-réel, ban manager, observer branché sur l'inbox (RFC 0028). UI admin guard si combinée à `fediverse-ui`. | Production exposée à des inconnus du fediverse public |
77 +
78 +**Exemple `Cargo.toml`** côté app consommatrice (calqué sur
79 +[`app/Cargo.toml`](../app/Cargo.toml)) :
80 +
81 +```toml
82 +[features]
83 +default = []
84 +fediverse = ["dep:rustwarden-fediverse"]
85 +fediverse-ui = ["fediverse", "dep:rustwarden-fediverse-ui"]
86 +fediverse-guard = ["fediverse", "dep:rustwarden-fediverse-guard"]
87 +
88 +[dependencies]
89 +rustwarden-core = { version = "*", features = ["oauth"] }
90 +rustwarden-ui = { version = "*" }
91 +rustwarden-fediverse = { version = "*", optional = true }
92 +rustwarden-fediverse-ui = { version = "*", optional = true }
93 +rustwarden-fediverse-guard = { version = "*", optional = true }
94 +```
95 +
96 +**À ne pas faire** : déclarer `rustwarden-fediverse` en dépendance
97 +non-optionnelle. Vous perdez la possibilité de fournir une variante
98 +sans fédération, et chaque utilisateur de votre app paye le coût compile.
99 +
100 +---
101 +
102 +## 4. Câbler la fédération dans votre `main`
103 +
104 +Le câblage de référence est dans [`app/src/main.rs`](../app/src/main.rs).
105 +Le **strict minimum** côté app — sous `#[cfg(feature = "fediverse")]` :
106 +
107 +```rust
108 +use std::sync::Arc;
109 +use rustwarden_core::prelude::*;
110 +
111 +let builder = RustwardenApp::builder()
112 + .from_env()
113 + .with_ui(rustwarden_ui::default_routes);
114 +
115 +#[cfg(feature = "fediverse")]
116 +let builder = {
117 + // 1. Connexion DB partagée avec le framework.
118 + let db_url = std::env::var("DATABASE_URL")?;
119 + let db = sea_orm::Database::connect(&db_url).await?;
120 +
121 + // 2. Config fédération chargée depuis l'environnement.
122 + let fediverse_config = rustwarden_fediverse::FediverseConfig::from_env()?;
123 +
124 + // 3. AuthConfig explicite — ANSSI F-NET-5 : on veut un échec
125 + // bruyant au démarrage si JWT_SECRET est un placeholder.
126 + let auth_config = Arc::new(
127 + rustwarden_core::config::auth::AuthConfig::from_env()?,
128 + );
129 +
130 + // 4. Résolveur d'utilisateurs locaux (lookup `users` par handle).
131 + let resolver: Arc<dyn rustwarden_fediverse::resolver::LocalUserResolver> =
132 + Arc::new(
133 + rustwarden_fediverse::resolver::SeaOrmLocalUserResolver::new(db.clone()),
134 + );
135 +
136 + // 5. Vérificateur de signatures HTTP. En prod : strict ;
137 + // en dev/testbed : AlwaysAcceptVerifier (vérifie seulement
138 + // que l'acteur résout, sans crypto).
139 + let verifier = Arc::new(
140 + rustwarden_fediverse::http_signatures::HttpSigVerifier::with_resolver(
141 + db.clone(),
142 + ),
143 + );
144 +
145 + // 6. Routeur fédération complet (well-known + actor + inbox +
146 + // auth + invites).
147 + let fediverse_router = rustwarden_fediverse::routes::router_with_auth(
148 + fediverse_config,
149 + resolver,
150 + verifier,
151 + auth_config,
152 + );
153 +
154 + // 7. Checker de révocation : un JWT remote dont la
155 + // `remote_identity` a reçu `Delete{Actor}` doit refuser
156 + // l'authentification au middleware.
157 + let checker: Arc<dyn rustwarden_core::middleware::revocation::RemoteIdentityRevocationChecker> =
158 + Arc::new(
159 + rustwarden_fediverse::services::revocation::SeaOrmRemoteIdentityRevocationChecker::new(
160 + db.clone(),
161 + ),
162 + );
163 +
164 + builder
165 + .database(db)
166 + .register_protected_prefix("/fediverse/consent")
167 + .register_protected_prefix("/fediverse/invite")
168 + .register_revocation_checker(checker)
169 + .merge_routes(fediverse_router)
170 +};
171 +
172 +#[cfg(feature = "fediverse-ui")]
173 +let builder = builder
174 + .merge_routes(rustwarden_fediverse_ui::default_routes())
175 + .add_dashboard_card(rustwarden_fediverse_ui::dashboard_card());
176 +
177 +builder.build().await?.run().await
178 +```
179 +
180 +Points d'attention :
181 +
182 +- **DB partagée**. Vous ouvrez `db` *avant* le builder et le passez via
183 + `.database(db)`. La fédération doit lire/écrire dans les mêmes tables
184 + que le reste du framework — n'ouvrez pas une seconde pool.
185 +- **Préfixes protégés**. `register_protected_prefix` câble le middleware
186 + d'auth sur les routes qui exigent une session humaine (consent,
187 + invitations acceptées par un user identifié).
188 +- **Revocation checker injecté**. Sans cet appel, un `Delete{Actor}`
189 + entrant n'invalide pas les JWT déjà émis pour ce remote.
190 +
191 +---
192 +
193 +## 5. Migrations à appliquer
194 +
195 +Les 8 tables / extensions sont créées par les migrations SeaORM
196 +`m20260510_000009` à `m20260510_000016` (cf. [`fediverse.md` §4](fediverse.md#4-migrations-seaorm)).
197 +Côté app consommatrice :
198 +
199 +```bash
200 +cargo run --bin migrate
201 +```
202 +
203 +Le binaire `migrate` est ré-exposé par `rustwarden-core` — vous
204 +n'avez **rien** à écrire côté app. Les migrations fédération
205 +s'enregistrent automatiquement quand la feature `fediverse` du crate
206 +est compilée.
207 +
208 +**Rollback** : chaque migration a un `down()` testé en up/down dans
209 +la CI du framework. Désactiver la fédération côté app **ne** rollback
210 +**pas** automatiquement la DB ; faites-le explicitement si besoin
211 +(`cargo run --bin migrate -- down --num 8`).
212 +
213 +---
214 +
215 +## 6. Statut `experimental-fediverse` — règles de versioning
216 +
217 +Conformément au [processus d'évolution §5](RUSTWARDEN_EVOLUTION_PROCESS.md#5-strategie-de-versioning),
218 +la fédération est encore **expérimentale** :
219 +
220 +- Le code de la fédération est gated derrière des features `fediverse*`
221 + qui restent en cycle "Birth → Adoption". Les RFCs sont **Accepted**
222 + mais l'API publique peut casser sans bump majeur tant que la
223 + promotion n'a pas eu lieu.
224 +- La promotion vers stable est conditionnée à la check-list
225 + [`docs/plans/2026-05-10-fediverse-promotion-stable-evaluation.md`](plans/2026-05-10-fediverse-promotion-stable-evaluation.md) :
226 + 6 scénarios E2E verts, 3 instances en topologie réelle, métriques
227 + exposées, doc opérateur tierce-personne-testable.
228 +- État au 2026-05-15 : tous les scénarios critiques E2E passent
229 + (cf. observation interne 618 du 2026-05-11). La promotion stable
230 + n'a **pas** encore été prononcée.
231 +
232 +**Recommandations de pinning** côté app consommatrice (cf. §11.1 du
233 +processus) :
234 +
235 +| Phase de votre app | Stratégie |
236 +|---|---|
237 +| Prototype / dev | `rustwarden-fediverse = "*"` |
238 +| Pré-prod sur version expérimentale | `rustwarden-fediverse = "=0.X.Y"` (pin exact) |
239 +| Production | Attendez la promotion stable, puis `rustwarden-fediverse = "~0.X.Y"` |
240 +
241 +Tant que vous restez sur l'expérimental, vous devez surveiller le
242 +[`CHANGELOG.md`](../CHANGELOG.md) à chaque bump : un breaking sans
243 +release majeure est légitime.
244 +
245 +---
246 +
247 +## 7. Suivre l'évolution upstream
248 +
249 +Le processus §11.2 impose à chaque consommateur de tenir un
250 +`docs/UPSTREAM_<framework>.md`. Le template Rustwarden vit dans
251 +[`UPSTREAM_RUSTWARDEN.md`](UPSTREAM_RUSTWARDEN.md). Pour la
252 +fédération, ajoutez-y au minimum :
253 +
254 +```markdown
255 +## Fédération (`fediverse*` features)
256 +
257 +- **Version pinned** : `rustwarden-fediverse = "=X.Y.Z"`.
258 +- **Features actives** : `fediverse`, `fediverse-ui`, `fediverse-guard`.
259 +- **Workarounds en cours** :
260 + - <aucun à ce jour>
261 +- **Breaking changes attendus** :
262 + - Promotion stable : `experimental-fediverse` → `fediverse` (suivre
263 + la closure du plan `2026-05-10-fediverse-promotion-stable-evaluation`).
264 + - Suivi RFCs > 0025 dans [`docs/rfcs/README.md`](rfcs/README.md).
265 +- **Hooks implémentés côté app** :
266 + - <ex. `ActiveTokenIterator` pour la blacklist JWT sur `Delete{Actor}`>
267 +- **Extensions custom au-dessus de `rustwarden-fediverse-ui`** :
268 + - <ex. dashboard card personnalisée, route admin supplémentaire>
269 +```
270 +
271 +À chaque release du framework, lisez la section **Fediverse** du
272 +CHANGELOG, mettez à jour ce fichier, puis bumpez (ou non) le pin.
273 +
274 +---
275 +
276 +## 8. Points d'extension consommateur
277 +
278 +Quand le comportement par défaut ne suffit pas, étendez **sans
279 +forker**. Cinq points d'extension stables :
280 +
281 +### 8.1 `LocalUserResolver` — qui est `@alice@local` ?
282 +
283 +Par défaut : `SeaOrmLocalUserResolver` lit la table `users`. Si vous
284 +avez un mapping custom (ex. les acteurs locaux sont une équipe + un
285 +user), implémentez `rustwarden_fediverse::resolver::LocalUserResolver`
286 +et injectez-le à `router_with_auth`.
287 +
288 +### 8.2 `SignatureVerifier` — politique de confiance crypto
289 +
290 +Trois variants livrés :
291 +- `HttpSigVerifier` : strict (prod, RFC 0015/0017).
292 +- `AlwaysAcceptVerifier` : accepte tout (testbed local uniquement).
293 +- À vous d'écrire le vôtre via le trait
294 + `rustwarden_fediverse::services::fediverse_signal_service::SignatureVerifier`
295 + si vous voulez ajouter une mitigation custom (ex. rate-limit par
296 + domaine).
297 +
298 +### 8.3 `FediverseEventObserver` — hook lifecycle
299 +
300 +Branché par `axum::Extension`. Implémentez le trait pour réagir à
301 +chaque inbox / outbound / signature (ex. compteurs custom, alerting
302 +Slack). Le crate `rustwarden-fediverse-guard` est un consommateur
303 +exemplaire de ce hook.
304 +
305 +### 8.4 `add_dashboard_card` — cartes admin custom
306 +
307 +Le builder expose `.add_dashboard_card(DashboardCardDTO)` (cf.
308 +observation interne 799 du 2026-05-13). `rustwarden-fediverse-ui`
309 +fournit une carte par défaut ; si vous voulez la remplacer ou ajouter
310 +la vôtre, appelez `add_dashboard_card` après le merge des routes.
311 +
312 +### 8.5 `merge_routes` — handlers consommateur
313 +
314 +Si votre app veut sa propre page `/profile/fediverse` ou un endpoint
315 +d'admin spécifique (ex. import/export de allowlist au format YAML),
316 +écrivez un `Router<AppState>` et fusionnez-le via
317 +`builder.merge_routes(my_router)`. **Ne** dupliquez **pas** un service
318 +existant : étendez-le via wrapper / trait (cf. processus §11.4).
319 +
320 +---
321 +
322 +## 9. Pilotage runtime (côté code consommateur)
323 +
324 +Trois services exposés publiquement, à consommer depuis vos handlers /
325 +jobs custom.
326 +
327 +### 9.1 Trust list (allowlist / blocklist)
328 +
329 +```rust
330 +use rustwarden_fediverse::services::fediverse_trust_service::FediverseTrustService;
331 +
332 +let svc = FediverseTrustService::new(db.clone());
333 +svc.add_trusted_domain("mastodon.social", Some("Mastodon public"), None).await?;
334 +svc.block_domain("evil.example.org", "spam massif").await?;
335 +```
336 +
337 +Le mode (`allowlist` / `open_with_blocklist`) est piloté dans
338 +`app_settings` — cf. [`fediverse.md` §5](fediverse.md#5-configurer-le-mode-de-confiance).
339 +
340 +### 9.2 Émettre une activité sortante
341 +
342 +```rust
343 +use rustwarden_fediverse::services::fediverse_outbound_service::FediverseOutboundService;
344 +
345 +let outbound = FediverseOutboundService::new(db.clone());
346 +outbound
347 + .enqueue(activity_json, target_inbox, target_domain)
348 + .await?;
349 +```
350 +
351 +Le worker (à lancer dans un `tokio::spawn` — cf. [`fediverse.md` §7](fediverse.md#7-demarrer-le-worker-outbound))
352 +applique le ladder de retry. Vous ne pilotez **pas** le retry depuis
353 +votre code : tracez les événements via le target tracing
354 +`rustwarden::fediverse::outbound`.
355 +
356 +### 9.3 Forger un signal lifecycle local
357 +
358 +```rust
359 +use rustwarden_fediverse::services::fediverse_signal_service::FediverseSignalService;
360 +
361 +let signal = FediverseSignalService::new(db.clone());
362 +signal.apply_delete_actor(&actor_iri).await?;
363 +```
364 +
365 +À utiliser quand votre app supprime un user local et veut publier le
366 +`Delete{Actor}` correspondant côté fediverse.
367 +
368 +---
369 +
370 +## 10. Anti-patterns spécifiques fédération
371 +
372 +- **Activer `experimental` en prod sans plan de bump**. Si vous ne
373 + surveillez pas le CHANGELOG hebdomadaire, vous prendrez un breaking
374 + par surprise. Pinnez exact ET planifiez l'audit des releases.
375 +- **Câbler `AlwaysAcceptVerifier` ailleurs qu'en testbed**. Tout
376 + message entrant signé "n'importe comment" passe — un faux
377 + `Delete{Actor}` peut détruire vos sessions remote. Ne le faites
378 + qu'avec `FEDIVERSE_DEBUG_LOCAL_FEDERATION=true` et un guard explicite
379 + dans `main`.
380 +- **Dupliquer un service framework**. Si `FediverseTrustService` ne
381 + suffit pas, ouvrez une RFC (processus §11.3) et proposez une
382 + extension. Une copie locale dérive en 1 release.
383 +- **Lancer le worker outbound sans tracing**. RFC 0019 expose 4
384 + événements clés (`delivered` / `deferred` / `dead` / `drain_failed`).
385 + Sans abonnement, vous découvrirez les `dead` par les clients qui
386 + râlent.
387 +- **Forker `rustwarden-fediverse-ui`** pour changer un template.
388 + L'i18n et la dashboard card sont les points d'extension prévus. Pour
389 + un template entier custom, ré-écrivez-le côté app et `merge_routes`
390 + votre router à la place du sien.
391 +
392 +---
393 +
394 +## 11. Check-list d'adoption
395 +
396 +Avant la première mise en route :
397 +
398 +- [ ] Décision §2 prise et tracée dans votre ADR app.
399 +- [ ] Features Cargo §3 ajoutées côté app, build vert avec et sans la
400 + feature `fediverse`.
401 +- [ ] Câblage §4 reproduit dans votre `main` ; au minimum :
402 + `FediverseConfig::from_env()`, `LocalUserResolver`,
403 + `SignatureVerifier`, `register_revocation_checker`,
404 + `merge_routes`.
405 +- [ ] Migrations §5 appliquées dans la DB cible.
406 +- [ ] Variables d'environnement §3 de [`fediverse.md`](fediverse.md#3-variables-denvironnement)
407 + configurées (`FEDIVERSE_DOMAIN`, `ENCRYPTION_KEY`, `JWT_SECRET`).
408 +- [ ] Worker outbound lancé dans un `tokio::spawn`.
409 +- [ ] Tracing abonné au target `rustwarden::fediverse::outbound`.
410 +- [ ] `UPSTREAM_<app>.md` §7 mis à jour avec votre pin + workarounds.
411 +- [ ] Plan de surveillance des bumps `rustwarden-fediverse` (CHANGELOG
412 + lecture systématique tant qu'on est en `experimental-*`).
413 +
414 +---
415 +
416 +## 12. Références
417 +
418 +- [Processus d'évolution Rustwarden](RUSTWARDEN_EVOLUTION_PROCESS.md)
419 +- [Master plan fédération](plans/2026-05-09-fediverse-identity-federation.md)
420 +- [Plan promotion stable](plans/2026-05-10-fediverse-promotion-stable-evaluation.md)
421 +- [Index des RFCs](rfcs/README.md) (0001 à 0025)
422 +- [Guide opérateur (runtime)](fediverse.md)
423 +- [Guide opérateur (ops avancées)](fediverse-operations.md)
424 +- [Modèle de sécurité](fediverse-security-model.md)
425 +- [Guard fédération](fediverse-guard.md)
426 +- [Tests E2E manuels](fediverse-e2e-manual.md)
427 +- [Template suivi upstream](UPSTREAM_RUSTWARDEN.md)
428 +- Binaire de référence : [`app/src/main.rs`](../app/src/main.rs)
M tools/rustwarden-cli/src/update_project.rs
+215 -3
@@ -15,6 +15,7 @@ const FRAMEWORK_CRATES: &[&str] = &[
15 15 "rustwarden-ui",
16 16 "rustwarden-fediverse",
17 17 "rustwarden-fediverse-ui",
18 + "rustwarden-fediverse-guard",
18 19 ];
19 20
20 21 /// Fichiers du projet consommateur qu'on ne touche JAMAIS.
@@ -51,13 +52,25 @@ pub fn run() -> Result<(), Box<dyn std::error::Error>> {
51 52 let has_ui = project_dir.join("crates/rustwarden-ui").exists();
52 53 let has_fediverse = project_dir.join("crates/rustwarden-fediverse").exists();
53 54 let has_fediverse_ui = project_dir.join("crates/rustwarden-fediverse-ui").exists();
55 + let has_fediverse_guard = project_dir.join("crates/rustwarden-fediverse-guard").exists();
54 56 let custom_templates = detect_custom_templates(&project_dir, &rustwarden_root, has_ui);
55 57
58 + // Si la fédération (ou une de ses crates) manque, proposer de l'ajouter.
59 + let mut newly_added: Vec<&'static str> = Vec::new();
60 + let (has_fediverse, has_fediverse_ui, has_fediverse_guard) = prompt_fediverse_addition(
61 + has_fediverse,
62 + has_fediverse_ui,
63 + has_fediverse_guard,
64 + &mut newly_added,
65 + )?;
66 +
56 67 print_update_plan(
57 68 &project_dir,
58 69 has_ui,
59 70 has_fediverse,
60 71 has_fediverse_ui,
72 + has_fediverse_guard,
73 + &newly_added,
61 74 &custom_templates,
62 75 );
63 76
@@ -80,6 +93,7 @@ pub fn run() -> Result<(), Box<dyn std::error::Error>> {
80 93 has_ui,
81 94 has_fediverse,
82 95 has_fediverse_ui,
96 + has_fediverse_guard,
83 97 &custom_templates,
84 98 )?;
85 99
@@ -87,11 +101,79 @@ pub fn run() -> Result<(), Box<dyn std::error::Error>> {
87 101 version::write_version(&project_dir, &new_version)?;
88 102
89 103 // --- Resume ---
90 - print_summary(&project_dir, &backup_dir, &custom_templates, &stats);
104 + print_summary(
105 + &project_dir,
106 + &backup_dir,
107 + &custom_templates,
108 + &newly_added,
109 + &stats,
110 + );
91 111
92 112 Ok(())
93 113 }
94 114
115 +/// Quand le projet n'inclut pas (encore) la fédération ActivityPub,
116 +/// propose à l'utilisateur de l'ajouter pendant l'update. Retourne le
117 +/// nouveau triplet `(has_fediverse, has_fediverse_ui, has_fediverse_guard)`
118 +/// et pousse dans `newly_added` les noms de crates effectivement ajoutées.
119 +fn prompt_fediverse_addition(
120 + mut has_fediverse: bool,
121 + mut has_fediverse_ui: bool,
122 + mut has_fediverse_guard: bool,
123 + newly_added: &mut Vec<&'static str>,
124 +) -> Result<(bool, bool, bool), Box<dyn std::error::Error>> {
125 + // Cas 1 : la fédération est absente — proposer le bundle complet.
126 + if !has_fediverse {
127 + println!("\n{}", style("── Fédération ActivityPub ──").cyan().bold());
128 + println!(
129 + " Ce projet n'inclut pas la fédération (rustwarden-fediverse).\n \
130 + Périmètre, features Cargo et câblage : docs/fediverse-consumer-guide.md"
131 + );
132 + if !Confirm::new()
133 + .with_prompt(format!(
134 + "{}",
135 + style("Ajouter le support fédération à ce projet ?").cyan()
136 + ))
137 + .default(false)
138 + .interact()?
139 + {
140 + return Ok((has_fediverse, has_fediverse_ui, has_fediverse_guard));
141 + }
142 + has_fediverse = true;
143 + newly_added.push("rustwarden-fediverse");
144 + }
145 +
146 + // Cas 2 / extension : -ui absent.
147 + if !has_fediverse_ui
148 + && Confirm::new()
149 + .with_prompt(format!(
150 + "{}",
151 + style("Inclure aussi l'UI HTML (rustwarden-fediverse-ui) ?").cyan()
152 + ))
153 + .default(false)
154 + .interact()?
155 + {
156 + has_fediverse_ui = true;
157 + newly_added.push("rustwarden-fediverse-ui");
158 + }
159 +
160 + // Cas 3 / extension : -guard absent.
161 + if !has_fediverse_guard
162 + && Confirm::new()
163 + .with_prompt(format!(
164 + "{}",
165 + style("Inclure aussi le guard anti-abus (rustwarden-fediverse-guard) ?").cyan()
166 + ))
167 + .default(false)
168 + .interact()?
169 + {
170 + has_fediverse_guard = true;
171 + newly_added.push("rustwarden-fediverse-guard");
172 + }
173 +
174 + Ok((has_fediverse, has_fediverse_ui, has_fediverse_guard))
175 +}
176 +
95 177 // ---------------------------------------------------------------------------
96 178 // Detection du projet consommateur
97 179 // ---------------------------------------------------------------------------
@@ -235,6 +317,8 @@ fn print_update_plan(
235 317 has_ui: bool,
236 318 has_fediverse: bool,
237 319 has_fediverse_ui: bool,
320 + has_fediverse_guard: bool,
321 + newly_added: &[&'static str],
238 322 custom_templates: &[String],
239 323 ) {
240 324 println!("\n{}", style("── Plan de mise a jour ──").cyan().bold());
@@ -246,11 +330,30 @@ fn print_update_plan(
246 330 if has_ui {
247 331 println!(" - crates/rustwarden-ui/");
248 332 }
333 + let added = |name: &str| newly_added.contains(&name);
249 334 if has_fediverse {
250 - println!(" - crates/rustwarden-fediverse/");
335 + let tag = if added("rustwarden-fediverse") {
336 + style(" (NOUVEAU)").green().bold().to_string()
337 + } else {
338 + String::new()
339 + };
340 + println!(" - crates/rustwarden-fediverse/{tag}");
251 341 }
252 342 if has_fediverse_ui {
253 - println!(" - crates/rustwarden-fediverse-ui/");
343 + let tag = if added("rustwarden-fediverse-ui") {
344 + style(" (NOUVEAU)").green().bold().to_string()
345 + } else {
346 + String::new()
347 + };
348 + println!(" - crates/rustwarden-fediverse-ui/{tag}");
349 + }
350 + if has_fediverse_guard {
351 + let tag = if added("rustwarden-fediverse-guard") {
352 + style(" (NOUVEAU)").green().bold().to_string()
353 + } else {
354 + String::new()
355 + };
356 + println!(" - crates/rustwarden-fediverse-guard/{tag}");
254 357 }
255 358
256 359 println!(
@@ -303,6 +406,7 @@ fn print_summary(
303 406 project_dir: &Path,
304 407 backup_dir: &Path,
305 408 custom_templates: &[String],
409 + newly_added: &[&'static str],
306 410 stats: &SyncStats,
307 411 ) {
308 412 let divider = style("─".repeat(55)).dim();
@@ -385,6 +489,102 @@ fn print_summary(
385 489 println!(" {} Lancer les tests :", style("4.").cyan().bold());
386 490 println!(" {}", style("cargo test").green());
387 491 println!();
492 +
493 + if !newly_added.is_empty() {
494 + let divider = style("─".repeat(55)).dim();
495 + println!("{divider}");
496 + println!(
497 + " {}",
498 + style("Etapes supplementaires pour la federation :")
499 + .yellow()
500 + .bold()
501 + );
502 + println!("{divider}\n");
503 +
504 + println!(" {} crate(s) ajoutee(s) :", style("+").green().bold());
505 + for c in newly_added {
506 + println!(" - crates/{c}/");
507 + }
508 +
509 + println!(
510 + "\n {} Ces crates ont ete copiees mais ne sont pas encore",
511 + style("!").yellow().bold()
512 + );
513 + println!(" cablees. Pour les activer dans votre app :");
514 + println!();
515 + println!(
516 + " {} Ajouter dans {} :",
517 + style("a)").cyan(),
518 + style("Cargo.toml").white()
519 + );
520 + if newly_added.contains(&"rustwarden-fediverse") {
521 + println!(
522 + " {}",
523 + style(
524 + "[features]\n \
525 + fediverse = [\"dep:rustwarden-fediverse\"]"
526 + )
527 + .green()
528 + );
529 + }
530 + if newly_added.contains(&"rustwarden-fediverse-ui") {
531 + println!(
532 + " {}",
533 + style(
534 + "fediverse-ui = [\"fediverse\", \"dep:rustwarden-fediverse-ui\"]"
535 + )
536 + .green()
537 + );
538 + }
539 + if newly_added.contains(&"rustwarden-fediverse-guard") {
540 + println!(
541 + " {}",
542 + style(
543 + "fediverse-guard = [\"fediverse\", \"dep:rustwarden-fediverse-guard\"]"
544 + )
545 + .green()
546 + );
547 + }
548 + println!();
549 + println!(
550 + " {} Reproduire le cablage de reference dans src/main.rs",
551 + style("b)").cyan()
552 + );
553 + println!(
554 + " (FediverseConfig::from_env, LocalUserResolver,"
555 + );
556 + println!(
557 + " SignatureVerifier, register_revocation_checker,"
558 + );
559 + println!(" merge_routes, OutboundWorker dans tokio::spawn).");
560 + println!();
561 + println!(
562 + " {} Configurer les variables d'environnement requises :",
563 + style("c)").cyan()
564 + );
565 + println!(
566 + " {}",
567 + style("FEDIVERSE_DOMAIN, ENCRYPTION_KEY, JWT_SECRET").green()
568 + );
569 + println!();
570 + println!(
571 + " {} Appliquer les migrations fediverse :",
572 + style("d)").cyan()
573 + );
574 + println!(" {}", style("cargo run --bin migrate").green());
575 + println!();
576 + println!(
577 + " {} Guide complet : {}",
578 + style("→").dim(),
579 + style("docs/fediverse-consumer-guide.md").cyan().underlined()
580 + );
581 + println!(
582 + " {} Reference API : {}",
583 + style("→").dim(),
584 + style("docs/SERVICES_REFERENCE.md §21").cyan().underlined()
585 + );
586 + println!();
587 + }
388 588 }
389 589
390 590 // ---------------------------------------------------------------------------
@@ -426,6 +626,7 @@ fn update_crates(
426 626 has_ui: bool,
427 627 has_fediverse: bool,
428 628 has_fediverse_ui: bool,
629 + has_fediverse_guard: bool,
429 630 custom_templates: &[String],
430 631 ) -> Result<SyncStats, Box<dyn std::error::Error>> {
431 632 println!("\n{}", style("── Mise a jour des crates ──").cyan().bold());
@@ -474,6 +675,17 @@ fn update_crates(
474 675 )?;
475 676 }
476 677
678 + // rustwarden-fediverse-guard : uniquement si le projet l'utilise
679 + if has_fediverse_guard {
680 + sync_crate(
681 + rustwarden_root,
682 + project_dir,
683 + "rustwarden-fediverse-guard",
684 + &[],
685 + &mut stats,
686 + )?;
687 + }
688 +
477 689 Ok(stats)
478 690 }
479 691