docs: refresh api.md, protocol.md, schema.md against current code (D7)

One-PR spec refresh per decision D7, using the 2026-07-19 audit's
conformance matrix as the checklist:

api.md
- Remove the deleted version field from /health and /api/v1/info
  (anti-fingerprinting C-2) and document the removal.
- Document the profile surface (PATCH /users/me, PUT /users/me/password,
  GET/DELETE /users/me/sessions), the user-blocks surface
  (GET/PUT/DELETE /api/v1/blocks), and the plugin admin surface
  (/api/v1/admin/plugins).
- Correct GET /api/v1/files/{id}: auth is required and caching is
  'private, no-cache' (was documented as public/immutable, unauthenticated).
- Add search (30/min) and upload (10/min) rate limits; note announcement
  channel type as planned-only.

protocol.md
- Document auth_ok.replay_source and the 3-tier reconnect replay
  (ring buffer -> events table -> full resync) with the visibility
  watermark.
- Add the Voice End-to-End Encryption section (voice_e2ee_announce/offer
  in both directions, key-holder semantics, rate limits) and
  voice_token.is_key_holder.
- Document user_update; mark voice_speakers and member_leave as defined
  but not currently emitted; extend voice_config fields.
- Update the reference tables (19 client->server / 30 server->client)
  and point them at protocol-schema.json as the generated inventory.

schema.md
- Correct the migration history to the real 001-015 numbering.
- Document the previously missing tables: login_attempts, settings,
  emoji, sounds (dead schema), rate_lockouts, user_blocks, events,
  plugins, plugin_kv; add attachments.uploader_id and new indexes.
- Fix the channel-type list to text/voice/dm (013 triggers) and correct
  the permission formula to (base & ~deny) | allow to match
  permissions.EffectivePerms.
- Note the sqlc/dbgen layer and link the architecture data-model doc.

Also correct the audit_log_v6 claim (transient rename inside migration
003, not a coexisting table) in the audit and data-model blueprint, and
update the audit/decisions trackers (A-2026-07-03 closed).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UA17KPvqGBX3XbXYnMf1rA
This commit is contained in:
Claude
2026-07-19 13:51:44 +00:00
parent 2e7a80171b
commit ca3b0fee3e
6 changed files with 532 additions and 44 deletions
+181 -11
View File
@@ -2,6 +2,14 @@
OwnCord uses a single SQLite database file (`data/chatserver.db`) with the pure-Go driver `modernc.org/sqlite` (no CGO). Migrations run automatically on startup.
> **Data-access layers:** queries currently run as hand-written SQL in
> `Server/db`; an sqlc-generated layer (`Server/db/dbgen`, from
> `Server/db/queries/`) exists and is slated to become the real query layer
> per decision D2 in
> [plans/audit-2026-07-19-decisions.md](plans/audit-2026-07-19-decisions.md).
> See [architecture/data-model.md](architecture/data-model.md) for the full
> picture.
---
## Database Configuration
@@ -37,13 +45,19 @@ CREATE TABLE IF NOT EXISTS schema_versions (
|------|-------------|
| `001_initial_schema.sql` | All core tables, default roles and settings |
| `002_voice_states.sql` | Adds `voice_states` table |
| `003_audit_log.sql` | Recreates `audit_log` with renamed columns |
| `003_voice_optimization.sql` | Adds `camera`, `screenshare` to voice_states; voice settings to channels |
| `004_fix_member_permissions.sql` | Fixes Member role permissions |
| `005_channel_overrides_index.sql` | Adds composite index on channel_overrides |
| `006_member_video_permissions.sql` | Adds USE_VIDEO and SHARE_SCREEN to Member role |
| `007_attachment_dimensions.sql` | Adds `width` and `height` to attachments |
| `008_dm_tables.sql` | Adds `dm_participants` and `dm_open_state` tables |
| `003_audit_log.sql` | Recreates `audit_log` with canonical column names (via a transient `audit_log_v6` rename) |
| `004_voice_optimization.sql` | Adds `camera`, `screenshare` to voice_states; voice settings to channels |
| `005_fix_member_permissions.sql` | Fixes Member role permissions |
| `006_channel_overrides_index.sql` | Adds composite index on channel_overrides |
| `007_member_video_permissions.sql` | Adds USE_VIDEO and SHARE_SCREEN to Member role |
| `008_attachment_dimensions.sql` | Adds `width` and `height` to attachments |
| `009_dm_tables.sql` | Adds `dm_participants` and `dm_open_state` tables |
| `010_attachment_uploader.sql` | Adds `attachments.uploader_id` + index for upload-ownership checks |
| `011_rate_lockouts.sql` | Adds `rate_lockouts` so rate-limit lockouts survive restarts |
| `012_user_blocks.sql` | Adds `user_blocks` (blocks DM creation/messaging between users) |
| `013_channel_type_constraint.sql` | INSERT/UPDATE triggers restricting `channels.type` to `text`/`voice`/`dm` |
| `014_events_table.sql` | Adds `events` — persistent broadcast log for reconnect cold-tier replay |
| `015_plugins.sql` | Adds `plugins` and `plugin_kv` for the WASM plugin runtime |
---
@@ -137,7 +151,10 @@ CREATE TABLE channels (
);
```
Channel types: `text`, `voice`, `announcement`, `dm`.
Channel types: `text`, `voice`, `dm`. Migration 013 installs INSERT/UPDATE
triggers that reject any other value at the database layer. (An `announcement`
type is planned but not yet implemented — see the D1 decision in
[plans/audit-2026-07-19-decisions.md](plans/audit-2026-07-19-decisions.md).)
---
@@ -208,11 +225,14 @@ CREATE TABLE attachments (
size INTEGER NOT NULL,
uploaded_at TEXT NOT NULL DEFAULT (datetime('now')),
width INTEGER,
height INTEGER
height INTEGER,
uploader_id INTEGER REFERENCES users(id)
);
```
Uses UUID primary keys. `message_id` is NULL during upload, linked when the message is sent.
Uses UUID primary keys. `message_id` is NULL during upload, linked when the
message is sent. `uploader_id` (added by migration 010) records who uploaded
the file and backs the ownership check when attaching an upload to a message.
---
@@ -324,6 +344,148 @@ CREATE TABLE dm_open_state (
---
### login_attempts
Login attempt log used for IP-based rate limiting and lockouts.
```sql
CREATE TABLE login_attempts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
ip_address TEXT NOT NULL,
username TEXT,
success INTEGER NOT NULL DEFAULT 0,
timestamp TEXT NOT NULL DEFAULT (datetime('now'))
);
```
---
### settings
Generic key/value store for server settings (`server_name`, `motd`,
`registration_open`, …). Written by the admin API; read by the REST layer and
the WebSocket hub (cached with a short TTL).
```sql
CREATE TABLE settings (
key TEXT PRIMARY KEY,
value TEXT NOT NULL
);
```
---
### emoji
Custom emoji metadata.
```sql
CREATE TABLE emoji (
id INTEGER PRIMARY KEY AUTOINCREMENT,
shortcode TEXT NOT NULL UNIQUE,
filename TEXT NOT NULL,
uploaded_by INTEGER NOT NULL REFERENCES users(id),
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
```
---
### sounds
**Dead schema.** Created by the initial schema for the soundboard feature,
which has since been removed; the table remains but nothing reads or writes it.
Slated for a cleanup migration (audit A-2026-07-13).
```sql
CREATE TABLE sounds (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
filename TEXT NOT NULL,
duration_ms INTEGER NOT NULL,
uploaded_by INTEGER NOT NULL REFERENCES users(id),
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
```
---
### rate_lockouts
Persists rate-limiter lockouts (e.g. repeated failed logins) so they survive
server restarts. Sliding-window counters themselves stay in memory.
```sql
CREATE TABLE rate_lockouts (
key TEXT PRIMARY KEY,
expires_at TEXT NOT NULL
);
```
---
### user_blocks
User blocking (added by migration 012): a block prevents DM creation and
messaging between the two users.
```sql
CREATE TABLE user_blocks (
blocker_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
blocked_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
PRIMARY KEY (blocker_id, blocked_id),
CHECK (blocker_id != blocked_id)
);
```
---
### events
Persistent broadcast log (migration 014) — the cold tier of the reconnect
replay pipeline (see [protocol.md](protocol.md)). Written asynchronously by
the event persister, pruned by retention (configurable, default 24h). The
hub's in-memory sequence counter is seeded from `MAX(events.seq)` at startup
so sequence numbers stay monotonic across restarts.
```sql
CREATE TABLE events (
seq INTEGER PRIMARY KEY AUTOINCREMENT,
event_type TEXT NOT NULL,
payload BLOB NOT NULL,
channel_id INTEGER NOT NULL DEFAULT 0,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
```
---
### plugins / plugin_kv
Plugin registry and per-plugin key/value storage (migration 015). `plugin_kv`
is namespaced per plugin via the composite primary key.
```sql
CREATE TABLE plugins (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
version TEXT NOT NULL,
enabled INTEGER NOT NULL DEFAULT 0,
manifest_json TEXT NOT NULL,
installed_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE plugin_kv (
plugin_id INTEGER NOT NULL REFERENCES plugins(id) ON DELETE CASCADE,
key TEXT NOT NULL,
value BLOB NOT NULL,
PRIMARY KEY (plugin_id, key)
);
```
---
## Indexes
| Index Name | Table | Columns | Purpose |
@@ -339,6 +501,10 @@ CREATE TABLE dm_open_state (
| `idx_voice_states_channel` | voice_states | `(channel_id)` | All users in a voice channel |
| `idx_channel_overrides_channel_role` | channel_overrides | `(channel_id, role_id)` | Permission lookup |
| `idx_dm_participants_user` | dm_participants | `(user_id)` | DM channel lookup |
| `idx_attachments_uploader` | attachments | `(uploader_id)` | Upload-ownership checks |
| `idx_user_blocks_blocked` | user_blocks | `(blocked_id, blocker_id)` | Reverse block lookup |
| `idx_events_channel_seq` | events | `(channel_id, seq)` | Cold-tier replay per channel |
| `idx_events_created_at` | events | `(created_at)` | Retention pruning |
---
@@ -377,10 +543,14 @@ Bits 2-4, 7, 13-15, 21-23, 28-29, 31 are reserved.
1. Get the user's role -> role.Permissions (base)
2. If (base & ADMINISTRATOR) != 0 -> ALLOW everything
3. Get channel_overrides for (channel_id, role_id) -> allow, deny
4. effective = (base | allow) & ~deny
4. effective = (base & ~deny) | allow
5. Check: (effective & required_permission) != 0
```
Deny is applied first (strips bits), then allow (adds bits), so allow wins
when both target the same bit — matching Discord's channel-override semantics
(`permissions.EffectivePerms`).
DM channels bypass role permissions entirely and use participant-based authorization instead.
### Default Role Permission Values