@@ -5,6 +5,7 @@
|
5 | 5 | * It is only intended for CLI use, not browser environments. |
6 | 6 | */ |
7 | 7 | |
| 8 | +import { parseStrictPositiveInteger } from "openclaw/plugin-sdk/number-runtime"; |
8 | 9 | import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/ssrf-runtime"; |
9 | 10 | import { resolveCodexAuthIdentity } from "./openai-codex-auth-identity.js"; |
10 | 11 | import { |
@@ -166,12 +167,17 @@ function formatMissingTokenResponseFields(json: TokenResponseJson): string {
|
166 | 167 | if (!json.refresh_token) { |
167 | 168 | missing.push("refresh_token"); |
168 | 169 | } |
169 | | -if (typeof json.expires_in !== "number") { |
| 170 | +if (parseStrictPositiveInteger(json.expires_in) === undefined) { |
170 | 171 | missing.push("expires_in"); |
171 | 172 | } |
172 | 173 | return missing.join(", "); |
173 | 174 | } |
174 | 175 | |
| 176 | +function resolveTokenExpiresAt(expiresIn: unknown, nowMs = Date.now()): number | undefined { |
| 177 | +const seconds = parseStrictPositiveInteger(expiresIn); |
| 178 | +return seconds === undefined ? undefined : nowMs + seconds * 1000; |
| 179 | +} |
| 180 | + |
175 | 181 | function formatTokenRequestError( |
176 | 182 | operation: "exchange" | "refresh", |
177 | 183 | error: unknown, |
@@ -253,7 +259,8 @@ async function exchangeAuthorizationCode(
|
253 | 259 | |
254 | 260 | const json = (await response.json()) as TokenResponseJson; |
255 | 261 | |
256 | | -if (!json.access_token || !json.refresh_token || typeof json.expires_in !== "number") { |
| 262 | +const expires = resolveTokenExpiresAt(json.expires_in); |
| 263 | +if (!json.access_token || !json.refresh_token || expires === undefined) { |
257 | 264 | return { |
258 | 265 | type: "failed", |
259 | 266 | message: `OpenAI Codex token exchange response missing fields: ${formatMissingTokenResponseFields(json)}`, |
@@ -264,7 +271,7 @@ async function exchangeAuthorizationCode(
|
264 | 271 | type: "success", |
265 | 272 | access: json.access_token, |
266 | 273 | refresh: json.refresh_token, |
267 | | -expires: Date.now() + json.expires_in * 1000, |
| 274 | + expires, |
268 | 275 | }; |
269 | 276 | } |
270 | 277 | |
@@ -294,7 +301,8 @@ async function refreshAccessToken(
|
294 | 301 | |
295 | 302 | const json = (await response.json()) as TokenResponseJson; |
296 | 303 | |
297 | | -if (!json.access_token || !json.refresh_token || typeof json.expires_in !== "number") { |
| 304 | +const expires = resolveTokenExpiresAt(json.expires_in); |
| 305 | +if (!json.access_token || !json.refresh_token || expires === undefined) { |
298 | 306 | return { |
299 | 307 | type: "failed", |
300 | 308 | message: `OpenAI Codex token refresh response missing fields: ${formatMissingTokenResponseFields(json)}`, |
@@ -305,7 +313,7 @@ async function refreshAccessToken(
|
305 | 313 | type: "success", |
306 | 314 | access: json.access_token, |
307 | 315 | refresh: json.refresh_token, |
308 | | -expires: Date.now() + json.expires_in * 1000, |
| 316 | + expires, |
309 | 317 | }; |
310 | 318 | } catch (error) { |
311 | 319 | return { |
|