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 |
GitRust