docs: document pending peering approval queue, new endpoints, and awaiting_approval status
This commit is contained in:
+26
-2
@@ -358,6 +358,29 @@ The temporary password is shown exactly once in the admin UI -- the UsersPanel d
|
|||||||
3. Force-disconnect all WebSocket sessions
|
3. Force-disconnect all WebSocket sessions
|
||||||
4. Return `{ success: true }`
|
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
|
## Frontend Architecture
|
||||||
@@ -399,11 +422,12 @@ All panels live under `packages/web/src/components/modals/instanceSettingsPanels
|
|||||||
|
|
||||||
#### GeneralPanel
|
#### 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)`
|
- 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.
|
- 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
|
- Peers with status `'revoked'` are filtered out of the visible list
|
||||||
- Revoke calls `api.federation.revokePeer(peerId)` and removes from local list
|
- Revoke calls `api.federation.revokePeer(peerId)` and removes from local list
|
||||||
|
|
||||||
|
|||||||
@@ -357,7 +357,7 @@ Migration flags (internal): `voice_bit_migrated`, `profile_attachments_cleaned`,
|
|||||||
| origin | text NOT NULL UNIQUE | | `https://domain.tld` |
|
| origin | text NOT NULL UNIQUE | | `https://domain.tld` |
|
||||||
| instanceName | text | | |
|
| instanceName | text | | |
|
||||||
| hmacSecret | text NOT NULL | | 256-bit hex |
|
| 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 | | |
|
| lastSeenAt | integer | | |
|
||||||
| lastFailureAt | integer | | |
|
| lastFailureAt | integer | | |
|
||||||
| consecutiveFailures | integer | 0 | >=10 → unreachable |
|
| consecutiveFailures | integer | 0 | >=10 → unreachable |
|
||||||
@@ -365,6 +365,18 @@ Migration flags (internal): `voice_bit_migrated`, `profile_attachments_cleaned`,
|
|||||||
| remoteMaxUploadSize | integer | | Bytes, from peer |
|
| remoteMaxUploadSize | integer | | Bytes, from peer |
|
||||||
| createdAt | integer NOT NULL | | |
|
| 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
|
### federation_outbox
|
||||||
UNIQUE: (peerId, entityId)
|
UNIQUE: (peerId, entityId)
|
||||||
| Column | Type | Default | Notes |
|
| Column | Type | Default | Notes |
|
||||||
|
|||||||
+50
-19
@@ -72,30 +72,33 @@ Both instances store the **same** HMAC secret. The initiating instance generates
|
|||||||
### Peer Status Lifecycle
|
### Peer Status Lifecycle
|
||||||
|
|
||||||
```
|
```
|
||||||
initiate
|
ensurePeered
|
||||||
(none) ──────────► pending ──────────► active
|
(none) ──────────► pending ──────────► active
|
||||||
▲ │
|
▲ │ │
|
||||||
│ 10+ consecutive │ delivery failures
|
│ │ remote 202 │ delivery failures
|
||||||
│ failures ▼
|
│ ▼ ▼
|
||||||
│ unreachable
|
│ awaiting_approval unreachable
|
||||||
│ health check OK │
|
│ │ │
|
||||||
│ ▼
|
│ ┌───────────┼──────────┐ │ health check OK
|
||||||
│ active
|
│ │ │ │ ▼
|
||||||
│ admin revoke │
|
│ accept denied expired active
|
||||||
│ ▼
|
│ (fresh) (admin) (janitor) │
|
||||||
│ revoked ──► (delete) ──► re-initiate
|
│ │ │ │ │ admin revoke
|
||||||
|
│ ▼ ▼ ▼ ▼
|
||||||
|
│ active rejected rejected revoked
|
||||||
│
|
│
|
||||||
│ auto-peer rejected (403 PEERING_REQUIRES_APPROVAL)
|
│ auto-peer rejected (403 PEERING_REQUIRES_APPROVAL)
|
||||||
└──────────────────────────── rejected
|
└──────────────────────────── rejected
|
||||||
```
|
```
|
||||||
|
|
||||||
| Status | Outbox delivery | Health check | Relay accepts | Re-initiation |
|
| Status | Outbox delivery | Health check | Relay accepts | Re-initiation | Admin clear |
|
||||||
|--------|----------------|--------------|---------------|---------------|
|
|--------|----------------|--------------|---------------|---------------|-------------|
|
||||||
| `active` | Yes | No | Yes | No (returns existing) |
|
| `active` | Yes | No | Yes | No (returns existing) | N/A |
|
||||||
| `pending` | No | No | No | No (returns 409) |
|
| `pending` | No | No | No | No (returns 409) | N/A |
|
||||||
| `unreachable` | No (entries wait) | Yes (1h interval) | Yes (resets to active) | No |
|
| `awaiting_approval` | No | No | No | Returns pending; no re-handshake | Yes (admin deletes) |
|
||||||
| `revoked` | No (entries purged) | No | No (returns 403) | Yes (old record deleted) |
|
| `unreachable` | No (entries wait) | Yes (1h interval) | Yes (resets to active) | No | N/A |
|
||||||
| `rejected` | No | No | No | Yes (admin deletes record, then re-initiates) |
|
| `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
|
### 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.
|
**`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
|
### Admin Endpoints
|
||||||
|
|
||||||
| Endpoint | Method | Auth | Purpose |
|
| 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` | DELETE | JWT + admin | Revoke peer, purge outbox |
|
||||||
| `/api/federation/peers/:id/permanent` | DELETE | JWT + admin | Hard-delete revoked peer record |
|
| `/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/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
|
### S2S Endpoints
|
||||||
|
|
||||||
| Endpoint | Method | Auth | Purpose |
|
| Endpoint | Method | Auth | Purpose |
|
||||||
|----------|--------|------|---------|
|
|----------|--------|------|---------|
|
||||||
| `/api/federation/peer/rotate` | POST | HMAC | Accept secret rotation from peer |
|
| `/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) |
|
| `/api/federation/identity` | DELETE | HMAC | Delete federated user identity (soft/full mode) |
|
||||||
|
|
||||||
### S2S Identity Deletion (`DELETE /api/federation/identity`)
|
### S2S Identity Deletion (`DELETE /api/federation/identity`)
|
||||||
|
|||||||
@@ -214,7 +214,10 @@ reason: `'displaced'` (new tab) | `'session_closed'`
|
|||||||
spaceVoiceStates?: Record<string, { spaceMuted, spaceDeafened, permissionMuted }>,
|
spaceVoiceStates?: Record<string, { spaceMuted, spaceDeafened, permissionMuted }>,
|
||||||
readStates?: ReadState[],
|
readStates?: ReadState[],
|
||||||
activeCalls?: ActiveCallInfo[], // includes federatedCallHost?, livekitUrl?, livekitToken? for federated calls
|
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
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user