返回 DeepSeek-Reasonix
device.ts
根目录 / workers / accounts / src / routes / device.ts
1 import { Hono } from "hono";
2 import type { AppEnv } from "../env";
3 import { toAccountUser } from "../types";
4 import { repos } from "../db";
5 import { formatUserCode } from "../db/deviceGrants";
6 import { requireAuth, currentUser } from "../http/auth";
7 import { authRateLimit } from "../http/ratelimit";
8 import { ApiError } from "../http/errors";
9 import { parseBody, parseQuery, DevicePollSchema, DeviceApproveSchema, DeviceCodeQuerySchema } from "../lib/validation";
10 import { DEVICE_CODE_TTL_MS, DEVICE_POLL_INTERVAL_S } from "../config";
11
12 const device = new Hono<AppEnv>();
13
14 // CLI/desktop begins sign-in: get a device code to poll and a user code the
15 // human types on the web to approve. Public, throttled per IP.
16 device.post("/start", authRateLimit, async (c) => {
17 const { deviceCode, userCode, expiresAt } = await repos(c.env).deviceGrants.start({
18 userAgent: c.req.header("user-agent") ?? "",
19 ttlMs: DEVICE_CODE_TTL_MS,
20 kind: "cli",
21 });
22 const base = c.env.APP_ORIGIN;
23 const display = formatUserCode(userCode);
24 return c.json({
25 deviceCode,
26 userCode: display,
27 verificationUri: `${base}/device`,
28 verificationUriComplete: `${base}/device?code=${encodeURIComponent(display)}`,
29 interval: DEVICE_POLL_INTERVAL_S,
30 expiresIn: Math.floor(DEVICE_CODE_TTL_MS / 1000),
31 expiresAt,
32 });
33 });
34
35 // CLI/desktop polls with its device code. Not under the per-IP limiter — polling
36 // is frequent by design; the slow_down hint plus the short TTL bound abuse.
37 device.post("/poll", async (c) => {
38 const { deviceCode } = await parseBody(c, DevicePollSchema);
39 const { deviceGrants, sessions, users } = repos(c.env);
40
41 const claimed = await deviceGrants.claim(deviceCode);
42 if (claimed) {
43 const row = await users.byId(claimed.userId);
44 if (!row || row.status !== "active") throw new ApiError(403, "account_unavailable", "This account is not available.");
45 const token = await sessions.create(claimed.userId, { kind: claimed.kind, userAgent: claimed.userAgent });
46 return c.json({ status: "complete", sessionToken: token, user: toAccountUser(row) });
47 }
48
49 const status = await deviceGrants.pollStatus(deviceCode);
50 switch (status.kind) {
51 case "pending":
52 return c.json({ status: status.slowDown ? "slow_down" : "authorization_pending", interval: DEVICE_POLL_INTERVAL_S });
53 case "denied":
54 throw new ApiError(403, "access_denied", "The sign-in request was denied.");
55 case "expired":
56 throw new ApiError(410, "expired_token", "This sign-in request has expired. Run login again.");
57 case "not_found":
58 throw new ApiError(400, "invalid_grant", "Unknown or already-used device code.");
59 }
60 });
61
62 // The approval screen (signed-in web session) fetches what it's about to
63 // authorize before the user confirms.
64 device.get("/info", authRateLimit, requireAuth, async (c) => {
65 const { userCode } = parseQuery(c, DeviceCodeQuerySchema);
66 const grant = await repos(c.env).deviceGrants.info(userCode);
67 if (!grant) throw new ApiError(404, "invalid_user_code", "That code is invalid or has expired.");
68 return c.json({ grant: { ...grant, userCode: formatUserCode(grant.userCode) } });
69 });
70
71 device.post("/approve", authRateLimit, requireAuth, async (c) => {
72 const user = currentUser(c);
73 const { userCode } = await parseBody(c, DeviceApproveSchema);
74 const ok = await repos(c.env).deviceGrants.approve(userCode, user.id);
75 if (!ok) throw new ApiError(400, "invalid_user_code", "That code is invalid or has expired. Double-check it and try again.");
76 return c.json({ ok: true });
77 });
78
79 device.post("/deny", authRateLimit, requireAuth, async (c) => {
80 const { userCode } = await parseBody(c, DeviceApproveSchema);
81 await repos(c.env).deviceGrants.deny(userCode);
82 return c.json({ ok: true });
83 });
84
85 export default device;
86
86 lines TYPESCRIPT