Skip to content
3 changes: 3 additions & 0 deletions apps/api/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ import { createGithubBindingRoutes } from "./routes/github-bindings.js";
import { createDriveVersionRoutes } from "./routes/drive-versions.js";
import { createSpotlightIntentRoutes } from "./routes/spotlight-intent.js";
import { createPersonalProjectRoutes } from "./routes/personal-projects.js";
import { createWorkspaceAuditRoutes } from "./routes/workspace-audit.js";
import { TaskPlanApprovalError } from "./services/task-plan-approval.js";
import { ProjectServiceError } from "./services/projects.js";
import { PilotDay1MetricsServiceError } from "./services/pilot-day1-metrics.js";
Expand Down Expand Up @@ -313,6 +314,8 @@ app.route("/api", createGithubBindingRoutes());
app.route("/api/drive", createDriveVersionRoutes());
app.route("/api", createSpotlightIntentRoutes());
app.route("/api", createPersonalProjectRoutes());
// R20 P2A(R19-21):工作区级审计列表(GET /api/workspace/audit,仅管理员,工作区硬隔离)。
app.route("/api", createWorkspaceAuditRoutes());
app.route("/api/pilot", createPilotRoutes());
app.route("/api/ai-worklog", createAiWorklogRoutes());

Expand Down
55 changes: 54 additions & 1 deletion apps/api/src/auth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2489,7 +2489,8 @@ test("desktop-bootstrap mints a device token for an existing admin nickname with
assert.deepEqual(await who.json(), { id: admin.id, is_admin: true });
});

test("desktop-bootstrap is disabled (404) in password mode", async () => {
test("desktop-bootstrap (password mode) refuses nickname self-provision without a session (404)", async () => {
// P1-02(REL-5):密码模式不再无条件 404——但无会话(首启尚未凭据登录)仍拒。桌面据此 404 渲凭据登录门。
const app = withErrors(new Hono<AuthEnv>());
app.route("/api/auth", createAuthRoutes(deps([], [], settings({ AUTH_MODE: "password" }))));
const res = await app.request("/api/auth/desktop-bootstrap", {
Expand All @@ -2499,3 +2500,55 @@ test("desktop-bootstrap is disabled (404) in password mode", async () => {
});
assert.equal(res.status, 404, "password mode must require credentials, not nickname self-provision");
});

test("desktop-bootstrap (password mode) exchanges a valid session for a device token", async () => {
// 修复前:密码模式对本请求无条件 404(无可用登录链路)。修复后:已凭据登录(持有效会话)→ 换设备令牌。
const runtimeSettings = settings({ AUTH_MODE: "password" });
const alice = user({ nickname: "alice" });
const sessions = new MemorySessions();
const authDeps: AuthDependencies = { ...deps([alice], [], runtimeSettings), sessions };
const app = withErrors(new Hono<AuthEnv>());
app.route("/api/auth", createAuthRoutes(authDeps));
app.get("/who", createCurrentUserMiddleware(authDeps), (c) => c.json({ id: c.var.currentUser.id }));

// 模拟「已通过 /api/auth/login」:建一条会话,其 secret 走 signed cookie(与 issueSessionCookie 同键)。
const { token: sessionToken } = await mintSession(authDeps, alice, { authMethod: "password" });
const res = await app.request("/api/auth/desktop-bootstrap", {
method: "POST",
headers: {
"content-type": "application/json",
Cookie: await signedCookie(sessionToken, runtimeSettings)
},
// nickname 在密码模式被忽略(身份来自会话);仍按 schema 传占位值(nickname 必填)。
body: JSON.stringify({ nickname: "ignored-in-password-mode", device_name: "Alice Mac", platform: "desktop" })
});
assert.equal(res.status, 201, "valid session must exchange for a device token in password mode");
const body = (await res.json()) as {
client_token: string;
identity: { id: string };
device: { device_name: string; user_id: string };
};
assert.ok(body.client_token.length >= 32, "returns a usable device client token in the body");
assert.equal(body.identity.id, alice.id);
assert.equal(body.device.user_id, alice.id);
assert.equal(body.device.device_name, "Alice Mac");

// 换到的设备令牌无 cookie 即可鉴权后续请求——这正是桌面跨源所需(同 nickname 引导的最终形态)。
const who = await app.request("/who", { headers: { [LOCAL_CLIENT_HEADER]: body.client_token } });
assert.equal(who.status, 200, "minted device token authenticates a follow-up request with no cookie");
assert.deepEqual(await who.json(), { id: alice.id });
});

test("desktop-bootstrap (password mode) rejects a garbage client token with 403", async () => {
// 呈递了 client-token header 却解析不到设备 → fail-closed 403(绝不落到 404/静默签发设备令牌)。
const runtimeSettings = settings({ AUTH_MODE: "password" });
const authDeps: AuthDependencies = { ...deps([], [], runtimeSettings), sessions: new MemorySessions() };
const app = withErrors(new Hono<AuthEnv>());
app.route("/api/auth", createAuthRoutes(authDeps));
const res = await app.request("/api/auth/desktop-bootstrap", {
method: "POST",
headers: { "content-type": "application/json", [LOCAL_CLIENT_HEADER]: "garbage-token" },
body: JSON.stringify({ nickname: "x", device_name: "x" })
});
assert.equal(res.status, 403, "bad client token must fail closed, not exchange for a device token");
});
Loading
Loading