import assert from "node:assert/strict"; import { dbGet, dbRun, withTransaction } from "../server/db.mjs"; import { hashApiClientKey } from "../server/api-client-secrets.mjs"; const api = process.env.AI_DRAMA_API_BASE || "http://127.0.0.1:8787"; const scope = { "x-organization-id": "org-studio-lab", "x-workspace-id": "ws-local-aidrama", "x-project-id": "thunder-mouth" }; async function request(path, headers = {}, options = {}) { const response = await fetch(`${api}${path}`, { ...options, headers: { "content-type": "application/json", ...headers, ...(options.headers || {}) } }); const payload = await response.json().catch(() => ({})); return { response, payload }; } async function login(email) { const result = await request("/api/auth/login", {}, { method: "POST", body: JSON.stringify({ email, password: "Demo@123456" }) }); assert.equal(result.response.ok, true, `${email} 登录失败`); return { authorization: `Bearer ${result.payload.session.token}` }; } const owner = { ...(await login("producer@local.test")), ...scope }; const writer = { ...(await login("writer@local.test")), ...scope }; let clientId = ""; let firstKey = ""; let modelId = ""; try { const denied = await request("/api/system/api-clients", writer); assert.equal(denied.response.status, 403, "普通用户不应访问 API 客户端目录"); const invalidScopes = await request("/api/system/api-clients", owner, { method: "POST", body: JSON.stringify({ name: `Invalid Scope ${Date.now()}`, scopes: ["admin:all"] }) }); assert.equal(invalidScopes.response.status, 400, "不支持的 API scope 必须被拒绝"); assert.equal(invalidScopes.payload.error, "api_client_scopes_invalid", "不支持的 API scope 错误码必须稳定"); const created = await request("/api/system/api-clients", owner, { method: "POST", body: JSON.stringify({ name: `Smoke API Client ${Date.now()}`, scopes: ["jobs:read", "models:read"] }) }); assert.equal(created.response.status, 201, "系统管理员创建 API 客户端失败"); clientId = created.payload.client.id; firstKey = created.payload.clientKey; assert.ok(firstKey.startsWith("local-") && firstKey.length > 24, "新 API 密钥熵不足或格式错误"); const stored = dbGet("SELECT client_key, client_key_hash, client_key_prefix, key_version FROM api_clients WHERE id = ?", [clientId]); assert.ok(stored, "新 API 客户端没有写入数据库"); assert.equal(stored.client_key_hash, hashApiClientKey(firstKey), "数据库中的 API 密钥摘要不匹配"); assert.equal(stored.key_version, 2, "API 客户端密钥版本没有升级"); assert.equal(stored.client_key_prefix, firstKey.slice(0, 12), "API 密钥预览前缀不匹配"); assert.notEqual(stored.client_key, firstKey, "数据库不能保存 API 密钥明文"); assert.ok(!String(stored.client_key).includes(firstKey), "数据库密钥字段不能包含可用密钥"); const listed = await request("/api/system/api-clients", owner); assert.equal(listed.response.ok, true, "系统管理员读取 API 客户端目录失败"); const listedClient = listed.payload.apiClients.find((item) => item.id === clientId); assert.ok(listedClient, "新 API 客户端没有出现在目录中"); assert.ok(!JSON.stringify(listedClient).includes(firstKey), "API 客户端目录不能返回完整密钥"); assert.deepEqual(listedClient.scopes, ["jobs:read", "models:read"], "API 客户端目录必须返回真实 scope"); const clientJobs = await request("/api/jobs", { authorization: `Bearer ${firstKey}`, ...scope }); assert.equal(clientJobs.response.ok, true, "新 API 密钥不能访问已授权任务读取接口"); const firstJobId = clientJobs.payload.jobs?.[0]?.id; if (firstJobId) { const clientJobDetail = await request(`/api/jobs/${encodeURIComponent(firstJobId)}`, { authorization: `Bearer ${firstKey}`, ...scope }); assert.equal(clientJobDetail.response.ok, true, "jobs:read 必须允许读取任务详情"); } const readOnlyCreate = await request("/api/jobs", { authorization: `Bearer ${firstKey}`, ...scope }, { method: "POST", body: "{}" }); assert.equal(readOnlyCreate.response.status, 403, "jobs:read 不能创建生成任务"); assert.equal(readOnlyCreate.payload.error, "api_client_scope_denied", "缺少 jobs:write 时必须返回 scope 错误"); const modelRead = await request("/api/platform/models", { authorization: `Bearer ${firstKey}`, ...scope }); assert.equal(modelRead.response.ok, true, "models:read 必须允许读取模型连接器"); const modelWriteDenied = await request("/api/platform/models/owned-image", { authorization: `Bearer ${firstKey}`, ...scope }, { method: "PATCH", body: JSON.stringify({}) }); assert.equal(modelWriteDenied.response.status, 403, "models:read 不能修改模型连接器"); assert.equal(modelWriteDenied.payload.error, "api_client_scope_denied", "缺少 models:write 时必须返回 scope 错误"); const scopeUpdate = await request(`/api/system/api-clients/${encodeURIComponent(clientId)}`, owner, { method: "PATCH", body: JSON.stringify({ scopes: ["jobs:read", "jobs:write", "models:read", "models:write", "audit:read"] }) }); assert.equal(scopeUpdate.response.ok, true, "API 客户端 scope 更新失败"); assert.deepEqual(scopeUpdate.payload.client.scopes, ["jobs:read", "jobs:write", "models:read", "models:write", "audit:read"], "API 客户端 scope 更新结果不正确"); const writeKey = firstKey; const dryRun = await request("/api/adapters/dry-run", { authorization: `Bearer ${writeKey}`, ...scope }, { method: "POST", body: JSON.stringify({ shotId: "shot-01", adapterId: "owned-image" }) }); assert.equal(dryRun.response.ok, true, "jobs:write 必须允许生成请求 dry-run"); const auditRead = await request("/api/audit", { authorization: `Bearer ${writeKey}`, ...scope }); assert.equal(auditRead.response.ok, true, "audit:read 必须允许读取审计日志"); modelId = `smoke-scope-model-${Date.now()}`; const modelWrite = await request("/api/platform/models/register", { authorization: `Bearer ${writeKey}`, ...scope }, { method: "POST", body: JSON.stringify({ id: modelId, label: "Scope 回归本地模型", endpoint: "http://127.0.0.1:7879", kind: "http-json", capability: ["text-to-image"], costMode: "local" }) }); assert.equal(modelWrite.response.status, 201, "models:write 必须允许登记模型连接器"); const rotated = await request(`/api/system/api-clients/${encodeURIComponent(clientId)}/rotate`, owner, { method: "POST", body: "{}" }); assert.equal(rotated.response.ok, true, "API 密钥轮换失败"); assert.notEqual(rotated.payload.clientKey, firstKey, "轮换后的 API 密钥不能与旧密钥相同"); const oldKey = await request("/api/jobs", { authorization: `Bearer ${firstKey}`, ...scope }); assert.equal(oldKey.response.status, 401, "旧 API 密钥轮换后必须立即失效"); const newKey = await request("/api/jobs", { authorization: `Bearer ${rotated.payload.clientKey}`, ...scope }); assert.equal(newKey.response.ok, true, "轮换后的 API 密钥不能访问已授权任务读取接口"); console.log(`api client smoke passed: scoped access, one-time rotation, old-key revocation (${clientId})`); } finally { if (clientId) { withTransaction(() => { if (modelId) { dbRun("DELETE FROM model_connectors WHERE id = ?", [modelId]); dbRun("DELETE FROM audit_logs WHERE target_id = ?", [modelId]); } dbRun("DELETE FROM api_clients WHERE id = ?", [clientId]); dbRun("DELETE FROM audit_logs WHERE target_id = ?", [clientId]); }); } }