





















11import { intro, note, outro, spinner } from "@clack/prompts";
22import { stylePromptTitle } from "openclaw/plugin-sdk/cli-runtime";
33import { logConfigUpdated, updateConfig } from "openclaw/plugin-sdk/config-mutation";
4+import {
5+parseStrictNonNegativeInteger,
6+parseStrictPositiveInteger,
7+} from "openclaw/plugin-sdk/number-runtime";
48import {
59applyAuthProfileConfig,
610ensureAuthProfileStore,
@@ -16,11 +20,12 @@ const GITHUB_DEVICE_VERIFICATION_URL = "https://github.com/login/device";
1620const GITHUB_AUTH_SSRF_POLICY: SsrFPolicy = { hostnameAllowlist: ["github.com"] };
17211822type DeviceCodeResponse = {
19-device_code: string;
20-user_code: string;
21-verification_uri: string;
22-expires_in: number;
23-interval: number;
23+deviceCode: string;
24+userCode: string;
25+verificationUri: string;
26+expiresInMs: number;
27+expiresAt: number;
28+intervalMs: number;
2429};
25302631type DeviceTokenResponse =
@@ -81,6 +86,48 @@ function parseJsonResponse(value: unknown): Record<string, unknown> {
8186return value as Record<string, unknown>;
8287}
838889+function secondsToSafeMilliseconds(seconds: number): number | undefined {
90+const milliseconds = seconds * 1000;
91+return Number.isSafeInteger(milliseconds) ? milliseconds : undefined;
92+}
93+94+function parseDeviceCodeResponse(
95+value: Record<string, unknown>,
96+issuedAt: number,
97+): DeviceCodeResponse {
98+const expiresInSeconds = parseStrictPositiveInteger(value.expires_in);
99+const intervalSeconds = parseStrictNonNegativeInteger(value.interval);
100+const expiresInMs =
101+expiresInSeconds === undefined ? undefined : secondsToSafeMilliseconds(expiresInSeconds);
102+const intervalMs =
103+intervalSeconds === undefined ? undefined : secondsToSafeMilliseconds(intervalSeconds);
104+const expiresAt = expiresInMs === undefined ? undefined : issuedAt + expiresInMs;
105+106+if (
107+typeof value.device_code !== "string" ||
108+!value.device_code ||
109+typeof value.user_code !== "string" ||
110+!value.user_code ||
111+typeof value.verification_uri !== "string" ||
112+!value.verification_uri ||
113+expiresInMs === undefined ||
114+expiresAt === undefined ||
115+!Number.isSafeInteger(expiresAt) ||
116+intervalMs === undefined
117+) {
118+throw new Error("GitHub device code response missing fields");
119+}
120+121+return {
122+deviceCode: value.device_code,
123+userCode: value.user_code,
124+verificationUri: value.verification_uri,
125+ expiresInMs,
126+ expiresAt,
127+ intervalMs,
128+};
129+}
130+84131async function postGitHubDeviceFlowForm(params: {
85132url: string;
86133body: URLSearchParams;
@@ -116,15 +163,13 @@ async function requestDeviceCode(params: { scope: string }): Promise<DeviceCodeR
116163scope: params.scope,
117164});
118165119-const json = (await postGitHubDeviceFlowForm({
166+const json = await postGitHubDeviceFlowForm({
120167url: DEVICE_CODE_URL,
121168 body,
122169failureLabel: "GitHub device code failed",
123-})) as DeviceCodeResponse;
124-if (!json.device_code || !json.user_code || !json.verification_uri) {
125-throw new Error("GitHub device code response missing fields");
126-}
127-return json;
170+});
171+// Anchor expiry to when GitHub issued the code, before UI prompts or browser launch.
172+return parseDeviceCodeResponse(json, Date.now());
128173}
129174130175async function pollForAccessToken(params: {
@@ -218,16 +263,12 @@ export async function runGitHubCopilotDeviceFlow(
218263io: GitHubCopilotDeviceFlowIO,
219264): Promise<GitHubCopilotDeviceFlowResult> {
220265const device = await requestDeviceCode({ scope: "read:user" });
221-const verificationUrl = normalizeGitHubDeviceVerificationUrl(device.verification_uri);
222-const userCode = normalizeGitHubDeviceUserCode(device.user_code);
223-const expiresInMs = device.expires_in * 1000;
224-// Anchor expiry to when GitHub issued the code, not when the UI finishes prompting.
225-const expiresAt = Date.now() + expiresInMs;
226-266+const verificationUrl = normalizeGitHubDeviceVerificationUrl(device.verificationUri);
267+const userCode = normalizeGitHubDeviceUserCode(device.userCode);
227268await io.showCode({
228269 verificationUrl,
229270 userCode,
230- expiresInMs,
271+expiresInMs: device.expiresInMs,
231272});
232273233274try {
@@ -238,9 +279,9 @@ export async function runGitHubCopilotDeviceFlow(
238279239280try {
240281const accessToken = await pollForAccessToken({
241-deviceCode: device.device_code,
242-intervalMs: Math.max(1000, device.interval * 1000),
243- expiresAt,
282+deviceCode: device.deviceCode,
283+intervalMs: Math.max(1000, device.intervalMs),
284+expiresAt: device.expiresAt,
244285});
245286return { status: "authorized", accessToken };
246287} catch (err) {
@@ -282,19 +323,18 @@ export async function githubCopilotLoginCommand(
282323spin.stop("Device code ready");
283324284325note(
285-[`Visit: ${device.verification_uri}`, `Code: ${device.user_code}`].join("\n"),
326+[`Visit: ${device.verificationUri}`, `Code: ${device.userCode}`].join("\n"),
286327stylePromptTitle("Authorize"),
287328);
288329289-const expiresAt = Date.now() + device.expires_in * 1000;
290-const intervalMs = Math.max(1000, device.interval * 1000);
330+const intervalMs = Math.max(1000, device.intervalMs);
291331292332const polling = spinner();
293333polling.start("Waiting for GitHub authorization...");
294334const accessToken = await pollForAccessToken({
295-deviceCode: device.device_code,
335+deviceCode: device.deviceCode,
296336 intervalMs,
297- expiresAt,
337+expiresAt: device.expiresAt,
298338});
299339polling.stop("GitHub access token acquired");
300340此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。