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)"