feat: Security
This commit is contained in:
67
agents-docs/features/authentication.md
Normal file
67
agents-docs/features/authentication.md
Normal file
@@ -0,0 +1,67 @@
|
||||
# Authentication
|
||||
|
||||
Session-token authentication for the signaling server and product client.
|
||||
|
||||
## Trust boundaries
|
||||
|
||||
| Surface | Identity proof | Notes |
|
||||
|---|---|---|
|
||||
| Signaling server REST (mutations) | `Authorization: Bearer <token>` | Actor user IDs in request bodies are ignored; server derives `authUserId` from the token |
|
||||
| Signaling server REST (discovery) | None | `GET /api/servers`, featured/trending/search remain public |
|
||||
| Signaling server WebSocket | `identify.token` | Connections must identify before any other message type |
|
||||
| Electron Local API | Separate in-memory bearer tokens | Proxies login to allowed signaling servers only |
|
||||
| Product client local DB | OS user account | SQLite and attachments are plaintext at rest |
|
||||
|
||||
## Login / register response
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "<uuid>",
|
||||
"username": "alice",
|
||||
"displayName": "Alice",
|
||||
"token": "<opaque-hex>",
|
||||
"expiresAt": 1710000000000
|
||||
}
|
||||
```
|
||||
|
||||
- Tokens are opaque 64-character hex strings stored in server SQLite (`session_tokens`).
|
||||
- Default TTL: 24 hours (`SESSION_TOKEN_TTL_MS` env override supported).
|
||||
- Passwords are stored with bcrypt; legacy SHA-256 hashes are upgraded transparently on successful login.
|
||||
|
||||
## Protected REST routes
|
||||
|
||||
Require `Authorization: Bearer`:
|
||||
|
||||
- `PUT/POST/DELETE` under `/api/servers/*` (except public `GET`)
|
||||
- `PUT /api/requests/:id`
|
||||
- Plugin-support mutations under `/api/servers/:serverId/plugins/*`
|
||||
- `/api/users/device-tokens/*`
|
||||
- `POST /api/users/logout`
|
||||
|
||||
## WebSocket identify contract
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "identify",
|
||||
"token": "<session-token>",
|
||||
"oderId": "<user-id>",
|
||||
"displayName": "Alice",
|
||||
"connectionScope": "ws://host:3001"
|
||||
}
|
||||
```
|
||||
|
||||
- `oderId` must match the token's user id when provided.
|
||||
- Server responds with `auth_error` or `auth_required` when authentication fails.
|
||||
|
||||
## Client storage
|
||||
|
||||
The product client stores tokens per signaling-server base URL in `localStorage` (`metoyou.authTokens`). An HTTP interceptor attaches the bearer token to `/api/*` requests targeting that server.
|
||||
|
||||
Persisted local user state (`metoyou_currentUserId` + IndexedDB/SQLite profile) is **not** sufficient to use chat or presence. On startup, `loadCurrentUser$` requires a non-expired session token for the user's home/active signaling server (or any stored token as a fallback). Missing or rejected tokens dispatch `SESSION_EXPIRED` and redirect to `/login`. WebSocket `auth_required` / `auth_error` responses trigger the same path.
|
||||
|
||||
## Security considerations
|
||||
|
||||
- Rate limits: login/register (100 / 15 min), server join (30 / min).
|
||||
- CORS allowlist: optional `corsAllowlist` in `server/data/variables.json` or `CORS_ALLOWLIST` env (comma-separated). Empty allowlist keeps permissive CORS for local development.
|
||||
- Push-token routes require bearer auth and user-id match.
|
||||
- RTC relay: direct-message/direct-call types always relay; server-icon types require shared server membership; WebRTC offer/answer/ice remain open for cross-server DM WebRTC.
|
||||
60
agents-docs/features/message-integrity.md
Normal file
60
agents-docs/features/message-integrity.md
Normal file
@@ -0,0 +1,60 @@
|
||||
# Message Integrity
|
||||
|
||||
Signed, append-only **message revisions** give P2P chat a verifiable history without central message storage. The materialized `Message` row in local SQLite/IDB is a cache; peers converge via inventory snapshots and revision events.
|
||||
|
||||
## Responsibilities
|
||||
|
||||
- **Revision chain** — Every create, edit, delete, moderation, or plugin mutation appends a `MessageRevision` with monotonically increasing `revision`, `prevRevisionHash`, and `headHash`.
|
||||
- **Dual emit** — Outgoing mutations broadcast the legacy event (`chat-message`, `message-edited`, `message-deleted`) **and** `message-revision` so older peers keep working while integrity-aware peers prefer revisions.
|
||||
- **Inventory** — Sync inventories include `{ id, ts, rc, ac, revision, headHash }`. Peers re-fetch when remote revision is newer or the same revision has a different hash (tamper detection).
|
||||
- **Signing** — Human authors sign revisions with per-user Ed25519 keys. Public keys are registered on the signaling server; private keys stay in browser `localStorage`.
|
||||
|
||||
## Boundaries
|
||||
|
||||
| Layer | Owns |
|
||||
| --- | --- |
|
||||
| Product client (`toju-app`) | Revision construction, merge, verification, P2P broadcast, local persistence |
|
||||
| Signaling server (`server`) | `PUT /api/users/me/signing-key`, `GET /api/users/:id/signing-public-key` — key directory only, no message storage |
|
||||
| Electron / mobile persistence | `revision` + `headHash` on message rows; revision audit log (IDB store / SQLite meta) |
|
||||
|
||||
Plugin API messages may emit unsigned revisions (`plugin-edit` / `plugin-delete`) when the actor is a synthetic plugin user.
|
||||
|
||||
## Key types
|
||||
|
||||
- `Message.revision`, `Message.headHash` — materialized cache fields on the shared `Message` model.
|
||||
- `MessageRevision` — wire + persistence audit record (`message-revision.models.ts`).
|
||||
- `MessageRevisionType` — `create`, `author-edit`, `author-delete`, `moderate-edit`, `moderate-delete`, `plugin-edit`, `plugin-delete`.
|
||||
- `ChatEvent.type: 'message-revision'` — P2P envelope carrying a full `MessageRevision`.
|
||||
|
||||
## Merge rules
|
||||
|
||||
1. Valid signed revision with higher `revision` wins over legacy timestamp edits.
|
||||
2. Same `revision`, different `headHash` → treat as stale/tampered and re-fetch.
|
||||
3. Unsigned revisions (no `signature`) are accepted for backward compatibility when verification is skipped.
|
||||
4. Legacy peers without `revision`/`headHash` in inventory fall back to `ts` / `rc` / `ac` comparison.
|
||||
|
||||
## Client touchpoints
|
||||
|
||||
- Domain rules: `message-integrity.rules.ts`, `message-revision.builder.rules.ts`, `message-sync.rules.ts`
|
||||
- Services: `MessageRevisionService`, `MessageSigningService`
|
||||
- Store: `messages.effects.ts` (outgoing dual-emit), `messages-incoming.handlers.ts` (`handleMessageRevision`), `messages.helpers.ts` (inventory + merge)
|
||||
- Plugins: `plugin-client-api.service.ts` emits revisions for send/edit/delete
|
||||
|
||||
## Server API
|
||||
|
||||
| Method | Path | Auth | Body / response |
|
||||
| --- | --- | --- | --- |
|
||||
| `PUT` | `/api/users/me/signing-key` | Bearer | `{ publicKeyJwk }` — stores Ed25519 public JWK on the user row |
|
||||
| `GET` | `/api/users/:id/signing-public-key` | Public | `{ publicKeyJwk }` — used by peers to verify signatures |
|
||||
|
||||
Registration runs automatically after login/register via `AuthenticationService`.
|
||||
|
||||
## Degraded-mode behavior
|
||||
|
||||
- Outgoing revision signing is **best-effort**: if `Ed25519` signing fails, the client still broadcasts the legacy `chat-message` envelope (unsigned revision).
|
||||
- Incoming signed revisions are accepted without cryptographic verification when the sender's public key is not yet registered on the server, so chat is not blocked during key-registration races.
|
||||
|
||||
## Testing
|
||||
|
||||
- Unit: `message-integrity.rules.spec.ts`, `message-revision.builder.rules.spec.ts`, `message-revision-signing.rules.spec.ts`, `message-sync.rules.spec.ts`, `messages-incoming.handlers.spec.ts`
|
||||
- Outgoing revision wiring is covered indirectly through existing message effect tests; add focused specs when changing merge or signing behavior.
|
||||
Reference in New Issue
Block a user