docs: document pending peering approval queue, new endpoints, and awaiting_approval status

This commit is contained in:
Jannis Braun
2026-04-20 15:16:07 +02:00
parent b0d3ee93b0
commit ea8f786c2b
4 changed files with 93 additions and 23 deletions
+26 -2
View File
@@ -358,6 +358,29 @@ The temporary password is shown exactly once in the admin UI -- the UsersPanel d
3. Force-disconnect all WebSocket sessions
4. Return `{ success: true }`
### Peering Approval Requests
All admin-only. Only present when `autoAcceptPeering` is `false` and incoming peering requests are queued.
```
GET /api/federation/approval-requests → PeerApprovalRequest[]
POST /api/federation/approval-requests/:id/approve → { success: boolean }
POST /api/federation/approval-requests/:id/deny → { success: boolean }
```
`PeerApprovalRequest` shape:
```typescript
{
id: string; // Snowflake
origin: string; // Requesting instance URL
instanceName: string | null;
requestedAt: number; // Epoch ms
expiresAt: number; // Epoch ms (requestedAt + 30 days)
}
```
See [federation.md](federation.md) — Peer Approval Queue section for the full approval/denial/expiry flow.
---
## Frontend Architecture
@@ -399,11 +422,12 @@ All panels live under `packages/web/src/components/modals/instanceSettingsPanels
#### GeneralPanel
Manages: instance name, registration toggle, discovery toggle, GIF API key, federation relay toggle/TTL, peered instances list.
Manages: instance name, registration toggle, discovery toggle, GIF API key, federation relay toggle/TTL, pending approval requests, peered instances list.
- Instance name input: max 32 chars, enforced client-side via `slice(0, 32)`
- GIF key: password input, separate dirty tracking (`gifKeyDirty`). Only sent on save if modified. "Clear key" button sets empty string.
- Federation peers: fetched via `api.federation.peers()`, displayed as a list with status badges (active/pending/unreachable), last-seen/synced times, revoke button
- **Pending Approvals section:** Visible only when `pendingApprovalCount > 0` (from ready payload). Positioned above the peer list. Each row shows the requesting instance name and origin with Approve and Deny buttons. Approve calls `api.federation.approveApprovalRequest(id)` and Deny calls `api.federation.denyApprovalRequest(id)`; both remove the row from the local list on success.
- Federation peers: fetched via `api.federation.peers()`, displayed as a list with status badges (active/pending/unreachable/awaiting_approval), last-seen/synced times, revoke button
- Peers with status `'revoked'` are filtered out of the visible list
- Revoke calls `api.federation.revokePeer(peerId)` and removes from local list
+13 -1
View File
@@ -357,7 +357,7 @@ Migration flags (internal): `voice_bit_migrated`, `profile_attachments_cleaned`,
| origin | text NOT NULL UNIQUE | | `https://domain.tld` |
| instanceName | text | | |
| hmacSecret | text NOT NULL | | 256-bit hex |
| status | text NOT NULL | `'active'` | active/pending/unreachable/revoked/rejected |
| status | text NOT NULL | `'active'` | active/pending/awaiting_approval/unreachable/revoked/rejected |
| lastSeenAt | integer | | |
| lastFailureAt | integer | | |
| consecutiveFailures | integer | 0 | >=10 → unreachable |
@@ -365,6 +365,18 @@ Migration flags (internal): `voice_bit_migrated`, `profile_attachments_cleaned`,
| remoteMaxUploadSize | integer | | Bytes, from peer |
| createdAt | integer NOT NULL | | |
### peer_approval_requests
Holds incoming peering requests queued for admin review when `autoAcceptPeering` is `false`. One row per requesting origin (UNIQUE constraint). Rows expire after 30 days via janitor cleanup.
| Column | Type | Default | Notes |
|--------|------|---------|-------|
| id | text PK | | Snowflake |
| origin | text NOT NULL UNIQUE | | Requesting instance's origin URL |
| instanceName | text | | Instance name sent by requester |
| hmacSecret | text NOT NULL | | Requester's HMAC secret; used to sign denial notification |
| requestedAt | integer NOT NULL | | Epoch ms |
| expiresAt | integer NOT NULL | | Epoch ms; requestedAt + 30 days |
### federation_outbox
UNIQUE: (peerId, entityId)
| Column | Type | Default | Notes |
+50 -19
View File
@@ -72,30 +72,33 @@ Both instances store the **same** HMAC secret. The initiating instance generates
### Peer Status Lifecycle
```
initiate
ensurePeered
(none) ──────────► pending ──────────► active
10+ consecutive │ delivery failures
failures
unreachable
health check OK
active
admin revoke │
revoked ──► (delete) ──► re-initiate
│ remote 202 │ delivery failures
awaiting_approval unreachable
┌───────────┼──────────┐ │ health check OK
accept denied expired active
(fresh) (admin) (janitor)
│ admin revoke
│ ▼ ▼ ▼ ▼
│ active rejected rejected revoked
│ auto-peer rejected (403 PEERING_REQUIRES_APPROVAL)
└──────────────────────────── rejected
```
| Status | Outbox delivery | Health check | Relay accepts | Re-initiation |
|--------|----------------|--------------|---------------|---------------|
| `active` | Yes | No | Yes | No (returns existing) |
| `pending` | No | No | No | No (returns 409) |
| `unreachable` | No (entries wait) | Yes (1h interval) | Yes (resets to active) | No |
| `revoked` | No (entries purged) | No | No (returns 403) | Yes (old record deleted) |
| `rejected` | No | No | No | Yes (admin deletes record, then re-initiates) |
| Status | Outbox delivery | Health check | Relay accepts | Re-initiation | Admin clear |
|--------|----------------|--------------|---------------|---------------|-------------|
| `active` | Yes | No | Yes | No (returns existing) | N/A |
| `pending` | No | No | No | No (returns 409) | N/A |
| `awaiting_approval` | No | No | No | Returns pending; no re-handshake | Yes (admin deletes) |
| `unreachable` | No (entries wait) | Yes (1h interval) | Yes (resets to active) | No | N/A |
| `revoked` | No (entries purged) | No | No (returns 403) | Yes (old record deleted) | N/A |
| `rejected` | No | No | No | Yes (admin deletes record, then re-initiates) | N/A |
### PEER_UNREACHABLE_THRESHOLD
@@ -113,6 +116,30 @@ When the server needs to relay events to an instance it has not yet peered with,
**`autoAcceptPeering` instance setting** — Controls whether `POST /api/federation/peer/accept` accepts unsolicited peering requests. Default: `true`. When `false`, the endpoint returns `403 PEERING_REQUIRES_APPROVAL` for requests where no local `pending` record exists (i.e., a request the local admin did not initiate). The determination is made by checking the local peer table — not a client-provided flag.
### Peer Approval Queue
When `autoAcceptPeering` is `false` and an instance calls `POST /api/federation/peer/accept` without a matching local `pending` record, the endpoint returns `202 Accepted` and creates a row in `peer_approval_requests` instead of immediately peering. The requesting instance receives `202` (not an error), so it enters `awaiting_approval` status rather than `rejected`.
**`peer_approval_requests` table** — Holds incoming peering requests pending admin review:
- `id` — Snowflake PK
- `origin` — Requesting instance's origin URL (UNIQUE; only one pending request per origin)
- `instance_name` — Instance name sent by requester
- `hmac_secret` — Requester's HMAC secret; used to sign the denial notification
- `requested_at` / `expires_at` — Epoch ms; expiry is `requested_at + 30 days`
**Approval flow** — Admin approves via `POST /api/federation/approval-requests/:id/approve`:
1. A fresh `federationPeer` record is created (or existing `rejected`/`awaiting_approval` record is upserted) with status `pending`
2. A standard `peer/accept` handshake is sent to the requesting origin
3. On success the local peer becomes `active`; the `peer_approval_requests` row is deleted
**Denial flow** — Admin denies via `POST /api/federation/approval-requests/:id/deny`:
1. Server sends `POST {origin}/api/federation/peer/denied` signed with the requester's `hmac_secret` (from the approval request row)
2. Receiving instance transitions its local peer record from `awaiting_approval``rejected`
3. A local `federationPeer` record is upserted with status `rejected` to block future unsolicited requests from the same origin
4. The `peer_approval_requests` row is deleted
**Expiry** — The janitor (`federationJanitor.ts`) runs on its scheduled interval and deletes rows where `expires_at < now`. Expired requests do NOT create a `rejected` peer — the requesting instance can re-submit. Admin denial, by contrast, does create a `rejected` peer record, blocking re-requests until an admin clears it.
### Admin Endpoints
| Endpoint | Method | Auth | Purpose |
@@ -125,14 +152,18 @@ When the server needs to relay events to an instance it has not yet peered with,
| `/api/federation/peers/:id` | DELETE | JWT + admin | Revoke peer, purge outbox |
| `/api/federation/peers/:id/permanent` | DELETE | JWT + admin | Hard-delete revoked peer record |
| `/api/federation/peers/:id/rotate` | POST | JWT + admin | Trigger immediate secret rotation |
| `/api/federation/approval-requests` | GET | JWT + admin | List pending peering approval requests |
| `/api/federation/approval-requests/:id/approve` | POST | JWT + admin | Approve request, initiate handshake |
| `/api/federation/approval-requests/:id/deny` | POST | JWT + admin | Deny request, notify requester |
**`POST /api/federation/peer/ensure`** — Wraps `ensurePeered()`. Accepts `{ remoteOrigin: string }` in body. Returns `{ peeringStatus, peerId?, error? }` where `peeringStatus` is one of `active`, `pending`, `rejected`, `unreachable`, or `revoked`.
**`POST /api/federation/peer/ensure`** — Wraps `ensurePeered()`. Accepts `{ remoteOrigin: string }` in body. Returns `{ peeringStatus, peerId?, error? }` where `peeringStatus` is one of `active`, `pending`, `awaiting_approval`, `rejected`, `unreachable`, or `revoked`.
### S2S Endpoints
| Endpoint | Method | Auth | Purpose |
|----------|--------|------|---------|
| `/api/federation/peer/rotate` | POST | HMAC | Accept secret rotation from peer |
| `/api/federation/peer/denied` | POST | HMAC | Receive denial notification for awaiting_approval peer |
| `/api/federation/identity` | DELETE | HMAC | Delete federated user identity (soft/full mode) |
### S2S Identity Deletion (`DELETE /api/federation/identity`)
+4 -1
View File
@@ -214,7 +214,10 @@ reason: `'displaced'` (new tab) | `'session_closed'`
spaceVoiceStates?: Record<string, { spaceMuted, spaceDeafened, permissionMuted }>,
readStates?: ReadState[],
activeCalls?: ActiveCallInfo[], // includes federatedCallHost?, livekitUrl?, livekitToken? for federated calls
userActivities?: Record<userId, Activity[]>
userActivities?: Record<userId, Activity[]>,
rejectedPeerOrigins: string[], // origins with status 'rejected'; used for DM unreachable indicators
awaitingApprovalPeerOrigins: string[], // origins with status 'awaiting_approval'
pendingApprovalCount: number // count of peer_approval_requests rows; only non-zero for admins
}
```