update test multi env fediverse
EBO <eric.bouhana@softalys.com> committé le 2026-05-13 12:49
be5d777a6887623d03a293e0521ab9f4858d392e
1 parent(s)
35 fichiers modifiés
+1018
-1037
M
Cargo.lock
+1
-0
@@ -4219,6 +4219,7 @@ dependencies = [
| 4219 | 4219 | "rsa", |
| 4220 | 4220 | "rustwarden-core", |
| 4221 | 4221 | "sea-orm", |
| 4222 | + "sea-orm-migration", | |
| 4222 | 4223 | "serde", |
| 4223 | 4224 | "serde_json", |
| 4224 | 4225 | "sha2", |
M
crates/rustwarden-core/migrations/src/lib.rs
+2
-16
@@ -8,14 +8,7 @@ pub mod m20260310_000005_create_oauth_accounts_table;
| 8 | 8 | pub mod m20260409_000006_create_totp_tables; |
| 9 | 9 | pub mod m20260422_000007_create_personal_access_tokens; |
| 10 | 10 | pub mod m20260423_000008_create_teams; |
| 11 | -pub mod m20260510_000009_remote_identities; | |
| 12 | -pub mod m20260510_000010_fediverse_trust_and_blocks; | |
| 13 | -pub mod m20260510_000011_extend_users_with_actor_keys; | |
| 14 | -pub mod m20260510_000012_fediverse_outbound_queue; | |
| 15 | -pub mod m20260510_000013_fediverse_auth_challenges; | |
| 16 | -pub mod m20260510_000014_identity_sibling_fks; | |
| 17 | -pub mod m20260510_000015_fediverse_pending_invites; | |
| 18 | -pub mod m20260510_000016_relax_team_members_pk; | |
| 11 | +pub mod m20260510_000009_fediverse_interop; | |
| 19 | 12 | |
| 20 | 13 | pub struct Migrator; |
| 21 | 14 |
@@ -31,14 +24,7 @@ impl MigratorTrait for Migrator {
| 31 | 24 | Box::new(m20260409_000006_create_totp_tables::Migration), |
| 32 | 25 | Box::new(m20260422_000007_create_personal_access_tokens::Migration), |
| 33 | 26 | Box::new(m20260423_000008_create_teams::Migration), |
| 34 | - Box::new(m20260510_000009_remote_identities::Migration), | |
| 35 | - Box::new(m20260510_000010_fediverse_trust_and_blocks::Migration), | |
| 36 | - Box::new(m20260510_000011_extend_users_with_actor_keys::Migration), | |
| 37 | - Box::new(m20260510_000012_fediverse_outbound_queue::Migration), | |
| 38 | - Box::new(m20260510_000013_fediverse_auth_challenges::Migration), | |
| 39 | - Box::new(m20260510_000014_identity_sibling_fks::Migration), | |
| 40 | - Box::new(m20260510_000015_fediverse_pending_invites::Migration), | |
| 41 | - Box::new(m20260510_000016_relax_team_members_pk::Migration), | |
| 27 | + Box::new(m20260510_000009_fediverse_interop::Migration), | |
| 42 | 28 | ] |
| 43 | 29 | } |
| 44 | 30 | } |
A
crates/rustwarden-core/migrations/src/m20260510_000009_fediverse_interop.rs
+364
-0
@@ -0,0 +1,364 @@
| 1 | +//! Migration `m20260510_000009_fediverse_interop`. | |
| 2 | +//! | |
| 3 | +//! Fused migration that defines the fediverse-aware surface of the | |
| 4 | +//! core schema. Replaces the legacy granular migrations: | |
| 5 | +//! | |
| 6 | +//! - `m20260510_000009_remote_identities` — `remote_identities` table (RFC 0003) | |
| 7 | +//! - `m20260510_000011_extend_users_with_actor_keys` — `users.ap_*` columns (RFC 0005) | |
| 8 | +//! - `m20260510_000014_identity_sibling_fks` — sibling FK columns on | |
| 9 | +//! `team_members`, `resource_shares`, `audit_logs` + their partial | |
| 10 | +//! indexes (RFC 0008) | |
| 11 | +//! - `m20260510_000016_relax_team_members_pk` — surrogate `team_members.id` | |
| 12 | +//! PK, XOR check on `(user_id, remote_identity_id)`, partial | |
| 13 | +//! uniqueness indexes (RFC 0018) | |
| 14 | +//! | |
| 15 | +//! Pure-protocol fediverse tables (`fediverse_trusted_domains`, | |
| 16 | +//! `fediverse_outbound_queue`, `fediverse_auth_challenges`, | |
| 17 | +//! `fediverse_pending_invites`) live in the dedicated | |
| 18 | +//! `rustwarden-fediverse` migrator instead. | |
| 19 | + | |
| 20 | +use sea_orm_migration::prelude::*; | |
| 21 | +use sea_orm_migration::sea_orm::ConnectionTrait; | |
| 22 | + | |
| 23 | +#[derive(DeriveMigrationName)] | |
| 24 | +pub struct Migration; | |
| 25 | + | |
| 26 | +#[async_trait::async_trait] | |
| 27 | +impl MigrationTrait for Migration { | |
| 28 | + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { | |
| 29 | + // ---- RFC 0003: cache for remote ActivityPub actors ----------- | |
| 30 | + manager | |
| 31 | + .create_table( | |
| 32 | + Table::create() | |
| 33 | + .table(RemoteIdentities::Table) | |
| 34 | + .col( | |
| 35 | + ColumnDef::new(RemoteIdentities::Id) | |
| 36 | + .uuid() | |
| 37 | + .not_null() | |
| 38 | + .primary_key(), | |
| 39 | + ) | |
| 40 | + .col( | |
| 41 | + ColumnDef::new(RemoteIdentities::ApId) | |
| 42 | + .text() | |
| 43 | + .not_null() | |
| 44 | + .unique_key(), | |
| 45 | + ) | |
| 46 | + .col(ColumnDef::new(RemoteIdentities::Username).text().not_null()) | |
| 47 | + .col(ColumnDef::new(RemoteIdentities::Domain).text().not_null()) | |
| 48 | + .col(ColumnDef::new(RemoteIdentities::DisplayName).text().null()) | |
| 49 | + .col(ColumnDef::new(RemoteIdentities::SummaryHtml).text().null()) | |
| 50 | + .col(ColumnDef::new(RemoteIdentities::InboxUrl).text().not_null()) | |
| 51 | + .col(ColumnDef::new(RemoteIdentities::SharedInboxUrl).text().null()) | |
| 52 | + .col( | |
| 53 | + ColumnDef::new(RemoteIdentities::PublicKeyPem) | |
| 54 | + .text() | |
| 55 | + .not_null(), | |
| 56 | + ) | |
| 57 | + .col(ColumnDef::new(RemoteIdentities::IconUrl).text().null()) | |
| 58 | + .col( | |
| 59 | + ColumnDef::new(RemoteIdentities::ActorType) | |
| 60 | + .text() | |
| 61 | + .not_null() | |
| 62 | + .default("Person"), | |
| 63 | + ) | |
| 64 | + .col(ColumnDef::new(RemoteIdentities::RawJson).json_binary().null()) | |
| 65 | + .col( | |
| 66 | + ColumnDef::new(RemoteIdentities::LastFetchedAt) | |
| 67 | + .timestamp_with_time_zone() | |
| 68 | + .not_null(), | |
| 69 | + ) | |
| 70 | + .col( | |
| 71 | + ColumnDef::new(RemoteIdentities::DisabledAt) | |
| 72 | + .timestamp_with_time_zone() | |
| 73 | + .null(), | |
| 74 | + ) | |
| 75 | + .col( | |
| 76 | + ColumnDef::new(RemoteIdentities::CreatedAt) | |
| 77 | + .timestamp_with_time_zone() | |
| 78 | + .not_null(), | |
| 79 | + ) | |
| 80 | + .col( | |
| 81 | + ColumnDef::new(RemoteIdentities::UpdatedAt) | |
| 82 | + .timestamp_with_time_zone() | |
| 83 | + .not_null(), | |
| 84 | + ) | |
| 85 | + .to_owned(), | |
| 86 | + ) | |
| 87 | + .await?; | |
| 88 | + | |
| 89 | + manager | |
| 90 | + .create_index( | |
| 91 | + Index::create() | |
| 92 | + .table(RemoteIdentities::Table) | |
| 93 | + .name("uniq_remote_identities_handle") | |
| 94 | + .col(RemoteIdentities::Username) | |
| 95 | + .col(RemoteIdentities::Domain) | |
| 96 | + .unique() | |
| 97 | + .to_owned(), | |
| 98 | + ) | |
| 99 | + .await?; | |
| 100 | + | |
| 101 | + // ---- RFC 0005: per-user ActivityPub actor keys --------------- | |
| 102 | + manager | |
| 103 | + .alter_table( | |
| 104 | + Table::alter() | |
| 105 | + .table(Users::Table) | |
| 106 | + .add_column(ColumnDef::new(Users::ApPublicKeyPem).text().null()) | |
| 107 | + .to_owned(), | |
| 108 | + ) | |
| 109 | + .await?; | |
| 110 | + manager | |
| 111 | + .alter_table( | |
| 112 | + Table::alter() | |
| 113 | + .table(Users::Table) | |
| 114 | + .add_column(ColumnDef::new(Users::ApPrivateKeyPem).text().null()) | |
| 115 | + .to_owned(), | |
| 116 | + ) | |
| 117 | + .await?; | |
| 118 | + manager | |
| 119 | + .alter_table( | |
| 120 | + Table::alter() | |
| 121 | + .table(Users::Table) | |
| 122 | + .add_column( | |
| 123 | + ColumnDef::new(Users::ApDiscoverable) | |
| 124 | + .boolean() | |
| 125 | + .not_null() | |
| 126 | + .default(true), | |
| 127 | + ) | |
| 128 | + .to_owned(), | |
| 129 | + ) | |
| 130 | + .await?; | |
| 131 | + | |
| 132 | + // ---- RFC 0008: sibling FK columns for remote identities ------ | |
| 133 | + manager | |
| 134 | + .alter_table( | |
| 135 | + Table::alter() | |
| 136 | + .table(TeamMembers::Table) | |
| 137 | + .add_column(ColumnDef::new(TeamMembers::RemoteIdentityId).uuid().null()) | |
| 138 | + .to_owned(), | |
| 139 | + ) | |
| 140 | + .await?; | |
| 141 | + manager | |
| 142 | + .alter_table( | |
| 143 | + Table::alter() | |
| 144 | + .table(ResourceShares::Table) | |
| 145 | + .add_column( | |
| 146 | + ColumnDef::new(ResourceShares::SharedWithRemoteIdentityId) | |
| 147 | + .uuid() | |
| 148 | + .null(), | |
| 149 | + ) | |
| 150 | + .to_owned(), | |
| 151 | + ) | |
| 152 | + .await?; | |
| 153 | + manager | |
| 154 | + .alter_table( | |
| 155 | + Table::alter() | |
| 156 | + .table(AuditLogs::Table) | |
| 157 | + .add_column( | |
| 158 | + ColumnDef::new(AuditLogs::ActorRemoteIdentityId) | |
| 159 | + .uuid() | |
| 160 | + .null(), | |
| 161 | + ) | |
| 162 | + .to_owned(), | |
| 163 | + ) | |
| 164 | + .await?; | |
| 165 | + manager | |
| 166 | + .alter_table( | |
| 167 | + Table::alter() | |
| 168 | + .table(AuditLogs::Table) | |
| 169 | + .add_column( | |
| 170 | + ColumnDef::new(AuditLogs::TargetRemoteIdentityId) | |
| 171 | + .uuid() | |
| 172 | + .null(), | |
| 173 | + ) | |
| 174 | + .to_owned(), | |
| 175 | + ) | |
| 176 | + .await?; | |
| 177 | + | |
| 178 | + let conn = manager.get_connection(); | |
| 179 | + conn.execute_unprepared( | |
| 180 | + "CREATE INDEX team_members_remote_identity_idx \ | |
| 181 | + ON team_members (remote_identity_id) \ | |
| 182 | + WHERE remote_identity_id IS NOT NULL", | |
| 183 | + ) | |
| 184 | + .await?; | |
| 185 | + conn.execute_unprepared( | |
| 186 | + "CREATE INDEX resource_shares_remote_identity_idx \ | |
| 187 | + ON resource_shares (shared_with_remote_identity_id) \ | |
| 188 | + WHERE shared_with_remote_identity_id IS NOT NULL", | |
| 189 | + ) | |
| 190 | + .await?; | |
| 191 | + | |
| 192 | + // ---- RFC 0018: surrogate team_members.id PK + XOR ------------ | |
| 193 | + conn.execute_unprepared("ALTER TABLE team_members DROP CONSTRAINT team_members_pkey") | |
| 194 | + .await?; | |
| 195 | + conn.execute_unprepared("ALTER TABLE team_members ALTER COLUMN user_id DROP NOT NULL") | |
| 196 | + .await?; | |
| 197 | + conn.execute_unprepared( | |
| 198 | + "ALTER TABLE team_members ADD CONSTRAINT chk_team_members_principal \ | |
| 199 | + CHECK ((user_id IS NOT NULL)::int + (remote_identity_id IS NOT NULL)::int = 1)", | |
| 200 | + ) | |
| 201 | + .await?; | |
| 202 | + conn.execute_unprepared("CREATE EXTENSION IF NOT EXISTS pgcrypto") | |
| 203 | + .await?; | |
| 204 | + conn.execute_unprepared( | |
| 205 | + "ALTER TABLE team_members ADD COLUMN id UUID NOT NULL DEFAULT gen_random_uuid()", | |
| 206 | + ) | |
| 207 | + .await?; | |
| 208 | + conn.execute_unprepared("ALTER TABLE team_members ADD PRIMARY KEY (id)") | |
| 209 | + .await?; | |
| 210 | + conn.execute_unprepared( | |
| 211 | + "CREATE UNIQUE INDEX team_members_local_unique \ | |
| 212 | + ON team_members (team_id, user_id) \ | |
| 213 | + WHERE user_id IS NOT NULL", | |
| 214 | + ) | |
| 215 | + .await?; | |
| 216 | + conn.execute_unprepared( | |
| 217 | + "CREATE UNIQUE INDEX team_members_remote_unique \ | |
| 218 | + ON team_members (team_id, remote_identity_id) \ | |
| 219 | + WHERE remote_identity_id IS NOT NULL", | |
| 220 | + ) | |
| 221 | + .await?; | |
| 222 | + | |
| 223 | + Ok(()) | |
| 224 | + } | |
| 225 | + | |
| 226 | + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { | |
| 227 | + let conn = manager.get_connection(); | |
| 228 | + | |
| 229 | + // ---- RFC 0018 reverse ---------------------------------------- | |
| 230 | + for sql in [ | |
| 231 | + "DROP INDEX IF EXISTS team_members_remote_unique", | |
| 232 | + "DROP INDEX IF EXISTS team_members_local_unique", | |
| 233 | + "ALTER TABLE team_members DROP CONSTRAINT IF EXISTS team_members_pkey", | |
| 234 | + "ALTER TABLE team_members DROP COLUMN IF EXISTS id", | |
| 235 | + "ALTER TABLE team_members DROP CONSTRAINT IF EXISTS chk_team_members_principal", | |
| 236 | + "ALTER TABLE team_members ALTER COLUMN user_id SET NOT NULL", | |
| 237 | + "ALTER TABLE team_members ADD PRIMARY KEY (team_id, user_id)", | |
| 238 | + ] { | |
| 239 | + conn.execute_unprepared(sql).await?; | |
| 240 | + } | |
| 241 | + | |
| 242 | + // ---- RFC 0008 reverse: drop partial indexes then columns ----- | |
| 243 | + for sql in [ | |
| 244 | + "DROP INDEX IF EXISTS resource_shares_remote_identity_idx", | |
| 245 | + "DROP INDEX IF EXISTS team_members_remote_identity_idx", | |
| 246 | + ] { | |
| 247 | + conn.execute_unprepared(sql).await?; | |
| 248 | + } | |
| 249 | + | |
| 250 | + manager | |
| 251 | + .alter_table( | |
| 252 | + Table::alter() | |
| 253 | + .table(AuditLogs::Table) | |
| 254 | + .drop_column(AuditLogs::TargetRemoteIdentityId) | |
| 255 | + .to_owned(), | |
| 256 | + ) | |
| 257 | + .await?; | |
| 258 | + manager | |
| 259 | + .alter_table( | |
| 260 | + Table::alter() | |
| 261 | + .table(AuditLogs::Table) | |
| 262 | + .drop_column(AuditLogs::ActorRemoteIdentityId) | |
| 263 | + .to_owned(), | |
| 264 | + ) | |
| 265 | + .await?; | |
| 266 | + manager | |
| 267 | + .alter_table( | |
| 268 | + Table::alter() | |
| 269 | + .table(ResourceShares::Table) | |
| 270 | + .drop_column(ResourceShares::SharedWithRemoteIdentityId) | |
| 271 | + .to_owned(), | |
| 272 | + ) | |
| 273 | + .await?; | |
| 274 | + manager | |
| 275 | + .alter_table( | |
| 276 | + Table::alter() | |
| 277 | + .table(TeamMembers::Table) | |
| 278 | + .drop_column(TeamMembers::RemoteIdentityId) | |
| 279 | + .to_owned(), | |
| 280 | + ) | |
| 281 | + .await?; | |
| 282 | + | |
| 283 | + // ---- RFC 0005 reverse ---------------------------------------- | |
| 284 | + manager | |
| 285 | + .alter_table( | |
| 286 | + Table::alter() | |
| 287 | + .table(Users::Table) | |
| 288 | + .drop_column(Users::ApDiscoverable) | |
| 289 | + .to_owned(), | |
| 290 | + ) | |
| 291 | + .await?; | |
| 292 | + manager | |
| 293 | + .alter_table( | |
| 294 | + Table::alter() | |
| 295 | + .table(Users::Table) | |
| 296 | + .drop_column(Users::ApPrivateKeyPem) | |
| 297 | + .to_owned(), | |
| 298 | + ) | |
| 299 | + .await?; | |
| 300 | + manager | |
| 301 | + .alter_table( | |
| 302 | + Table::alter() | |
| 303 | + .table(Users::Table) | |
| 304 | + .drop_column(Users::ApPublicKeyPem) | |
| 305 | + .to_owned(), | |
| 306 | + ) | |
| 307 | + .await?; | |
| 308 | + | |
| 309 | + // ---- RFC 0003 reverse ---------------------------------------- | |
| 310 | + manager | |
| 311 | + .drop_table(Table::drop().table(RemoteIdentities::Table).to_owned()) | |
| 312 | + .await?; | |
| 313 | + | |
| 314 | + Ok(()) | |
| 315 | + } | |
| 316 | +} | |
| 317 | + | |
| 318 | +#[derive(DeriveIden)] | |
| 319 | +enum RemoteIdentities { | |
| 320 | + Table, | |
| 321 | + Id, | |
| 322 | + ApId, | |
| 323 | + Username, | |
| 324 | + Domain, | |
| 325 | + DisplayName, | |
| 326 | + SummaryHtml, | |
| 327 | + InboxUrl, | |
| 328 | + SharedInboxUrl, | |
| 329 | + PublicKeyPem, | |
| 330 | + IconUrl, | |
| 331 | + ActorType, | |
| 332 | + RawJson, | |
| 333 | + LastFetchedAt, | |
| 334 | + DisabledAt, | |
| 335 | + CreatedAt, | |
| 336 | + UpdatedAt, | |
| 337 | +} | |
| 338 | + | |
| 339 | +#[derive(DeriveIden)] | |
| 340 | +enum Users { | |
| 341 | + Table, | |
| 342 | + ApPublicKeyPem, | |
| 343 | + ApPrivateKeyPem, | |
| 344 | + ApDiscoverable, | |
| 345 | +} | |
| 346 | + | |
| 347 | +#[derive(DeriveIden)] | |
| 348 | +enum TeamMembers { | |
| 349 | + Table, | |
| 350 | + RemoteIdentityId, | |
| 351 | +} | |
| 352 | + | |
| 353 | +#[derive(DeriveIden)] | |
| 354 | +enum ResourceShares { | |
| 355 | + Table, | |
| 356 | + SharedWithRemoteIdentityId, | |
| 357 | +} | |
| 358 | + | |
| 359 | +#[derive(DeriveIden)] | |
| 360 | +enum AuditLogs { | |
| 361 | + Table, | |
| 362 | + ActorRemoteIdentityId, | |
| 363 | + TargetRemoteIdentityId, | |
| 364 | +} |
D
crates/rustwarden-core/migrations/src/m20260510_000009_remote_identities.rs
+0
-114
@@ -1,114 +0,0 @@
| 1 | -//! Migration `m20260510_000009_remote_identities` (RFC 0003). | |
| 2 | -//! | |
| 3 | -//! Creates the `remote_identities` table — local cache of ActivityPub actors | |
| 4 | -//! resolved on remote instances. Backs `RemoteIdentityService::resolve_handle`. | |
| 5 | - | |
| 6 | -use sea_orm_migration::prelude::*; | |
| 7 | - | |
| 8 | -#[derive(DeriveMigrationName)] | |
| 9 | -pub struct Migration; | |
| 10 | - | |
| 11 | -#[async_trait::async_trait] | |
| 12 | -impl MigrationTrait for Migration { | |
| 13 | - async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { | |
| 14 | - manager | |
| 15 | - .create_table( | |
| 16 | - Table::create() | |
| 17 | - .table(RemoteIdentities::Table) | |
| 18 | - .col( | |
| 19 | - ColumnDef::new(RemoteIdentities::Id) | |
| 20 | - .uuid() | |
| 21 | - .not_null() | |
| 22 | - .primary_key(), | |
| 23 | - ) | |
| 24 | - .col( | |
| 25 | - ColumnDef::new(RemoteIdentities::ApId) | |
| 26 | - .text() | |
| 27 | - .not_null() | |
| 28 | - .unique_key(), | |
| 29 | - ) | |
| 30 | - .col(ColumnDef::new(RemoteIdentities::Username).text().not_null()) | |
| 31 | - .col(ColumnDef::new(RemoteIdentities::Domain).text().not_null()) | |
| 32 | - .col(ColumnDef::new(RemoteIdentities::DisplayName).text().null()) | |
| 33 | - .col(ColumnDef::new(RemoteIdentities::SummaryHtml).text().null()) | |
| 34 | - .col(ColumnDef::new(RemoteIdentities::InboxUrl).text().not_null()) | |
| 35 | - .col(ColumnDef::new(RemoteIdentities::SharedInboxUrl).text().null()) | |
| 36 | - .col( | |
| 37 | - ColumnDef::new(RemoteIdentities::PublicKeyPem) | |
| 38 | - .text() | |
| 39 | - .not_null(), | |
| 40 | - ) | |
| 41 | - .col(ColumnDef::new(RemoteIdentities::IconUrl).text().null()) | |
| 42 | - .col( | |
| 43 | - ColumnDef::new(RemoteIdentities::ActorType) | |
| 44 | - .text() | |
| 45 | - .not_null() | |
| 46 | - .default("Person"), | |
| 47 | - ) | |
| 48 | - .col(ColumnDef::new(RemoteIdentities::RawJson).json_binary().null()) | |
| 49 | - .col( | |
| 50 | - ColumnDef::new(RemoteIdentities::LastFetchedAt) | |
| 51 | - .timestamp_with_time_zone() | |
| 52 | - .not_null(), | |
| 53 | - ) | |
| 54 | - .col( | |
| 55 | - ColumnDef::new(RemoteIdentities::DisabledAt) | |
| 56 | - .timestamp_with_time_zone() | |
| 57 | - .null(), | |
| 58 | - ) | |
| 59 | - .col( | |
| 60 | - ColumnDef::new(RemoteIdentities::CreatedAt) | |
| 61 | - .timestamp_with_time_zone() | |
| 62 | - .not_null(), | |
| 63 | - ) | |
| 64 | - .col( | |
| 65 | - ColumnDef::new(RemoteIdentities::UpdatedAt) | |
| 66 | - .timestamp_with_time_zone() | |
| 67 | - .not_null(), | |
| 68 | - ) | |
| 69 | - .to_owned(), | |
| 70 | - ) | |
| 71 | - .await?; | |
| 72 | - | |
| 73 | - manager | |
| 74 | - .create_index( | |
| 75 | - Index::create() | |
| 76 | - .table(RemoteIdentities::Table) | |
| 77 | - .name("uniq_remote_identities_handle") | |
| 78 | - .col(RemoteIdentities::Username) | |
| 79 | - .col(RemoteIdentities::Domain) | |
| 80 | - .unique() | |
| 81 | - .to_owned(), | |
| 82 | - ) | |
| 83 | - .await?; | |
| 84 | - | |
| 85 | - Ok(()) | |
| 86 | - } | |
| 87 | - | |
| 88 | - async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { | |
| 89 | - manager | |
| 90 | - .drop_table(Table::drop().table(RemoteIdentities::Table).to_owned()) | |
| 91 | - .await | |
| 92 | - } | |
| 93 | -} | |
| 94 | - | |
| 95 | -#[derive(DeriveIden)] | |
| 96 | -enum RemoteIdentities { | |
| 97 | - Table, | |
| 98 | - Id, | |
| 99 | - ApId, | |
| 100 | - Username, | |
| 101 | - Domain, | |
| 102 | - DisplayName, | |
| 103 | - SummaryHtml, | |
| 104 | - InboxUrl, | |
| 105 | - SharedInboxUrl, | |
| 106 | - PublicKeyPem, | |
| 107 | - IconUrl, | |
| 108 | - ActorType, | |
| 109 | - RawJson, | |
| 110 | - LastFetchedAt, | |
| 111 | - DisabledAt, | |
| 112 | - CreatedAt, | |
| 113 | - UpdatedAt, | |
| 114 | -} |
D
crates/rustwarden-core/migrations/src/m20260510_000010_fediverse_trust_and_blocks.rs
+0
-153
@@ -1,153 +0,0 @@
| 1 | -//! Migration `m20260510_000010_fediverse_trust_and_blocks` (RFC 0004). | |
| 2 | -//! | |
| 3 | -//! Creates the trust-model tables (`fediverse_trusted_domains`, | |
| 4 | -//! `fediverse_blocked_domains`) and seeds default `app_settings` keys. | |
| 5 | - | |
| 6 | -use sea_orm_migration::prelude::*; | |
| 7 | -use sea_orm_migration::sea_orm::ConnectionTrait; | |
| 8 | -use uuid::Uuid; | |
| 9 | - | |
| 10 | -#[derive(DeriveMigrationName)] | |
| 11 | -pub struct Migration; | |
| 12 | - | |
| 13 | -#[async_trait::async_trait] | |
| 14 | -impl MigrationTrait for Migration { | |
| 15 | - async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { | |
| 16 | - manager | |
| 17 | - .create_table( | |
| 18 | - Table::create() | |
| 19 | - .table(FediverseTrustedDomains::Table) | |
| 20 | - .col( | |
| 21 | - ColumnDef::new(FediverseTrustedDomains::Id) | |
| 22 | - .uuid() | |
| 23 | - .not_null() | |
| 24 | - .primary_key(), | |
| 25 | - ) | |
| 26 | - .col( | |
| 27 | - ColumnDef::new(FediverseTrustedDomains::Domain) | |
| 28 | - .text() | |
| 29 | - .not_null() | |
| 30 | - .unique_key(), | |
| 31 | - ) | |
| 32 | - .col(ColumnDef::new(FediverseTrustedDomains::Note).text().null()) | |
| 33 | - .col( | |
| 34 | - ColumnDef::new(FediverseTrustedDomains::AddedByUserId) | |
| 35 | - .uuid() | |
| 36 | - .null(), | |
| 37 | - ) | |
| 38 | - .col( | |
| 39 | - ColumnDef::new(FediverseTrustedDomains::CreatedAt) | |
| 40 | - .timestamp_with_time_zone() | |
| 41 | - .not_null(), | |
| 42 | - ) | |
| 43 | - .to_owned(), | |
| 44 | - ) | |
| 45 | - .await?; | |
| 46 | - | |
| 47 | - manager | |
| 48 | - .create_table( | |
| 49 | - Table::create() | |
| 50 | - .table(FediverseBlockedDomains::Table) | |
| 51 | - .col( | |
| 52 | - ColumnDef::new(FediverseBlockedDomains::Id) | |
| 53 | - .uuid() | |
| 54 | - .not_null() | |
| 55 | - .primary_key(), | |
| 56 | - ) | |
| 57 | - .col( | |
| 58 | - ColumnDef::new(FediverseBlockedDomains::Domain) | |
| 59 | - .text() | |
| 60 | - .not_null() | |
| 61 | - .unique_key(), | |
| 62 | - ) | |
| 63 | - .col( | |
| 64 | - ColumnDef::new(FediverseBlockedDomains::Severity) | |
| 65 | - .text() | |
| 66 | - .not_null() | |
| 67 | - .default("suspend"), | |
| 68 | - ) | |
| 69 | - .col( | |
| 70 | - ColumnDef::new(FediverseBlockedDomains::Reason) | |
| 71 | - .text() | |
| 72 | - .null(), | |
| 73 | - ) | |
| 74 | - .col( | |
| 75 | - ColumnDef::new(FediverseBlockedDomains::AddedByUserId) | |
| 76 | - .uuid() | |
| 77 | - .null(), | |
| 78 | - ) | |
| 79 | - .col( | |
| 80 | - ColumnDef::new(FediverseBlockedDomains::CreatedAt) | |
| 81 | - .timestamp_with_time_zone() | |
| 82 | - .not_null(), | |
| 83 | - ) | |
| 84 | - .to_owned(), | |
| 85 | - ) | |
| 86 | - .await?; | |
| 87 | - | |
| 88 | - // Seed default app_settings keys. Idempotent via ON CONFLICT — | |
| 89 | - // the `app_settings` table is guaranteed to exist (created by | |
| 90 | - // migration 0002) at this point of the migration chain. Values are | |
| 91 | - // hard-coded constants under our control, so embedding them in the | |
| 92 | - // SQL via `execute_unprepared` is safe (no user input). | |
| 93 | - for (key, value) in [ | |
| 94 | - ("fediverse_enabled", "false"), | |
| 95 | - ("fediverse_trust_mode", "allowlist"), | |
| 96 | - ("fediverse_interop_scope", "rustwarden_only"), | |
| 97 | - ("fediverse_authorized_fetch", "false"), | |
| 98 | - ("fediverse_node_name", "Rustwarden"), | |
| 99 | - ("fediverse_node_description", ""), | |
| 100 | - ] { | |
| 101 | - let sql = format!( | |
| 102 | - "INSERT INTO app_settings (id, key, value, updated_at) \ | |
| 103 | - VALUES ('{}', '{}', '{}', NOW()) \ | |
| 104 | - ON CONFLICT (key) DO NOTHING", | |
| 105 | - Uuid::new_v4(), | |
| 106 | - key, | |
| 107 | - value | |
| 108 | - ); | |
| 109 | - manager.get_connection().execute_unprepared(&sql).await?; | |
| 110 | - } | |
| 111 | - | |
| 112 | - Ok(()) | |
| 113 | - } | |
| 114 | - | |
| 115 | - async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { | |
| 116 | - manager | |
| 117 | - .drop_table( | |
| 118 | - Table::drop() | |
| 119 | - .table(FediverseTrustedDomains::Table) | |
| 120 | - .to_owned(), | |
| 121 | - ) | |
| 122 | - .await?; | |
| 123 | - manager | |
| 124 | - .drop_table( | |
| 125 | - Table::drop() | |
| 126 | - .table(FediverseBlockedDomains::Table) | |
| 127 | - .to_owned(), | |
| 128 | - ) | |
| 129 | - .await?; | |
| 130 | - Ok(()) | |
| 131 | - } | |
| 132 | -} | |
| 133 | - | |
| 134 | -#[derive(DeriveIden)] | |
| 135 | -enum FediverseTrustedDomains { | |
| 136 | - Table, | |
| 137 | - Id, | |
| 138 | - Domain, | |
| 139 | - Note, | |
| 140 | - AddedByUserId, | |
| 141 | - CreatedAt, | |
| 142 | -} | |
| 143 | - | |
| 144 | -#[derive(DeriveIden)] | |
| 145 | -enum FediverseBlockedDomains { | |
| 146 | - Table, | |
| 147 | - Id, | |
| 148 | - Domain, | |
| 149 | - Severity, | |
| 150 | - Reason, | |
| 151 | - AddedByUserId, | |
| 152 | - CreatedAt, | |
| 153 | -} |
D
crates/rustwarden-core/migrations/src/m20260510_000011_extend_users_with_actor_keys.rs
+0
-91
@@ -1,91 +0,0 @@
| 1 | -//! Migration `m20260510_000011_extend_users_with_actor_keys` (RFC 0005). | |
| 2 | -//! | |
| 3 | -//! Adds three columns to `users` for ActivityPub actor-key storage: | |
| 4 | -//! | |
| 5 | -//! - `ap_public_key_pem TEXT NULL` — public RSA key (SPKI / PEM). | |
| 6 | -//! - `ap_private_key_pem TEXT NULL` — private RSA key (PKCS#8 / PEM), | |
| 7 | -//! encrypted at rest via `CryptoService`. | |
| 8 | -//! - `ap_discoverable BOOLEAN NOT NULL DEFAULT TRUE`. | |
| 9 | - | |
| 10 | -use sea_orm_migration::prelude::*; | |
| 11 | - | |
| 12 | -#[derive(DeriveMigrationName)] | |
| 13 | -pub struct Migration; | |
| 14 | - | |
| 15 | -#[async_trait::async_trait] | |
| 16 | -impl MigrationTrait for Migration { | |
| 17 | - async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { | |
| 18 | - manager | |
| 19 | - .alter_table( | |
| 20 | - Table::alter() | |
| 21 | - .table(Users::Table) | |
| 22 | - .add_column(ColumnDef::new(Users::ApPublicKeyPem).text().null()) | |
| 23 | - .to_owned(), | |
| 24 | - ) | |
| 25 | - .await?; | |
| 26 | - | |
| 27 | - manager | |
| 28 | - .alter_table( | |
| 29 | - Table::alter() | |
| 30 | - .table(Users::Table) | |
| 31 | - .add_column(ColumnDef::new(Users::ApPrivateKeyPem).text().null()) | |
| 32 | - .to_owned(), | |
| 33 | - ) | |
| 34 | - .await?; | |
| 35 | - | |
| 36 | - manager | |
| 37 | - .alter_table( | |
| 38 | - Table::alter() | |
| 39 | - .table(Users::Table) | |
| 40 | - .add_column( | |
| 41 | - ColumnDef::new(Users::ApDiscoverable) | |
| 42 | - .boolean() | |
| 43 | - .not_null() | |
| 44 | - .default(true), | |
| 45 | - ) | |
| 46 | - .to_owned(), | |
| 47 | - ) | |
| 48 | - .await?; | |
| 49 | - | |
| 50 | - Ok(()) | |
| 51 | - } | |
| 52 | - | |
| 53 | - async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { | |
| 54 | - manager | |
| 55 | - .alter_table( | |
| 56 | - Table::alter() | |
| 57 | - .table(Users::Table) | |
| 58 | - .drop_column(Users::ApDiscoverable) | |
| 59 | - .to_owned(), | |
| 60 | - ) | |
| 61 | - .await?; | |
| 62 | - | |
| 63 | - manager | |
| 64 | - .alter_table( | |
| 65 | - Table::alter() | |
| 66 | - .table(Users::Table) | |
| 67 | - .drop_column(Users::ApPrivateKeyPem) | |
| 68 | - .to_owned(), | |
| 69 | - ) | |
| 70 | - .await?; | |
| 71 | - | |
| 72 | - manager | |
| 73 | - .alter_table( | |
| 74 | - Table::alter() | |
| 75 | - .table(Users::Table) | |
| 76 | - .drop_column(Users::ApPublicKeyPem) | |
| 77 | - .to_owned(), | |
| 78 | - ) | |
| 79 | - .await?; | |
| 80 | - | |
| 81 | - Ok(()) | |
| 82 | - } | |
| 83 | -} | |
| 84 | - | |
| 85 | -#[derive(DeriveIden)] | |
| 86 | -enum Users { | |
| 87 | - Table, | |
| 88 | - ApPublicKeyPem, | |
| 89 | - ApPrivateKeyPem, | |
| 90 | - ApDiscoverable, | |
| 91 | -} |
D
crates/rustwarden-core/migrations/src/m20260510_000012_fediverse_outbound_queue.rs
+0
-139
@@ -1,139 +0,0 @@
| 1 | -//! Migration `m20260510_000012_fediverse_outbound_queue` (RFC 0011). | |
| 2 | -//! | |
| 3 | -//! Creates the persistent outbound delivery queue used by the | |
| 4 | -//! ActivityPub outbound worker. The slice livered by RFC 0011 is the | |
| 5 | -//! Phase-2 minimum: status transitions `pending → delivered|failed`. | |
| 6 | -//! Full retry/backoff logic lands with RFC 0010. | |
| 7 | - | |
| 8 | -use sea_orm_migration::prelude::*; | |
| 9 | - | |
| 10 | -#[derive(DeriveMigrationName)] | |
| 11 | -pub struct Migration; | |
| 12 | - | |
| 13 | -#[async_trait::async_trait] | |
| 14 | -impl MigrationTrait for Migration { | |
| 15 | - async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { | |
| 16 | - manager | |
| 17 | - .create_table( | |
| 18 | - Table::create() | |
| 19 | - .table(FediverseOutboundQueue::Table) | |
| 20 | - .col( | |
| 21 | - ColumnDef::new(FediverseOutboundQueue::Id) | |
| 22 | - .uuid() | |
| 23 | - .not_null() | |
| 24 | - .primary_key(), | |
| 25 | - ) | |
| 26 | - .col( | |
| 27 | - ColumnDef::new(FediverseOutboundQueue::ActivityId) | |
| 28 | - .text() | |
| 29 | - .not_null(), | |
| 30 | - ) | |
| 31 | - .col( | |
| 32 | - ColumnDef::new(FediverseOutboundQueue::PayloadJson) | |
| 33 | - .text() | |
| 34 | - .not_null(), | |
| 35 | - ) | |
| 36 | - .col( | |
| 37 | - ColumnDef::new(FediverseOutboundQueue::TargetInbox) | |
| 38 | - .text() | |
| 39 | - .not_null(), | |
| 40 | - ) | |
| 41 | - .col( | |
| 42 | - ColumnDef::new(FediverseOutboundQueue::TargetDomain) | |
| 43 | - .text() | |
| 44 | - .not_null(), | |
| 45 | - ) | |
| 46 | - .col( | |
| 47 | - ColumnDef::new(FediverseOutboundQueue::Status) | |
| 48 | - .text() | |
| 49 | - .not_null() | |
| 50 | - .default("pending"), | |
| 51 | - ) | |
| 52 | - .col( | |
| 53 | - ColumnDef::new(FediverseOutboundQueue::Attempts) | |
| 54 | - .integer() | |
| 55 | - .not_null() | |
| 56 | - .default(0), | |
| 57 | - ) | |
| 58 | - .col( | |
| 59 | - ColumnDef::new(FediverseOutboundQueue::MaxAttempts) | |
| 60 | - .integer() | |
| 61 | - .not_null() | |
| 62 | - .default(8), | |
| 63 | - ) | |
| 64 | - .col(ColumnDef::new(FediverseOutboundQueue::LastError).text().null()) | |
| 65 | - .col( | |
| 66 | - ColumnDef::new(FediverseOutboundQueue::NextRetryAt) | |
| 67 | - .timestamp_with_time_zone() | |
| 68 | - .null(), | |
| 69 | - ) | |
| 70 | - .col( | |
| 71 | - ColumnDef::new(FediverseOutboundQueue::CreatedAt) | |
| 72 | - .timestamp_with_time_zone() | |
| 73 | - .not_null(), | |
| 74 | - ) | |
| 75 | - .col( | |
| 76 | - ColumnDef::new(FediverseOutboundQueue::DeliveredAt) | |
| 77 | - .timestamp_with_time_zone() | |
| 78 | - .null(), | |
| 79 | - ) | |
| 80 | - .col( | |
| 81 | - ColumnDef::new(FediverseOutboundQueue::ActorUserId) | |
| 82 | - .uuid() | |
| 83 | - .null(), | |
| 84 | - ) | |
| 85 | - .to_owned(), | |
| 86 | - ) | |
| 87 | - .await?; | |
| 88 | - | |
| 89 | - manager | |
| 90 | - .create_index( | |
| 91 | - Index::create() | |
| 92 | - .name("fediverse_outbound_pending_idx") | |
| 93 | - .table(FediverseOutboundQueue::Table) | |
| 94 | - .col(FediverseOutboundQueue::Status) | |
| 95 | - .col(FediverseOutboundQueue::NextRetryAt) | |
| 96 | - .to_owned(), | |
| 97 | - ) | |
| 98 | - .await?; | |
| 99 | - | |
| 100 | - Ok(()) | |
| 101 | - } | |
| 102 | - | |
| 103 | - async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { | |
| 104 | - manager | |
| 105 | - .drop_index( | |
| 106 | - Index::drop() | |
| 107 | - .name("fediverse_outbound_pending_idx") | |
| 108 | - .table(FediverseOutboundQueue::Table) | |
| 109 | - .to_owned(), | |
| 110 | - ) | |
| 111 | - .await?; | |
| 112 | - manager | |
| 113 | - .drop_table( | |
| 114 | - Table::drop() | |
| 115 | - .table(FediverseOutboundQueue::Table) | |
| 116 | - .to_owned(), | |
| 117 | - ) | |
| 118 | - .await?; | |
| 119 | - Ok(()) | |
| 120 | - } | |
| 121 | -} | |
| 122 | - | |
| 123 | -#[derive(DeriveIden)] | |
| 124 | -enum FediverseOutboundQueue { | |
| 125 | - Table, | |
| 126 | - Id, | |
| 127 | - ActivityId, | |
| 128 | - PayloadJson, | |
| 129 | - TargetInbox, | |
| 130 | - TargetDomain, | |
| 131 | - Status, | |
| 132 | - Attempts, | |
| 133 | - MaxAttempts, | |
| 134 | - LastError, | |
| 135 | - NextRetryAt, | |
| 136 | - CreatedAt, | |
| 137 | - DeliveredAt, | |
| 138 | - ActorUserId, | |
| 139 | -} |
D
crates/rustwarden-core/migrations/src/m20260510_000013_fediverse_auth_challenges.rs
+0
-122
@@ -1,122 +0,0 @@
| 1 | -//! Migration `m20260510_000013_fediverse_auth_challenges` (RFC 0006). | |
| 2 | -//! | |
| 3 | -//! Persists ActivityPub authentication challenges issued by the | |
| 4 | -//! client-side `FediverseAuthService::initiate` step. | |
| 5 | - | |
| 6 | -use sea_orm_migration::prelude::*; | |
| 7 | - | |
| 8 | -#[derive(DeriveMigrationName)] | |
| 9 | -pub struct Migration; | |
| 10 | - | |
| 11 | -#[async_trait::async_trait] | |
| 12 | -impl MigrationTrait for Migration { | |
| 13 | - async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { | |
| 14 | - manager | |
| 15 | - .create_table( | |
| 16 | - Table::create() | |
| 17 | - .table(FediverseAuthChallenges::Table) | |
| 18 | - .col( | |
| 19 | - ColumnDef::new(FediverseAuthChallenges::Id) | |
| 20 | - .uuid() | |
| 21 | - .not_null() | |
| 22 | - .primary_key(), | |
| 23 | - ) | |
| 24 | - .col( | |
| 25 | - ColumnDef::new(FediverseAuthChallenges::Nonce) | |
| 26 | - .text() | |
| 27 | - .not_null() | |
| 28 | - .unique_key(), | |
| 29 | - ) | |
| 30 | - .col( | |
| 31 | - ColumnDef::new(FediverseAuthChallenges::Handle) | |
| 32 | - .text() | |
| 33 | - .not_null(), | |
| 34 | - ) | |
| 35 | - .col( | |
| 36 | - ColumnDef::new(FediverseAuthChallenges::Domain) | |
| 37 | - .text() | |
| 38 | - .not_null(), | |
| 39 | - ) | |
| 40 | - .col( | |
| 41 | - ColumnDef::new(FediverseAuthChallenges::RequestedRemoteId) | |
| 42 | - .uuid() | |
| 43 | - .null(), | |
| 44 | - ) | |
| 45 | - .col( | |
| 46 | - ColumnDef::new(FediverseAuthChallenges::Status) | |
| 47 | - .text() | |
| 48 | - .not_null() | |
| 49 | - .default("pending"), | |
| 50 | - ) | |
| 51 | - .col( | |
| 52 | - ColumnDef::new(FediverseAuthChallenges::RedirectUrl) | |
| 53 | - .text() | |
| 54 | - .null(), | |
| 55 | - ) | |
| 56 | - .col( | |
| 57 | - ColumnDef::new(FediverseAuthChallenges::CreatedAt) | |
| 58 | - .timestamp_with_time_zone() | |
| 59 | - .not_null(), | |
| 60 | - ) | |
| 61 | - .col( | |
| 62 | - ColumnDef::new(FediverseAuthChallenges::ExpiresAt) | |
| 63 | - .timestamp_with_time_zone() | |
| 64 | - .not_null(), | |
| 65 | - ) | |
| 66 | - .col( | |
| 67 | - ColumnDef::new(FediverseAuthChallenges::ConsumedAt) | |
| 68 | - .timestamp_with_time_zone() | |
| 69 | - .null(), | |
| 70 | - ) | |
| 71 | - .to_owned(), | |
| 72 | - ) | |
| 73 | - .await?; | |
| 74 | - | |
| 75 | - manager | |
| 76 | - .create_index( | |
| 77 | - Index::create() | |
| 78 | - .name("fediverse_auth_challenges_pending_idx") | |
| 79 | - .table(FediverseAuthChallenges::Table) | |
| 80 | - .col(FediverseAuthChallenges::Status) | |
| 81 | - .col(FediverseAuthChallenges::ExpiresAt) | |
| 82 | - .to_owned(), | |
| 83 | - ) | |
| 84 | - .await?; | |
| 85 | - | |
| 86 | - Ok(()) | |
| 87 | - } | |
| 88 | - | |
| 89 | - async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { | |
| 90 | - manager | |
| 91 | - .drop_index( | |
| 92 | - Index::drop() | |
| 93 | - .name("fediverse_auth_challenges_pending_idx") | |
| 94 | - .table(FediverseAuthChallenges::Table) | |
| 95 | - .to_owned(), | |
| 96 | - ) | |
| 97 | - .await?; | |
| 98 | - manager | |
| 99 | - .drop_table( | |
| 100 | - Table::drop() | |
| 101 | - .table(FediverseAuthChallenges::Table) | |
| 102 | - .to_owned(), | |
| 103 | - ) | |
| 104 | - .await?; | |
| 105 | - Ok(()) | |
| 106 | - } | |
| 107 | -} | |
| 108 | - | |
| 109 | -#[derive(DeriveIden)] | |
| 110 | -enum FediverseAuthChallenges { | |
| 111 | - Table, | |
| 112 | - Id, | |
| 113 | - Nonce, | |
| 114 | - Handle, | |
| 115 | - Domain, | |
| 116 | - RequestedRemoteId, | |
| 117 | - Status, | |
| 118 | - RedirectUrl, | |
| 119 | - CreatedAt, | |
| 120 | - ExpiresAt, | |
| 121 | - ConsumedAt, | |
| 122 | -} |
D
crates/rustwarden-core/migrations/src/m20260510_000014_identity_sibling_fks.rs
+0
-150
@@ -1,150 +0,0 @@
| 1 | -//! Migration `m20260510_000014_identity_sibling_fks` (RFC 0008). | |
| 2 | -//! | |
| 3 | -//! Adds **nullable sibling FK columns** so that team membership, | |
| 4 | -//! resource sharing, and audit logs can reference a | |
| 5 | -//! `remote_identities.id` once RFC 0009 lands. | |
| 6 | -//! | |
| 7 | -//! No CHECK constraints are added in this migration: the existing | |
| 8 | -//! `team_members.user_id` is part of the PK (NOT NULL by structure) | |
| 9 | -//! and `resource_shares` already has a XOR check between | |
| 10 | -//! `shared_with_user_id` / `shared_with_team_id`. RFC 0009 will | |
| 11 | -//! relax the PK and extend the XOR to include the remote sibling | |
| 12 | -//! when the service actually starts emitting remote rows. | |
| 13 | - | |
| 14 | -use sea_orm_migration::prelude::*; | |
| 15 | -use sea_orm_migration::sea_orm::ConnectionTrait; | |
| 16 | - | |
| 17 | -#[derive(DeriveMigrationName)] | |
| 18 | -pub struct Migration; | |
| 19 | - | |
| 20 | -#[async_trait::async_trait] | |
| 21 | -impl MigrationTrait for Migration { | |
| 22 | - async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { | |
| 23 | - manager | |
| 24 | - .alter_table( | |
| 25 | - Table::alter() | |
| 26 | - .table(TeamMembers::Table) | |
| 27 | - .add_column(ColumnDef::new(TeamMembers::RemoteIdentityId).uuid().null()) | |
| 28 | - .to_owned(), | |
| 29 | - ) | |
| 30 | - .await?; | |
| 31 | - | |
| 32 | - manager | |
| 33 | - .alter_table( | |
| 34 | - Table::alter() | |
| 35 | - .table(ResourceShares::Table) | |
| 36 | - .add_column( | |
| 37 | - ColumnDef::new(ResourceShares::SharedWithRemoteIdentityId) | |
| 38 | - .uuid() | |
| 39 | - .null(), | |
| 40 | - ) | |
| 41 | - .to_owned(), | |
| 42 | - ) | |
| 43 | - .await?; | |
| 44 | - | |
| 45 | - manager | |
| 46 | - .alter_table( | |
| 47 | - Table::alter() | |
| 48 | - .table(AuditLogs::Table) | |
| 49 | - .add_column( | |
| 50 | - ColumnDef::new(AuditLogs::ActorRemoteIdentityId) | |
| 51 | - .uuid() | |
| 52 | - .null(), | |
| 53 | - ) | |
| 54 | - .to_owned(), | |
| 55 | - ) | |
| 56 | - .await?; | |
| 57 | - manager | |
| 58 | - .alter_table( | |
| 59 | - Table::alter() | |
| 60 | - .table(AuditLogs::Table) | |
| 61 | - .add_column( | |
| 62 | - ColumnDef::new(AuditLogs::TargetRemoteIdentityId) | |
| 63 | - .uuid() | |
| 64 | - .null(), | |
| 65 | - ) | |
| 66 | - .to_owned(), | |
| 67 | - ) | |
| 68 | - .await?; | |
| 69 | - | |
| 70 | - // Partial indexes for the lookup paths RFC 0009 will exercise. | |
| 71 | - let conn = manager.get_connection(); | |
| 72 | - conn.execute_unprepared( | |
| 73 | - "CREATE INDEX team_members_remote_identity_idx \ | |
| 74 | - ON team_members (remote_identity_id) \ | |
| 75 | - WHERE remote_identity_id IS NOT NULL", | |
| 76 | - ) | |
| 77 | - .await?; | |
| 78 | - conn.execute_unprepared( | |
| 79 | - "CREATE INDEX resource_shares_remote_identity_idx \ | |
| 80 | - ON resource_shares (shared_with_remote_identity_id) \ | |
| 81 | - WHERE shared_with_remote_identity_id IS NOT NULL", | |
| 82 | - ) | |
| 83 | - .await?; | |
| 84 | - | |
| 85 | - Ok(()) | |
| 86 | - } | |
| 87 | - | |
| 88 | - async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { | |
| 89 | - let conn = manager.get_connection(); | |
| 90 | - for sql in [ | |
| 91 | - "DROP INDEX IF EXISTS resource_shares_remote_identity_idx", | |
| 92 | - "DROP INDEX IF EXISTS team_members_remote_identity_idx", | |
| 93 | - ] { | |
| 94 | - conn.execute_unprepared(sql).await?; | |
| 95 | - } | |
| 96 | - | |
| 97 | - manager | |
| 98 | - .alter_table( | |
| 99 | - Table::alter() | |
| 100 | - .table(AuditLogs::Table) | |
| 101 | - .drop_column(AuditLogs::TargetRemoteIdentityId) | |
| 102 | - .to_owned(), | |
| 103 | - ) | |
| 104 | - .await?; | |
| 105 | - manager | |
| 106 | - .alter_table( | |
| 107 | - Table::alter() | |
| 108 | - .table(AuditLogs::Table) | |
| 109 | - .drop_column(AuditLogs::ActorRemoteIdentityId) | |
| 110 | - .to_owned(), | |
| 111 | - ) | |
| 112 | - .await?; | |
| 113 | - manager | |
| 114 | - .alter_table( | |
| 115 | - Table::alter() | |
| 116 | - .table(ResourceShares::Table) | |
| 117 | - .drop_column(ResourceShares::SharedWithRemoteIdentityId) | |
| 118 | - .to_owned(), | |
| 119 | - ) | |
| 120 | - .await?; | |
| 121 | - manager | |
| 122 | - .alter_table( | |
| 123 | - Table::alter() | |
| 124 | - .table(TeamMembers::Table) | |
| 125 | - .drop_column(TeamMembers::RemoteIdentityId) | |
| 126 | - .to_owned(), | |
| 127 | - ) | |
| 128 | - .await?; | |
| 129 | - Ok(()) | |
| 130 | - } | |
| 131 | -} | |
| 132 | - | |
| 133 | -#[derive(DeriveIden)] | |
| 134 | -enum TeamMembers { | |
| 135 | - Table, | |
| 136 | - RemoteIdentityId, | |
| 137 | -} | |
| 138 | - | |
| 139 | -#[derive(DeriveIden)] | |
| 140 | -enum ResourceShares { | |
| 141 | - Table, | |
| 142 | - SharedWithRemoteIdentityId, | |
| 143 | -} | |
| 144 | - | |
| 145 | -#[derive(DeriveIden)] | |
| 146 | -enum AuditLogs { | |
| 147 | - Table, | |
| 148 | - ActorRemoteIdentityId, | |
| 149 | - TargetRemoteIdentityId, | |
| 150 | -} |
D
crates/rustwarden-core/migrations/src/m20260510_000015_fediverse_pending_invites.rs
+0
-129
@@ -1,129 +0,0 @@
| 1 | -//! Migration `m20260510_000015_fediverse_pending_invites` (RFC 0009). | |
| 2 | -//! | |
| 3 | -//! Persists cross-instance team-membership invitations and adds the | |
| 4 | -//! partial unique index needed to prevent duplicate remote | |
| 5 | -//! memberships within the same team. | |
| 6 | - | |
| 7 | -use sea_orm_migration::prelude::*; | |
| 8 | -use sea_orm_migration::sea_orm::ConnectionTrait; | |
| 9 | - | |
| 10 | -#[derive(DeriveMigrationName)] | |
| 11 | -pub struct Migration; | |
| 12 | - | |
| 13 | -#[async_trait::async_trait] | |
| 14 | -impl MigrationTrait for Migration { | |
| 15 | - async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { | |
| 16 | - manager | |
| 17 | - .create_table( | |
| 18 | - Table::create() | |
| 19 | - .table(FediversePendingInvites::Table) | |
| 20 | - .col( | |
| 21 | - ColumnDef::new(FediversePendingInvites::Id) | |
| 22 | - .uuid() | |
| 23 | - .not_null() | |
| 24 | - .primary_key(), | |
| 25 | - ) | |
| 26 | - .col( | |
| 27 | - ColumnDef::new(FediversePendingInvites::ActivityId) | |
| 28 | - .text() | |
| 29 | - .not_null() | |
| 30 | - .unique_key(), | |
| 31 | - ) | |
| 32 | - .col( | |
| 33 | - ColumnDef::new(FediversePendingInvites::TeamId) | |
| 34 | - .uuid() | |
| 35 | - .not_null(), | |
| 36 | - ) | |
| 37 | - .col( | |
| 38 | - ColumnDef::new(FediversePendingInvites::InvitedHandle) | |
| 39 | - .text() | |
| 40 | - .not_null(), | |
| 41 | - ) | |
| 42 | - .col( | |
| 43 | - ColumnDef::new(FediversePendingInvites::InvitedRemoteIdentityId) | |
| 44 | - .uuid() | |
| 45 | - .null(), | |
| 46 | - ) | |
| 47 | - .col( | |
| 48 | - ColumnDef::new(FediversePendingInvites::Role) | |
| 49 | - .text() | |
| 50 | - .not_null(), | |
| 51 | - ) | |
| 52 | - .col( | |
| 53 | - ColumnDef::new(FediversePendingInvites::InvitedByUserId) | |
| 54 | - .uuid() | |
| 55 | - .not_null(), | |
| 56 | - ) | |
| 57 | - .col( | |
| 58 | - ColumnDef::new(FediversePendingInvites::Status) | |
| 59 | - .text() | |
| 60 | - .not_null() | |
| 61 | - .default("pending"), | |
| 62 | - ) | |
| 63 | - .col( | |
| 64 | - ColumnDef::new(FediversePendingInvites::CreatedAt) | |
| 65 | - .timestamp_with_time_zone() | |
| 66 | - .not_null(), | |
| 67 | - ) | |
| 68 | - .col( | |
| 69 | - ColumnDef::new(FediversePendingInvites::ExpiresAt) | |
| 70 | - .timestamp_with_time_zone() | |
| 71 | - .not_null(), | |
| 72 | - ) | |
| 73 | - .col( | |
| 74 | - ColumnDef::new(FediversePendingInvites::DecidedAt) | |
| 75 | - .timestamp_with_time_zone() | |
| 76 | - .null(), | |
| 77 | - ) | |
| 78 | - .to_owned(), | |
| 79 | - ) | |
| 80 | - .await?; | |
| 81 | - | |
| 82 | - let conn = manager.get_connection(); | |
| 83 | - conn.execute_unprepared( | |
| 84 | - "CREATE INDEX fediverse_pending_invites_status_idx \ | |
| 85 | - ON fediverse_pending_invites (status, expires_at)", | |
| 86 | - ) | |
| 87 | - .await?; | |
| 88 | - conn.execute_unprepared( | |
| 89 | - "CREATE UNIQUE INDEX team_members_remote_unique \ | |
| 90 | - ON team_members (team_id, remote_identity_id) \ | |
| 91 | - WHERE remote_identity_id IS NOT NULL", | |
| 92 | - ) | |
| 93 | - .await?; | |
| 94 | - | |
| 95 | - Ok(()) | |
| 96 | - } | |
| 97 | - | |
| 98 | - async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { | |
| 99 | - let conn = manager.get_connection(); | |
| 100 | - conn.execute_unprepared("DROP INDEX IF EXISTS team_members_remote_unique") | |
| 101 | - .await?; | |
| 102 | - conn.execute_unprepared("DROP INDEX IF EXISTS fediverse_pending_invites_status_idx") | |
| 103 | - .await?; | |
| 104 | - manager | |
| 105 | - .drop_table( | |
| 106 | - Table::drop() | |
| 107 | - .table(FediversePendingInvites::Table) | |
| 108 | - .to_owned(), | |
| 109 | - ) | |
| 110 | - .await?; | |
| 111 | - Ok(()) | |
| 112 | - } | |
| 113 | -} | |
| 114 | - | |
| 115 | -#[derive(DeriveIden)] | |
| 116 | -enum FediversePendingInvites { | |
| 117 | - Table, | |
| 118 | - Id, | |
| 119 | - ActivityId, | |
| 120 | - TeamId, | |
| 121 | - InvitedHandle, | |
| 122 | - InvitedRemoteIdentityId, | |
| 123 | - Role, | |
| 124 | - InvitedByUserId, | |
| 125 | - Status, | |
| 126 | - CreatedAt, | |
| 127 | - ExpiresAt, | |
| 128 | - DecidedAt, | |
| 129 | -} |
D
crates/rustwarden-core/migrations/src/m20260510_000016_relax_team_members_pk.rs
+0
-89
@@ -1,89 +0,0 @@
| 1 | -//! Migration `m20260510_000016_relax_team_members_pk` (RFC 0018). | |
| 2 | -//! | |
| 3 | -//! Drops the composite PK `(team_id, user_id)`, makes `user_id` | |
| 4 | -//! nullable, adds a CHECK XOR with `remote_identity_id`, and switches | |
| 5 | -//! to a synthetic `id` PK so SeaORM can address the row through a | |
| 6 | -//! single column lookup. A partial UNIQUE index preserves the | |
| 7 | -//! original `(team_id, user_id)` uniqueness for local rows. | |
| 8 | - | |
| 9 | -use sea_orm_migration::prelude::*; | |
| 10 | -use sea_orm_migration::sea_orm::ConnectionTrait; | |
| 11 | - | |
| 12 | -#[derive(DeriveMigrationName)] | |
| 13 | -pub struct Migration; | |
| 14 | - | |
| 15 | -#[async_trait::async_trait] | |
| 16 | -impl MigrationTrait for Migration { | |
| 17 | - async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { | |
| 18 | - let conn = manager.get_connection(); | |
| 19 | - | |
| 20 | - // 1. Drop composite PK. | |
| 21 | - conn.execute_unprepared( | |
| 22 | - "ALTER TABLE team_members DROP CONSTRAINT team_members_pkey", | |
| 23 | - ) | |
| 24 | - .await?; | |
| 25 | - | |
| 26 | - // 2. Make user_id nullable (remote rows have user_id IS NULL). | |
| 27 | - conn.execute_unprepared( | |
| 28 | - "ALTER TABLE team_members ALTER COLUMN user_id DROP NOT NULL", | |
| 29 | - ) | |
| 30 | - .await?; | |
| 31 | - | |
| 32 | - // 3. CHECK XOR. | |
| 33 | - conn.execute_unprepared( | |
| 34 | - "ALTER TABLE team_members ADD CONSTRAINT chk_team_members_principal \ | |
| 35 | - CHECK ((user_id IS NOT NULL)::int + (remote_identity_id IS NOT NULL)::int = 1)", | |
| 36 | - ) | |
| 37 | - .await?; | |
| 38 | - | |
| 39 | - // 4. Surrogate id PK (uses pgcrypto's gen_random_uuid()). | |
| 40 | - conn.execute_unprepared( | |
| 41 | - "CREATE EXTENSION IF NOT EXISTS pgcrypto", | |
| 42 | - ) | |
| 43 | - .await?; | |
| 44 | - conn.execute_unprepared( | |
| 45 | - "ALTER TABLE team_members ADD COLUMN id UUID NOT NULL DEFAULT gen_random_uuid()", | |
| 46 | - ) | |
| 47 | - .await?; | |
| 48 | - conn.execute_unprepared( | |
| 49 | - "ALTER TABLE team_members ADD PRIMARY KEY (id)", | |
| 50 | - ) | |
| 51 | - .await?; | |
| 52 | - | |
| 53 | - // 5. Preserve uniqueness for local rows. | |
| 54 | - conn.execute_unprepared( | |
| 55 | - "CREATE UNIQUE INDEX team_members_local_unique \ | |
| 56 | - ON team_members (team_id, user_id) \ | |
| 57 | - WHERE user_id IS NOT NULL", | |
| 58 | - ) | |
| 59 | - .await?; | |
| 60 | - | |
| 61 | - Ok(()) | |
| 62 | - } | |
| 63 | - | |
| 64 | - async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { | |
| 65 | - let conn = manager.get_connection(); | |
| 66 | - | |
| 67 | - // Reverse order. | |
| 68 | - conn.execute_unprepared("DROP INDEX IF EXISTS team_members_local_unique") | |
| 69 | - .await?; | |
| 70 | - conn.execute_unprepared("ALTER TABLE team_members DROP CONSTRAINT IF EXISTS team_members_pkey") | |
| 71 | - .await?; | |
| 72 | - conn.execute_unprepared("ALTER TABLE team_members DROP COLUMN IF EXISTS id") | |
| 73 | - .await?; | |
| 74 | - conn.execute_unprepared( | |
| 75 | - "ALTER TABLE team_members DROP CONSTRAINT IF EXISTS chk_team_members_principal", | |
| 76 | - ) | |
| 77 | - .await?; | |
| 78 | - conn.execute_unprepared( | |
| 79 | - "ALTER TABLE team_members ALTER COLUMN user_id SET NOT NULL", | |
| 80 | - ) | |
| 81 | - .await?; | |
| 82 | - conn.execute_unprepared( | |
| 83 | - "ALTER TABLE team_members ADD PRIMARY KEY (team_id, user_id)", | |
| 84 | - ) | |
| 85 | - .await?; | |
| 86 | - | |
| 87 | - Ok(()) | |
| 88 | - } | |
| 89 | -} |
M
crates/rustwarden-core/src/identity.rs
+5
-2
@@ -6,8 +6,11 @@
| 6 | 6 | //! hazard: an authoriser cannot accidentally treat a federated subject |
| 7 | 7 | //! as if it had local roles. |
| 8 | 8 | //! |
| 9 | -//! Gated by `experimental-fediverse` (the trait/enum compile only when | |
| 10 | -//! the feature is on, since RFC 0009 is the first consumer). | |
| 9 | +//! Always compiled in core: [`Identity`] and [`IdentityResolver`] are | |
| 10 | +//! generic abstractions that do not depend on `rustwarden-fediverse`. | |
| 11 | +//! Consumers can use them whether or not federation is enabled — | |
| 12 | +//! federation simply implements [`IdentityResolver`] to populate the | |
| 13 | +//! `Remote` variant. | |
| 11 | 14 | |
| 12 | 15 | use async_trait::async_trait; |
| 13 | 16 | use serde::{Deserialize, Serialize}; |
M
crates/rustwarden-core/src/models/user.rs
+1
-1
@@ -41,7 +41,7 @@ pub struct Model {
| 41 | 41 | /// |
| 42 | 42 | /// Populated lazily by `ActorKeyService::ensure_keys` (RFC 0005). The |
| 43 | 43 | /// column is added by migration |
| 44 | - /// `m20260510_000011_extend_users_with_actor_keys`. | |
| 44 | + /// `m20260510_000009_fediverse_interop`. | |
| 45 | 45 | pub ap_public_key_pem: Option<String>, |
| 46 | 46 | /// ActivityPub private key PEM (PKCS#8), encrypted at rest via |
| 47 | 47 | /// `CryptoService` (`enc:` prefix + AES-256-GCM). Never returned in |
M
crates/rustwarden-fediverse/Cargo.toml
+5
-0
@@ -22,6 +22,7 @@ serde_json = "1.0"
| 22 | 22 | tracing = "0.1" |
| 23 | 23 | # ORM |
| 24 | 24 | sea-orm = { version = "2.0.0-rc.20", features = ["sqlx-postgres", "runtime-tokio-native-tls", "macros"] } |
| 25 | +sea-orm-migration = { version = "2.0.0-rc.20", features = ["sqlx-postgres", "runtime-tokio-native-tls"] } | |
| 25 | 26 | # Time & identifiers |
| 26 | 27 | chrono = { version = "0.4", features = ["serde"] } |
| 27 | 28 | uuid = { version = "1.0", features = ["v4", "serde"] } |
@@ -55,3 +56,7 @@ workspace = true
| 55 | 56 | [dev-dependencies] |
| 56 | 57 | sea-orm = { version = "2.0.0-rc.20", features = ["mock"] } |
| 57 | 58 | wiremock = "0.6" |
| 59 | + | |
| 60 | +[[bin]] | |
| 61 | +name = "migrate-fediverse" | |
| 62 | +path = "migration/src/main.rs" |
A
crates/rustwarden-fediverse/migration/src/lib.rs
+39
-0
@@ -0,0 +1,39 @@
| 1 | +//! SeaORM migrations for the rustwarden-fediverse crate. | |
| 2 | +//! | |
| 3 | +//! Single fused migration that creates all fediverse-protocol-only | |
| 4 | +//! tables and seeds default fediverse `app_settings` keys. | |
| 5 | +//! | |
| 6 | +//! Tables created: | |
| 7 | +//! - `fediverse_trusted_domains` (RFC 0004) | |
| 8 | +//! - `fediverse_blocked_domains` (RFC 0004) | |
| 9 | +//! - `fediverse_outbound_queue` (RFC 0011) | |
| 10 | +//! - `fediverse_auth_challenges` (RFC 0006) | |
| 11 | +//! - `fediverse_pending_invites` (RFC 0009) | |
| 12 | +//! | |
| 13 | +//! Run after the core migrator: | |
| 14 | +//! ```bash | |
| 15 | +//! cargo run --bin migrate -- up | |
| 16 | +//! cargo run --bin migrate-fediverse -- up | |
| 17 | +//! ``` | |
| 18 | + | |
| 19 | +pub use sea_orm_migration::prelude::*; | |
| 20 | + | |
| 21 | +pub mod m20260512_000001_fediverse_protocol_schema; | |
| 22 | + | |
| 23 | +/// Migrator for the rustwarden-fediverse crate. | |
| 24 | +pub struct FediverseMigrator; | |
| 25 | + | |
| 26 | +#[async_trait::async_trait] | |
| 27 | +impl MigratorTrait for FediverseMigrator { | |
| 28 | + fn migration_table_name() -> sea_orm::DynIden { | |
| 29 | + Alias::new("seaql_fediverse_migrations").into_iden() | |
| 30 | + } | |
| 31 | + | |
| 32 | + fn migrations() -> Vec<Box<dyn MigrationTrait>> { | |
| 33 | + vec![Box::new( | |
| 34 | + m20260512_000001_fediverse_protocol_schema::Migration, | |
| 35 | + )] | |
| 36 | + } | |
| 37 | +} | |
| 38 | + | |
| 39 | + |
A
crates/rustwarden-fediverse/migration/src/m20260512_000001_fediverse_protocol_schema.rs
+528
-0
@@ -0,0 +1,528 @@
| 1 | +//! Migration `m20260512_000001_fediverse_protocol_schema`. | |
| 2 | +//! | |
| 3 | +//! Fused migration that creates every fediverse-protocol-only table | |
| 4 | +//! in one step. Replaces (and supersedes) the granular legacy | |
| 5 | +//! migrations `m20260510_000010/000012/000013/000015` that used to | |
| 6 | +//! live in `rustwarden-core/migrations`. | |
| 7 | +//! | |
| 8 | +//! - `fediverse_trusted_domains` + `fediverse_blocked_domains` (RFC 0004) | |
| 9 | +//! - seeds for default `app_settings` fediverse keys (RFC 0004) | |
| 10 | +//! - `fediverse_outbound_queue` (RFC 0011) | |
| 11 | +//! - `fediverse_auth_challenges` (RFC 0006) | |
| 12 | +//! - `fediverse_pending_invites` (RFC 0009) | |
| 13 | +//! | |
| 14 | +//! Indexes on the core `team_members` table that enforce remote-row | |
| 15 | +//! uniqueness live in the core `fediverse_interop` migration instead, | |
| 16 | +//! since they target a core-owned relation. | |
| 17 | + | |
| 18 | +use sea_orm_migration::prelude::*; | |
| 19 | +use sea_orm_migration::sea_orm::ConnectionTrait; | |
| 20 | +use sea_orm_migration::sea_orm::sea_query::{Expr, OnConflict, Query}; | |
| 21 | +use uuid::Uuid; | |
| 22 | + | |
| 23 | +#[derive(DeriveMigrationName)] | |
| 24 | +pub struct Migration; | |
| 25 | + | |
| 26 | +#[async_trait::async_trait] | |
| 27 | +impl MigrationTrait for Migration { | |
| 28 | + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { | |
| 29 | + // ---- RFC 0004: trust model ----------------------------------- | |
| 30 | + manager | |
| 31 | + .create_table( | |
| 32 | + Table::create() | |
| 33 | + .table(FediverseTrustedDomains::Table) | |
| 34 | + .col( | |
| 35 | + ColumnDef::new(FediverseTrustedDomains::Id) | |
| 36 | + .uuid() | |
| 37 | + .not_null() | |
| 38 | + .primary_key(), | |
| 39 | + ) | |
| 40 | + .col( | |
| 41 | + ColumnDef::new(FediverseTrustedDomains::Domain) | |
| 42 | + .text() | |
| 43 | + .not_null() | |
| 44 | + .unique_key(), | |
| 45 | + ) | |
| 46 | + .col(ColumnDef::new(FediverseTrustedDomains::Note).text().null()) | |
| 47 | + .col( | |
| 48 | + ColumnDef::new(FediverseTrustedDomains::AddedByUserId) | |
| 49 | + .uuid() | |
| 50 | + .null(), | |
| 51 | + ) | |
| 52 | + .col( | |
| 53 | + ColumnDef::new(FediverseTrustedDomains::CreatedAt) | |
| 54 | + .timestamp_with_time_zone() | |
| 55 | + .not_null(), | |
| 56 | + ) | |
| 57 | + .to_owned(), | |
| 58 | + ) | |
| 59 | + .await?; | |
| 60 | + | |
| 61 | + manager | |
| 62 | + .create_table( | |
| 63 | + Table::create() | |
| 64 | + .table(FediverseBlockedDomains::Table) | |
| 65 | + .col( | |
| 66 | + ColumnDef::new(FediverseBlockedDomains::Id) | |
| 67 | + .uuid() | |
| 68 | + .not_null() | |
| 69 | + .primary_key(), | |
| 70 | + ) | |
| 71 | + .col( | |
| 72 | + ColumnDef::new(FediverseBlockedDomains::Domain) | |
| 73 | + .text() | |
| 74 | + .not_null() | |
| 75 | + .unique_key(), | |
| 76 | + ) | |
| 77 | + .col( | |
| 78 | + ColumnDef::new(FediverseBlockedDomains::Severity) | |
| 79 | + .text() | |
| 80 | + .not_null() | |
| 81 | + .default("suspend"), | |
| 82 | + ) | |
| 83 | + .col(ColumnDef::new(FediverseBlockedDomains::Reason).text().null()) | |
| 84 | + .col( | |
| 85 | + ColumnDef::new(FediverseBlockedDomains::AddedByUserId) | |
| 86 | + .uuid() | |
| 87 | + .null(), | |
| 88 | + ) | |
| 89 | + .col( | |
| 90 | + ColumnDef::new(FediverseBlockedDomains::CreatedAt) | |
| 91 | + .timestamp_with_time_zone() | |
| 92 | + .not_null(), | |
| 93 | + ) | |
| 94 | + .to_owned(), | |
| 95 | + ) | |
| 96 | + .await?; | |
| 97 | + | |
| 98 | + // Seed default app_settings keys. Idempotent via ON CONFLICT. | |
| 99 | + // `app_settings` is a core table created by an earlier core | |
| 100 | + // migration; this seeding is fediverse-specific but writes into | |
| 101 | + // a shared table, so it lives here on purpose. Bound parameters | |
| 102 | + // are used rather than `format!()` so the pattern stays safe if | |
| 103 | + // any seed value ever becomes dynamic. | |
| 104 | + for (key, value) in [ | |
| 105 | + ("fediverse_enabled", "false"), | |
| 106 | + ("fediverse_trust_mode", "allowlist"), | |
| 107 | + ("fediverse_interop_scope", "rustwarden_only"), | |
| 108 | + ("fediverse_authorized_fetch", "false"), | |
| 109 | + ("fediverse_node_name", "Rustwarden"), | |
| 110 | + ("fediverse_node_description", ""), | |
| 111 | + ] { | |
| 112 | + let insert = Query::insert() | |
| 113 | + .into_table(AppSettings::Table) | |
| 114 | + .columns([ | |
| 115 | + AppSettings::Id, | |
| 116 | + AppSettings::Key, | |
| 117 | + AppSettings::Value, | |
| 118 | + AppSettings::UpdatedAt, | |
| 119 | + ]) | |
| 120 | + .values_panic([ | |
| 121 | + Uuid::new_v4().into(), | |
| 122 | + key.into(), | |
| 123 | + value.into(), | |
| 124 | + Expr::current_timestamp().into(), | |
| 125 | + ]) | |
| 126 | + .on_conflict( | |
| 127 | + OnConflict::column(AppSettings::Key) | |
| 128 | + .do_nothing() | |
| 129 | + .to_owned(), | |
| 130 | + ) | |
| 131 | + .to_owned(); | |
| 132 | + manager.get_connection().execute(&insert).await?; | |
| 133 | + } | |
| 134 | + | |
| 135 | + // ---- RFC 0011: outbound delivery queue ------------------------ | |
| 136 | + manager | |
| 137 | + .create_table( | |
| 138 | + Table::create() | |
| 139 | + .table(FediverseOutboundQueue::Table) | |
| 140 | + .col( | |
| 141 | + ColumnDef::new(FediverseOutboundQueue::Id) | |
| 142 | + .uuid() | |
| 143 | + .not_null() | |
| 144 | + .primary_key(), | |
| 145 | + ) | |
| 146 | + .col( | |
| 147 | + ColumnDef::new(FediverseOutboundQueue::ActivityId) | |
| 148 | + .text() | |
| 149 | + .not_null(), | |
| 150 | + ) | |
| 151 | + .col( | |
| 152 | + ColumnDef::new(FediverseOutboundQueue::PayloadJson) | |
| 153 | + .text() | |
| 154 | + .not_null(), | |
| 155 | + ) | |
| 156 | + .col( | |
| 157 | + ColumnDef::new(FediverseOutboundQueue::TargetInbox) | |
| 158 | + .text() | |
| 159 | + .not_null(), | |
| 160 | + ) | |
| 161 | + .col( | |
| 162 | + ColumnDef::new(FediverseOutboundQueue::TargetDomain) | |
| 163 | + .text() | |
| 164 | + .not_null(), | |
| 165 | + ) | |
| 166 | + .col( | |
| 167 | + ColumnDef::new(FediverseOutboundQueue::Status) | |
| 168 | + .text() | |
| 169 | + .not_null() | |
| 170 | + .default("pending"), | |
| 171 | + ) | |
| 172 | + .col( | |
| 173 | + ColumnDef::new(FediverseOutboundQueue::Attempts) | |
| 174 | + .integer() | |
| 175 | + .not_null() | |
| 176 | + .default(0), | |
| 177 | + ) | |
| 178 | + .col( | |
| 179 | + ColumnDef::new(FediverseOutboundQueue::MaxAttempts) | |
| 180 | + .integer() | |
| 181 | + .not_null() | |
| 182 | + .default(8), | |
| 183 | + ) | |
| 184 | + .col(ColumnDef::new(FediverseOutboundQueue::LastError).text().null()) | |
| 185 | + .col( | |
| 186 | + ColumnDef::new(FediverseOutboundQueue::NextRetryAt) | |
| 187 | + .timestamp_with_time_zone() | |
| 188 | + .null(), | |
| 189 | + ) | |
| 190 | + .col( | |
| 191 | + ColumnDef::new(FediverseOutboundQueue::CreatedAt) | |
| 192 | + .timestamp_with_time_zone() | |
| 193 | + .not_null(), | |
| 194 | + ) | |
| 195 | + .col( | |
| 196 | + ColumnDef::new(FediverseOutboundQueue::DeliveredAt) | |
| 197 | + .timestamp_with_time_zone() | |
| 198 | + .null(), | |
| 199 | + ) | |
| 200 | + .col( | |
| 201 | + ColumnDef::new(FediverseOutboundQueue::ActorUserId) | |
| 202 | + .uuid() | |
| 203 | + .null(), | |
| 204 | + ) | |
| 205 | + .to_owned(), | |
| 206 | + ) | |
| 207 | + .await?; | |
| 208 | + | |
| 209 | + manager | |
| 210 | + .create_index( | |
| 211 | + Index::create() | |
| 212 | + .name("fediverse_outbound_pending_idx") | |
| 213 | + .table(FediverseOutboundQueue::Table) | |
| 214 | + .col(FediverseOutboundQueue::Status) | |
| 215 | + .col(FediverseOutboundQueue::NextRetryAt) | |
| 216 | + .to_owned(), | |
| 217 | + ) | |
| 218 | + .await?; | |
| 219 | + | |
| 220 | + // ---- RFC 0006: client-side auth challenges ------------------- | |
| 221 | + manager | |
| 222 | + .create_table( | |
| 223 | + Table::create() | |
| 224 | + .table(FediverseAuthChallenges::Table) | |
| 225 | + .col( | |
| 226 | + ColumnDef::new(FediverseAuthChallenges::Id) | |
| 227 | + .uuid() | |
| 228 | + .not_null() | |
| 229 | + .primary_key(), | |
| 230 | + ) | |
| 231 | + .col( | |
| 232 | + ColumnDef::new(FediverseAuthChallenges::Nonce) | |
| 233 | + .text() | |
| 234 | + .not_null() | |
| 235 | + .unique_key(), | |
| 236 | + ) | |
| 237 | + .col( | |
| 238 | + ColumnDef::new(FediverseAuthChallenges::Handle) | |
| 239 | + .text() | |
| 240 | + .not_null(), | |
| 241 | + ) | |
| 242 | + .col( | |
| 243 | + ColumnDef::new(FediverseAuthChallenges::Domain) | |
| 244 | + .text() | |
| 245 | + .not_null(), | |
| 246 | + ) | |
| 247 | + .col( | |
| 248 | + ColumnDef::new(FediverseAuthChallenges::RequestedRemoteId) | |
| 249 | + .uuid() | |
| 250 | + .null(), | |
| 251 | + ) | |
| 252 | + .col( | |
| 253 | + ColumnDef::new(FediverseAuthChallenges::Status) | |
| 254 | + .text() | |
| 255 | + .not_null() | |
| 256 | + .default("pending"), | |
| 257 | + ) | |
| 258 | + .col( | |
| 259 | + ColumnDef::new(FediverseAuthChallenges::RedirectUrl) | |
| 260 | + .text() | |
| 261 | + .null(), | |
| 262 | + ) | |
| 263 | + .col( | |
| 264 | + ColumnDef::new(FediverseAuthChallenges::CreatedAt) | |
| 265 | + .timestamp_with_time_zone() | |
| 266 | + .not_null(), | |
| 267 | + ) | |
| 268 | + .col( | |
| 269 | + ColumnDef::new(FediverseAuthChallenges::ExpiresAt) | |
| 270 | + .timestamp_with_time_zone() | |
| 271 | + .not_null(), | |
| 272 | + ) | |
| 273 | + .col( | |
| 274 | + ColumnDef::new(FediverseAuthChallenges::ConsumedAt) | |
| 275 | + .timestamp_with_time_zone() | |
| 276 | + .null(), | |
| 277 | + ) | |
| 278 | + .to_owned(), | |
| 279 | + ) | |
| 280 | + .await?; | |
| 281 | + | |
| 282 | + manager | |
| 283 | + .create_index( | |
| 284 | + Index::create() | |
| 285 | + .name("fediverse_auth_challenges_pending_idx") | |
| 286 | + .table(FediverseAuthChallenges::Table) | |
| 287 | + .col(FediverseAuthChallenges::Status) | |
| 288 | + .col(FediverseAuthChallenges::ExpiresAt) | |
| 289 | + .to_owned(), | |
| 290 | + ) | |
| 291 | + .await?; | |
| 292 | + | |
| 293 | + // ---- RFC 0009: cross-instance team invites ------------------- | |
| 294 | + manager | |
| 295 | + .create_table( | |
| 296 | + Table::create() | |
| 297 | + .table(FediversePendingInvites::Table) | |
| 298 | + .col( | |
| 299 | + ColumnDef::new(FediversePendingInvites::Id) | |
| 300 | + .uuid() | |
| 301 | + .not_null() | |
| 302 | + .primary_key(), | |
| 303 | + ) | |
| 304 | + .col( | |
| 305 | + ColumnDef::new(FediversePendingInvites::ActivityId) | |
| 306 | + .text() | |
| 307 | + .not_null() | |
| 308 | + .unique_key(), | |
| 309 | + ) | |
| 310 | + .col( | |
| 311 | + ColumnDef::new(FediversePendingInvites::TeamId) | |
| 312 | + .uuid() | |
| 313 | + .not_null(), | |
| 314 | + ) | |
| 315 | + .col( | |
| 316 | + ColumnDef::new(FediversePendingInvites::InvitedHandle) | |
| 317 | + .text() | |
| 318 | + .not_null(), | |
| 319 | + ) | |
| 320 | + .col( | |
| 321 | + ColumnDef::new(FediversePendingInvites::InvitedRemoteIdentityId) | |
| 322 | + .uuid() | |
| 323 | + .null(), | |
| 324 | + ) | |
| 325 | + .col( | |
| 326 | + ColumnDef::new(FediversePendingInvites::Role) | |
| 327 | + .text() | |
| 328 | + .not_null(), | |
| 329 | + ) | |
| 330 | + .col( | |
| 331 | + ColumnDef::new(FediversePendingInvites::InvitedByUserId) | |
| 332 | + .uuid() | |
| 333 | + .not_null(), | |
| 334 | + ) | |
| 335 | + .col( | |
| 336 | + ColumnDef::new(FediversePendingInvites::Status) | |
| 337 | + .text() | |
| 338 | + .not_null() | |
| 339 | + .default("pending"), | |
| 340 | + ) | |
| 341 | + .col( | |
| 342 | + ColumnDef::new(FediversePendingInvites::CreatedAt) | |
| 343 | + .timestamp_with_time_zone() | |
| 344 | + .not_null(), | |
| 345 | + ) | |
| 346 | + .col( | |
| 347 | + ColumnDef::new(FediversePendingInvites::ExpiresAt) | |
| 348 | + .timestamp_with_time_zone() | |
| 349 | + .not_null(), | |
| 350 | + ) | |
| 351 | + .col( | |
| 352 | + ColumnDef::new(FediversePendingInvites::DecidedAt) | |
| 353 | + .timestamp_with_time_zone() | |
| 354 | + .null(), | |
| 355 | + ) | |
| 356 | + .to_owned(), | |
| 357 | + ) | |
| 358 | + .await?; | |
| 359 | + | |
| 360 | + manager | |
| 361 | + .get_connection() | |
| 362 | + .execute_unprepared( | |
| 363 | + "CREATE INDEX fediverse_pending_invites_status_idx \ | |
| 364 | + ON fediverse_pending_invites (status, expires_at)", | |
| 365 | + ) | |
| 366 | + .await?; | |
| 367 | + | |
| 368 | + Ok(()) | |
| 369 | + } | |
| 370 | + | |
| 371 | + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { | |
| 372 | + let conn = manager.get_connection(); | |
| 373 | + conn.execute_unprepared("DROP INDEX IF EXISTS fediverse_pending_invites_status_idx") | |
| 374 | + .await?; | |
| 375 | + manager | |
| 376 | + .drop_table( | |
| 377 | + Table::drop() | |
| 378 | + .table(FediversePendingInvites::Table) | |
| 379 | + .to_owned(), | |
| 380 | + ) | |
| 381 | + .await?; | |
| 382 | + | |
| 383 | + manager | |
| 384 | + .drop_index( | |
| 385 | + Index::drop() | |
| 386 | + .name("fediverse_auth_challenges_pending_idx") | |
| 387 | + .table(FediverseAuthChallenges::Table) | |
| 388 | + .to_owned(), | |
| 389 | + ) | |
| 390 | + .await?; | |
| 391 | + manager | |
| 392 | + .drop_table( | |
| 393 | + Table::drop() | |
| 394 | + .table(FediverseAuthChallenges::Table) | |
| 395 | + .to_owned(), | |
| 396 | + ) | |
| 397 | + .await?; | |
| 398 | + | |
| 399 | + manager | |
| 400 | + .drop_index( | |
| 401 | + Index::drop() | |
| 402 | + .name("fediverse_outbound_pending_idx") | |
| 403 | + .table(FediverseOutboundQueue::Table) | |
| 404 | + .to_owned(), | |
| 405 | + ) | |
| 406 | + .await?; | |
| 407 | + manager | |
| 408 | + .drop_table( | |
| 409 | + Table::drop() | |
| 410 | + .table(FediverseOutboundQueue::Table) | |
| 411 | + .to_owned(), | |
| 412 | + ) | |
| 413 | + .await?; | |
| 414 | + | |
| 415 | + manager | |
| 416 | + .drop_table( | |
| 417 | + Table::drop() | |
| 418 | + .table(FediverseBlockedDomains::Table) | |
| 419 | + .to_owned(), | |
| 420 | + ) | |
| 421 | + .await?; | |
| 422 | + manager | |
| 423 | + .drop_table( | |
| 424 | + Table::drop() | |
| 425 | + .table(FediverseTrustedDomains::Table) | |
| 426 | + .to_owned(), | |
| 427 | + ) | |
| 428 | + .await?; | |
| 429 | + | |
| 430 | + // Best-effort: remove the seeded settings. Parameter-bound for | |
| 431 | + // safety even though the keys are literals. | |
| 432 | + for key in [ | |
| 433 | + "fediverse_enabled", | |
| 434 | + "fediverse_trust_mode", | |
| 435 | + "fediverse_interop_scope", | |
| 436 | + "fediverse_authorized_fetch", | |
| 437 | + "fediverse_node_name", | |
| 438 | + "fediverse_node_description", | |
| 439 | + ] { | |
| 440 | + let delete = Query::delete() | |
| 441 | + .from_table(AppSettings::Table) | |
| 442 | + .and_where(Expr::col(AppSettings::Key).eq(key)) | |
| 443 | + .to_owned(); | |
| 444 | + conn.execute(&delete).await?; | |
| 445 | + } | |
| 446 | + | |
| 447 | + Ok(()) | |
| 448 | + } | |
| 449 | +} | |
| 450 | + | |
| 451 | +#[derive(DeriveIden)] | |
| 452 | +enum AppSettings { | |
| 453 | + Table, | |
| 454 | + Id, | |
| 455 | + Key, | |
| 456 | + Value, | |
| 457 | + UpdatedAt, | |
| 458 | +} | |
| 459 | + | |
| 460 | +#[derive(DeriveIden)] | |
| 461 | +enum FediverseTrustedDomains { | |
| 462 | + Table, | |
| 463 | + Id, | |
| 464 | + Domain, | |
| 465 | + Note, | |
| 466 | + AddedByUserId, | |
| 467 | + CreatedAt, | |
| 468 | +} | |
| 469 | + | |
| 470 | +#[derive(DeriveIden)] | |
| 471 | +enum FediverseBlockedDomains { | |
| 472 | + Table, | |
| 473 | + Id, | |
| 474 | + Domain, | |
| 475 | + Severity, | |
| 476 | + Reason, | |
| 477 | + AddedByUserId, | |
| 478 | + CreatedAt, | |
| 479 | +} | |
| 480 | + | |
| 481 | +#[derive(DeriveIden)] | |
| 482 | +enum FediverseOutboundQueue { | |
| 483 | + Table, | |
| 484 | + Id, | |
| 485 | + ActivityId, | |
| 486 | + PayloadJson, | |
| 487 | + TargetInbox, | |
| 488 | + TargetDomain, | |
| 489 | + Status, | |
| 490 | + Attempts, | |
| 491 | + MaxAttempts, | |
| 492 | + LastError, | |
| 493 | + NextRetryAt, | |
| 494 | + CreatedAt, | |
| 495 | + DeliveredAt, | |
| 496 | + ActorUserId, | |
| 497 | +} | |
| 498 | + | |
| 499 | +#[derive(DeriveIden)] | |
| 500 | +enum FediverseAuthChallenges { | |
| 501 | + Table, | |
| 502 | + Id, | |
| 503 | + Nonce, | |
| 504 | + Handle, | |
| 505 | + Domain, | |
| 506 | + RequestedRemoteId, | |
| 507 | + Status, | |
| 508 | + RedirectUrl, | |
| 509 | + CreatedAt, | |
| 510 | + ExpiresAt, | |
| 511 | + ConsumedAt, | |
| 512 | +} | |
| 513 | + | |
| 514 | +#[derive(DeriveIden)] | |
| 515 | +enum FediversePendingInvites { | |
| 516 | + Table, | |
| 517 | + Id, | |
| 518 | + ActivityId, | |
| 519 | + TeamId, | |
| 520 | + InvitedHandle, | |
| 521 | + InvitedRemoteIdentityId, | |
| 522 | + Role, | |
| 523 | + InvitedByUserId, | |
| 524 | + Status, | |
| 525 | + CreatedAt, | |
| 526 | + ExpiresAt, | |
| 527 | + DecidedAt, | |
| 528 | +} |
A
crates/rustwarden-fediverse/migration/src/main.rs
+11
-0
@@ -0,0 +1,11 @@
| 1 | +//! CLI entry point for SeaORM migrations of the rustwarden-fediverse crate. | |
| 2 | + | |
| 3 | +use sea_orm_migration::prelude::*; | |
| 4 | + | |
| 5 | +#[path = "lib.rs"] | |
| 6 | +mod migration; | |
| 7 | + | |
| 8 | +#[tokio::main] | |
| 9 | +async fn main() { | |
| 10 | + cli::run_cli(migration::FediverseMigrator).await; | |
| 11 | +} |
M
crates/rustwarden-fediverse/src/auth_routes.rs
+2
-3
@@ -10,7 +10,6 @@
| 10 | 10 | //! | `/fediverse/auth/inbox` | POST | IdP (`a.tld`) | |
| 11 | 11 | //! | `/api/v1/fediverse/auth/consent` | POST | IdP | |
| 12 | 12 | //! |
| 13 | -//! Gated by `experimental-fediverse`. | |
| 14 | 13 | |
| 15 | 14 | use std::sync::Arc; |
| 16 | 15 | use std::time::Duration; |
@@ -218,7 +217,7 @@ async fn auth_inbox(
| 218 | 217 | match activity_type.as_str() { |
| 219 | 218 | "AuthRequest" => { |
| 220 | 219 | // Resolve this IdP's canonical domain from the env (set by |
| 221 | - // `with_fediverse()` at app boot) for the target-validation | |
| 220 | + // the federation router builder at app boot) for the target-validation | |
| 222 | 221 | // check. Strip scheme/trailing-slash so the comparison against |
| 223 | 222 | // `target`'s bare host part inside `persist_inbound_request` |
| 224 | 223 | // works whether FEDIVERSE_DOMAIN was set as `example.org` or |
@@ -278,7 +277,7 @@ async fn consent(
| 278 | 277 | } |
| 279 | 278 | |
| 280 | 279 | /// Build the auth-flow router. Mounted by |
| 281 | -/// [`crate::fediverse::routes::router_with`] alongside the discovery | |
| 280 | +/// [`crate::routes::router_with`] alongside the discovery | |
| 282 | 281 | /// + lifecycle inbox routes. |
| 283 | 282 | pub fn router(state: AuthFlowState) -> Router<DatabaseConnection> { |
| 284 | 283 | Router::new() |
M
crates/rustwarden-fediverse/src/config.rs
+1
-2
@@ -1,8 +1,7 @@
| 1 | 1 | //! Runtime configuration for ActivityPub identity federation (RFC 0001). |
| 2 | 2 | //! |
| 3 | -//! This module is gated behind the `experimental-fediverse` Cargo feature. | |
| 4 | 3 | //! It loads federation parameters from environment variables at startup and |
| 5 | -//! is consumed by [`crate::app::RustwardenBuilder::with_fediverse`] to wire | |
| 4 | +//! is consumed by the federation router builder to wire | |
| 6 | 5 | //! the federation router and outbound worker into the Axum application. |
| 7 | 6 | //! |
| 8 | 7 | //! See [`docs/rfcs/0001-fediverse-config-and-builder.md`](https://github.com/EBO/rustwarden/blob/main/docs/rfcs/0001-fediverse-config-and-builder.md) |
M
crates/rustwarden-fediverse/src/http_signatures.rs
+0
-1
@@ -11,7 +11,6 @@
| 11 | 11 | //! |
| 12 | 12 | //! The signing side is integrated end-to-end via [`HttpSignedDelivery`]. |
| 13 | 13 | //! |
| 14 | -//! Gated by `experimental-fediverse`. | |
| 15 | 14 | |
| 16 | 15 | use std::collections::BTreeMap; |
| 17 | 16 |
M
crates/rustwarden-fediverse/src/inbox.rs
+1
-2
@@ -7,7 +7,6 @@
| 7 | 7 | //! `type`. Only `Update`, `Delete`, and `Revoke` are supported in this |
| 8 | 8 | //! slice; the rest get `501 Not Implemented`. |
| 9 | 9 | //! |
| 10 | -//! Gated by `experimental-fediverse`. | |
| 11 | 10 | |
| 12 | 11 | use std::sync::Arc; |
| 13 | 12 |
@@ -24,7 +23,7 @@ use crate::services::fediverse_signal_service::{
| 24 | 23 | /// Per-router state injected into the inbox handler. |
| 25 | 24 | /// |
| 26 | 25 | /// Cheaply clonable (the verifier is `Arc`-ed). The application |
| 27 | -/// instantiates this once via `RustwardenBuilder::with_fediverse`. | |
| 26 | +/// instantiates this once via the federation router builder. | |
| 28 | 27 | #[derive(Clone)] |
| 29 | 28 | pub struct InboxState { |
| 30 | 29 | /// Pluggable signature verifier. Defaults to `AlwaysAcceptVerifier` |
M
crates/rustwarden-fediverse/src/invite_routes.rs
+1
-2
@@ -13,7 +13,6 @@
| 13 | 13 | //! - `POST /api/v1/fediverse/invites/{invite_id}/accept` |
| 14 | 14 | //! - `POST /api/v1/fediverse/invites/{invite_id}/reject` |
| 15 | 15 | //! |
| 16 | -//! Gated by `experimental-fediverse`. | |
| 17 | 16 | |
| 18 | 17 | use std::sync::Arc; |
| 19 | 18 | use std::time::Duration; |
@@ -38,7 +37,7 @@ use rustwarden_core::services::team_service::TeamRole;
| 38 | 37 | pub struct InviteRoutesState { |
| 39 | 38 | /// TTL applied to every freshly-issued invite. Defaults to 7 days |
| 40 | 39 | /// when the consumer does not override (set by |
| 41 | - /// [`crate::fediverse::routes::router_with_auth`]). | |
| 40 | + /// [`crate::routes::router_with_auth`]). | |
| 42 | 41 | pub default_ttl: Duration, |
| 43 | 42 | /// FK of the user the invite is recorded as having been sent |
| 44 | 43 | /// **by**. In production the consumer overrides this with the |
M
crates/rustwarden-fediverse/src/models/remote_identity.rs
+0
-1
@@ -4,7 +4,6 @@
| 4 | 4 | //! Created and maintained by |
| 5 | 5 | //! [`RemoteIdentityService`](rustwarden_core::services::remote_identity_service::RemoteIdentityService). |
| 6 | 6 | //! |
| 7 | -//! Gated by the `experimental-fediverse` Cargo feature. | |
| 8 | 7 | |
| 9 | 8 | use chrono::{DateTime, Utc}; |
| 10 | 9 | use sea_orm::entity::prelude::*; |
M
crates/rustwarden-fediverse/src/routes.rs
+2
-2
@@ -2,7 +2,7 @@
| 2 | 2 | //! |
| 3 | 3 | //! Assembles the four discovery routes (`webfinger`, `nodeinfo` discovery, |
| 4 | 4 | //! `nodeinfo` detail, actor) into a single `Router` ready to be merged into |
| 5 | -//! the application by [`crate::app::RustwardenBuilder::build`]. | |
| 5 | +//! the application by the app entry point. | |
| 6 | 6 | |
| 7 | 7 | use std::sync::Arc; |
| 8 | 8 |
@@ -106,7 +106,7 @@ pub fn router_with_auth(
| 106 | 106 | |
| 107 | 107 | fn placeholder_auth_config() -> AuthConfig { |
| 108 | 108 | // Env vars not set at router-build time — use harmless defaults. |
| 109 | - // Production callers should always go through `with_fediverse()` | |
| 109 | + // Production callers should always go through the federation router builder | |
| 110 | 110 | // which loads `AuthConfig` once at app startup. |
| 111 | 111 | AuthConfig { |
| 112 | 112 | jwt_secret: "0".repeat(64), |
M
crates/rustwarden-fediverse/src/services/actor_key_service.rs
+0
-1
@@ -6,7 +6,6 @@
| 6 | 6 | //! one Mastodon and other ActivityPub servers read from |
| 7 | 7 | //! `/users/{username}` to verify HTTP signatures. |
| 8 | 8 | //! |
| 9 | -//! Gated by `experimental-fediverse`. | |
| 10 | 9 | |
| 11 | 10 | use rsa::pkcs1::EncodeRsaPublicKey; |
| 12 | 11 | use rsa::pkcs8::{EncodePrivateKey, EncodePublicKey, LineEnding}; |
M
crates/rustwarden-fediverse/src/services/fediverse_auth_service.rs
+1
-2
@@ -10,7 +10,6 @@
| 10 | 10 | //! 4. `consume` — mint a remote JWT from an `Approved` challenge, |
| 11 | 11 | //! flipping the row to `Consumed` (one-shot). |
| 12 | 12 | //! |
| 13 | -//! Gated by `experimental-fediverse`. | |
| 14 | 13 | |
| 15 | 14 | use base64::Engine; |
| 16 | 15 | use chrono::{DateTime, Duration as ChronoDuration, Utc}; |
@@ -206,7 +205,7 @@ impl FediverseAuthService {
| 206 | 205 | /// otherwise `AppError::Validation`. |
| 207 | 206 | /// - `body.actor` must point to an existing `remote_identities` row — |
| 208 | 207 | /// the row is created upstream by the signature verifier |
| 209 | - /// ([`crate::fediverse::http_signatures::HttpSigVerifier::with_resolver`]) | |
| 208 | + /// ([`crate::http_signatures::HttpSigVerifier::with_resolver`]) | |
| 210 | 209 | /// when the actor doc is fetched the first time. |
| 211 | 210 | pub async fn persist_inbound_request( |
| 212 | 211 | db: &DatabaseConnection, |
M
crates/rustwarden-fediverse/src/services/fediverse_invite_service.rs
+0
-1
@@ -7,7 +7,6 @@
| 7 | 7 | //! in [`crate::services::fediverse_invite_payloads`] and dispatched by |
| 8 | 8 | //! the consumer through [`super::fediverse_outbound_service::FediverseOutboundService`]. |
| 9 | 9 | //! |
| 10 | -//! Gated by `experimental-fediverse`. | |
| 11 | 10 | |
| 12 | 11 | use chrono::{Duration as ChronoDuration, Utc}; |
| 13 | 12 | use sea_orm::{ |
M
crates/rustwarden-fediverse/src/services/fediverse_outbound_service.rs
+2
-3
@@ -6,7 +6,6 @@
| 6 | 6 | //! that RFC 0010 can plug in HTTP-Sig signing without disturbing the |
| 7 | 7 | //! queue/worker plumbing. |
| 8 | 8 | //! |
| 9 | -//! Gated by `experimental-fediverse`. | |
| 10 | 9 | |
| 11 | 10 | use async_trait::async_trait; |
| 12 | 11 | use chrono::Utc; |
@@ -283,8 +282,8 @@ pub async fn is_fediverse_observability_enabled(
| 283 | 282 | |
| 284 | 283 | /// Background loop that periodically drains the queue. |
| 285 | 284 | /// |
| 286 | -/// Spawned by `RustwardenBuilder::with_fediverse()` (RFC 0011 hooks | |
| 287 | -/// the spawn into `with_fediverse` in a follow-up patch — RFC 0006 | |
| 285 | +/// Spawned by the federation router builder (RFC 0011 hooks | |
| 286 | +/// the spawn into the federation router builder in a follow-up patch — RFC 0006 | |
| 288 | 287 | /// will be the first consumer and triggers the spawn explicitly). |
| 289 | 288 | pub struct OutboundWorker<T: OutboundDelivery> { |
| 290 | 289 | db: DatabaseConnection, |
M
crates/rustwarden-fediverse/src/services/fediverse_signal_service.rs
+1
-2
@@ -7,7 +7,6 @@
| 7 | 7 | //! is `AlwaysAcceptVerifier` (test-only); a follow-up RFC will plug |
| 8 | 8 | //! the real RFC 9421 verifier. |
| 9 | 9 | //! |
| 10 | -//! Gated by `experimental-fediverse`. | |
| 11 | 10 | |
| 12 | 11 | use async_trait::async_trait; |
| 13 | 12 | use chrono::Utc; |
@@ -225,7 +224,7 @@ pub struct VerifyCtx<'a> {
| 225 | 224 | /// Implementations decide whether to accept a given signature. The |
| 226 | 225 | /// `AlwaysAcceptVerifier` shipped here is **test/dev only**. |
| 227 | 226 | /// `HttpSigVerifier` from |
| 228 | -/// [`crate::fediverse::http_signatures`] is the production | |
| 227 | +/// [`crate::http_signatures`] is the production | |
| 229 | 228 | /// implementation. |
| 230 | 229 | #[async_trait] |
| 231 | 230 | pub trait SignatureVerifier: Send + Sync { |
M
crates/rustwarden-fediverse/src/services/fediverse_trust_service.rs
+0
-1
@@ -6,7 +6,6 @@
| 6 | 6 | //! interop scope are stored in `app_settings`; the lists themselves live |
| 7 | 7 | //! in `fediverse_trusted_domains` and `fediverse_blocked_domains`. |
| 8 | 8 | //! |
| 9 | -//! Gated by `experimental-fediverse`. | |
| 10 | 9 | |
| 11 | 10 | use chrono::Utc; |
| 12 | 11 | use sea_orm::{ActiveModelTrait, ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter, Set}; |
M
crates/rustwarden-fediverse/src/services/remote_identity_service.rs
+0
-1
@@ -4,7 +4,6 @@
| 4 | 4 | //! `resolve_handle` which performs WebFinger + actor fetch when the cache |
| 5 | 5 | //! is stale or empty. |
| 6 | 6 | //! |
| 7 | -//! Gated by `experimental-fediverse`. | |
| 8 | 7 | |
| 9 | 8 | use std::sync::Arc; |
| 10 | 9 | use std::time::Duration; |
M
crates/rustwarden-fediverse/src/state.rs
+1
-1
@@ -12,7 +12,7 @@ use super::resolver::LocalUserResolver;
| 12 | 12 | /// Cheap to clone — the resolver is behind `Arc`, the config is `Clone`. |
| 13 | 13 | #[derive(Clone)] |
| 14 | 14 | pub struct FediverseState { |
| 15 | - /// Runtime configuration loaded by [`crate::app::RustwardenBuilder::with_fediverse`]. | |
| 15 | + /// Runtime configuration loaded by the federation router builder. | |
| 16 | 16 | pub config: FediverseConfig, |
| 17 | 17 | /// User store projection used by handlers. |
| 18 | 18 | pub resolver: Arc<dyn LocalUserResolver>, |
M
tests/fediverse-testbed/Dockerfile
+5
-2
@@ -13,11 +13,13 @@ COPY app ./app
| 13 | 13 | COPY crates ./crates |
| 14 | 14 | COPY tools ./tools |
| 15 | 15 | |
| 16 | -# rustwarden-app + migrate (SeaORM CLI) + seed helper. | |
| 16 | +# rustwarden-app + migrate (SeaORM CLI) + migrate-fediverse + seed helper. | |
| 17 | 17 | RUN cargo build --release \ |
| 18 | 18 | -p rustwarden-app --features fediverse \ |
| 19 | 19 | && cargo build --release \ |
| 20 | - -p rustwarden-core --bin seed --bin migrate | |
| 20 | + -p rustwarden-core --bin seed --bin migrate \ | |
| 21 | + && cargo build --release \ | |
| 22 | + -p rustwarden-fediverse --bin migrate-fediverse | |
| 21 | 23 | |
| 22 | 24 | FROM debian:bookworm-slim AS runtime |
| 23 | 25 | RUN apt-get update \ |
@@ -38,6 +40,7 @@ COPY static ./static
| 38 | 40 | COPY --from=builder /build/target/release/rustwarden /usr/local/bin/rustwarden |
| 39 | 41 | COPY --from=builder /build/target/release/seed /usr/local/bin/seed |
| 40 | 42 | COPY --from=builder /build/target/release/migrate /usr/local/bin/migrate |
| 43 | +COPY --from=builder /build/target/release/migrate-fediverse /usr/local/bin/migrate-fediverse | |
| 41 | 44 | |
| 42 | 45 | EXPOSE 80 |
| 43 | 46 | CMD ["rustwarden"] |
M
tests/fediverse-testbed/docker-compose.yml
+42
-3
@@ -102,12 +102,51 @@ services:
| 102 | 102 | condition: service_healthy |
| 103 | 103 | networks: [fediverse-net] |
| 104 | 104 | |
| 105 | - app-alpha: | |
| 105 | + # Fediverse-specific schema (tables `fediverse_*`). Runs after the | |
| 106 | + # core migrator and uses its own tracking table | |
| 107 | + # `seaql_fediverse_migrations`. | |
| 108 | + migrate-fediverse-alpha: | |
| 106 | 109 | <<: *app-common |
| 110 | + restart: "no" | |
| 111 | + healthcheck: | |
| 112 | + disable: true | |
| 107 | 113 | env_file: env/alpha.env |
| 114 | + command: ["migrate-fediverse", "up"] | |
| 108 | 115 | depends_on: |
| 109 | 116 | migrate-alpha: |
| 110 | 117 | condition: service_completed_successfully |
| 118 | + networks: [fediverse-net] | |
| 119 | + | |
| 120 | + migrate-fediverse-beta: | |
| 121 | + <<: *app-common | |
| 122 | + restart: "no" | |
| 123 | + healthcheck: | |
| 124 | + disable: true | |
| 125 | + env_file: env/beta.env | |
| 126 | + command: ["migrate-fediverse", "up"] | |
| 127 | + depends_on: | |
| 128 | + migrate-beta: | |
| 129 | + condition: service_completed_successfully | |
| 130 | + networks: [fediverse-net] | |
| 131 | + | |
| 132 | + migrate-fediverse-gamma: | |
| 133 | + <<: *app-common | |
| 134 | + restart: "no" | |
| 135 | + healthcheck: | |
| 136 | + disable: true | |
| 137 | + env_file: env/gamma.env | |
| 138 | + command: ["migrate-fediverse", "up"] | |
| 139 | + depends_on: | |
| 140 | + migrate-gamma: | |
| 141 | + condition: service_completed_successfully | |
| 142 | + networks: [fediverse-net] | |
| 143 | + | |
| 144 | + app-alpha: | |
| 145 | + <<: *app-common | |
| 146 | + env_file: env/alpha.env | |
| 147 | + depends_on: | |
| 148 | + migrate-fediverse-alpha: | |
| 149 | + condition: service_completed_successfully | |
| 111 | 150 | ports: |
| 112 | 151 | - "3001:80" |
| 113 | 152 | networks: |
@@ -118,7 +157,7 @@ services:
| 118 | 157 | <<: *app-common |
| 119 | 158 | env_file: env/beta.env |
| 120 | 159 | depends_on: |
| 121 | - migrate-beta: | |
| 160 | + migrate-fediverse-beta: | |
| 122 | 161 | condition: service_completed_successfully |
| 123 | 162 | ports: |
| 124 | 163 | - "3002:80" |
@@ -130,7 +169,7 @@ services:
| 130 | 169 | <<: *app-common |
| 131 | 170 | env_file: env/gamma.env |
| 132 | 171 | depends_on: |
| 133 | - migrate-gamma: | |
| 172 | + migrate-fediverse-gamma: | |
| 134 | 173 | condition: service_completed_successfully |
| 135 | 174 | ports: |
| 136 | 175 | - "3003:80" |
M
tests/fediverse-testbed/scripts/setup.sh
+3
-1
@@ -19,7 +19,9 @@ echo "==> docker compose up (build + start, wait for health)"
| 19 | 19 | docker compose up -d --wait --wait-timeout 300 |
| 20 | 20 | |
| 21 | 21 | echo "==> Verifying one-shot jobs exited 0" |
| 22 | -for job in migrate-alpha migrate-beta migrate-gamma seed-alpha seed-beta seed-gamma; do | |
| 22 | +for job in migrate-alpha migrate-beta migrate-gamma \ | |
| 23 | + migrate-fediverse-alpha migrate-fediverse-beta migrate-fediverse-gamma \ | |
| 24 | + seed-alpha seed-beta seed-gamma; do | |
| 23 | 25 | code=$(docker inspect -f '{{.State.ExitCode}}' "rustwarden-fediverse-testbed-${job}-1") |
| 24 | 26 | if [ "$code" != "0" ]; then |
| 25 | 27 | echo " $job FAILED (exit $code)" |
GitRust