-- 0002_oauth_tables.sql -- OAuth 2.1 authorization-server state per design.md §4.2. -- -- Storage rules: -- * Broker tokens (access, refresh) are stored as hex-encoded SHA-256 hashes. -- Plaintext leaves the broker exactly once, when handed to the MCP client. -- * Forgejo tokens are stored cleartext — the broker has to be able to use -- them to spawn forgejo-mcp subprocesses. The SQLite file is therefore a -- sensitive secret at rest; protect it via volume permissions and (in -- production) an encrypted underlying volume. -- * Timestamps are unix epoch seconds (INTEGER). Comparing deadlines as -- ints is dramatically simpler than parsing ISO-8601 strings, and SQLite's -- type affinity makes mixed-type rows easy to misuse. -- * No DEFAULT clauses for timestamps — the application supplies them so a -- test clock can be injected later (phase 5c). CREATE TABLE clients ( client_id TEXT PRIMARY KEY, client_secret TEXT, -- NULL for public clients (PKCE-only) redirect_uris TEXT NOT NULL, -- JSON array of strings metadata_json TEXT, -- raw RFC 7591 client-metadata blob, optional created_at INTEGER NOT NULL, last_used INTEGER -- bumped on successful authorize/token use ); CREATE INDEX idx_clients_last_used ON clients(last_used); CREATE TABLE auth_codes ( code TEXT PRIMARY KEY, client_id TEXT NOT NULL REFERENCES clients(client_id) ON DELETE CASCADE, redirect_uri TEXT NOT NULL, code_challenge TEXT NOT NULL, code_challenge_method TEXT NOT NULL, -- only 'S256' accepted by the AS scopes TEXT NOT NULL, -- space-separated forgejo_access_token TEXT NOT NULL, forgejo_refresh_token TEXT, forgejo_token_expires_at INTEGER NOT NULL, forgejo_user_id INTEGER NOT NULL, forgejo_username TEXT NOT NULL, expires_at INTEGER NOT NULL, -- short TTL (~600s); reaper sweeps expired rows used_at INTEGER -- non-NULL after exchange; replay defense ); CREATE INDEX idx_auth_codes_expires_at ON auth_codes(expires_at); CREATE TABLE access_tokens ( token_hash TEXT PRIMARY KEY, -- hex(sha256(plaintext_token)) client_id TEXT NOT NULL REFERENCES clients(client_id) ON DELETE CASCADE, forgejo_user_id INTEGER NOT NULL, forgejo_username TEXT NOT NULL, scopes TEXT NOT NULL, forgejo_access_token TEXT NOT NULL, forgejo_refresh_token TEXT, forgejo_token_expires_at INTEGER NOT NULL, expires_at INTEGER NOT NULL, created_at INTEGER NOT NULL, revoked_at INTEGER -- NULL = active ); CREATE INDEX idx_access_tokens_expires_at ON access_tokens(expires_at); CREATE INDEX idx_access_tokens_forgejo_uid ON access_tokens(forgejo_user_id); CREATE TABLE refresh_tokens ( token_hash TEXT PRIMARY KEY, -- hex(sha256(plaintext_token)) access_token_hash TEXT NOT NULL REFERENCES access_tokens(token_hash) ON DELETE CASCADE, client_id TEXT NOT NULL REFERENCES clients(client_id) ON DELETE CASCADE, expires_at INTEGER NOT NULL, created_at INTEGER NOT NULL, revoked_at INTEGER ); CREATE INDEX idx_refresh_tokens_expires_at ON refresh_tokens(expires_at); INSERT INTO broker_meta (key, value) VALUES ('oauth_schema_version', '1');