-- 0016_api_keys: API key 哈希落盘(K1 缺陷修复)。 -- -- 为什么必须落盘:此前 key 表只活在 internal/auth 的内存 map 里,管理面板 -- POST /api/v1/admin/users/apikey 签出去的 key 一遇进程重启全部失效(X-API-Key -- 一律 401)。而 design/11 §2 把 key 定位成「管理员调试、甚至生产环境救火」的 -- 长凭据 —— 签了就必须真能用,不能重启即作废。本表是那份凭据的权威载体: -- 启动时 auth.SQLiteKeyStore 载入全部 revoked_at IS NULL 的行到内存表,之后 -- Generate/Revoke 先落库、成功后才改内存。 -- -- 口径: -- * key_hash 是明文 key 的 **SHA-256 十六进制**,库里**只存哈希**;明文只在 -- 签发响应里出现一次,绝不落库、绝不落日志(与 0008 refresh_tokens 的 -- token_hash 同一纪律,且本包没有任何接受明文的写库入口)。 -- * revoked_at 为 NULL 表示仍有效;吊销只打时间戳、不删行,留审计痕迹。 -- * name 是可空备注;created_at / last_used_at 是运营时间戳。last_used_at -- 当前**没有写入方**(最后一用时间不能靠「每次鉴权同步写库」拿,那样会把 -- 鉴权路径变成写路径),有意留 NULL,等异步/批量方案落地再填。 -- * 表名用复数,援引 AGENTS §10 已登记的例外(既有迁移口径一致)。 CREATE TABLE IF NOT EXISTS api_keys ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, key_hash TEXT NOT NULL, created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')), last_used_at TEXT, revoked_at TEXT ); -- 唯一索引同时承担两件事:同一把 key 只能有一行;按 hash 查表/吊销有索引可走。 CREATE UNIQUE INDEX IF NOT EXISTS idx_api_keys_key_hash ON api_keys (key_hash);