mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-21 10:16:44 +00:00
fix(openai): prevent device-code login hangs on stalled requests (#109494)
* fix(openai): bound device code requests * fix(openai): preserve proxy routing for device code auth * test(openai): simplify device proxy assertions --------- Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
co-authored by
Peter Steinberger
parent
085c1d4602
commit
6bd15d00c4
@@ -1,5 +1,5 @@
|
||||
// Openai tests cover openai chatgpt device code plugin behavior.
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { resolveCodexAccessTokenExpiry } from "./openai-chatgpt-auth-identity.js";
|
||||
import { loginOpenAICodexDeviceCode } from "./openai-chatgpt-device-code.js";
|
||||
|
||||
@@ -48,7 +48,139 @@ function fetchCall(fetchMock: ReturnType<typeof vi.fn<typeof fetch>>, index: num
|
||||
return call;
|
||||
}
|
||||
|
||||
function waitForFetchAbort(init?: RequestInit): Promise<Response> {
|
||||
const signal = init?.signal;
|
||||
if (!signal) {
|
||||
return Promise.reject(new Error("expected fetch signal"));
|
||||
}
|
||||
return new Promise((_resolve, reject) => {
|
||||
const rejectWithReason = () =>
|
||||
reject(signal.reason instanceof Error ? signal.reason : new Error("request aborted"));
|
||||
if (signal.aborted) {
|
||||
rejectWithReason();
|
||||
return;
|
||||
}
|
||||
signal.addEventListener("abort", rejectWithReason, { once: true });
|
||||
});
|
||||
}
|
||||
|
||||
function createBodyThatStallsUntilAbort(init?: RequestInit): Response {
|
||||
const signal = init?.signal;
|
||||
if (!signal) {
|
||||
throw new Error("expected fetch signal");
|
||||
}
|
||||
return new Response(
|
||||
new ReadableStream<Uint8Array>({
|
||||
start(controller) {
|
||||
const fail = () => controller.error(signal.reason);
|
||||
if (signal.aborted) {
|
||||
fail();
|
||||
return;
|
||||
}
|
||||
signal.addEventListener("abort", fail, { once: true });
|
||||
},
|
||||
}),
|
||||
{ status: 200, headers: { "Content-Type": "application/json" } },
|
||||
);
|
||||
}
|
||||
|
||||
describe("loginOpenAICodexDeviceCode", () => {
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
vi.useRealTimers();
|
||||
vi.unstubAllEnvs();
|
||||
});
|
||||
|
||||
it("times out while waiting for device-code response headers", async () => {
|
||||
vi.useFakeTimers();
|
||||
const fetchMock = vi.fn<typeof fetch>((_input, init) => waitForFetchAbort(init));
|
||||
|
||||
const login = loginOpenAICodexDeviceCode({
|
||||
fetchFn: fetchMock,
|
||||
onVerification: async () => {},
|
||||
});
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledOnce();
|
||||
expect(fetchCall(fetchMock, 0)[1]?.signal).toBeInstanceOf(AbortSignal);
|
||||
const rejected = expect(login).rejects.toThrow(
|
||||
"OpenAI device code user code request timed out after 30000ms",
|
||||
);
|
||||
await vi.advanceTimersByTimeAsync(30_000);
|
||||
await rejected;
|
||||
});
|
||||
|
||||
it("keeps the request timeout active while reading the response body", async () => {
|
||||
vi.useFakeTimers();
|
||||
const fetchMock = vi.fn<typeof fetch>(async (_input, init) =>
|
||||
createBodyThatStallsUntilAbort(init),
|
||||
);
|
||||
|
||||
const login = loginOpenAICodexDeviceCode({
|
||||
fetchFn: fetchMock,
|
||||
onVerification: async () => {},
|
||||
});
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledOnce();
|
||||
const rejected = expect(login).rejects.toThrow(
|
||||
"OpenAI device code user code request timed out after 30000ms",
|
||||
);
|
||||
await vi.advanceTimersByTimeAsync(30_000);
|
||||
await rejected;
|
||||
});
|
||||
|
||||
it("still honors caller cancellation during an active device-code request", async () => {
|
||||
const callerController = new AbortController();
|
||||
const fetchMock = vi.fn<typeof fetch>((_input, init) => waitForFetchAbort(init));
|
||||
|
||||
const login = loginOpenAICodexDeviceCode({
|
||||
fetchFn: fetchMock,
|
||||
onVerification: async () => {},
|
||||
signal: callerController.signal,
|
||||
});
|
||||
await vi.waitFor(() => expect(fetchMock).toHaveBeenCalledOnce());
|
||||
|
||||
callerController.abort(new Error("cancelled by caller"));
|
||||
await expect(login).rejects.toThrow("cancelled by caller");
|
||||
});
|
||||
|
||||
it("routes device-code auth through a configured HTTPS proxy", async () => {
|
||||
vi.stubEnv("https_proxy", "http://127.0.0.1:7897");
|
||||
vi.stubEnv("no_proxy", "");
|
||||
const fetchMock = vi.fn<typeof fetch>().mockResolvedValue(new Response(null, { status: 503 }));
|
||||
|
||||
await expect(
|
||||
loginOpenAICodexDeviceCode({
|
||||
fetchFn: fetchMock,
|
||||
onVerification: async () => {},
|
||||
}),
|
||||
).rejects.toThrow("OpenAI device code request failed: HTTP 503");
|
||||
|
||||
const requestInit = fetchCall(fetchMock, 0)[1] as
|
||||
| (RequestInit & { dispatcher?: { constructor?: { name?: string } } })
|
||||
| undefined;
|
||||
expect(requestInit?.dispatcher?.constructor?.name).toContain("EnvHttpProxyAgent");
|
||||
});
|
||||
|
||||
it("keeps strict guarded fetch when NO_PROXY bypasses OpenAI auth", async () => {
|
||||
vi.stubEnv("https_proxy", "http://127.0.0.1:7897");
|
||||
vi.stubEnv("no_proxy", "auth.openai.com");
|
||||
const fetchMock = vi.fn<typeof fetch>().mockResolvedValue(new Response(null, { status: 503 }));
|
||||
|
||||
await expect(
|
||||
loginOpenAICodexDeviceCode({
|
||||
fetchFn: fetchMock,
|
||||
onVerification: async () => {},
|
||||
}),
|
||||
).rejects.toThrow("OpenAI device code request failed: HTTP 503");
|
||||
|
||||
const requestInit = fetchCall(fetchMock, 0)[1] as
|
||||
| (RequestInit & { dispatcher?: unknown })
|
||||
| undefined;
|
||||
expect(requestInit?.dispatcher).toBeUndefined();
|
||||
});
|
||||
|
||||
it("requests a device code, polls for authorization, and exchanges OAuth tokens", async () => {
|
||||
vi.useFakeTimers();
|
||||
vi.stubEnv("OPENCLAW_VERSION", "2026.3.22");
|
||||
@@ -108,6 +240,7 @@ describe("loginOpenAICodexDeviceCode", () => {
|
||||
const userCodeRequest = fetchCall(fetchMock, 0);
|
||||
expect(userCodeRequest[0]).toBe("https://auth.openai.com/api/accounts/deviceauth/usercode");
|
||||
expect(userCodeRequest[1]?.method).toBe("POST");
|
||||
expect(userCodeRequest[1]?.signal).toBeInstanceOf(AbortSignal);
|
||||
expect(userCodeRequest[1]?.headers).toEqual({
|
||||
"Content-Type": "application/json",
|
||||
originator: "openclaw",
|
||||
@@ -118,6 +251,7 @@ describe("loginOpenAICodexDeviceCode", () => {
|
||||
const deviceTokenRequest = fetchCall(fetchMock, 1);
|
||||
expect(deviceTokenRequest[0]).toBe("https://auth.openai.com/api/accounts/deviceauth/token");
|
||||
expect(deviceTokenRequest[1]?.method).toBe("POST");
|
||||
expect(deviceTokenRequest[1]?.signal).toBeInstanceOf(AbortSignal);
|
||||
expect(deviceTokenRequest[1]?.headers).toEqual({
|
||||
"Content-Type": "application/json",
|
||||
originator: "openclaw",
|
||||
@@ -128,6 +262,7 @@ describe("loginOpenAICodexDeviceCode", () => {
|
||||
const oauthTokenRequest = fetchCall(fetchMock, 3);
|
||||
expect(oauthTokenRequest[0]).toBe("https://auth.openai.com/oauth/token");
|
||||
expect(oauthTokenRequest[1]?.method).toBe("POST");
|
||||
expect(oauthTokenRequest[1]?.signal).toBeInstanceOf(AbortSignal);
|
||||
expect(oauthTokenRequest[1]?.headers).toEqual({
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
originator: "openclaw",
|
||||
@@ -153,6 +288,55 @@ describe("loginOpenAICodexDeviceCode", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("retries a timed-out authorization poll within the overall device deadline", async () => {
|
||||
vi.useFakeTimers();
|
||||
const accessToken = createJwt({
|
||||
exp: Math.floor(Date.now() / 1000) + 600,
|
||||
});
|
||||
let pollAttempts = 0;
|
||||
const fetchMock = vi.fn<typeof fetch>(async (input, init) => {
|
||||
const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
|
||||
if (url.endsWith("/api/accounts/deviceauth/usercode")) {
|
||||
return createJsonResponse({
|
||||
device_auth_id: "device-auth-123",
|
||||
user_code: "CODE-12345",
|
||||
interval: "0",
|
||||
});
|
||||
}
|
||||
if (url.endsWith("/api/accounts/deviceauth/token")) {
|
||||
pollAttempts += 1;
|
||||
if (pollAttempts === 1) {
|
||||
return await waitForFetchAbort(init);
|
||||
}
|
||||
return createJsonResponse({
|
||||
authorization_code: "authorization-code-123",
|
||||
code_verifier: "code-verifier-123",
|
||||
});
|
||||
}
|
||||
if (url.endsWith("/oauth/token")) {
|
||||
return createJsonResponse({
|
||||
access_token: accessToken,
|
||||
refresh_token: "refresh-token-123",
|
||||
expires_in: 600,
|
||||
});
|
||||
}
|
||||
throw new Error(`unexpected OpenAI device-code URL: ${url}`);
|
||||
});
|
||||
|
||||
const login = loginOpenAICodexDeviceCode({
|
||||
fetchFn: fetchMock,
|
||||
onVerification: async () => {},
|
||||
});
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
expect(pollAttempts).toBe(1);
|
||||
|
||||
const resolved = expect(login).resolves.toMatchObject({ refresh: "refresh-token-123" });
|
||||
await vi.advanceTimersByTimeAsync(30_000);
|
||||
await resolved;
|
||||
expect(pollAttempts).toBe(2);
|
||||
expect(fetchMock).toHaveBeenCalledTimes(4);
|
||||
});
|
||||
|
||||
it("aborts device-code polling without another request", async () => {
|
||||
vi.useFakeTimers();
|
||||
const controller = new AbortController();
|
||||
|
||||
@@ -1,15 +1,21 @@
|
||||
// Openai plugin module implements openai chatgpt device code behavior.
|
||||
import {
|
||||
shouldUseEnvHttpProxyForUrl,
|
||||
withTrustedEnvProxyGuardedFetchMode,
|
||||
} from "openclaw/plugin-sdk/fetch-runtime";
|
||||
import {
|
||||
positiveSecondsToSafeMilliseconds,
|
||||
resolveExpiresAtMsFromDurationSeconds,
|
||||
} from "openclaw/plugin-sdk/number-runtime";
|
||||
import { readResponseTextLimited } from "openclaw/plugin-sdk/provider-http";
|
||||
import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/ssrf-runtime";
|
||||
import { resolveCodexAccessTokenExpiry } from "./openai-chatgpt-auth-identity.js";
|
||||
import { trimNonEmptyString } from "./openai-chatgpt-shared.js";
|
||||
|
||||
const OPENAI_AUTH_BASE_URL = "https://auth.openai.com";
|
||||
const OPENAI_CODEX_CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
|
||||
const OPENAI_CODEX_DEVICE_CODE_TIMEOUT_MS = 15 * 60_000;
|
||||
const OPENAI_CODEX_DEVICE_REQUEST_TIMEOUT_MS = 30_000;
|
||||
const OPENAI_CODEX_DEVICE_CODE_DEFAULT_INTERVAL_MS = 5_000;
|
||||
const OPENAI_CODEX_DEVICE_CODE_MIN_INTERVAL_MS = 1_000;
|
||||
const OPENAI_CODEX_DEVICE_CALLBACK_URL = `${OPENAI_AUTH_BASE_URL}/deviceauth/callback`;
|
||||
@@ -69,6 +75,12 @@ type DeviceCodeAuthorizationCode = {
|
||||
codeVerifier: string;
|
||||
};
|
||||
|
||||
type DeviceCodeHttpResult = {
|
||||
ok: boolean;
|
||||
status: number;
|
||||
bodyText: string;
|
||||
};
|
||||
|
||||
function parseJsonObject(text: string): Record<string, unknown> | null {
|
||||
try {
|
||||
const parsed = JSON.parse(text);
|
||||
@@ -101,6 +113,20 @@ function resolveNextDeviceCodePollDelayMs(intervalMs: number, deadlineMs: number
|
||||
return Math.min(Math.max(intervalMs, OPENAI_CODEX_DEVICE_CODE_MIN_INTERVAL_MS), remainingMs);
|
||||
}
|
||||
|
||||
function resolveDeviceCodePollRequestTimeoutMs(deadlineMs: number): number {
|
||||
return Math.min(OPENAI_CODEX_DEVICE_REQUEST_TIMEOUT_MS, Math.max(0, deadlineMs - Date.now()));
|
||||
}
|
||||
|
||||
function isDeviceCodeOperationTimeoutError(error: unknown): boolean {
|
||||
return error instanceof Error && (error.name === "TimeoutError" || error.name === "AbortError");
|
||||
}
|
||||
|
||||
function rethrowIfDeviceCodeCallerAborted(signal: AbortSignal | undefined, error: unknown): void {
|
||||
if (signal?.aborted) {
|
||||
throw signal.reason instanceof Error ? signal.reason : error;
|
||||
}
|
||||
}
|
||||
|
||||
function formatDeviceCodeError(params: {
|
||||
prefix: string;
|
||||
status: number;
|
||||
@@ -132,23 +158,83 @@ async function readOpenAICodexDeviceBody(response: Response): Promise<string> {
|
||||
);
|
||||
}
|
||||
|
||||
async function runOpenAICodexDeviceRequest(params: {
|
||||
fetchFn: typeof fetch;
|
||||
url: string;
|
||||
init: Omit<RequestInit, "signal">;
|
||||
timeoutMs: number;
|
||||
signal?: AbortSignal;
|
||||
}): Promise<DeviceCodeHttpResult> {
|
||||
const guardedOptions = {
|
||||
url: params.url,
|
||||
fetchImpl: params.fetchFn,
|
||||
init: params.init,
|
||||
timeoutMs: params.timeoutMs,
|
||||
...(params.signal ? { signal: params.signal } : {}),
|
||||
requireHttps: true,
|
||||
auditContext: "openai-chatgpt-device-code",
|
||||
};
|
||||
const { response, release } = await fetchWithSsrFGuard(
|
||||
shouldUseEnvHttpProxyForUrl(params.url)
|
||||
? withTrustedEnvProxyGuardedFetchMode(guardedOptions)
|
||||
: guardedOptions,
|
||||
);
|
||||
try {
|
||||
return {
|
||||
ok: response.ok,
|
||||
status: response.status,
|
||||
bodyText: await readOpenAICodexDeviceBody(response),
|
||||
};
|
||||
} finally {
|
||||
await release();
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchOpenAICodexDeviceCode(params: {
|
||||
fetchFn: typeof fetch;
|
||||
url: string;
|
||||
init: Omit<RequestInit, "signal">;
|
||||
timeoutOperation: string;
|
||||
signal?: AbortSignal;
|
||||
}): Promise<DeviceCodeHttpResult> {
|
||||
try {
|
||||
return await runOpenAICodexDeviceRequest({
|
||||
...params,
|
||||
timeoutMs: OPENAI_CODEX_DEVICE_REQUEST_TIMEOUT_MS,
|
||||
});
|
||||
} catch (error) {
|
||||
rethrowIfDeviceCodeCallerAborted(params.signal, error);
|
||||
if (isDeviceCodeOperationTimeoutError(error)) {
|
||||
throw new Error(
|
||||
`OpenAI device code ${params.timeoutOperation} timed out after ${OPENAI_CODEX_DEVICE_REQUEST_TIMEOUT_MS}ms`,
|
||||
{ cause: error },
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function requestOpenAICodexDeviceCode(
|
||||
fetchFn: typeof fetch,
|
||||
signal?: AbortSignal,
|
||||
): Promise<RequestedDeviceCode> {
|
||||
signal?.throwIfAborted();
|
||||
const response = await fetchFn(`${OPENAI_AUTH_BASE_URL}/api/accounts/deviceauth/usercode`, {
|
||||
method: "POST",
|
||||
headers: resolveOpenAICodexDeviceCodeHeaders("application/json"),
|
||||
body: JSON.stringify({
|
||||
client_id: OPENAI_CODEX_CLIENT_ID,
|
||||
}),
|
||||
const result = await fetchOpenAICodexDeviceCode({
|
||||
fetchFn,
|
||||
url: `${OPENAI_AUTH_BASE_URL}/api/accounts/deviceauth/usercode`,
|
||||
init: {
|
||||
method: "POST",
|
||||
headers: resolveOpenAICodexDeviceCodeHeaders("application/json"),
|
||||
body: JSON.stringify({
|
||||
client_id: OPENAI_CODEX_CLIENT_ID,
|
||||
}),
|
||||
},
|
||||
timeoutOperation: "user code request",
|
||||
...(signal ? { signal } : {}),
|
||||
});
|
||||
|
||||
const bodyText = await readOpenAICodexDeviceBody(response);
|
||||
if (!response.ok) {
|
||||
if (response.status === 404) {
|
||||
if (!result.ok) {
|
||||
if (result.status === 404) {
|
||||
throw new Error(
|
||||
"OpenAI Codex device code login is not enabled for this server. Use ChatGPT OAuth instead.",
|
||||
);
|
||||
@@ -156,13 +242,13 @@ async function requestOpenAICodexDeviceCode(
|
||||
throw new Error(
|
||||
formatDeviceCodeError({
|
||||
prefix: "OpenAI device code request failed",
|
||||
status: response.status,
|
||||
bodyText,
|
||||
status: result.status,
|
||||
bodyText: result.bodyText,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
const body = parseJsonObject(bodyText) as DeviceCodeUserCodePayload | null;
|
||||
const body = parseJsonObject(result.bodyText) as DeviceCodeUserCodePayload | null;
|
||||
const deviceAuthId = trimNonEmptyString(body?.device_auth_id);
|
||||
const userCode = trimNonEmptyString(body?.user_code) ?? trimNonEmptyString(body?.usercode);
|
||||
if (!deviceAuthId || !userCode) {
|
||||
@@ -190,19 +276,38 @@ async function pollOpenAICodexDeviceCode(params: {
|
||||
|
||||
while (Date.now() < deadline) {
|
||||
params.signal?.throwIfAborted();
|
||||
const response = await params.fetchFn(`${OPENAI_AUTH_BASE_URL}/api/accounts/deviceauth/token`, {
|
||||
method: "POST",
|
||||
headers: resolveOpenAICodexDeviceCodeHeaders("application/json"),
|
||||
body: JSON.stringify({
|
||||
device_auth_id: params.deviceAuthId,
|
||||
user_code: params.userCode,
|
||||
}),
|
||||
...(params.signal ? { signal: params.signal } : {}),
|
||||
});
|
||||
const requestTimeoutMs = resolveDeviceCodePollRequestTimeoutMs(deadline);
|
||||
if (requestTimeoutMs <= 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
const bodyText = await readOpenAICodexDeviceBody(response);
|
||||
if (response.ok) {
|
||||
const body = parseJsonObject(bodyText) as DeviceCodeTokenPayload | null;
|
||||
let result: DeviceCodeHttpResult;
|
||||
try {
|
||||
result = await runOpenAICodexDeviceRequest({
|
||||
fetchFn: params.fetchFn,
|
||||
url: `${OPENAI_AUTH_BASE_URL}/api/accounts/deviceauth/token`,
|
||||
init: {
|
||||
method: "POST",
|
||||
headers: resolveOpenAICodexDeviceCodeHeaders("application/json"),
|
||||
body: JSON.stringify({
|
||||
device_auth_id: params.deviceAuthId,
|
||||
user_code: params.userCode,
|
||||
}),
|
||||
},
|
||||
timeoutMs: requestTimeoutMs,
|
||||
...(params.signal ? { signal: params.signal } : {}),
|
||||
});
|
||||
} catch (error) {
|
||||
rethrowIfDeviceCodeCallerAborted(params.signal, error);
|
||||
// A stalled poll is transient; keep the overall 15-minute authorization deadline.
|
||||
if (isDeviceCodeOperationTimeoutError(error)) {
|
||||
continue;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (result.ok) {
|
||||
const body = parseJsonObject(result.bodyText) as DeviceCodeTokenPayload | null;
|
||||
const authorizationCode = trimNonEmptyString(body?.authorization_code);
|
||||
const codeVerifier = trimNonEmptyString(body?.code_verifier);
|
||||
if (!authorizationCode || !codeVerifier) {
|
||||
@@ -214,7 +319,7 @@ async function pollOpenAICodexDeviceCode(params: {
|
||||
};
|
||||
}
|
||||
|
||||
if (response.status === 403 || response.status === 404) {
|
||||
if (result.status === 403 || result.status === 404) {
|
||||
await waitForDeviceCodePoll(
|
||||
resolveNextDeviceCodePollDelayMs(params.intervalMs, deadline),
|
||||
params.signal,
|
||||
@@ -225,8 +330,8 @@ async function pollOpenAICodexDeviceCode(params: {
|
||||
throw new Error(
|
||||
formatDeviceCodeError({
|
||||
prefix: "OpenAI device authorization failed",
|
||||
status: response.status,
|
||||
bodyText,
|
||||
status: result.status,
|
||||
bodyText: result.bodyText,
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -241,31 +346,35 @@ async function exchangeOpenAICodexDeviceCode(params: {
|
||||
signal?: AbortSignal;
|
||||
}): Promise<OpenAICodexDeviceCodeCredentials> {
|
||||
params.signal?.throwIfAborted();
|
||||
const response = await params.fetchFn(`${OPENAI_AUTH_BASE_URL}/oauth/token`, {
|
||||
method: "POST",
|
||||
headers: resolveOpenAICodexDeviceCodeHeaders("application/x-www-form-urlencoded"),
|
||||
body: new URLSearchParams({
|
||||
grant_type: "authorization_code",
|
||||
code: params.authorizationCode,
|
||||
redirect_uri: OPENAI_CODEX_DEVICE_CALLBACK_URL,
|
||||
client_id: OPENAI_CODEX_CLIENT_ID,
|
||||
code_verifier: params.codeVerifier,
|
||||
}),
|
||||
const result = await fetchOpenAICodexDeviceCode({
|
||||
fetchFn: params.fetchFn,
|
||||
url: `${OPENAI_AUTH_BASE_URL}/oauth/token`,
|
||||
init: {
|
||||
method: "POST",
|
||||
headers: resolveOpenAICodexDeviceCodeHeaders("application/x-www-form-urlencoded"),
|
||||
body: new URLSearchParams({
|
||||
grant_type: "authorization_code",
|
||||
code: params.authorizationCode,
|
||||
redirect_uri: OPENAI_CODEX_DEVICE_CALLBACK_URL,
|
||||
client_id: OPENAI_CODEX_CLIENT_ID,
|
||||
code_verifier: params.codeVerifier,
|
||||
}),
|
||||
},
|
||||
timeoutOperation: "token exchange",
|
||||
...(params.signal ? { signal: params.signal } : {}),
|
||||
});
|
||||
|
||||
const bodyText = await readOpenAICodexDeviceBody(response);
|
||||
if (!response.ok) {
|
||||
if (!result.ok) {
|
||||
throw new Error(
|
||||
formatDeviceCodeError({
|
||||
prefix: "OpenAI device token exchange failed",
|
||||
status: response.status,
|
||||
bodyText,
|
||||
status: result.status,
|
||||
bodyText: result.bodyText,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
const body = parseJsonObject(bodyText) as OAuthTokenPayload | null;
|
||||
const body = parseJsonObject(result.bodyText) as OAuthTokenPayload | null;
|
||||
const access = trimNonEmptyString(body?.access_token);
|
||||
const refresh = trimNonEmptyString(body?.refresh_token);
|
||||
if (!access || !refresh) {
|
||||
|
||||
Reference in New Issue
Block a user