CHANGELOG.md 95185 octets

Changelog

All notable changes to this project are documented in this file.

The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.

[Unreleased]

Rien pour l'instant.

[1.2.0] - 2026-10-02 — Breaking (RFC 0042) + Security (RFC 0039 phase 2)

Minor assumé breaking sur un comportement par défaut (décision D1 du 2026-10-02, docs/rfcs/0042-auth-middleware-resolved-auth-config.md) : les consommateurs sont adaptés manuellement au déploiement. Les sept manifestes Cargo.toml du workspace passent à 1.2.0 (ils étaient restés à 1.0.0 malgré les trains 1.0.1 et 1.1.0).

⚠️ BREAKING — RFC 0042 : les overrides du builder atteignent enfin les middlewares de préfixe

AuthService (préfixes enregistrés via register_protected_prefix) et AdminService (/admin*, /api/v1/admin*) étaient posés avant le layer Extension(Arc<AuthConfig>) dans RustwardenBuilder::build() ; ils retombaient donc à chaque requête sur AuthConfig::from_env() et ignoraient .login_path(..), .auth_failure_mode(..) et la bascule Json401 de .headless(). Ils reçoivent désormais la configuration résolue à la construction.

Ce qui change pour vous :

Votre câblageAvant 1.2.0 (bug)Depuis 1.2.0
with_ui(..)303 → /logininchangé
.headless() + .auth_failure_mode(Redirect) (ex. BotPlace)303 (par l'env uniquement)303 (par le builder, comme documenté)
.headless() + AUTH_FAILURE_MODE=redirect303303 (l'env prime désormais sur la bascule headless)
.headless() sans setter ni env303 → /login vers une page inexistante401 {"error":"unauthorized"} sur préfixes protégés et /api/v1/admin*
.login_path("/x")ignoré par les middlewareshonoré partout

Précédence (nouvelle, explicite) : .auth_failure_mode(..) > AUTH_FAILURE_MODE > bascule .headless() > Redirect.

Intervention consommateur au déploiement : si votre app .headless() sert une UI SSR et comptait sur le 303, ajoutez .auth_failure_mode(AuthFailureMode::Redirect) au builder (ou AUTH_FAILURE_MODE=redirect dans l'environnement — rollback sans redéploiement de code). Si vous composez votre pile tower à la main, passez à AuthService::with_auth_config / AdminService::with_auth_config.

Fixed

  • RFC 0042. AuthService et AdminService honorent RustwardenBuilder::login_path, ::auth_failure_mode et la bascule Json401 de ::headless(). Préfixes protégés, /admin* et extracteurs AuthUser / auth_required partagent la même configuration : les middlewares la posent eux-mêmes dans les extensions de la requête, ce qui corrige aussi les piles tower composées à la main.
  • rustwarden-ui::root_redirect (GET /) lit la configuration injectée avant de retomber sur l'environnement, et redirige vers login_path au lieu de /login codé en dur.

Changed

  • AuthConfig::from_env() n'est plus exécutée à chaque requête par les middlewares de préfixe ni par root_redirect ; le log Auth configuration loaded n'apparaît plus qu'au démarrage dans le câblage canonique.
  • Variables AUTH_FAILURE_MODE et LOGIN_PATH documentées dans .env.sample et template/.env.sample.
  • RustwardenBuilder::build() résout l'AuthConfig avant la connexion à la base, les migrations et la création de l'admin par défaut : un JWT_SECRET absent ou placeholder fait échouer build() immédiatement (fail-fast), là où ces trois étapes s'exécutaient auparavant d'abord. L'ordre des logs de démarrage change en conséquence.
  • Une valeur AUTH_FAILURE_MODE vide ou invalide est ignorée (avertissement au démarrage) et ne supprime pas la bascule Json401 de .headless() : seule une valeur valide (redirect / json401) compte comme override opérateur.

Deprecated

  • AuthService::new(inner, db, prefixes) → AuthService::with_auth_config(inner, db, prefixes, auth_config).
  • AdminService::new(inner, db) → AdminService::with_auth_config(inner, db, auth_config). Le chemin déprécié émet un tracing::warn! une fois par processus. Suppression au plus tôt en 1.5.0, effective dans la prochaine major.

Security

  • RFC 0039 phase 2. SecurityHeadersConfig::default n'émet plus 'unsafe-eval'. L'UI charge le build Alpine CSP. 'unsafe-inline' est conservé. La politique 1.0.1 reste disponible via DEFAULT_CONTENT_SECURITY_POLICY et RustwardenBuilder::security_headers.

Added

  • Mesure d'audience générique (config::analytics). Support d'un script de suivi tiers modélisé comme une URL plus des attributs data-* : couvre Rybbit, Plausible, Umami et Matomo-tag sans code spécifique par fournisseur. Ajout additif — par défaut le suivi est désactivé, rien n'est rendu et la CSP reste bit-identique.

    • AnalyticsConfig (prelude) : from_env, validate, script_origin, render_snippet, setters chaînables.
    • AnalyticsState / AnalyticsContext / AnalyticsHandle (prelude) : snippet pré-rendu au démarrage et rechargeable à chaud, plus deux extracteurs Axum infaillibles pour les templates et l'écran d'administration.
    • RustwardenBuilder::analytics(cfg).
    • SecurityHeadersConfig::allowing_script_origin(origin) : ajoute une origine à script-src et connect-src. Appliqué par build(), donc l'ordre des appels .analytics(..) / .security_headers(..) est indifférent.
    • rustwarden-ui : section « Mesure d'audience » dans /admin/settings, composant d'opposition components/analytics_opt_out.html (clé localStorage configurable), traductions analytics.opt_out.* et admin.settings.analytics_* dans les six locales.
    • Trois clés app_settings : analytics_enabled, analytics_site_id, analytics_attributes.

    Aucune migration SeaORM. Aucun changement de comportement pour les consommateurs existants.

    ⚠️ L'URL du script n'est pas modifiable depuis l'administration : elle alimente la CSP, capturée une fois au build() (contrainte RFC 0039), et reste donc une variable d'environnement (ANALYTICS_SCRIPT_URL). C'est aussi ce qui empêche un compte d'administration compromis d'élargir la CSP vers un domaine arbitraire. Détail : docs/SERVICES_REFERENCE.md §23.

[1.1.0] - 2026-09-18 — Security

Minor de sécurité livrant les RFC 0036 à 0040, toutes passées Accepted le 2026-09-18 et implémentées dans rustwarden-core. Deux ajouts d'API publique (types::Username, config::security_headers::SecurityHeadersConfig) et quatre durcissements de comportement sur des chemins d'authentification existants. Fenêtre d'adoption 14 jours pour les consommateurs (docs/RUSTWARDEN_EVOLUTION_PROCESS.md §9).

⚠️ Une migration SeaORM est à jouer : m20260918_000010_totp_last_used_step (colonne user_totp.last_used_step). Avec auto_migrate(true) elle part au boot ; sinon cargo run --bin migrate. Jouer la migration avant de déployer le nouveau binaire. Le modèle SeaORM user_totp déclare last_used_step, donc SeaORM émet la liste de colonnes explicite : sur un schéma pré-migration, toute requête sur user_totp échoue — y compris TotpService::is_totp_enabled, appelée à chaque login. Conséquence : un réplica 1.1.0 sur un schéma pré-migration fait échouer tous les logins en HTTP 500, pas seulement ceux des comptes 2FA, tant que la colonne user_totp.last_used_step est absente. Procédure complète : docs/MIGRATION_GUIDE.md § 2026-09-18 — RFC 0036-0040 (1.1.0).

Le versioning minor (et non patch) reflète les deux API ajoutées ; la section Security ci-dessous liste les changements de comportement, qui sont techniquement non-breaking à la compilation mais peuvent refuser des entrées auparavant acceptées (usernames hors charset, login sans e-mail vérifié via appel service).

Added

  • Newtype Username (docs/rfcs/0036-username-charset-validation.md) — rustwarden_core::types::Username, re-exporté au prelude. Username::parse(&str) -> Result<Username, AppError> applique des règles GitHub-like : 3 à 39 caractères après trim, charset [a-z0-9-], pas de --, pas de - en tête ni en queue, majuscules rejetées (aucun fold silencieux : Alice échoue, il ne devient pas alice), et noms réservés refusés (types::team_slug::RESERVED + EXTRA_RESERVED = me, explore). Accesseurs as_str(), into_inner(), Display, Serialize/Deserialize transparents. Calqué sur TeamSlug.
  • SecurityHeadersConfig (docs/rfcs/0039-security-headers-config.md) — rustwarden_core::config::security_headers::SecurityHeadersConfig, re-exporté au prelude, avec la méthode builder RustwardenApp::builder().security_headers(cfg). Champs : content_security_policy: Cow<'static, str>, cross_origin_opener_policy: Option<Cow<'static, str>>, cross_origin_resource_policy: Option<Cow<'static, str>>, plus le helper with_same_origin_isolation() (COOP + CORP à same-origin). Constantes DEFAULT_CONTENT_SECURITY_POLICY et CSP_WITHOUT_UNSAFE_EVAL. Le défaut est bit-identique à 1.0.1 : même CSP ('unsafe-inline' et 'unsafe-eval' conservés pour Alpine.js), COOP/CORP non émis. Un consommateur qui n'appelle jamais .security_headers(..) ne voit aucun changement d'en-tête. CSP_WITHOUT_UNSAFE_EVAL est fournie mais non utilisée par Default : le durcissement du défaut est une Phase 2 ultérieure, après la migration Alpine CSP (docs/plans/PLAN_CSP_HTMX.md).
  • EmailChangeService et la table pending_email_changes (docs/rfcs/0041-pending-email-changes.md) — rustwarden_core::services::email_change_service::EmailChangeService, le changement d'adresse en deux temps. Méthodes publiques new, with_i18n, request_change, confirm_change, report_suspicious, admin_force_change, cleanup_expired, plus les lectures sans effet de bord inspect_confirmation_token / inspect_report_token. Nouvelle migration m20260919_000011_pending_email_changes (table pending_email_changes : token_hash et report_token_hash uniques en SHA-256, expires_at, used_at, reported_at, FK ON DELETE CASCADE). La même migration élargit la contrainte chk_email_queue_email_type aux quatre nouveaux types (email_change_validation, email_change_notice, email_change_reported, email_change_by_admin) : sans cet élargissement le premier INSERT du flux échouerait. La contrainte reste fermée — un email_type inconnu est toujours rejeté — et le down() ne la resserre que si plus aucune ligne n'utilise ces types, pour ne jamais détruire d'historique d'envoi. Nouveau modèle models::pending_email_change, nouveau processeur de purge PendingEmailChangeCleanupProcessor (prelude) — comme les processeurs existants, app.rs ne le démarre pas : appelez .start() vous-même. Durée de validité du lien : email_validation_expiration_hours, le réglage déjà utilisé par la validation d'adresse.
  • Cinq routes de changement d'adresse (canal machine, auth_api_routes()) — POST /api/v1/auth/email-change (Bearer + mot de passe courant), puis GET/POST sur /api/v1/auth/email-change/confirm/{token} et /api/v1/auth/email-change/report/{token}. Les liens envoyés par e-mail ouvrent une page interstitielle : le GET n'a aucun effet de bord et rend un formulaire, c'est le POST du bouton qui agit — une passerelle anti-phishing qui précharge les liens ne peut donc pas confirmer un changement d'adresse à la place de l'utilisateur. Ces pages sont du HTML minimal autoportant produit par le core (ni Askama, ni rustwarden-ui, ni JavaScript, ni ressource externe), donc montables en headless. Quatre nouveaux templates d'e-mail et deux nouvelles sections de locales (email_change, email_change_page) dans les six langues.

Changed

  • RegisterRequest.username (POST /api/v1/auth/register) passe de #[validate(length(min = 3, max = 64))] à max = 39, et le handler appelle Username::parse après validate() (défense en profondeur — la porte de validation unique reste UserService::create_user).
  • UserService::create_user valide désormais son argument username via Username::parse. Un chemin bootstrap pub(crate) create_user_allow_reserved (charset + longueur, mais noms réservés autorisés) est réservé à ensure_default_admin, pour que ADMIN_USERNAME=admin continue de booter. AdminConfig::validate délègue au même parse permissif.
  • OAuthService::generate_unique_username produit des candidats conformes au nouveau charset : minuscules, [a-z0-9] uniquement, base tronquée à 30 caractères (marge pour les suffixes -NNN / -<hex8>), suffixe -user si la base tombe sur un nom réservé. Les séparateurs sont des - (plus de _).
  • rustwarden-ui — handlers::user_handler::CreateUserRequest.username passe à #[validate(length(min = 3, max = 39))].
  • rustwarden-ui — les formulaires livrés appliquent enfin la RFC 0036 côté client : templates/profile.html passe de maxlength="50" pattern="[a-zA-Z0-9_-]+" à maxlength="39" pattern="[a-z0-9-]+", le champ username du formulaire d'inscription de templates/login.html (maxlength="50", aucun pattern) reçoit les mêmes contraintes, et templates/admin/user_form.html (mode create), qui n'avait ni pattern ni maxlength, les reçoit également. Les six locales (fr, en, es, it, de, pt) sont corrigées : profile.username_pattern et profile.username_help décrivaient une règle devenue fausse (« lettres, chiffres, tirets et underscores », « 3-50 caractères ») et annoncent désormais « minuscules, chiffres et tirets uniquement, 3 à 39 caractères » ; deux clés sont ajoutées (login.username_pattern_register, admin.user_form.username_pattern) et les placeholders d'exemple contenant un _ sont passés au -. Sans cela l'UI de référence laissait saisir des valeurs que le serveur refuse en 400.
  • rustwarden-cli — le wizard start-project valide désormais le username admin saisi (ask_admin_config) contre les règles RFC 0036 : minuscules, chiffres et tirets uniquement, 3 à 39 caractères, pas de --, pas de tiret en tête ni en queue. Les noms réservés (admin) restent acceptés, le bootstrap passant par parse_allow_reserved. Auparavant une saisie comme Admin, admin_user ou jean.dupont générait un projet dont le premier build() échouait sur username_format sans message actionnable.
  • rustwarden-ui — PUT /settings/profile ne traite plus du tout l'e-mail : l'alerte account_modified et l'appel applicatif à PasswordChangeService::invalidate_on_email_change ont été retirés, le core s'en charge au moment de la confirmation (RFC 0041). L'alerte « username modifié » n'est envoyée que sur un vrai rename (le formulaire de profil renvoyant tous les champs, un echo ne déclenche plus rien). Le champ e-mail du formulaire continue de passer tant qu'il renvoie l'adresse courante ; une vraie modification remonte désormais l'erreur Validation du core qui renvoie vers POST /api/v1/auth/email-change. Les écrans dédiés (demande en cours, annulation) sont un chantier UI séparé.

Security

  • Anti-rejeu TOTP persistant (docs/rfcs/0037-totp-last-used-step.md) — nouvelle migration m20260918_000010_totp_last_used_step ajoutant user_totp.last_used_step BIGINT NULL. TotpService::verify_code et verify_and_enable consomment le pas qui a réellement matché (matching_totp_step, skew ±1) via un compare-and-set SeaORM (UPDATE user_totp SET last_used_step = $step WHERE user_id = $id AND (last_used_step IS NULL OR last_used_step < $step), succès ssi rows_affected == 1). Un rejeu renvoie Ok(false) — sémantique identique à un code invalide, donc pas d'oracle de rejeu. Ferme le rejeu inter-réplicas que la garde in-process de 1.0.1 ne couvrait pas (une map par processus). La garde in-process est conservée en défense en profondeur, après le CAS. Les erreurs DB remontent en AppError : le garde ne fail jamais open. NULL signifie « aucun pas consommé depuis la migration » : le premier code présenté après migration est toujours accepté (grandfathering des lignes existantes).
  • Changement d'adresse e-mail en deux temps (BREAKING pour les appelants de update_profile — voir § Migration) (docs/rfcs/0041-pending-email-changes.md) — UserService::update_profile n'écrit plus jamais users.email. Un vrai changement renvoie une AppError::Validation explicite qui renvoie vers EmailChangeService::request_change ; un echo de l'adresse courante reste un no-op complet (les formulaires qui resoumettent tous les champs continuent de fonctionner). Le nouveau chemin :
    1. request_change re-authentifie l'utilisateur avec son mot de passe courant, vérifie l'unicité et le format, consomme toute demande pendante du même compte, écrit une ligne pending_email_changes et met deux mails en file : le lien de confirmation vers la nouvelle boîte, et un avis vers l'ancienne portant un lien de signalement. users.email et email_verified ne bougent pas.
    2. La confirmation écrit la nouvelle adresse déjà vérifiée (email_verified = true), révoque tous les refresh tokens du compte (les sessions en cours tombent) et invalide les demandes de changement de mot de passe pendantes. Trace audit_logs email_changed.
    3. Le signalement trace en audit_logs (niveau WARN) et alerte tous les administrateurs ; il ne bloque pas le changement — la boîte qui reçoit ce lien peut alerter, pas décider. Les envois ne sont pas best-effort : si la file d'e-mails est indisponible, request_change échoue au lieu de laisser une demande dont le lien n'arrivera jamais. Rien n'est écrit sur le compte dans ce cas. Limiteur dédié : 3 demandes par heure et par utilisateur (email_change:{user_id}, budget distinct de email_verify et pwd_reset), plus le budget IP existant sur POST /api/v1/auth/email-change. Les adresses sont masquées dans les logs, l'audit et les hooks (jo***@ex***), y compris dans le corps de réponse de la demande. Ce que cela ferme par rapport à la v1 annoncée (RFC 0038, jamais publiée, désormais Superseded) : plus de lock-out sur faute de frappe — une adresse mal saisie ne reçoit simplement jamais de clic, la demande expire et le compte n'a jamais bougé ; plus de fenêtre « adresse changée mais session toujours valide » ; plus de couplage avec la politique validation_email_required de la RFC 0040. Recours opérateur : admin_force_change remplace le changement d'adresse par PUT /admin/users/{id}. Elle exige un acteur portant le rôle admin (sinon AppError::Forbidden), écrit l'adresse sans confirmation, révoque les sessions de la cible, notifie les deux boîtes et trace en audit_logs (email_changed_by_admin, WARN) + on_audit_event. Le hook on_email_verified n'est pas émis : il n'y a eu aucune preuve de possession. Événements on_audit_event émis — les trois nomment leur cible, un consommateur peut donc router sur target sans distinguer les chemins : user.email_changed (actor_id et target = le compte, chemin libre-service), user.email_change_reported (actor_id = None — le signalement vient d'un lien de mail, pas d'une session — target = le compte), user.email_changed_by_admin (actor_id = l'opérateur, target = la cible). Adresses masquées dans les trois metadata. Limite connue : un utilisateur qui a perdu à la fois son mot de passe et l'accès à son adresse courante ne se débloque plus seul — c'est un recours administrateur après vérification d'identité hors bande. La faute de frappe à l'inscription relève du même recours : register écrit l'adresse saisie sans double vérification, ce flux n'est pas traité par cette RFC. Détail : docs/developer-guide/07-security-model.md § Cycle de vie de l'e-mail vérifié.
  • Charset username appliqué aux frontières d'écriture (docs/rfcs/0036-username-charset-validation.md) — UserService::create_user, UserService::update_profile et POST /api/v1/auth/register refusent les identifiants hors charset (backticks, _, ., majuscules, noms réservés). Réduit la surface d'injection dans les chemins d'URL, les templates et les logs. Aucun CHECK SQL, aucune migration, aucun rewrite : les comptes existants hors charset restent utilisables et continuent de se connecter. update_profile ne valide que sur un vrai rename — un client qui renvoie le username courant (y compris un handle grandfathered) est traité en no-op.
  • AuthService::authenticate_user honore validation_email_required (docs/rfcs/0040-authenticate-user-email-verified.md) — le contrôle « e-mail vérifié » a été déplacé du handler HTTP login vers le service. Mêmes clés de réglage (validation_email_required, alias legacy validation_email_requise), même défaut true, même comportement fail-closed (une erreur de lookup vaut « vérification requise »), même message d'erreur français et même AppError::Validation (HTTP 400). Le contrôle intervient après la vérification du mot de passe, donc pas d'oracle « le compte existe mais n'est pas vérifié ». Conséquence : une app consommatrice qui appelait le service (SSR maison, handler custom) ne peut plus contourner la politique. AuthService::verify_credentials reste la primitive « mot de passe seul » (utilisée par le re-prompt TOTP) et n'applique pas ce contrôle. Effet de bord documenté (décision v1) : le blocage n'émet plus le hook on_audit_event / auth.login_blocked — la ligne audit_logs est toujours écrite par AuthLoggingService::log_blocked_login, mais le paramètre hooks vaut None depuis le service.

Migration

  1. Sauvegarder : pg_dump avant toute opération.
  2. cargo run -p rustwarden-cli -- update-project (ou vendor manuel) pour récupérer rustwarden-core / rustwarden-ui et les docs framework-owned (docs/SERVICES_REFERENCE.md, docs/MIGRATION_GUIDE.md).
  3. Aligner les pins de dépendances de crates/rustwarden-core/Cargo.toml (sea-orm / sea-orm-migration 2.0.0, sea-bae = "0.2.2", validator_derive = "0.20.1"), puis cargo update.
  4. Jouer la migration avant de déployer le binaire : cargo run --bin migrate (ou auto_migrate(true) sur un déploiement mono-instance).
  5. Rediriger les appels qui changeaient l'e-mail (RFC 0041) : grep -rn "update_profile" src/ — tout appel passant une nouvelle adresse doit basculer sur EmailChangeService::request_change (chemin utilisateur) ou EmailChangeService::admin_force_change (chemin opérateur, en remplacement de PUT /admin/users/{id} pour l'adresse). Un echo de l'adresse courante reste un no-op : rien à changer pour les formulaires qui resoumettent tous les champs. Vérifier avant le bump que SMTP et le worker email_queue tournent : sans file fonctionnelle, request_change échoue volontairement au lieu d'écrire une adresse non prouvée.
  6. cargo clippy --workspace -- -D warnings puis les smoke tests auth (login, register, changement d'e-mail, 2FA).
  7. Retirer les workarounds locaux devenus redondants : newtype Username maison, overlay SetResponseHeaderLayer::overriding(CONTENT_SECURITY_POLICY, …), tout patch re-vérifiant email_verified après authenticate_user, tout code applicatif qui, après un changement d'adresse, remettait email_verified à la main, enqueuait un mail de validation / une alerte account_modified, ou appelait PasswordChangeService::invalidate_on_email_change.
  8. Facultatif : démarrer la purge des demandes expirées — le framework ne le fait pas. PendingEmailChangeCleanupProcessor::new(db.clone()).start(); (rétention 30 jours après expiration, constante de code, aucune variable d'environnement). Sans ce démarrage, une purge opportuniste limitée à l'utilisateur courant s'exécute à chaque nouvelle demande.

Table Avant / Après par RFC, greps de repérage et pièges connus : docs/MIGRATION_GUIDE.md § 2026-09-18 — RFC 0036-0040 (1.1.0).

[1.0.1] - 2026-09-18 — Security

Patch de sécurité (pas de nouvelle API publique). Fenêtre d'adoption 14 jours pour les consommateurs (docs/RUSTWARDEN_EVOLUTION_PROCESS.md §9).

Security

  • E-3 / IP rate-limit — run() sert via into_make_service_with_connect_info ; layer auth_ip_rate_limit sur login, register, refresh, forgot-password, 2FA verify et resend-verification. Le budget n'est plus global auth:unknown.
  • E-3 / TOTP — plafond somme des tentatives par utilisateur (15 / 15 min) au lieu de compter les challenges saturés ; anti-rejeu in-process du même pas TOTP (TTL 90 s). Pas de migration last_used_step (RFC 0037).
  • C-7 — JWT_SECRET rejette le placeholder CLI REMPLACER_PAR_UNE_CLE_SECRETE_LONGUE (égalité exacte, pas de contains). AdminConfig::validate refuse password et REMPLACER_PAR_UN_MOT_DE_PASSE_FORT. .env.example du wizard n'embarque plus ces littéraux.
  • M-2 — PasswordResetService::reset_password applique validate_password_complexity avant le hash et avant de consommer le token.
  • M-3 — limiteurs reset mot de passe / validation email partagés (OnceLock) au lieu d'une instance par requête ; éviction LRU au cap au lieu d'un 429 sur une clé neuve.
  • M-8 — vérification des backup codes TOTP dans spawn_blocking.
  • Force-2FA / lookup / blacklist — fail-closed : erreur DB TOTP ou lookup utilisateur → 503 ; user absent → non authentifié ; blacklist JWT en erreur → refus. Plus de unwrap_or(false) / is_ok_and fail-open sur ces chemins.
  • Admin bootstrap — AdminConfig::validate + PasswordRules uniquement sur le chemin de création (un ADMIN_PASSWORD faible résiduel ne casse plus le boot si un admin existe déjà).
  • sanitize_redirect — normalisation \ → / pour bloquer /\evil.example.
  • Dépendances — bcrypt 0.19.3, rustls 0.23.45, h2 0.4.19 (lockfile).
  • PostgreSQL dev — bind 127.0.0.1:5432 ; POSTGRES_PASSWORD obligatoire (:?, plus de fallback pass3ord).

[Unreleased]

Fixed

  • Middleware AuthService — branche optional auth alignée sur la branche protected (cf. docs/rfcs/0031-auth-service-optional-fallback.md, plan docs/plans/2026-05-16-auth-service-optional-fallback.md). La branche optional applique désormais le fallback AuthConfig::from_env() quand l'extension Extension(Arc<AuthConfig>) n'est pas encore posée par les layers downstream — symétrise le comportement avec la branche protected patchée précédemment par d4d6828. Conséquence : tout consommateur qui mappait des routes auth-required hors protected_prefixes (typiquement les intégrations fédérées qui consomment Option<Extension<Claims>>) voit enfin les claims injectés au lieu de recevoir silencieusement None puis d'être redirigé vers /login. Les register_protected_prefix() ajoutés uniquement pour contourner ce bug peuvent être retirés (garder ceux qui sont là pour rejeter les non-authentifiés). Aucun changement d'API publique. Test de non-régression ajouté dans crates/rustwarden-core/tests/middleware_authconfig_fallback.rs.

Changed (BREAKING)

  • Extraction du crate rustwarden-fediverse (cf. docs/plans/2026-05-11-extract-rustwarden-fediverse-crate.md). La fédération ActivityPub vit désormais dans un crate dédié (crates/rustwarden-fediverse/, ~9 000 lignes) plutôt que dans le module rustwarden_core::fediverse. Les consommateurs externes doivent migrer leurs imports :
    • rustwarden_core::fediverse::* → rustwarden_fediverse::*
    • rustwarden_core::services::fediverse_*_service → rustwarden_fediverse::services::fediverse_*_service
    • rustwarden_core::services::actor_key_service → rustwarden_fediverse::services::actor_key_service
    • rustwarden_core::services::remote_identity_service → rustwarden_fediverse::services::remote_identity_service
    • rustwarden_core::models::{remote_identity, fediverse_*} → rustwarden_fediverse::models::*
    • rustwarden_core::config::FediverseConfig → rustwarden_fediverse::FediverseConfig La feature Cargo experimental-fediverse (côté rustwarden-core) est supprimée ; côté rustwarden-ui et app la nouvelle feature s'appelle fediverse et tire la dépendance optionnelle rustwarden-fediverse. La méthode RustwardenBuilder::with_fediverse() est remplacée par un mount explicite : ouvrir la connexion DB tôt, construire rustwarden_fediverse::routes::router(config, db), puis enchaîner .register_protected_prefix("/fediverse/consent").register_protected_prefix("/fediverse/invite").register_revocation_checker(checker).merge_routes(router) sur le builder (voir app/src/main.rs). Schéma DB inchangé — toutes les migrations restent dans rustwarden-core/migrations/ (code SQL DSL pur, aucune dépendance sur les modèles Rust).
  • AuthService::new change de signature : prend désormais protected_prefixes: Arc<Vec<String>> en troisième argument au lieu d'une liste hardcodée. Les consommateurs enregistrent leurs préfixes via RustwardenBuilder::register_protected_prefix(prefix). Les valeurs par défaut installées par le builder (/home, /profile, /test-base, /settings, /admin) sont préservées ; rustwarden-fediverse ajoute /fediverse/consent et /fediverse/invite lors du mount.
  • F10 (révocation remote JWT) via trait : le check remote_identities.disabled_at effectué par AuthService au passage d'une route protégée est désormais délégué à un trait RemoteIdentityRevocationChecker injecté en Extension. rustwarden-fediverse fournit l'implémentation SeaORM (SeaOrmRemoteIdentityRevocationChecker) ; sans le crate de fédération, le check est sauté — comportement correct car aucun remote JWT ne peut être émis.
  • Module identity (RFC 0008) reste dans rustwarden-core sans gate de feature. Les traits Identity et IdentityResolver sont des primitives d'auth/RBAC indépendantes de la couche fédération.
  • Race FEDIVERSE_DOMAIN corrigée dans les tests : les helpers test_fediverse_config() des services fediverse_auth_service et fediverse_invite_service n'utilisent plus unsafe { std::env::set_var } ; ils appellent directement FediverseConfig::for_tests("test.local"), ce qui supprime un risque de flakiness inter-tests.
  • i18n complétée : les 5 namespaces fediverse (fediverse, fediverse_nodeinfo, fediverse_invites, fediverse_connected_apps, fediverse_invite_landing) sont désormais présents dans les 6 locales (fr/en/de/es/it/pt) — auparavant seuls fr et en étaient traduits.
  • Tests E2E doivent tourner en série : cargo test -p rustwarden-fediverse --test e2e_fediverse -- --test-threads=1. Les 32 scénarios mutent l'état partagé du testbed Docker (trust list, kill_domain, remote_identities.disabled_at, fediverse_trust_mode) et se cassent en parallèle. La règle est documentée dans tests/fediverse-testbed/README.md. Le test S3 (s3_untrusted_domain_is_rejected_with_403) flippe désormais lui-même fediverse_trust_mode en allowlist (testbed boot default open_with_blocklist) et restore avant l'assertion.
  • Validation finale Phase 5 : cargo build --workspace, cargo clippy --workspace --features rustwarden-app/fediverse -- -D warnings et cargo test --workspace passent ; les 32 tests E2E + les tests interop wiremock contre le testbed Docker tri-instance passent en --test-threads=1. Le Dockerfile du testbed a été mis à jour pour utiliser la nouvelle feature fediverse au lieu de l'ancien experimental-fediverse.

Added

  • RFC 0029 — RemoteResourceShareService (rustwarden_fediverse::services::remote_resource_share_service) : partage de ressources métier (repository, wall, …) avec une identité ActivityPub remote, sans passer par une team. 5 méthodes publiques (share, unshare, effective_permission_for_remote_identity, list_shares, list_for_remote_identity) — upsert par (resource_id, remote_identity_id), hiérarchie read < write < admin, FK cascade sur resources / remote_identities / users. Migration m20260516_000002_remote_resource_shares. 11 tests unitaires MockDatabase. Origine : RFC 0001 gitrust. Doc : SERVICES_REFERENCE §21.15.
  • RFC 0030 — RemotePersonalAccessTokenService<S> (rustwarden_fediverse::services::remote_personal_access_token_service) : PAT pour identités ActivityPub remote, generic sur le trait TokenScope framework. 5 méthodes publiques (create, verify, revoke, list_for_remote_identity, revoke_all_for_remote_identity) — SHA-256 + constant-time compare via subtle, soft-revoke, refus du token si l'identité remote est disabled_at. Préfixe distinct rwfed_pat_<64 hex> pour discrimination côté consommateur. Migration m20260516_000003_remote_pat (table + partial index sur token_hash WHERE revoked_at IS NULL). 20 tests unitaires MockDatabase. Origine : RFC 0002 gitrust. Doc : SERVICES_REFERENCE §21.16.
  • Documentation opérateur : nouveau guide docs/fediverse-operations.md (10 sections, Diátaxis how-to + reference) : activation, env vars, app_settings clés, trust/block list, incident playbook, observabilité, GC, critères promotion stable, URLs exposées, limites connues.
  • Observabilité fédiverse — toggle DB default-off : nouveau réglage app_settings.fediverse_observability_enabled (default false) et helper public services::fediverse_outbound_service::is_fediverse_observability_enabled. Les tracing::info!/warn!/error! structurés (RFC 0019) émis par FediverseOutboundService::drain_one sont désormais conditionnés à ce flag. Sans agent (Vector/Promtail) qui consomme, laisser false pour éviter le bruit logs ; activer quand un pipeline d'observabilité est en place.
  • Audit privacy passé : revue manuelle confirme que les payloads AP exposés (Person, WebFinger, NodeInfo) sont minimaux — aucune fuite email / password hash / rôles internes / IPs. UserSummary est une DTO curatée. Aucun tracing::* ne log password / JWT / clé privée / encryption key brut. Finding F12 latent identifié (summary_html reçu via Update(Person) stocké brut, mais jamais rendu UI — non exploitable ; ajouter ammonia::clean() à l'INSERT si jamais utilisé en rendu).
  • Suite E2E à 32 tests : ajout des scénarios S5 (modes de panne — 4 tests) et S6 (lifecycle Update/Delete Person — 2 tests). Les 6 scénarios du plan parent (S1-S6) sont désormais automatisés en HTTP réel avec crypto réelle.
    • S5 valide la replay protection (UNIQUE constraint sur fediverse_auth_challenges.nonce), le WebFinger 404, la validation handle malformé, et documente F11 (clock skew non-enforced avec AlwaysAcceptVerifier — non-bloquant, by-design dev mode).
    • S6 valide que Update(Person) et Delete(Person) signés sur /inbox propagent correctement : refresh display_name, désactivation remote_identities.disabled_at, et invalidation instantanée des JWT remote via F10.
  • F10 — Révocation immédiate des JWT remote lors de kill_domain : ajout d'un check disabled_at sur remote_identities dans le middleware AuthService (feature-gated experimental-fediverse). Un JWT remote dont l'identité backing a été désactivée est immédiatement rejeté (redirect 303 vers /login) au prochain request, sans nécessiter d'entrée dans jwt_blacklist. Cheap : un SELECT par PK.
  • F9 — Auto-kill sur add_blocked_domain : le handler admin add_trust_entry (mode OpenWithBlocklist) effectue désormais atomiquement l'INSERT dans fediverse_blocked_domains ET un UPDATE disabled_at sur tous les remote_identities du domaine. Un seul clic admin pour « couper l'instance hostile » — combiné avec F10 les sessions actives sont rejetées au prochain request.
  • Suite E2E étendue à 26 tests : +4 tests S4 (s4_blocked_domain_payload_is_rejected_with_403, s4_kill_domain_marks_remote_identities_disabled, s4_add_blocked_domain_auto_kills_remote_identities, s4_kill_domain_invalidates_active_remote_jwt). Le dernier valide bout-en-bout via vrai HTTP (POST signé → JWT remote minté → GET protégé OK → kill_domain → GET protégé redirige /login).
  • F8 — Trust enforcement au niveau protocole : la trust list était décorative — FediverseTrustService::is_domain_allowed existait uniquement appelé depuis l'UI. Un domaine evil.tld signé valide pouvait POST des activités à n'importe quelle instance Rustwarden, indépendamment de la trust list.
    • Nouvelle méthode FediverseTrustService::assert_domain_allowed qui retourne AppError::Validation quand un domaine n'est pas autorisé par le mode courant.
    • Nouvelle variante FediverseError::Forbidden(String) → 403.
    • Appel assert_domain_allowed au début de auth_inbox et /inbox, avant la vérification de signature (rejette les domaines hostiles sans même fetcher leur actor).
    • 2 tests E2E S3 dédiés : s3_untrusted_domain_is_rejected_with_403 confirme l'enforcement, s3_trusted_domain_still_passes_protocol_inbox vérifie l'absence de régression.
  • Suite E2E étendue : passe de 14 à 22 tests (8 nouveaux pour S2 + S3) en 7.83s. Nouvelle couverture :
    • 4 tests S2 (admin invite → team_member binding, reject, revoke, listing IdP-side).
    • 4 tests S3 (non-transitivité du trust, F8 enforcement positif et négatif).
  • RFC 0025 — Fediverse back-channel Accept(AuthRequest) (RFC 0006 Phase 3) : comble le trou identifié par l'évaluation E2E (iter 1c) où A approuvait un challenge mais B ne le savait jamais. Le SSO bilatéral A ↔ B est désormais opérationnel bout en bout sans DB-direct.
    • FediverseAuthService::record_inbound_decision — applique un Accept ou Reject reçu sur l'inbox SP. Sécurité : domaine de l'approver vérifié contre challenge.domain (anti-spoofing). Idempotent sur replay.
    • auth_inbox dispatche désormais sur le champ type AP (AuthRequest / Accept / Reject / inconnu → silent ignore per AP §7).
    • Helper public build_auth_decision_payload(...) qui sérialise le payload JSON-LD au format AP standard avec object en forme inline-object pour porter le nonce.
    • consent_submit (UI) enqueue le Accept/Reject sortant via FediverseOutboundService::enqueue après record_decision.
    • Recherche externe : on utilise les types AP standards Accept / Reject (alignés sur le pattern Mastodon Accept(Follow)) plutôt qu'inventer AuthApproval/AuthDenial.
  • F2 — AlwaysAcceptVerifier populate remote_identities : le verifier test/dev (with_resolver(db)) fetche désormais l'actor URL et upsert sa ligne remote_identities au lieu de laisser le cache vide. Restaure le contrat documenté de persist_inbound_request.
    • Helper public réutilisable services::fediverse_signal_service::fetch_and_cache_remote_actor avec hardening : timeout 10s, body cap 1 MiB (Content-Length pre-flight
      • bytes() post-read), SSRF guard rejetant loopback / private IPv4 / link-local / ULA IPv6 (allow_private_ips=true réservé au testbed).
    • 9 nouveaux tests unitaires couvrant happy path INSERT, refetch UPDATE, 404, JSON malformé, publicKey manquant, inbox manquant, URL invalide, body trop gros, SSRF guard loopback.
  • F1 — Normalisation FEDIVERSE_DOMAIN : auth_inbox accepte désormais la forme http://example.org aussi bien que example.org. Strip scheme / trailing slash avant comparaison contre le target.domain.
  • Testbed tests/fediverse-testbed/ : topologie 3 instances locales 100 % automatisable (Docker Compose), 4 scripts cycle de vie (setup.sh, start.sh, stop.sh, teardown.sh). Image multi-stage rustc 1.91-slim embarquant les binaires rustwarden, migrate, seed. Réseau Docker avec network_aliases alpha.local / beta.local / gamma.local ; pas de TLS, pas de /etc/hosts requis grâce à FEDIVERSE_DEBUG_LOCAL_FEDERATION.
  • Suite E2E crates/rustwarden-core/tests/e2e_fediverse.rs — 14 tests HTTP réels contre le testbed, dont 6 du flux S1 SSO complet avec signatures RSA-SHA256 réelles (iter 1a / 1b / 1c / 1d) :
    • 7 smoke tests (WebFinger, NodeInfo, Actor fetch, inbox 401)
    • 1 test signed AuthRequest B → A (iter 1b)
    • 1 test consume B → JWT remote (iter 1c)
    • 5 tests F3 round-trip Accept/Reject (iter 1d) : happy path, reject, unknown nonce, expired challenge, replay idempotence
  • Workspace : edition Rust 2024 + rust-version = "1.85" au niveau [workspace.package]. Toutes les crates héritent (edition.workspace = true).
  • RFC 0006 Phase 2 — inbox enrichment côté IdP : auth_inbox persiste désormais le challenge entrant + résout la remote_identities du requester.
    • Nouvelle méthode publique FediverseAuthService::persist_inbound_request(db, body, idp_domain) qui :
      1. Parse les champs actor / target / nonce / expiresAt du payload AP AuthRequest.
      2. Valide que target_domain correspond bien à l'IdP (refus si requête mal-routée).
      3. Résout remote_identities par ap_id == actor_url (la row est créée upstream par le verifier de signature, RFC 0017).
      4. Insère un challenge Pending avec requested_remote_id peuplé.
    • Câblage dans crates/rustwarden-core/src/fediverse/auth_routes.rs:auth_inbox après vérification de signature : sur erreur de Validation, 400 BadRequest renvoyé proprement (au lieu du précédent 202 silencieux).
    • Débloque :
      • Slice B Approve E2E (RFC 0023 iter. 4) : consent_submit peut désormais lire requested_remote_id rempli et passer à record_decision::Approve sans exception Validation.
      • Slice C /profile/connected_apps (RFC 0024 iter. 5) : la table d'apps connectées sera désormais peuplée dès qu'un user remote authentifie via cet IdP.
    • 4 nouveaux tests unitaires sur persist_inbound_request : happy path, mismatched IdP domain, missing actor, unknown requester.
    • Test existant auth_inbox_returns_202_for_signed_authrequest renommé en auth_inbox_returns_400_when_requester_actor_unknown et adapté pour valider la nouvelle sémantique 400.
    • 5 quality gates verts : build, tests 413 pass / 0 fail / 19 ignored (+4 vs avant), clippy -D warnings, doc clean, pas de migration.
  • RFC 0024 (Slice C iter. 6 — close-out) — Fediverse UI: page d'acceptation invitation team cross-instance /fediverse/invite/{id}.
    • 2 nouveaux handlers dans crates/rustwarden-ui/src/handlers/fediverse_user_auth.rs : invite_landing_form (GET) charge l'invite + le team, affiche le statut, indique si l'utilisateur courant peut accepter (= Claims::is_remote() && remote_identity_id().is_some()) ; invite_landing_submit (POST, CSRF) appelle FediverseInviteService::accept(invite_id, remote_id) sur Approve, ou reject(invite_id) sur tout autre.
    • Nouveau template standalone crates/rustwarden-ui/templates/fediverse/invite_landing.html avec card DaisyUI affichant team / rôle / statut, badges colorés par status, alert warning quand l'utilisateur courant ne peut pas accepter (local user — explication explicite de la mécanique AP-native), bouton Accept conditionnel {% if can_accept %}.
    • 2 nouvelles routes UI authentifiées (gated feature, mode dégradé 404). Extension AuthService::call : crates/rustwarden-core/src/middleware/auth_service.rs ajoute /fediverse/invite à protected_routes pour injecter Claims dans la requête.
    • i18n : namespace fediverse_invite_landing: (top-level dédié) — 11 nouvelles clés FR + EN avec note explicite UX sur la contrainte « accept = remote identity uniquement ».
    • Slice C clôturé : 6 itérations livrées (RFC 0024 + queue + sessions_remote + nodeinfo + team_invites + connected_apps + invite_landing). 11 handlers, 6 templates HTML, 14 routes, ~110 clés i18n FR+EN.
    • 5 quality gates verts : build, tests 409 pass / 0 fail / 19 ignored, clippy -D warnings, doc clean, pas de migration.
  • RFC 0024 (Slice C iter. 5) — Fediverse UI: section profil utilisateur « Apps connectées ».
    • Nouveau handler connected_apps dans crates/rustwarden-ui/src/handlers/fediverse_user_auth.rs qui interroge fediverse_auth_challenges pour le handle de l'utilisateur courant (@username@FEDIVERSE_DOMAIN) avec status IN ('approved', 'consumed'), regroupe par domaine, calcule le compte de sessions + dernière activité.
    • Nouveau template standalone crates/rustwarden-ui/templates/profile/fediverse_connected_apps.html avec table zebra (Instance / Sessions / Dernière activité), alert info expliquant que la révocation manuelle arrivera avec RFC 0006 Phase 2, empty state, lien retour profil.
    • Route GET /profile/connected_apps (gated feature, mode dégradé 404). Intercepte Claims via le préfixe /profile déjà dans AuthService::protected_routes.
    • i18n : namespace fediverse_connected_apps: (top-level dédié) — 11 nouvelles clés FR + EN avec note explicite sur la limitation de révocation actuelle.
    • Iter. 5 lecture-seule : la révocation explicite côté utilisateur est différée à RFC 0006 Phase 2 (méthode FediverseAuthService::revoke_remote_session(challenge_id) avec blacklist JWT) ; en attendant, l'admin peut révoquer via /admin/fediverse/sessions_remote (iter. 2) ou attendre l'expiration naturelle du JWT.
    • 5 quality gates verts : build, tests 409 pass / 0 fail / 19 ignored, clippy -D warnings, doc, pas de migration.
  • RFC 0024 (Slice C iter. 4) — Fediverse UI: invitations team cross-instance /admin/fediverse/invites/{team_id}.
    • 3 nouveaux handlers dans crates/rustwarden-ui/src/handlers/admin_fediverse.rs :
      • get_team_invites (GET) liste les 100 dernières invitations pour un team_id, tous statuts confondus.
      • create_team_invite (POST add, CSRF) parse handle + role, appelle FediverseInviteService::invite_remote avec TTL 7j.
      • revoke_team_invite (POST revoke, CSRF) appelle FediverseInviteService::revoke (autorisé sur Pending + Accepted).
    • Nouveau template crates/rustwarden-ui/templates/admin/fediverse/team_invites.html avec form ajout (handle pattern + select rôle), table zebra avec badges colorés par status, bouton révoquer conditionnel (Pending/Accepted seulement), modal confirmation Alpine.
    • 3 nouvelles routes admin (gated feature, mode dégradé 404, vérifie l'existence du team avant rendu).
    • i18n : namespace fediverse_invites: (top-level dédié) — 25 nouvelles clés FR + EN.
    • 5 quality gates verts : build, tests 409 pass / 0 fail / 19 ignored, clippy -D warnings, doc, pas de migration.
  • RFC 0024 (Slice C iter. 3) — Fediverse UI: page NodeInfo humanisée /admin/fediverse/nodeinfo.
    • Nouveau handler get_nodeinfo dans crates/rustwarden-ui/src/handlers/admin_fediverse.rs qui calcule en live :
      • software : nom (rustwarden), version (CARGO_PKG_VERSION), domaine (FEDIVERSE_DOMAIN env) ;
      • runtime : trust_mode, interop_scope, authorized_fetch (lus depuis app_settings) ;
      • users : total, actifs 30 j, actifs 180 j (sur last_login_at) ;
      • remote identities : actives + nb domaines distincts.
    • Nouveau template crates/rustwarden-ui/templates/admin/fediverse/nodeinfo.html avec 4 cards (grid-cols-1 md:grid-cols-2) : Logiciel / Configuration runtime / Utilisateurs locaux / Identités distantes. Stats DaisyUI colorées.
    • Route GET /admin/fediverse/nodeinfo (gated feature, mode dégradé 404).
    • i18n : namespace dédié fediverse_nodeinfo: + 2 clés common.yes/ common.no (raison : éviter de re-ouvrir fediverse: qui aurait écrasé sous yaml-rust2 toutes les sous-clés settings/trust/auth/queue/sessions_remote). 22 nouvelles clés fediverse_nodeinfo.* FR + EN.
    • 5 quality gates verts : build, tests 409 pass / 0 fail / 19 ignored, clippy -D warnings, doc, pas de migration.
  • RFC 0024 (Slice C iter. 2) — Fediverse UI: page sessions distantes
    • kill switch domaine.
    • 3 nouveaux handlers dans crates/rustwarden-ui/src/handlers/admin_fediverse.rs : get_sessions_remote (GET) liste les rows remote_identities actives (disabled_at IS NULL, ordonnées last_fetched_at desc, limit 200) ; revoke_remote_session (POST, CSRF) marque une row spécifique disabled_at = now ; kill_domain (POST, CSRF) bulk-update toutes les rows actives d'un domaine.
    • Nouveau template crates/rustwarden-ui/templates/admin/fediverse/sessions_remote.html avec card kill-switch (border error + select des domaines distincts)
      • table zebra des sessions + 2 modals confirmation Alpine (role="alertdialog" Plan UI §3 P2) + empty state.
    • 3 nouvelles routes admin (gated feature, mode dégradé 404).
    • i18n FR + EN : 21 nouvelles clés fediverse.sessions_remote.*.
    • 5 quality gates verts : build, tests 409 pass / 0 fail / 19 ignored, clippy -D warnings, doc, pas de migration.
  • RFC 0024 (Slice C iter. 1) — Fediverse UI: dashboard file outbound /admin/fediverse/queue.
    • Nouvelle RFC docs/rfcs/0024-fediverse-ui-ops-slice-c.md — cadre les 6 écrans Slice C (queue, sessions_remote, nodeinfo, invites team, connected_apps, invite_landing).
    • Nouveau handler get_queue dans crates/rustwarden-ui/src/handlers/admin_fediverse.rs qui :
      • compte les rows par status (pending / delivered / failed / dead) via PaginatorTrait::count ;
      • charge les 100 dernières rows ordonnées created_at desc, tronque les URLs (60 chars) et erreurs (80 chars) pour la lisibilité ;
      • rend le template avec stats grid + table zebra + empty state explicite.
    • Nouveau template crates/rustwarden-ui/templates/admin/fediverse/queue.html avec 4 stat cards colorées par sévérité + table avec badges conditionnels (warning/success/error/neutral selon status) + empty state.
    • Route GET /admin/fediverse/queue (gated feature, mode dégradé 404 si fediverse_enabled = false).
    • i18n FR + EN : 18 nouvelles clés fediverse.queue.*.
    • 5 quality gates verts : build, tests 409 pass / 0 fail / 19 ignored, clippy -D warnings, doc, pas de migration.
  • RFC 0023 (Slice B iter. 5 — close-out) — Procédure E2E manuelle documentée.
    • Nouveau guide docs/fediverse-e2e-manual.md décrivant la procédure de test bout-en-bout deux instances locales (sans Docker, deux cargo run sur des ports différents avec bases distinctes) et qualifiant 5 chemins négatifs testables aujourd'hui (Reject, untrusted, malformed handle, timeout, mode dégradé 404).
    • Slice B clôturé : 4 itérations livrées (RFC 0023 + error_unauthorized, login_form, login_waiting+SSE, consent_screen). 8 routes UI, 38 clés i18n FR + EN, 6 nouveaux tests unitaires. Le « happy path Approve » E2E reste bloqué par la dette RFC 0006 Phase 2 (enrichment du challenge avec requested_remote_id côté inbox), documenté §7 du guide.
  • RFC 0023 (Slice B iter. 4) — Fediverse UI: écran de consentement IdP-side.
    • 2 nouveaux handlers dans crates/rustwarden-ui/src/handlers/fediverse_user_auth.rs : consent_form (GET, requiert local user loggué) renvoie l'écran de validation, consent_submit (POST, CSRF) appelle FediverseAuthService::record_decision.
    • Sur Reject : décision enregistrée, redirect /home.
    • Sur Approve : exige que la row challenge porte déjà un requested_remote_id (mis par l'inbox handler côté IdP — voir TODO dans auth_routes.rs:188). Sans ce binding, le handler refuse l'approbation avec une erreur Validation explicite (évite qu'un attaquant injecte un remote_identity_id arbitraire via le form). Le wiring complet landera en RFC 0006 Phase 2.
    • Nouveau template standalone crates/rustwarden-ui/templates/fediverse/consent_screen.html avec card DaisyUI affichant domain + handle, deux boutons submit name="decision", et bascule vers un alert aria-live quand la challenge est déjà décidée.
    • 2 nouvelles routes UI authentifiées (gated feature, mode dégradé 404) : GET + POST /fediverse/consent/{challenge_id}.
    • Extension AuthService::call : crates/rustwarden-core/src/middleware/auth_service.rs ajoute /fediverse/consent à la liste protected_routes pour que Claims soit injecté dans la request avant le handler. Aucun impact sur les routes existantes.
    • i18n FR + EN : 8 nouvelles clés fediverse.auth.consent.*.
    • 5 quality gates verts : build, tests 409 pass / 0 fail / 19 ignored, clippy -D warnings, doc, pas de migration.
  • RFC 0023 (Slice B iter. 3) — Fediverse UI: page d'attente + Server-Sent Events.
    • 2 nouveaux handlers dans crates/rustwarden-ui/src/handlers/fediverse_user_auth.rs : waiting (GET) renvoie la page spinner, sse_status (GET) émet un événement SSE event: status toutes les 2 s avec {"state":"pending|approved|rejected|expired|consumed"}.
    • Stream construit via futures::stream::unfold (nouvelle dev-dep optionnelle futures = "0.3" activée par experimental-fediverse). Cap à 150 polls (5 min ↔ TTL challenge RFC 0006) puis fermeture propre. KeepAlive::default() envoie un :ping toutes les 15 s.
    • Nouveau template standalone crates/rustwarden-ui/templates/fediverse/login_waiting.html avec spinner DaisyUI (role="status" aria-live="polite") et JS EventSource natif (≤ 50 LoC, pas de framework lourd Plan UI §3 P10) qui :
      • sur approved → POST /api/v1/auth/login/fediverse/consume/{id} pour minter le JWT remote (RFC 0007), redirige /home ;
      • sur rejected → error?reason=rejected ;
      • sur expired → error?reason=timeout ;
      • sur consumed (déjà échangé) → /home ;
      • sur onerror (connexion perdue persistante) → fallback error?reason=timeout après 30 s.
    • 2 nouvelles routes publiques (gated feature, mode dégradé 404) : GET /login/fediverse/waiting/{id}, GET /login/fediverse/sse/{id}.
    • i18n FR + EN : 5 nouvelles clés fediverse.auth.waiting.*.
    • 5 quality gates verts : build, tests 409 pass / 0 fail, clippy -D warnings, doc, pas de migration.
  • RFC 0023 (Slice B iter. 2) — Fediverse UI: formulaire login ActivityPub.
    • 2 nouveaux handlers dans crates/rustwarden-ui/src/handlers/fediverse_user_auth.rs : login_form (GET) renvoie le formulaire HTML, login_initiate (POST) valide CSRF, parse @user@domain, vérifie FediverseTrustService::is_domain_allowed, appelle FediverseAuthService::initiate avec TTL 5 min, redirige vers /login/fediverse/waiting/{challenge_id} (page livrée iter. 3).
    • Sur mauvais handle → redirect error?reason=unknown.
    • Sur domaine refusé → redirect error?reason=untrusted.
    • Nouveau template standalone crates/rustwarden-ui/templates/fediverse/login_form.html avec input pattern="^@?[A-Za-z0-9._-]+@[A-Za-z0-9.-]+$", label for=, help-text via aria-describedby.
    • 2 routes publiques : GET + POST /login/fediverse (gated feature, mode dégradé 404).
    • i18n FR + EN : 8 nouvelles clés fediverse.auth.login.*.
    • 2 nouveaux tests unitaires sur extract_handle_domain (canonical
      • malformed). Workspace : 409 pass / 0 fail / 19 ignored.
    • 5 quality gates verts : build, tests, clippy -D warnings, doc, pas de migration.
  • RFC 0023 (Slice B iter. 1) — Fediverse UI: page d'erreur d'authentification fédérée (docs/rfcs/0023-fediverse-ui-user-auth.md).
    • Nouveau handler crates/rustwarden-ui/src/handlers/fediverse_user_auth.rs avec ErrorReason enum (5 variantes : timeout / rejected / untrusted / invalid_signature / unknown fallback).
    • Nouveau template standalone (sans extends, comme login.html) crates/rustwarden-ui/templates/fediverse/error_unauthorized.html avec icône SVG conditionnelle (clock/lock/alert) selon la variante, H1 + body i18n, CTA « Retour à la connexion ».
    • Route GET /login/fediverse/error?reason=… (publique, gated par feature). Mode dégradé silencieux Plan UI §3 P9 → 404 quand fediverse_enabled = false.
    • i18n FR + EN : 13 nouvelles clés fediverse.auth.error.* (page title + subtitle + back_to_login + 5 × {title, body}).
    • 4 tests unitaires sur ErrorReason (parsing, fallback, trim/lowercase, round-trip). Tests workspace : 407 pass / 0 fail / 19 ignored.
    • 5 quality gates verts : build, tests, clippy -D warnings, doc, pas de migration.
  • RFC 0022 — Smoke tests anti-régression (docs/rfcs/0022-smoke-tests-regression.md) : deux fichiers de tests qui ferment les boucles des deux régressions observées le 2026-05-10.
    • crates/rustwarden-core/tests/middleware_authconfig_fallback.rs — 3 tests vérifient que AuthConfig::from_env() reste fiable (succès avec JWT_SECRET, échec sans, idempotence). C'est le pré-requis du fallback ajouté à AuthService / AdminService / admin_helper.
    • crates/rustwarden-ui/tests/admin_handler_pagination.rs — 5 tests vérifient les formes additives utilisées dans users_list.html après le fix (p + 2 >= page, p + 3 == page) sur tout (page, p) ∈ [0..10]². Empêche le retour à p >= page - 2 qui underflow sur u64.
    • Quality gate : cargo test --workspace passe maintenant à 403 tests (395 + 3 RFC 0022 middleware + 5 RFC 0022 pagination).
  • Fix admin_handler::list_users panic attempt to subtract with overflow (pré-existant) :
    • Handler : ((page - 1) * per_page) as usize paniquait sur ?page=0. Corrigé par params.page.unwrap_or(1).max(1)
      • params.per_page.unwrap_or(50).max(1).
    • Template templates/admin/users_list.html lignes 211 / 214 : p >= page - 2 et p == page - 3 underflowaient sur u64 quand page < 3. Toute première visite /admin/users?page=1 (par défaut) crashait le worker tokio sans log d'erreur. Remplacé par les formes additives équivalentes p + 2 >= page && p <= page + 2 et p + 3 == page || p == page + 3. Couvert par RFC 0022.
  • rustwarden-app : feature experimental-fediverse exposée dans app/Cargo.toml — délègue à rustwarden-core/experimental-fediverse ET rustwarden-ui/experimental-fediverse. Permet cargo run -p rustwarden-app --features experimental-fediverse pour monter /admin/fediverse/settings (RFC 0021) et le backend ActivityPub (RFC 0001+).
  • Fix régression AuthService / AdminService / admin_helper (commit d4d6828) — quand Extension(Arc<AuthConfig>) n'est pas appliqué avant le layer AuthService dans le ServiceBuilder du consommateur, toute requête sur une route protégée renvoyait 500 Internal Server Error silencieusement (sans log ERROR). AuthService::call retombe désormais sur AuthConfig::from_env() en fallback, comportement identique à l'avant d4d6828. Un tracing::error! est levé seulement si from_env échoue aussi.
  • RFC 0021 (Slice A iter. 2) — Fediverse: page admin /admin/fediverse/trust_list. Deuxième écran de la couche UI fédération, fermant Slice A.
    • 3 nouveaux handlers dans crates/rustwarden-ui/src/handlers/admin_fediverse.rs : get_trust_list (GET), add_trust_entry (POST add), delete_trust_entry (POST delete). Dispatchent allow vs blocklist selon app_settings.fediverse_trust_mode.
    • Nouveau template crates/rustwarden-ui/templates/admin/fediverse/trust_list.html avec table zebra, badge mode actif, modal de confirmation Alpine (role="alertdialog"), formulaire d'ajout (champ severity conditionnel blocklist), état vide explicite (Plan UI §3 P4).
    • Mode dégradé silencieux (Plan UI §3 P9) : 404 retourné quand fediverse_enabled = false sur les 3 routes trust_list, settings reste accessible pour activer.
    • i18n FR + EN : 28 nouvelles clés fediverse.trust.*.
    • 5 quality gates verts : build, tests 403 pass / 0 fail / 19 ignored, clippy -D warnings, doc clean, pas de migration.
    • Différé iter. 3 : tests handler trust_list (5 états P4), partials Askama réutilisables (le partage cross-template via {% include %} nécessite une refacto en macros Askama qui dépasse Slice A).
  • RFC 0021 (Slice A iter. 1) — Fediverse: page admin /admin/fediverse/settings. Premier écran HTML de la couche UI fédération.
    • Nouveau handler crates/rustwarden-ui/src/handlers/admin_fediverse.rs (GET + POST), gated par experimental-fediverse.
    • Nouveau template crates/rustwarden-ui/templates/admin/fediverse/settings.html avec 4 cards (Activation / Confiance / Interopérabilité / Sécurité avancée), CSRF, breadcrumb, toast Alpine ?saved=1.
    • Persistence SeaORM via AppSettingsService::set_setting sur 4 clés : fediverse_enabled, fediverse_trust_mode, fediverse_interop_scope, fediverse_authorized_fetch. Validation des sélecteurs côté serveur (whitelist).
    • i18n complète FR + EN (24 clés fediverse.settings.*) ; les autres locales (de/es/it/pt) tombent automatiquement en fallback FR via I18nService (cf. service.rs:159).
    • Conformité Plan UI HTML §3 : P1 (CTA primaire unique), P2 (confirmation modale — pour révocations dans Slice C), P3 (toast succès auto-dismiss 4 s), P7 (<label for>, aria-current, aria-live="polite"), P10 (HTMX + Alpine seuls).
    • 5 quality gates verts : build clean, 395 tests pass / 0 fail / 19 ignored, clippy -D warnings zéro, doc clean (2 warnings préexistants admin_dto.rs:134 hors scope), pas de migration.
    • Reste pour Slice A (iter. 2) : page /admin/fediverse/trust_list, partials _modal_confirm.html + _toast.html + _empty_state.html, tests handlers, couverture des 5 états (empty / 5xx / 403 / loading / happy).
  • Phase 5 — quality gate cargo audit : nouvelle config .cargo/audit.toml qui figue les advisories transitives sans fix upstream avec justification par entrée (RUSTSEC-2023-0071 rsa Marvin Attack ; RUSTSEC-2023-0086 lexical-core via imap-proto ; RUSTSEC-2026-0097 rand custom-logger unsoundness). cargo audit passe désormais clean (0 vulnerability) en local et en CI. Toute nouvelle ignore exige une ligne traçant le pourquoi vers docs/SECURITY_AUDIT_2026_05.md §H3.
  • RFC 0020 — Fediverse: tests d'interopérabilité wiremock (signed delivery round-trip).
    • Nouvelle dev-dependency wiremock = "0.6" sur rustwarden-core (test only, non livrée aux apps consommatrices).
    • Nouveau binaire de tests d'intégration crates/rustwarden-core/tests/fediverse_interop.rs sous flag experimental-fediverse :
      1. signed_request_round_trip_through_wiremock — POST signé capturé par wiremock puis re-vérifié via parse_and_verify_cavage contre la même clé publique.
      2. outbound_delivers_when_inbox_returns_202 — drain_one flippe la row à Delivered quand l'inbox répond 202.
      3. outbound_defers_when_inbox_returns_503 — drain_one laisse la row Pending avec next_retry_at calé sur le ladder RFC 0010 (5 min pour la 1ʳᵉ tentative) quand l'inbox répond 503.
    • Pas d'API publique modifiée, pas de migration. Les tests n'utilisent ni Docker ni vrai Mastodon : ils exercent la chaîne rsa_sha256_sign → reqwest::Client → wiremock → parse_and_verify_cavage pour détecter tout mismatch silencieux de format de signature avant la prod.
  • RFC 0019 — Fediverse: observabilité du worker outbound (tracing counters). Quatre points d'instrumentation tracing sous le target dédié rustwarden::fediverse::outbound :
    • event = "delivered" (level info) — succès, avec attempts, target_inbox, target_domain, activity_id.
    • event = "deferred" (level warn) — échec retryable, avec next_retry_at, last_error, attempts, target_domain.
    • event = "dead" (level error) — attempts >= max_attempts ; la row reste persistée pour audit.
    • event = "drain_failed" (level error) — erreur DB en bouclant le worker, ne casse pas la boucle.
    • Aucune nouvelle dépendance, aucun schéma DB. Les apps consommatrices branchent l'exporter de leur choix au niveau tracing-subscriber (Prometheus, Loki, OTEL).
    • Aucun nouveau test ajouté — la sémantique des transitions est déjà couverte par les tests RFC 0010 / 0011 (les events sont best-effort et ne changent pas les valeurs de retour).
  • RFC 0018 — Fediverse: clôture de la dette technique (PK relax team_members + JWT blacklist par remote identity).
    • Nouvelle migration SeaORM m20260510_000016_relax_team_members_pk : drop de la PK composite (team_id, user_id), user_id devient nullable, ajout de id UUID PRIMARY KEY (surrogate), CHECK XOR chk_team_members_principal, et index unique partiel team_members_local_unique sur (team_id, user_id) WHERE user_id IS NOT NULL qui préserve l'unicité des rows locales.
    • Breaking sur team_member::Model :
      • Nouveau champ id: Uuid (PK SeaORM).
      • user_id passe de Uuid à Option<Uuid>.
      • Les call-sites Entity::find_by_id((team_id, user_id)) ont été migrés vers Entity::find().filter(...) (deux call-sites internes mis à jour).
    • team_service::add_member_identity(Identity::Remote(_)) insère désormais avec user_id = NULL (au lieu du sentinel Uuid::nil()).
    • FediverseInviteService::accept également mis à jour.
    • Levée de l'#[ignore] sur le test rfc0009_add_member_identity_remote_writes_remote_identity_id_column qui passe désormais en CI.
    • Nouveaux items publics gated experimental-fediverse :
      • JwtBlacklistService::blacklist_by_remote_identity_id(db, id, iter).
      • Trait ActiveTokenIterator + impl NoopActiveTokenIterator.
      • FediverseSignalService::apply_delete_actor_with_iterator(db, ap_id, iter).
    • apply_delete_actor legacy continue de retourner jwts_blacklisted = 0 (rétrocompatible) ; les apps qui veulent l'invalidation effective utilisent la variante _with_iterator.
    • 2 tests RFC 0018 ajoutés (apply_delete_actor_with_noop_iterator_keeps_jwts_blacklisted_zero, noop_active_token_iterator_returns_empty_list).
  • RFC 0017 — Fediverse: HttpSigVerifier auto-résout le PEM public via RemoteIdentityService (RFC 0003) quand ctx.public_key_pem est vide. Élimine le contrat caller-side laissé ouvert par RFC 0016.
    • Nouveau constructeur HttpSigVerifier::with_resolver(db). HttpSigVerifier::new() reste disponible pour les apps qui veulent gérer la résolution elles-mêmes.
    • Lookup direct remote_identities.ap_id == ctx.actor_ap_id, sans nouveau cache (le service de RFC 0003 a déjà son TTL).
    • 2 tests unitaires : http_sig_verifier_resolves_pem_from_remote_identities_when_empty (round-trip RSA via cache), http_sig_verifier_rejects_when_actor_not_in_cache.
  • RFC 0016 — Fediverse: élargissement du trait SignatureVerifier + HttpSignedDelivery complet. Branche cryptographique end-to-end de rustwarden ↔ Mastodon.
    • Nouvelle struct publique VerifyCtx<'a> portant la full request context (method, path, HeaderMap, body, public_key_pem).
    • Breaking sur SignatureVerifier::verify (gated experimental-fediverse) : ancienne signature verify(actor, sig, body, pem) remplacée par verify(&VerifyCtx<'_>). Les apps consommatrices qui implémentaient leur propre verifier doivent migrer.
    • Nouveaux items publics : rustwarden_core::fediverse::http_signatures::HttpSigVerifier, rustwarden_core::fediverse::http_signatures::HttpSignedDelivery, rustwarden_core::services::fediverse_signal_service::VerifyCtx.
    • inbox::handle et auth_routes::auth_inbox reconstituent désormais le VerifyCtx complet à partir du Request Axum, ce qui permet à HttpSigVerifier d'effectuer la vérification réelle draft-cavage sans plus aucun TODO côté handler.
    • HttpSignedDelivery :
      • Charge la paire RSA via ActorKeyService::ensure_keys.
      • Construit (request-target) host date, signe RSA-SHA256.
      • POST l'activité avec reqwest::Client + headers Date / Host / Signature / Content-Type.
      • Maps les statuts HTTP : 2xx → Ok(()), le reste → Err(Internal), ce qui réinjecte la row dans le ladder retry de RFC 0010.
    • reqwest ajouté à experimental-fediverse deps (réutilise la version 0.12 déjà tirée par oauth).
    • 4 tests RFC 0016 : http_sig_verifier_rejects_when_signature_header_missing, http_sig_verifier_rejects_when_public_key_pem_empty, http_sig_verifier_accepts_valid_signature (round-trip RSA), http_signed_delivery_rejects_when_actor_user_id_none.
    • Limitations : la résolution automatique du PEM public via RemoteIdentityService reste à câbler (le verifier exige pour l'instant que le caller passe public_key_pem). Tests d'intégration HTTP réel via wiremock ou mockito différés à un follow-up tests/interop/.
  • RFC 0015 — Fediverse: signatures HTTP draft-cavage (RSA-SHA256). Premier déblocage cryptographique réel pour interop Mastodon.
    • Nouveau module public gated experimental-fediverse : rustwarden_core::fediverse::http_signatures::{ParsedSignature, parse_signature_header, build_signature_input, rsa_sha256_sign, rsa_sha256_verify, parse_and_verify_cavage}.
    • Aucune nouvelle dépendance externe : réutilise rsa (RFC 0005) + sha2 + base64 déjà tirés.
    • Helpers de bas niveau (parse + canonical input + sign/verify primitives) + helper haut niveau parse_and_verify_cavage pour les inbox handlers qui ont accès aux full headers.
    • 11 tests unit (parsing + canonical + crypto round-trip + tampered rejection + algorithm/base64 validation).
    • Limitations : couverture draft-cavage-12 uniquement (Mastodon- compat) ; pas de RFC 9421 final ; pas de support hs2019 ; intégration dans le SignatureVerifier trait (qui reçoit seulement signature_header + body) reste à étendre dans un follow-up qui veut accepter une struct InboundRequest complète. La fonction parse_and_verify_cavage est déjà la cible.
  • RFC 0014 — Fediverse: 6 routes HTTP des invitations cross-instance (slice 3/N des routes). Couverture HTTP complète de RFC 0009 :
    • Côté b.tld (admin) :
      • POST /api/v1/teams/{team_id}/members/invite-fediverse (201).
      • GET /api/v1/teams/{team_id}/invites (200).
      • POST /api/v1/teams/{team_id}/invites/{invite_id}/revoke (200).
    • Côté a.tld (IdP) :
      • GET /api/v1/fediverse/invites?handle=… (200).
      • POST /api/v1/fediverse/invites/{invite_id}/accept (200).
      • POST /api/v1/fediverse/invites/{invite_id}/reject (200).
    • Nouveau module public gated experimental-fediverse : rustwarden_core::fediverse::invite_routes::{router, InviteRoutesState, InviteBody, InviteResponse, InviteRow, IdpListQuery}.
    • Nouvelle méthode FediverseInviteService::list_pending_for_handle (utilisée par le handler IdP-side ; retourne les invitations pending pour un @user@domain donné).
    • 12 tests unit axum::Router + tower::ServiceExt::oneshot (5 inviter-side + 5 IdP-side + 2 default-TTL/empty-list).
    • Limitations : pas de UI HTML, pas de pagination, et le mode IdP suppose actuellement une DB partagée (debug_local_federation) — un follow-up RFC ajoutera le mirror IdP-side via inbox dispatch.
  • RFC 0013 — Fediverse: 5 routes HTTP du flow d'auth AP-native (slice 2/N des routes). Couverture HTTP complète de RFC 0006 :
    • POST /api/v1/auth/login/fediverse/initiate (201, body InitiateBody, réponse InitiateResponse).
    • GET /api/v1/auth/login/fediverse/status/{challenge_id} (200, réponse StatusResponse).
    • POST /api/v1/auth/login/fediverse/consume/{challenge_id} (200, réponse ConsumeResponse — JWT IdentityKind::Remote).
    • POST /fediverse/auth/inbox (202, vérifié par SignatureVerifier).
    • POST /api/v1/fediverse/auth/consent (200, body ConsentBody).
    • Nouveau module public gated experimental-fediverse : rustwarden_core::fediverse::auth_routes::{router, AuthFlowState, InitiateBody, InitiateResponse, StatusResponse, ConsumeResponse, ConsentBody}.
    • Nouvelle fonction routes::router_with_auth(...) permettant de plugger un AuthConfig explicite quand les env vars ne sont pas disponibles au moment du build.
    • 10 tests unit axum::Router + tower::ServiceExt::oneshot.
    • Limitations : pas de UI HTML / SSE polling, pas de rate-limit spécifique, pas de CSRF middleware (les apps consommatrices le branchent à leur niveau).
  • RFC 0012 — Fediverse: route HTTP POST /inbox (slice 1/N des routes). Première route HTTP qui rend la couche service livrée par RFC 0010 réellement utilisable end-to-end.
    • Nouveau module public gated par experimental-fediverse : rustwarden_core::fediverse::inbox exposant handle et InboxState.
    • Dispatch table : Update → apply_update_actor, Delete → apply_delete_actor, Revoke → apply_revoke_membership.
    • Réponses : 202 Accepted, 400 Bad Request, 401 Unauthorized (signature manquante / verifier rejette), 501 Not Implemented (activité inconnue).
    • Nouvelle fonction routes::router_with(config, resolver, verifier) permettant de plugger une Arc<dyn SignatureVerifier> custom. L'ancienne routes::router continue de monter AlwaysAcceptVerifier par défaut ; routes::router_with_resolver est marquée #[deprecated].
    • Nouvelles variantes FediverseError::{Unauthorized, BadRequest, Unsupported}.
    • 8 tests unit axum::Router + tower::ServiceExt::oneshot (3 success-paths + 5 error-paths).
    • Limitations : impl HTTP-Sig RFC 9421 réelle restera reportée à une RFC crypto dédiée ; les routes auth-flow et team-invite arrivent dans 0013 / 0014.
  • RFC 0010 — Fediverse: signaux lifecycle + retry/backoff outbound. Pièce maîtresse de la Phase 4.
    • Nouveau service public gated par experimental-fediverse : rustwarden_core::services::fediverse_signal_service::{FediverseSignalService, UpdatedActor, DeleteOutcome, SignatureVerifier, AlwaysAcceptVerifier}.
      • apply_update_actor upsert idempotent dans remote_identities.
      • apply_delete_actor flip disabled_at = NOW() (compteur jwts_blacklisted = 0 en attendant une méthode dédiée JwtBlacklistService::blacklist_by_remote_identity_id).
      • apply_revoke_membership drop ciblé du team_members row distant.
    • Trait pluggable SignatureVerifier + impl test-only AlwaysAcceptVerifier. L'implémentation HTTP-Sig RFC 9421 réelle sera livrée par un follow-up RFC crypto dédié.
    • Retry/backoff ladder pour FediverseOutboundService::drain_one : sur échec, la row reste pending avec next_retry_at calculé selon le ladder (5min → 30min → 2h → 6h → 24h → 48h). Transition terminale pending → dead quand attempts >= max_attempts. Nouvelle méthode OutboundStatus::next_retry_offset(attempts).
    • 10 tests unit signal (1 pure + 9 DB-mocked) + 2 nouveaux tests outbound (drain_one_marks_failed_pending_with_next_retry_at_when_below_max_attempts, drain_one_marks_dead_when_attempts_reach_max, next_retry_offset_grows_then_caps) en remplacement de l'ancien drain_one_marks_failed_on_error (la sémantique de Failed a évolué vers transitoire).
    • Limitations : routes HTTP /inbox + UI HTML "apps connectées" / sessions remote admin reportées à un follow-up RFC routes ; JwtBlacklistService ne ferme pas encore les sessions associées à une identité remote supprimée.
  • RFC 0009 — Fediverse: équipes et partages cross-instance + invitations.
    • Nouvelle migration SeaORM m20260510_000015_fediverse_pending_invites (table + 2 indexes dont le UNIQUE partiel team_members_remote_unique qui interdit les doublons remote pour la même paire (team, identity)).
    • Nouveaux items publics gated par experimental-fediverse : rustwarden_core::services::fediverse_invite_service::{FediverseInviteService, InviteStatus, build_invite_payload, build_accept_invite_payload, build_reject_invite_payload, build_revoke_membership_payload}, rustwarden_core::models::fediverse_pending_invite::Model, rustwarden_core::services::team_service::TeamService::{add_member_identity, teams_of_identity}.
    • 11 tests unitaires (3 pure + 8 DB-mocked) + 2 tests d'intégration team_service (gated experimental-fediverse, vrai DB).
    • Limitation : routes HTTP admin/IdP (5 endpoints) reportées à un follow-up RFC ; resource_service::share_with_remote_identity également différé (la table support existe).
  • RFC 0008 — Fediverse: abstraction Identity + colonnes sibling FK. Fondation de la Phase 3 : enum public Identity { Local(Uuid), Remote(Uuid) } + trait IdentityResolver, plus colonnes nullables remote_identity_id ajoutées à team_members, resource_shares.shared_with_remote_identity_id, et audit_logs.{actor,target}_remote_identity_id.
    • Nouvelle migration SeaORM m20260510_000014_identity_sibling_fks (4 colonnes nullable + 2 indexes partiels).
    • Nouveau module public gated par experimental-fediverse : rustwarden_core::identity::{Identity, IdentityResolver}.
    • Helpers Identity::is_local(), is_remote(), id(), as_local(), as_remote(), from_claims(&Claims) (préserve le guard confused-deputy de RFC 0007).
    • Sérialisation JSON snake_case {"kind":"local","id":"<uuid>"}.
    • 8 tests unitaires (helpers + serde round-trip + conversion claims).
    • Limitations : aucune modification des services existants (team_service, resource_service, audit_service) ni de la PK composite team_members(team_id, user_id) — RFC 0009 consomme ces colonnes via un index unique partiel sans relax de la PK.
  • RFC 0006 — Fediverse: flow d'authentification AP-native (couche service). State machine Pending → Approved | Rejected → Consumed avec one-shot guard et nonce cryptographique URL-safe base64 256 bits. Les routes HTTP /api/v1/auth/login/fediverse/* + UI consent sont volontairement reportées à un follow-up — la couche service livrée ici est suffisante pour brancher des handlers consommateurs.
    • Nouvelle migration SeaORM m20260510_000013_fediverse_auth_challenges (table + index partiel pending).
    • Nouveaux items publics gated par experimental-fediverse : rustwarden_core::services::fediverse_auth_service::{FediverseAuthService, ChallengeStatus, InitiateRequest, InitiatedChallenge, ChallengeDecision, ConsumedSession, build_auth_request_payload}, rustwarden_core::models::fediverse_auth_challenge::Model.
    • 12 tests unitaires (3 pure + 9 DB-mocked).
  • RFC 0007 — Fediverse: extension du JWT Claims pour distinguer les sujets locaux (users.id) des sujets distants (remote_identities.id).
    • Nouvel enum public IdentityKind (Local / Remote), sérialisé snake_case sous la clé kind. #[serde(default)] garantit la rétrocompatibilité avec les JWT pré-RFC 0007.
    • Nouveau champ Claims.remote_id: Option<Uuid> (omis du JSON quand None).
    • Helpers Claims::is_remote(), is_local(), remote_identity_id() (guard confused-deputy : retourne None si kind = Local même si un remote_id parasite est présent).
    • Nouvelle méthode JwtService::generate_remote_token(config, remote_id, handle) (sub = remote_id, kind = Remote, roles/permissions vides).
    • 8 tests unitaires (helpers, serde round-trip, legacy compat).
  • RFC 0011 — Fediverse: file outbound + worker (slice Phase 2 minimal). Persistance des activités sortantes + worker polling + trait pluggable OutboundDelivery (impl NoopDelivery ; la signature HTTP réelle vient avec RFC 0010).
    • Nouvelle migration SeaORM m20260510_000012_fediverse_outbound_queue.
    • Nouveaux items publics gated par experimental-fediverse : rustwarden_core::services::fediverse_outbound_service::{FediverseOutboundService, OutboundStatus, EnqueueRequest, OutboundDelivery, NoopDelivery, OutboundWorker}, rustwarden_core::models::fediverse_outbound_queue::Model.
    • 10 tests unitaires (2 pure + 8 DB-mocked + worker smoke test).
    • Limitation : transitions retry/backoff + transport HTTP-Sig réel déléguées à RFC 0010.
  • RFC 0005 — Fediverse: stockage des clés d'acteur RSA chiffrées. Génération paresseuse d'une paire RSA-2048 (PKCS#8 / SPKI) lors de la première résolution AP du user, persistance avec clé privée chiffrée via CryptoService (AES-256-GCM), et exposition automatique de la clé publique dans PersonJson::public_key.
    • Nouvelle migration SeaORM m20260510_000011_extend_users_with_actor_keys : ajoute users.ap_public_key_pem TEXT NULL, users.ap_private_key_pem TEXT NULL (chiffré at-rest), users.ap_discoverable BOOLEAN NOT NULL DEFAULT TRUE.
    • Nouveaux items publics gated par experimental-fediverse : rustwarden_core::services::actor_key_service::{ActorKeyService, ActorKeyMaterial}.
    • Wiring : SeaOrmLocalUserResolver::find_local_user appelle ActorKeyService::ensure_keys lazy quand CryptoService::is_available(), sinon retourne un acteur sans clé (mieux qu'une 500). Une erreur de génération est tracée mais ne casse pas la résolution.
    • 13 tests unitaires (4 pure RSA + 8 DB-mocked + 1 DB-mocked NotFound).
    • Limitations : pas de rotation automatique de clé, pas de backfill de masse au démarrage (commande CLI dédiée prévue dans RFC 0011), pas de support Ed25519. Les routes signature HTTP sortantes/entrantes qui consomment ces clés viendront avec RFC 0010 / 0011.
    • Nouvelle dépendance : rsa = { version = "0.9", features = ["pem", "getrandom"], optional = true }, tirée uniquement avec experimental-fediverse.
  • RFC 0004 — Fediverse: modèle de confiance (allowlist / blocklist + scope d'interop). Permet à l'admin de choisir entre allowlist (zero-trust par défaut) et open_with_blocklist (modèle Mastodon), et expose le check central is_domain_allowed(domain) que les RFCs suivantes appliqueront avant toute opération avec un domaine distant.
    • Nouvelle migration SeaORM m20260510_000010_fediverse_trust_and_blocks : tables fediverse_trusted_domains et fediverse_blocked_domains (PK uuid, domain UNIQUE, severity pour blocklist, audit trail added_by_user_id), plus 6 entrées app_settings : fediverse_enabled (false), fediverse_trust_mode (allowlist), fediverse_interop_scope (rustwarden_only), fediverse_authorized_fetch (false), fediverse_node_name (Rustwarden), fediverse_node_description (vide).
    • Nouveaux items publics gated par experimental-fediverse : rustwarden_core::models::fediverse_trusted_domain::Model, rustwarden_core::models::fediverse_blocked_domain::Model, rustwarden_core::services::fediverse_trust_service::{FediverseTrustService, TrustMode, InteropScope, BlockSeverity}.
    • Normalisation domaine systématique (lowercase + strip www.) avant comparaison.
    • 12 tests unitaires (6 pure logic + 6 DB-mocked via sea-orm/mock).
  • RFC 0003 — Fediverse: stockage des identités distantes + résolution @user@domain. Cache local persistant des acteurs ActivityPub résolus, avec TTL configurable.
    • Nouvelle migration SeaORM m20260510_000009_remote_identities : table remote_identities (PK uuid, ap_id UNIQUE, contrainte UNIQUE sur (username, domain), disabled_at pour soft-delete, raw_json pour audit / re-processing).
    • Nouveaux items publics gated par experimental-fediverse : rustwarden_core::models::remote_identity::Model, rustwarden_core::services::remote_identity_service::RemoteIdentityService, rustwarden_core::fediverse::{RemoteFetcher, UnconfiguredFetcher}.
    • 11 tests unitaires (5 pure logic + 6 DB-mocked via sea-orm/mock).
    • Limitations : la livraison RemoteFetcher réelle (HTTP signé) arrive avec RFC 0010. Pour l'instant UnconfiguredFetcher sert de placeholder et retourne AppError::Config à toute tentative d'appel.

Tooling

  • Ajout de la feature mock de SeaORM en [dev-dependencies] pour permettre les tests unitaires sans connexion DB réelle.

  • RFC 0002 — Fediverse: WebFinger + Acteur + NodeInfo (couche découverte). Câbles automatiquement par with_fediverse() quand la feature experimental-fediverse est activée et que la méthode est appelée.

    • Nouvelles routes HTTP :
      • GET /.well-known/webfinger?resource=acct:user@domain (CORS *, content-type application/jrd+json)
      • GET /.well-known/nodeinfo
      • GET /nodeinfo/2.1
      • GET /users/{username} (négociation Accept : AP JSON ou 406 si seulement text/html et UI absente)
    • Nouveaux items publics gated par experimental-fediverse :
      • Module rustwarden_core::fediverse (sous-modules webfinger, actor, nodeinfo, routes, state, types, resolver, error).
      • Trait LocalUserResolver avec impl par défaut SeaOrmLocalUserResolver.
      • Types JSON sérialisés : PersonJson, PublicKeyJson, EndpointsJson, WebFingerResponse, WebFingerLink, NodeInfoDiscovery, NodeInfo, NodeInfoSoftware, NodeInfoServices, NodeInfoUsage, NodeInfoUsageUsers, NodeInfoLink.
      • Erreur FediverseError (404 / 400 / 406, conversion From<AppError>).
      • Helper actor_context() pour @context Mastodon-compat.
    • 15 tests unitaires (5 webfinger, 6 actor, 3 nodeinfo, 1 helper).
    • Limitation connue : PersonJson::public_key est None tant que la RFC 0005 (actor-key-storage) n'est pas livrée — l'acteur est discoverable mais Mastodon refusera de fédérer (signatures HTTP requises).
  • RFC 0001 — Fediverse: FediverseConfig + with_fediverse() builder. Nouvelle Cargo feature optionnelle experimental-fediverse sur rustwarden-core et rustwarden-ui, off par défaut. Charge la configuration de fédération ActivityPub depuis l'environnement et la stocke sur le builder. Les routes HTTP, le middleware de fédération et le worker outbound arrivent dans les RFCs 0002+.

    • Nouvelles dépendances optionnelles (gated experimental-fediverse) : activitypub_federation = "=0.7.0-beta.11" (features ["axum"], no default), ammonia = "4", url = "2" (avec serde).
    • Nouveaux items publics gated par experimental-fediverse : rustwarden_core::config::FediverseConfig, rustwarden_core::config::FediverseConfigError, RustwardenBuilder::with_fediverse().

Fixed

  • Race env var FEDIVERSE_DOMAIN dans la suite tests unit (révélée par l'élargissement de la fenêtre de mutation après ajout de F8). auth_inbox_returns_400_when_requester_actor_unknown mutait FEDIVERSE_DOMAIN puis remove_var, ce qui faisait sporadiquement rater les 4 tests build_*_payload qui lisent FediverseConfig::from_env() sur thread parallèle. Corrigé en supprimant la mutation env du test (l'assertion BadRequest reste vraie quelle que soit la valeur de l'env grâce au mismatch target.domain). Dette restante : la mutation env globale dans test_fediverse_config est un anti-pattern à traiter dans une RFC dette technique future.

Changed

  • 4 occurrences de std::env::set_var enveloppées dans unsafe { ... } avec commentaire // SAFETY: (1 prod dans app.rs, 3 tests dans crypto_service.rs et totp_service.rs). Requis par edition 2024 — comportement runtime inchangé.
  • 9 lints clippy résolus (7× explicit_auto_deref sur &*auth_config dans les middlewares, 2× let_and_return dans error_handler.rs et services/oauth/service.rs).
  • oauth_callback reçoit #[allow(clippy::too_many_arguments)] local avec commentaire — handler Axum à 8 extractors, refacto sans valeur.

Migration steps

Pour les apps consommatrices qui ne veulent pas la fédération : aucune action requise. La feature est off par défaut et n'a aucun coût (compile-time ou runtime).

Pour activer la fédération :

  1. Dans Cargo.toml :
    rustwarden-core = { version = "X.Y.Z", features = ["experimental-fediverse"] }
    rustwarden-ui   = { version = "X.Y.Z", features = ["experimental-fediverse"] }
    
  2. Dans .env, au minimum :
    FEDIVERSE_DOMAIN=your.tld
    
  3. Dans main.rs :
    RustwardenApp::builder()
        .from_env()
        .with_fediverse()?
        .build().await?
        .run().await
    

À ce stade aucune route HTTP n'est exposée — ces dernières arrivent avec la RFC 0002 (webfinger-actor-nodeinfo).