

























@@ -1,4 +1,5 @@
11import { createHash, randomBytes } from "node:crypto";
2+import { parseStrictPositiveInteger } from "../infra/parse-finite-number.js";
23import type { OAuthCredentials } from "../llm/oauth.js";
34import { normalizeOptionalString } from "../shared/string-coerce.js";
45@@ -81,9 +82,17 @@ export function parseOAuthCallbackInput(
8182return { code, state };
8283}
838484-function coerceExpiresAt(expiresInSeconds: number, now: number): number {
85-const value = now + Math.max(0, Math.floor(expiresInSeconds)) * 1000 - DEFAULT_EXPIRES_BUFFER_MS;
86-return Math.max(value, now + 30_000);
85+function resolveChutesExpiresAt(value: unknown, now: number): number | undefined {
86+const expiresInSeconds = parseStrictPositiveInteger(value);
87+if (expiresInSeconds === undefined) {
88+return undefined;
89+}
90+const lifetimeMs = expiresInSeconds * 1000;
91+const expiresAt = now + lifetimeMs - DEFAULT_EXPIRES_BUFFER_MS;
92+if (!Number.isSafeInteger(lifetimeMs) || !Number.isSafeInteger(expiresAt)) {
93+return undefined;
94+}
95+return Math.max(expiresAt, now + 30_000);
8796}
88978998async function fetchChutesUserInfo(params: {
@@ -144,21 +153,24 @@ export async function exchangeChutesCodeForTokens(params: {
144153145154const access = data.access_token?.trim();
146155const refresh = data.refresh_token?.trim();
147-const expiresIn = data.expires_in ?? 0;
156+const expires = resolveChutesExpiresAt(data.expires_in, now);
148157149158if (!access) {
150159throw new Error("Chutes token exchange returned no access_token");
151160}
152161if (!refresh) {
153162throw new Error("Chutes token exchange returned no refresh_token");
154163}
164+if (expires === undefined) {
165+throw new Error("Chutes token exchange returned invalid expires_in");
166+}
155167156168const info = await fetchChutesUserInfo({ accessToken: access, fetchFn });
157169158170return {
159171 access,
160172 refresh,
161-expires: coerceExpiresAt(expiresIn, now),
173+ expires,
162174email: info?.username,
163175accountId: info?.sub,
164176clientId: params.app.clientId,
@@ -210,18 +222,21 @@ export async function refreshChutesTokens(params: {
210222};
211223const access = data.access_token?.trim();
212224const newRefresh = data.refresh_token?.trim();
213-const expiresIn = data.expires_in ?? 0;
225+const expires = resolveChutesExpiresAt(data.expires_in, now);
214226215227if (!access) {
216228throw new Error("Chutes token refresh returned no access_token");
217229}
230+if (expires === undefined) {
231+throw new Error("Chutes token refresh returned invalid expires_in");
232+}
218233219234return {
220235 ...params.credential,
221236 access,
222237// RFC 6749 section 6: new refresh token is optional; if present, replace old.
223238refresh: newRefresh || refreshToken,
224-expires: coerceExpiresAt(expiresIn, now),
239+ expires,
225240 clientId,
226241} as unknown as ChutesStoredOAuth;
227242}
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。