




























@@ -2,6 +2,10 @@
22 * GitHub Copilot OAuth flow
33 */
445+import {
6+parseStrictNonNegativeInteger,
7+parseStrictPositiveInteger,
8+} from "../../../infra/parse-finite-number.js";
59import type { Model } from "../../types.js";
610import type { OAuthCredentials, OAuthLoginCallbacks, OAuthProviderInterface } from "./types.js";
711@@ -28,8 +32,8 @@ type DeviceCodeResponse = {
2832device_code: string;
2933user_code: string;
3034verification_uri: string;
31-interval: number;
32-expires_in: number;
35+intervalMs: number;
36+expiresAt: number;
3337};
34383539type DeviceTokenSuccessResponse = {
@@ -56,6 +60,42 @@ type CopilotRequestOptions = {
5660timeoutMs?: number;
5761};
586263+function secondsToSafeMilliseconds(value: unknown): number | undefined {
64+const seconds = parseStrictPositiveInteger(value);
65+if (seconds === undefined) {
66+return undefined;
67+}
68+const milliseconds = seconds * 1000;
69+return Number.isSafeInteger(milliseconds) ? milliseconds : undefined;
70+}
71+72+function nonNegativeSecondsToSafeMilliseconds(value: unknown): number | undefined {
73+const seconds = parseStrictNonNegativeInteger(value);
74+if (seconds === undefined) {
75+return undefined;
76+}
77+const milliseconds = seconds * 1000;
78+return Number.isSafeInteger(milliseconds) ? milliseconds : undefined;
79+}
80+81+function resolveExpiresAtFromDurationSeconds(value: unknown): number | undefined {
82+const durationMs = secondsToSafeMilliseconds(value);
83+if (durationMs === undefined) {
84+return undefined;
85+}
86+const expiresAt = Date.now() + durationMs;
87+return Number.isSafeInteger(expiresAt) ? expiresAt : undefined;
88+}
89+90+function resolveExpiresAtFromEpochSeconds(value: unknown): number | undefined {
91+const epochMs = secondsToSafeMilliseconds(value);
92+if (epochMs === undefined) {
93+return undefined;
94+}
95+const expiresAt = epochMs - 5 * 60 * 1000;
96+return Number.isSafeInteger(expiresAt) ? expiresAt : undefined;
97+}
98+5999export function normalizeDomain(input: string): string | null {
60100const trimmed = input.trim();
61101if (!trimmed) {
@@ -203,14 +243,17 @@ async function startDeviceFlow(
203243const userCode = (data as Record<string, unknown>).user_code;
204244const verificationUri = (data as Record<string, unknown>).verification_uri;
205245const interval = (data as Record<string, unknown>).interval;
206-const expiresIn = (data as Record<string, unknown>).expires_in;
246+const intervalMs = nonNegativeSecondsToSafeMilliseconds(interval);
247+const expiresAt = resolveExpiresAtFromDurationSeconds(
248+(data as Record<string, unknown>).expires_in,
249+);
207250208251if (
209252typeof deviceCode !== "string" ||
210253typeof userCode !== "string" ||
211254typeof verificationUri !== "string" ||
212-typeof interval !== "number" ||
213-typeof expiresIn !== "number"
255+intervalMs === undefined ||
256+expiresAt === undefined
214257) {
215258throw new Error("Invalid device code response fields");
216259}
@@ -219,8 +262,8 @@ async function startDeviceFlow(
219262device_code: deviceCode,
220263user_code: userCode,
221264verification_uri: verificationUri,
222-interval,
223-expires_in: expiresIn,
265+intervalMs,
266+expiresAt,
224267};
225268}
226269@@ -250,13 +293,12 @@ function abortableSleep(ms: number, signal?: AbortSignal): Promise<void> {
250293async function pollForGitHubAccessToken(
251294domain: string,
252295deviceCode: string,
253-intervalSeconds: number,
254-expiresIn: number,
296+intervalMs: number,
297+deadline: number,
255298signal?: AbortSignal,
256299) {
257300const urls = getUrls(domain);
258-const deadline = Date.now() + expiresIn * 1000;
259-let intervalMs = Math.max(1000, Math.floor(intervalSeconds * 1000));
301+let pollingIntervalMs = Math.max(1000, intervalMs);
260302let intervalMultiplier = INITIAL_POLL_INTERVAL_MULTIPLIER;
261303let slowDownResponses = 0;
262304@@ -266,7 +308,7 @@ async function pollForGitHubAccessToken(
266308}
267309268310const remainingMs = deadline - Date.now();
269-const waitMs = Math.min(Math.ceil(intervalMs * intervalMultiplier), remainingMs);
311+const waitMs = Math.min(Math.ceil(pollingIntervalMs * intervalMultiplier), remainingMs);
270312await abortableSleep(waitMs, signal);
271313272314const raw = await fetchJson(
@@ -308,10 +350,11 @@ async function pollForGitHubAccessToken(
308350309351if (error === "slow_down") {
310352slowDownResponses += 1;
311-intervalMs =
312-typeof interval === "number" && interval > 0
313- ? interval * 1000
314- : Math.max(1000, intervalMs + 5000);
353+const slowDownIntervalMs = secondsToSafeMilliseconds(interval);
354+pollingIntervalMs =
355+slowDownIntervalMs === undefined
356+ ? Math.max(1000, pollingIntervalMs + 5000)
357+ : Math.max(1000, slowDownIntervalMs);
315358intervalMultiplier = SLOW_DOWN_POLL_INTERVAL_MULTIPLIER;
316359continue;
317360}
@@ -359,16 +402,16 @@ export async function refreshGitHubCopilotToken(
359402}
360403361404const token = (raw as Record<string, unknown>).token;
362-const expiresAt = (raw as Record<string, unknown>).expires_at;
405+const expires = resolveExpiresAtFromEpochSeconds((raw as Record<string, unknown>).expires_at);
363406364-if (typeof token !== "string" || typeof expiresAt !== "number") {
407+if (typeof token !== "string" || expires === undefined) {
365408throw new Error("Invalid Copilot token response fields");
366409}
367410368411return {
369412refresh: refreshToken,
370413access: token,
371-expires: expiresAt * 1000 - 5 * 60 * 1000,
414+ expires,
372415enterpriseUrl: enterpriseDomain,
373416};
374417}
@@ -514,8 +557,8 @@ export async function loginGitHubCopilot(options: {
514557const githubAccessToken = await pollForGitHubAccessToken(
515558domain,
516559device.device_code,
517-device.interval,
518-device.expires_in,
560+device.intervalMs,
561+device.expiresAt,
519562options.signal,
520563);
521564const credentials = await refreshGitHubCopilotToken(
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。