import assert from "node:assert/strict"; import { dbGet, dbRun, withTransaction } from "../server/db.mjs"; const api = process.env.AI_DRAMA_API_BASE || "http://127.0.0.1:8787"; async function request(path, options = {}) { const response = await fetch(`${api}${path}`, { ...options, headers: { "content-type": "application/json", ...(options.headers || {}) } }); const payload = await response.json().catch(() => ({})); return { response, payload }; } async function login(email, deviceId, deviceLabel) { const result = await request("/api/auth/login", { method: "POST", headers: { "x-device-id": deviceId, "x-device-label": deviceLabel }, body: JSON.stringify({ email, password: "Demo@123456" }) }); assert.equal(result.response.status, 200, `${email} 登录失败`); assert.ok(result.payload.session?.token, `${email} 未返回登录会话`); return { authorization: `Bearer ${result.payload.session.token}`, session: result.payload.session }; } const suffix = `${Date.now()}-${Math.random().toString(16).slice(2)}`; const ownerDeviceId = `smoke-owner-device-${suffix}`; const secondDeviceId = `smoke-owner-device-2-${suffix}`; const ownerLabel = "Device Risk Smoke Browser"; const owner = await login("producer@local.test", ownerDeviceId, ownerLabel); let clientId = ""; let clientKey = ""; try { assert.equal(owner.session.riskLevel, "high", "首次出现的设备应该标记为高风险"); assert.ok(owner.session.riskScore >= 75, "首次设备风险分数不足"); assert.ok(!JSON.stringify(owner.session).includes(ownerDeviceId), "登录响应不能返回原始设备 ID"); const firstDevices = await request("/api/auth/devices", { headers: owner }); assert.equal(firstDevices.response.status, 200, "登录用户无法读取自己的设备列表"); assert.ok(Array.isArray(firstDevices.payload.devices), "设备接口必须返回 devices 数组"); const firstDevice = firstDevices.payload.devices.find((device) => device.label === ownerLabel); assert.ok(firstDevice, "首次登录没有登记设备"); assert.equal(firstDevice.status, "known", "首次设备默认不能直接标记为信任"); assert.ok(!JSON.stringify(firstDevice).includes(ownerDeviceId), "设备接口不能返回原始设备 ID"); assert.ok(!Object.keys(firstDevice).some((key) => /(hash|token|secret|key)/i.test(key)), "设备接口不能返回设备摘要或令牌字段"); const storedDevice = dbGet("SELECT device_key_hash, fingerprint_hash FROM auth_devices WHERE id = ?", [firstDevice.id]); assert.ok(storedDevice && /^[a-f0-9]{64}$/i.test(storedDevice.device_key_hash), "数据库必须保存设备 ID 的摘要"); assert.notEqual(storedDevice.device_key_hash, ownerDeviceId, "数据库不能保存原始设备 ID"); const secondLogin = await login("producer@local.test", ownerDeviceId, ownerLabel); assert.ok(secondLogin.session.riskScore < owner.session.riskScore, "同设备再次登录的风险分数应该下降"); assert.equal(secondLogin.session.riskLevel, "medium", "已知但未信任的设备应该是中风险"); const trusted = await request(`/api/auth/devices/${encodeURIComponent(firstDevice.id)}/trust`, { method: "POST", headers: owner, body: "{}" }); assert.equal(trusted.response.status, 200, "信任设备失败"); assert.equal(trusted.payload.device.status, "trusted", "设备信任状态没有落库"); assert.ok(trusted.payload.devices.some((device) => device.id === firstDevice.id && device.status === "trusted"), "设备列表没有返回信任状态"); const trustedLogin = await login("producer@local.test", ownerDeviceId, ownerLabel); assert.equal(trustedLogin.session.riskLevel, "low", "信任设备再次登录应该是低风险"); assert.ok(trustedLogin.session.riskScore < secondLogin.session.riskScore, "信任设备风险分数应该继续下降"); const untrusted = await request(`/api/auth/devices/${encodeURIComponent(firstDevice.id)}/untrust`, { method: "POST", headers: owner, body: "{}" }); assert.equal(untrusted.response.status, 200, "取消设备信任失败"); assert.equal(untrusted.payload.device.status, "known", "取消信任后设备状态不正确"); const secondDeviceLogin = await login("producer@local.test", secondDeviceId, "Second Smoke Browser"); assert.equal(secondDeviceLogin.session.riskLevel, "high", "新设备应该重新标记为高风险"); const ownerEvents = await request("/api/auth/security-events", { headers: owner }); assert.equal(ownerEvents.response.status, 200, "无法读取设备安全事件"); const eventTypes = new Set(ownerEvents.payload.events.map((event) => event.eventType)); assert.ok(eventTypes.has("device.first_seen"), "安全事件缺少 device.first_seen"); assert.ok(eventTypes.has("device.trusted"), "安全事件缺少 device.trusted"); assert.ok(eventTypes.has("device.untrusted"), "安全事件缺少 device.untrusted"); assert.ok(!JSON.stringify(ownerEvents.payload.events).includes(ownerDeviceId), "安全事件不能记录原始设备 ID"); const systemDetail = await request("/api/system/users/u-owner", { headers: owner }); assert.equal(systemDetail.response.status, 200, "系统管理员无法读取自己的设备风险详情"); assert.ok(Array.isArray(systemDetail.payload.devices), "系统用户详情缺少设备风险列表"); assert.ok(systemDetail.payload.devices.some((device) => device.latestRiskLevel === "high"), "系统用户详情没有显示高风险设备"); assert.ok(systemDetail.payload.sessions.some((session) => session.riskLevel && session.device), "系统用户详情会话缺少设备风险字段"); assert.ok(!JSON.stringify(systemDetail.payload).includes(ownerDeviceId), "系统用户详情不能返回原始设备 ID"); const deniedWriter = await login("writer@local.test", `smoke-writer-device-${suffix}`, "Writer Smoke Browser"); const ordinarySystemDenied = await request("/api/system/users/u-owner", { headers: deniedWriter }); assert.equal(ordinarySystemDenied.response.status, 403, "普通用户不能访问系统用户设备详情"); const createdClient = await request("/api/system/api-clients", { method: "POST", headers: { authorization: owner.authorization, "x-organization-id": "org-studio-lab", "x-workspace-id": "ws-local-aidrama" }, body: JSON.stringify({ name: `Device Risk Smoke ${suffix}`, scopes: ["jobs:read"] }) }); assert.equal(createdClient.response.status, 201, "创建设备风险 smoke API Client 失败"); clientId = createdClient.payload.client.id; clientKey = createdClient.payload.clientKey; assert.ok(clientKey, "设备风险 smoke 没有拿到 API Client 密钥"); const apiClientDenied = await request("/api/auth/devices", { headers: { authorization: `Bearer ${clientKey}` } }); assert.equal(apiClientDenied.response.status, 403, "API Client 不能管理浏览器设备"); assert.equal(apiClientDenied.payload.error, "device_management_unavailable", "设备管理 API Client 错误码不稳定"); console.log(`device risk smoke passed: registration, trust boundary, risk scoring, and sensitive-field checks (${firstDevice.id})`); } finally { if (clientId) { withTransaction(() => { dbRun("DELETE FROM api_clients WHERE id = ?", [clientId]); dbRun("DELETE FROM audit_logs WHERE target_id = ?", [clientId]); }); } }