RFC 0001 — Fediverse: FediverseConfig + with_fediverse() builder
- Auteur : EBO (rustwarden)
- Date : 2026-05-09
- Statut : Accepted (auto-revue mainteneur unique 2026-05-09, score 28/35)
- Apps consommatrices concernées : toute app héritant de
rustwarden-corequi souhaite proposer la fédération d'identité ActivityPub (SSO cross-instance, invitations cross-instance dans les équipes). - Plan associé :
docs/plans/2026-05-09-fediverse-identity-federation.md, Phase 1. - Implémentation :
docs/plans/2026-05-09-rfc-0001-implementation.md.
Summary
Introduire FediverseConfig (struct de configuration runtime de la fédération) et la méthode RustwardenBuilder::with_fediverse() qui active conditionnellement le module fediverse du framework, gardé derrière une nouvelle feature Cargo experimental-fediverse.
Motivation
Le plan d'intégration de la fédération d'identité (SSO cross-instance entre apps Rustwarden + invitations team par handle @user@domain) nécessite un point d'entrée unique côté builder. Sans ce point d'entrée :
- Aucune des autres RFCs (0002–0011) ne peut atterrir : elles dépendent toutes du fait que le module
fediversesoit activable de façon contrôlée. - Les apps consommatrices n'ont aucun moyen de configurer le domaine fédéré, le mode trust, le worker outbound, etc., sans dupliquer la logique de configuration dans leur propre code.
- Les guidelines (HTTPS obligatoire en prod, blocage placeholder, validation domaine) ne sont pas centralisées.
Conséquence si on ne fait rien : Phase 1+ du plan inexécutable. Bloquant.
Délai : pas d'urgence sécurité ; cette RFC ouvre simplement la voie au travail planifié.
Non-goals
Cette RFC ne couvre pas :
- Les routes HTTP de la fédération (WebFinger, NodeInfo, Acteur) → RFC 0002.
- La table
remote_identitieset le service associé → RFC 0003. - Les tables
fediverse_trusted_domains/fediverse_blocked_domainset le service trust → RFC 0004. - La génération de clés RSA et leur stockage chiffré → RFC 0005.
- L'abstraction
IdentityetIdentityResolver→ RFC 0008 (Phase 3). - Le worker outbound → RFC 0011.
- Le toggle runtime
app_settings.fediverse_enabled(intégré dans RFC 0004 puisqu'il vit avec les settings trust).
Cette RFC se concentre uniquement sur :
- La struct
FediverseConfiget son chargement depuis l'environnement. - La méthode builder
with_fediverse(). - La feature Cargo
experimental-fediverseet ses dépendances.
API proposée
Feature Cargo
crates/rustwarden-core/Cargo.toml :
[dependencies] # ... (existing) activitypub_federation = { version = "=0.7.0-beta.11", features = ["axum"], default-features = false, optional = true } ammonia = { version = "4", optional = true } url = { version = "2", features = ["serde"], optional = true } [features] default = [] redis-session = ["tower-sessions-redis-store"] oauth = ["openidconnect", "reqwest"] experimental-fediverse = ["dep:activitypub_federation", "dep:ammonia", "dep:url"]
crates/rustwarden-ui/Cargo.toml :
[features] default = [] experimental-fediverse = ["rustwarden-core/experimental-fediverse"]
FediverseConfig
crates/rustwarden-core/src/config/fediverse.rs (nouveau fichier, gated par cfg(feature = "experimental-fediverse")) :
use std::time::Duration; use url::Url; /// Runtime configuration for ActivityPub identity federation. /// /// This struct is loaded from environment variables at startup via /// [`FediverseConfig::from_env`]. It is then injected into the Axum /// application via [`crate::app::RustwardenBuilder::with_fediverse`]. /// /// # Example /// /// ```rust,ignore /// # // Doctest gated to the `experimental-fediverse` feature. /// use rustwarden_core::config::FediverseConfig; /// /// // SAFETY: doctest fixture; values are stable test constants. /// unsafe { /// std::env::set_var("FEDIVERSE_DOMAIN", "example.com"); /// } /// /// let config = FediverseConfig::from_env().unwrap(); /// assert_eq!(config.domain(), "example.com"); /// assert_eq!(config.base_url().as_str(), "https://example.com/"); /// ``` #[derive(Debug, Clone)] pub struct FediverseConfig { domain: String, base_url: Url, user_agent: String, authorized_fetch: bool, outbound_workers: usize, outbound_max_attempts: u32, outbound_request_timeout: Duration, fetch_actor_cache_ttl: Duration, auth_challenge_ttl: Duration, debug_local_federation: bool, } /// Errors that can occur while loading or validating [`FediverseConfig`]. #[derive(Debug, thiserror::Error)] pub enum FediverseConfigError { /// `FEDIVERSE_DOMAIN` environment variable is missing or empty. #[error("FEDIVERSE_DOMAIN environment variable is missing or empty")] MissingDomain, /// `FEDIVERSE_DOMAIN` value is a known placeholder (e.g. `example.com`, /// `localhost.localdomain`) and refused outside debug mode. #[error("FEDIVERSE_DOMAIN value `{0}` is a placeholder; set a real domain")] PlaceholderDomain(String), /// Computed base URL is not HTTPS while `debug_local_federation` is off. #[error("base URL `{0}` must use HTTPS in production")] InsecureBaseUrl(String), /// `FEDIVERSE_DOMAIN` cannot be parsed as a valid URL host. #[error("FEDIVERSE_DOMAIN value `{0}` is not a valid host: {1}")] InvalidDomain(String, String), /// A numeric environment variable could not be parsed. #[error("environment variable `{0}` has an invalid value: {1}")] InvalidNumeric(&'static str, String), } impl FediverseConfig { /// Load and validate a `FediverseConfig` from environment variables. /// /// Required variables: /// - `FEDIVERSE_DOMAIN` — the federated domain (no scheme, no path), /// e.g. `example.com`. /// /// Optional variables (with defaults): /// - `FEDIVERSE_AUTHORIZED_FETCH` (`false`) /// - `FEDIVERSE_OUTBOUND_WORKERS` (`4`) /// - `FEDIVERSE_OUTBOUND_MAX_ATTEMPTS` (`8`) /// - `FEDIVERSE_OUTBOUND_REQUEST_TIMEOUT_SECS` (`30`) /// - `FEDIVERSE_FETCH_ACTOR_CACHE_TTL_SECS` (`86400`) /// - `FEDIVERSE_AUTH_CHALLENGE_TTL_SECS` (`300`) /// - `FEDIVERSE_DEBUG_LOCAL_FEDERATION` (`false`) — when `true`, allows /// HTTP base URLs for local 2-instance integration tests. /// /// # Errors /// /// Returns [`FediverseConfigError`] when validation fails (missing domain, /// placeholder domain, insecure URL outside debug mode, …). pub fn from_env() -> Result<Self, FediverseConfigError> { /* … */ } /// Federated domain (no scheme, no trailing slash). Example: `example.com`. pub fn domain(&self) -> &str { &self.domain } /// Canonical base URL, always with a trailing slash. /// Example: `https://example.com/`. pub fn base_url(&self) -> &Url { &self.base_url } /// HTTP `User-Agent` to send on outbound federation requests. pub fn user_agent(&self) -> &str { &self.user_agent } /// Whether incoming `GET` requests on actor/object endpoints must be /// signed (Mastodon "secure mode"). pub fn authorized_fetch(&self) -> bool { self.authorized_fetch } /// Number of concurrent worker tasks delivering outbound activities. pub fn outbound_workers(&self) -> usize { self.outbound_workers } /// Maximum delivery attempts before a job is marked `dead`. pub fn outbound_max_attempts(&self) -> u32 { self.outbound_max_attempts } /// Per-request HTTP timeout for outbound deliveries. pub fn outbound_request_timeout(&self) -> Duration { self.outbound_request_timeout } /// TTL for cached remote actor documents. pub fn fetch_actor_cache_ttl(&self) -> Duration { self.fetch_actor_cache_ttl } /// TTL for federated authentication challenges (cf. Phase 2). pub fn auth_challenge_ttl(&self) -> Duration { self.auth_challenge_ttl } /// When `true`, HTTP base URLs are allowed (for local 2-instance tests). pub fn debug_local_federation(&self) -> bool { self.debug_local_federation } }
RustwardenBuilder::with_fediverse()
crates/rustwarden-core/src/app.rs (modification, gated par cfg(feature = "experimental-fediverse")) :
impl RustwardenBuilder { /// Enable ActivityPub identity federation on the application. /// /// Loads [`FediverseConfig`] from environment variables and registers /// the federation router and outbound worker on the underlying Axum app. /// /// This method is **only available when the `experimental-fediverse` /// feature is enabled** at compile time. Without the feature, the symbol /// is not present and consumer code that calls it will fail to compile, /// which is the desired behaviour (compile-time opt-in). /// /// At runtime, the federation router additionally honours the /// `app_settings.fediverse_enabled` flag (covered by RFC 0004): when /// `false`, the routes return `404 Not Found` even though they are /// mounted. /// /// # Errors /// /// Returns [`anyhow::Error`] if [`FediverseConfig::from_env`] fails. /// /// # Example /// /// ```rust,ignore /// use rustwarden_core::app::RustwardenApp; /// /// #[tokio::main] /// async fn main() -> anyhow::Result<()> { /// RustwardenApp::builder() /// .from_env() /// .with_fediverse()? // compile-time opt-in via feature /// .build() /// .await? /// .run() /// .await /// } /// ``` #[cfg(feature = "experimental-fediverse")] pub fn with_fediverse(self) -> anyhow::Result<Self> { let config = crate::config::fediverse::FediverseConfig::from_env()?; // The actual wiring (router mount, FederationMiddleware, outbound // worker spawn) is added by RFCs 0002, 0004, 0005, 0011. // For RFC 0001 we only store the config on the builder. Ok(Self { fediverse_config: Some(config), ..self }) } }
Réexport
crates/rustwarden-core/src/config/mod.rs :
#[cfg(feature = "experimental-fediverse")] pub mod fediverse; #[cfg(feature = "experimental-fediverse")] pub use fediverse::{FediverseConfig, FediverseConfigError};
crates/rustwarden-core/src/lib.rs (prelude étendu) :
pub mod prelude { // ... existing re-exports #[cfg(feature = "experimental-fediverse")] pub use crate::config::FediverseConfig; }
Analyse de compatibilité
- API existante non modifiée → non-breaking (patch/minor possible).
-
API existante étendue avec défaut → non-breaking (minor) : ajout de méthode
with_fediverse()derrière feature flag. - API existante modifiée ou supprimée → non.
-
Nouveau comportement par défaut → la feature
experimental-fediverseest off par défaut. Aucune app existante n'est affectée. - Changement de schéma DB → non (RFC 0003-0005 introduiront les tables).
- Changement de format sérialisé → non.
Conclusion : non-breaking, additif pur. Peut sortir en minor release (0.x+1.0).
Alternatives considérées
Alternative A (recommandée) — FediverseConfig + with_fediverse() + feature experimental-fediverse
Pros :
- Conforme au pattern existant (
oauth,redis-session). - Compile-time opt-in via feature flag → coût nul pour les apps qui n'en veulent pas.
- Validation centralisée (HTTPS, placeholders, parsing).
- API simple, alignée avec
with_default_ui()et autres méthodes du builder. - Permet un naming progressif
experimental-*→*après stabilisation (cf. Evolution Process §5.1).
Cons :
- Ajoute 3 nouvelles dépendances optionnelles (
activitypub_federation,ammonia,url). - Une seconde version transitive de
reqwest(0.13 via crate AP) coexistera avec la 0.12 actuelle quandoauthETexperimental-fediversesont activés. ~2 Mo de poids binaire. Mitigation : à ré-évaluer quandoauth2 6(compatible reqwest 0.13) sortira.
Alternative B — Boot fully driven by env-vars (no explicit builder method)
L'intégration se ferait automatiquement dès que la feature est compilée et que FEDIVERSE_DOMAIN est défini, sans appel explicite à with_fediverse().
Pros :
- Une étape de moins pour le consommateur.
Cons :
- Implicite : un consommateur qui active la feature par mégarde voit la fédération démarrer.
- Pas de flexibilité pour avoir la feature compilée mais la désactiver à un seul endroit.
- Diverge du pattern de builder utilisé partout ailleurs (
with_default_ui,with_admin, …). - Les erreurs de config seraient remontées implicitement, plus difficile à diagnostiquer.
Alternative C — Configuration via TOML/YAML file
Au lieu de from_env(), charger depuis un fichier fediverse.toml ou fediverse.yaml.
Pros :
- Configuration plus riche et versionnable.
- Plus lisible pour des configs complexes.
Cons :
- Ajoute une dépendance
toml/serde_yaml. - Diverge des autres configs Rustwarden (
AuthConfig,EmailConfig, …) qui utilisent toutesfrom_env(). - Inutile à ce stade : la dizaine de variables qui composent la config tient parfaitement en env.
Alternative D (ne rien faire / workaround)
Demander aux apps consommatrices de gérer elles-mêmes la config et l'intégration.
Pros : zéro coût, zéro risque pour le framework.
Cons :
- Chaque app consommatrice devra réimplémenter la même logique (validation, parsing).
- Aucune garantie de cohérence (une app pourrait oublier de valider HTTPS, de rejeter les placeholders).
- Bloque le plan : aucune des autres RFCs ne peut s'appuyer sur une config commune.
- Anti-pattern explicite (cf. Evolution Process §11.4 : "Dupliquer un service du framework dans l'app : dette certaine").
Testing plan
Tests unitaires (crates/rustwarden-core/src/config/fediverse.rs#tests)
-
Happy path —
from_env_returns_valid_config_with_required_var_only: seulFEDIVERSE_DOMAINest défini, tous les autres prennent leurs défauts. La config est construite avec succès,domain(),base_url(),outbound_workers()etc. retournent les valeurs attendues. -
Erreur — domain manquant :
from_env_returns_missing_domain_when_var_unset. Test vérifieErr(FediverseConfigError::MissingDomain). -
Erreur — domain placeholder :
from_env_returns_placeholder_error_for_example_com,..._for_localhost_localdomain. Test vérifieErr(FediverseConfigError::PlaceholderDomain(_)). -
Erreur — base URL non HTTPS hors debug :
from_env_rejects_http_base_url_in_production. AvecFEDIVERSE_DEBUG_LOCAL_FEDERATION=falseet un domain qui produithttp://, attenduErr(FediverseConfigError::InsecureBaseUrl(_)). -
Cas debug —
from_env_allows_http_when_debug_local_federation: avecFEDIVERSE_DEBUG_LOCAL_FEDERATION=true, http://localhost:3000 est accepté. -
Erreur — numeric invalid :
from_env_returns_invalid_numeric_for_bad_workers_value(env varFEDIVERSE_OUTBOUND_WORKERS=abc). -
Default —
defaults_match_documented_values: sans aucune variable opt-in, vérifieroutbound_workers == 4,max_attempts == 8,request_timeout == 30s, etc.
TDD : ces tests sont écrits avant l'implémentation. Ils doivent échouer initialement (red), puis passer une fois
from_envimplémenté (green). Les tests utilisent le patternunsafe { std::env::set_var(…) }+// SAFETY:(constitution ANSSI Rust + amendement edition 2024 réglé en Phase 0).
Doctests
- Au moins 1 exemple sur
FediverseConfig(cf.# Exampledans la rustdoc ci-dessus). - Au moins 1 exemple sur
with_fediverse()(idem).
Tests d'intégration
- N/A à ce stade (RFC 0001 introduit la config, pas le routing). Les tests d'intégration arriveront à la RFC 0002 (WebFinger / Actor / NodeInfo).
Tests de non-régression
cargo build --no-default-featuresdoit toujours passer (workspace fonctionne sans la featureexperimental-fediverse).cargo build --features experimental-fediverse(surrustwarden-core) doit compiler.- Tous les tests existants doivent rester verts (
cargo test --workspace).
Quality gates
cargo build --workspace✅ 0 erreur.cargo test --workspace -- --test-threads=1✅ tous verts.cargo clippy --workspace --all-targets -- -D warnings✅ 0 warning.cargo fmt --check✅.cargo doc --no-deps -p rustwarden-core✅ 0 warning rustdoc.- Couverture sur
config/fediverse.rs: viser 95 %+ (composant simple, validation pure).
Migration guide
### Added - New optional Cargo feature `experimental-fediverse` in both `rustwarden-core` and `rustwarden-ui`. Disabled by default. Enables ActivityPub identity federation primitives. - `rustwarden_core::config::FediverseConfig` and `FediverseConfigError` (re-exported from the prelude when the feature is on). - `RustwardenBuilder::with_fediverse()` method to wire the federation into the Axum app at startup. ### Migration steps For consumers who do **not** want federation: nothing to do. The feature is off by default and adds zero overhead. For consumers who want federation: 1. In `Cargo.toml`, add the feature flag: \`\`\`toml rustwarden-core = { version = "X.Y.Z", features = ["experimental-fediverse"] } rustwarden-ui = { version = "X.Y.Z", features = ["experimental-fediverse"] } \`\`\` 2. In `.env`, define at minimum: \`\`\` FEDIVERSE_DOMAIN=your.tld \`\`\` 3. In `main.rs`, call `.with_fediverse()` on the builder: \`\`\`rust RustwardenApp::builder() .from_env() .with_fediverse()? .build().await? .run().await \`\`\` This RFC alone does not yet expose any HTTP routes — those arrive with RFC 0002 (WebFinger/Actor/NodeInfo).
Open questions
Résolues lors de l'auto-revue 2026-05-09 :
-
Naming feature flag — Décision :
experimental-fediverse. Cohérent avec Evolution Process §5.1 (cycle birth → adoption → promotion → cleanup) et avec la méthodologie §10/E du master plan. Promotion enfediversestandard à prévoir après ≥ 2 apps adoptantes ou 30 j sans bug critique. -
Visibilité des champs — Décision : accesseurs
pub fnuniquement, champs privés. Permet d'évoluer vers du calcul lazy ou de la validation différée sans breaking change. -
from_env()vsfrom_env_with(prefix)— Décision :from_env()sans préfixe. Cohérent avec les configs existantes du framework (AuthConfig::from_env(),EmailConfig::from_env(),ServerConfig::from_env()). -
Stockage dans
RustwardenBuilder— Décision : ajouter le champpub(crate) fediverse_config: Option<FediverseConfig>au builder, gatedcfg(feature = "experimental-fediverse"). Pas exposé publiquement.
Tracking issue
À créer sur le repo upstream avec le label rfc une fois la RFC en Under Review.
Score d'évaluation (auto-revue 2026-05-09, mainteneur unique)
| Critère | Note | Commentaire |
|---|---|---|
| Généralité | 4 | Sert toute app héritière qui veut la fédération. Pas universel (apps SPA-only n'ont pas besoin du builder, mais peuvent toujours appeler FediverseConfig::from_env() directement). |
| Stabilité API | 5 | Additif pur derrière feature flag off par défaut. Non-breaking. Sortable en minor. |
| Performance | 5 | Zéro impact runtime si feature off (cargo n'inclut pas le code). À l'on : seulement chargement env + parsing au démarrage, négligeable. |
| Sécurité | 4 | Centralise validation HTTPS, rejet placeholders, parsing URL strict. Pas de hardening net (pas de signature/auth ici), mais base saine pour les RFCs suivantes. |
| Complexité d'implémentation | 5 | < 200 lignes (struct + parser env + builder method). Patterns existants (AuthConfig::from_env()). Aucun unsafe, aucun refacto. |
| Charge de maintenance | 2 | 3 nouvelles deps directes optionnelles. activitypub_federation 0.7.0-beta est en API instable (pin strict requis, surveiller releases). ammonia et url sont stables. Risque modéré. |
| Documentation | 3 | Rustdoc EN sur tous les pub prévue, 2 doctests (struct + builder method). CHANGELOG préparé dans la migration guide. Pas encore d'exemple examples/ — à ajouter en phase ultérieure. |
| Total | 28/35 | ≥ 25 → Accepted |
GitRust