per-user-billing · admin keys

给 Mai 的两个 Key 管理接口

unified-generation-api 上新增 mint / revoke 两个接口。Mai 用它按 email 铸 sk_live_ key、用户 disable 时吊销。计费、定价、ignis 建账户都与这两个接口无关。

环境 已在 test 验证 · 日期 2026-06-16 · 鉴权 master key(X-Worker-Auth-Key)· DB 仅加 1 列 source(migration 0032)

它是什么two master-key-only endpoints on unified-generation-api

两个接口都挂在 unified-generation-api 上,只接受 master key。铸出来的是普通 sk_live_ 用户 key —— 跟 dashboard 早就在发的那种完全一样,落同一张 api_keys 表。Mai 派发任务前 get-or-create,disable 用户时 revoke。

① MINT
POST /admin/keys
email → 新 sk_live_ key。token 明文只返一次。
② REVOKE
POST /admin/keys/revoke
按 email 或 key_id 吊销。幂等,已吊销再吊不报错。
鉴权
master key
X-Worker-Auth-Key header。用户 key 调这俩 → 403。
不归这两个接口管 计费、定价、ignis 建账户都不在这里。这俩只做 key 的发放与吊销;模型契约(omnihuman / elevenlabs 的 /generate + /task)和"Authorization: Bearer sk_live_… 自动注入 user_email"在 api-contract 那页。

① Mint —— 铸 keyPOST /admin/keys

传 email(+ 可选 source),返回一把新 key。token 明文仅此一次,丢了只能 revoke 重铸。

# 请求
POST /admin/keys
X-Worker-Auth-Key: <master key>          # 不是 Bearer,是这个 header
Content-Type: application/json
{ "email": "user@x.com", "source": "ignis" }

# 返回 201(token 明文只此一次)
{
  "key_id":     "key_1781602556958_1da36a3d",
  "token":      "sk_live_lz8yUO3IEZXOv70f0tJxvj07NKG...",
  "email":      "user@x.com",
  "source":     "ignis",
  "expires_at": null      # source=ignis 不过期
}
字段位置说明
emailbody · 必填用户邮箱,自动 trim + 小写。格式非法返 400。
sourcebody · 可选Mai 传 "ignis"ignis → 不过期;其它/缺省 → 90 天 TTL(可用 ttl_days 覆盖)。
key_id返回吊销时用它定位。形如 key_<ts>_<rand>
token返回sk_live_ 明文,只返一次,Mai 必须存下。
expires_at返回ignisnull;其它返 "YYYY-MM-DD HH:MM:SS"(UTC)。
同 email 再铸 = 发新 key 不复用旧 key。所以 Mai 的 get-or-create 流程要"先 revoke 再 mint",否则一个 email 会攒出多把 active key(都能用)。见下方接线流程。

② Revoke —— 吊销POST /admin/keys/revoke · DELETE /admin/keys/{key_id}

emailkey_id 吊销(二选一)。幂等:已吊销/不存在再吊不报错,只是 revoked 计数为 0。

# 请求 —— 按 email(吊销该用户所有 active key)
POST /admin/keys/revoke
X-Worker-Auth-Key: <master key>
Content-Type: application/json
{ "email": "user@x.com" }        # 或 { "key_id": "key_..." }

# 也支持 —— 按路径吊销单把
DELETE /admin/keys/key_1781602556958_1da36a3d
X-Worker-Auth-Key: <master key>

# 返回 200(幂等)
{ "ok": true, "revoked": 1 }      # revoked = 本次实际吊销条数
入参语义
email把该 email 所有 active key 置为 revoked。Mai 清残留首选。
key_id只吊销这一把(POST body 或 DELETE 路径)。
revoked返回:本次真正从 active→revoked 的行数。0 = 本来就没有 active 的。

email 和 key_id 都没传 → 400。两个都传时优先用 key_id。

DB 改了什么 —— 只加了一列 sourceapi_keys.source added (migration 0032)

复用已有的 api_keys 表,只新增了一列 source(migration 0032_add_source_to_api_keys.sql),用来区分"接口发放"还是"手动创建"。其余列、约束全没动。

为什么加 source 现有 api_keys 里的行都是手动在 dashboard 创建的。今后通过 POST /admin/keys 接口铸的 key 带上调用方传的 source(Mai 传 "ignis"),库里一眼能看出哪些是接口发的。历史手动 key 的 source 保持 NULL —— NULL 就代表"手动创建"。
-- migration 0032(additive + 幂等)
ALTER TABLE api_keys ADD COLUMN IF NOT EXISTS source TEXT;

-- 只对非 NULL 建部分索引(历史手动 key 都是 NULL,不进索引)
CREATE INDEX IF NOT EXISTS idx_api_keys_source
  ON api_keys (source) WHERE source IS NOT NULL;
来源source 列含义
历史 / dashboard 手动创建NULL本次迁移前就有的行,保持 NULL。
接口 mint,Mai 传 ignis"ignis"通过 POST /admin/keys 铸,带 source。
接口 mint,不传 sourceNULL接口可不传,落 NULL。

Test 验证结果11/11 passed on unified-generation-api-test

在 test worker 上端到端跑了 11 个场景,全过。

#场景预期结果
1mint source=ignisexpires_at:null + token
2新 key 调 /models200
3用户 key 调 mint403
4revoke by email{ok:true,revoked:1}
5吊销后 key 调 /models401
6重复 revoke(幂等){ok:true,revoked:0}
7mint 默认 sourceexpires_at 90 天后
8revoke by key_idrevoked:1
9DELETE /admin/keys/{id}revoked:1
10非法 email400
11revoke 空 body400

Mai 接线 —— get-or-create + revokehow Mai wires these in

推荐流程,避免一个 email 攒出多把 active key。

1
派发任务前,按 user_email 查 Mai 本地表。命中 active token → 直接用,注入给 Tobatsu。
2
未命中 → 先 POST /admin/keys/revoke {email} 清掉可能的残留,再 POST /admin/keys {email, source:"ignis"} 铸新的。
3
把 token + key_id 存进 Mai 本地表(token 明文只返一次,必须落库)。注入给 Tobatsu。
4
用户被 disable → POST /admin/keys/revoke {email},同时清掉本地表记录。