From b8ab162570a2113fde48305990d11bcbb2be6c85 Mon Sep 17 00:00:00 2001 From: Jannis Braun <151788261+TheZwiss@users.noreply.github.com> Date: Wed, 1 Apr 2026 18:25:24 +0200 Subject: [PATCH] fix(server): add registry size/duplicate validation; update database and API docs --- docs/systems/api.md | 2 ++ docs/systems/database.md | 19 +++++++++++++++++++ packages/server/src/routes/users.ts | 11 +++++++++++ 3 files changed, 32 insertions(+) diff --git a/docs/systems/api.md b/docs/systems/api.md index 6d24d007..4fd7d28e 100644 --- a/docs/systems/api.md +++ b/docs/systems/api.md @@ -22,6 +22,8 @@ POST /users/@me/verify-password { password } → { valid } POST /users/@me/change-password { currentPassword?, newPassword } → { token } DELETE /users/@me { password, username } → { success } PUT /users/@me/space-layout { items, folders, updatedAt? } → { items, folders, updatedAt } +GET /users/@me/federation-registry → { registry: FederationRegistryEntry[], updatedAt: number } +PUT /users/@me/federation-registry { registry, updatedAt } → { ok: true, updatedAt } (409 if not newer) GET /users/:id → { user } GET /users/:id/mutuals ?homeUserId= → { mutualFriends[], mutualSpaces[] } ``` diff --git a/docs/systems/database.md b/docs/systems/database.md index da59f492..dd7d7b6b 100644 --- a/docs/systems/database.md +++ b/docs/systems/database.md @@ -32,6 +32,7 @@ IDs: Snowflake text, permissions: bigint decimal strings | profileUpdatedAt | integer | | Epoch ms | | passwordChangedAt | integer | | Token revocation: tokens before this rejected | | showActivity | integer NOT NULL | 1 | Rich presence visibility | +| federationRegistryUpdatedAt | integer | 0 | LWW timestamp for federation registry sync | | createdAt | integer NOT NULL | | Epoch ms | ### spaces @@ -409,3 +410,21 @@ UNIQUE: (peerId, entityId) | mutatedAt | integer NOT NULL | | Checkpoint for sync | | payload | text | | JSON | Retention: 90 days (cleaned by federation janitor) + +### user_federation_registry +Persistent registry of all instances a user has federated with. Tracks full lifecycle. + +| Column | Type | Constraints | Purpose | +|--------|------|-------------|---------| +| user_id | TEXT | NOT NULL, FK→users(id) CASCADE | Owner | +| origin | TEXT | NOT NULL | Instance origin URL (e.g., `https://domain.com`) | +| label | TEXT | NOT NULL DEFAULT '' | Instance display name | +| username | TEXT | NOT NULL DEFAULT '' | Federated username on remote | +| remote_user_id | TEXT | NOT NULL DEFAULT '' | Snowflake ID on remote | +| status | TEXT | NOT NULL DEFAULT 'connected' | connected/disconnected/unreachable/auth_expired | +| added_at | INTEGER | NOT NULL | Epoch ms — when first federated | +| last_connected_at | INTEGER | | Epoch ms — last successful connection | +| disconnected_at | INTEGER | | Epoch ms — when user disconnected | +| error_message | TEXT | | Last error message | + +**PK:** `(user_id, origin)` diff --git a/packages/server/src/routes/users.ts b/packages/server/src/routes/users.ts index 759e13ff..0452791a 100644 --- a/packages/server/src/routes/users.ts +++ b/packages/server/src/routes/users.ts @@ -510,6 +510,17 @@ export async function userRoutes(app: FastifyInstance): Promise { return reply.code(400).send({ error: 'updatedAt must be a positive number', statusCode: 400 }); } + // Size cap — prevent unbounded registry writes + if (registry.length > 100) { + return reply.code(400).send({ error: 'Registry cannot exceed 100 entries', statusCode: 400 }); + } + + // Duplicate origin check + const origins = registry.map(e => e.origin); + if (new Set(origins).size !== origins.length) { + return reply.code(400).send({ error: 'Duplicate origins are not allowed', statusCode: 400 }); + } + const validStatuses = ['connected', 'disconnected', 'unreachable', 'auth_expired']; for (const entry of registry) { if (!entry || typeof entry.origin !== 'string' || !entry.origin) {