openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
276 lines (275 loc) • 11.9 kB
JavaScript
import { T as positiveSecondsToSafeMilliseconds, k as resolveExpiresAtMsFromDurationSeconds } from "../../number-coercion-CLj0HTDM.js";
import { i as extractErrorCode, n as collectErrorGraphCandidates } from "../../error-coercion-D_-xJ90S.js";
import { c as shouldUseEnvHttpProxyForUrl } from "../../proxy-env-BepksPz2.js";
import { g as readResponseTextLimited } from "../../provider-http-errors-U-nhuk_f.js";
import { i as fetchWithSsrFGuard, s as withTrustedEnvProxyGuardedFetchMode } from "../../fetch-guard-BMdGQhbb.js";
import { r as resolveOpenAICodexAccessTokenExpiry } from "../../provider-openai-chatgpt-auth-BoLGEnud.js";
import "../../provider-auth-BeZ7NZUU.js";
import "../../error-runtime-Bz9Tw57Z.js";
import "../../fetch-runtime-Dww9PPyp.js";
import "../../number-runtime-Cy4drVnh.js";
import { t as classifyTransientNetworkErrorCode } from "../../retry-runtime-CdeiDBfj.js";
import "../../ssrf-runtime-Bum5C6NN.js";
import "../../provider-http-k9RMI7iG.js";
import { t as trimNonEmptyString } from "../../openai-chatgpt-shared-CMyrB84e.js";
//#region extensions/openai/openai-chatgpt-device-code.ts
const OPENAI_AUTH_BASE_URL = "https://auth.openai.com";
const OPENAI_CODEX_CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
const OPENAI_CODEX_DEVICE_CODE_TIMEOUT_MS = 9e5;
const OPENAI_CODEX_DEVICE_REQUEST_TIMEOUT_MS = 3e4;
const OPENAI_CODEX_DEVICE_CODE_DEFAULT_INTERVAL_MS = 5e3;
const OPENAI_CODEX_DEVICE_CODE_MIN_INTERVAL_MS = 1e3;
const OPENAI_CODEX_DEVICE_CALLBACK_URL = `${OPENAI_AUTH_BASE_URL}/deviceauth/callback`;
const OPENAI_CODEX_DEVICE_ERROR_BODY_LIMIT_BYTES = 8192;
const OPENAI_CODEX_DEVICE_JSON_BODY_LIMIT_BYTES = 262144;
function resolveOpenAICodexDeviceCodeHeaders(contentType) {
const version = process.env.OPENCLAW_VERSION?.trim();
return {
"Content-Type": contentType,
originator: "openclaw",
...version ? { version } : {},
"User-Agent": version ? `openclaw/${version}` : "openclaw"
};
}
function parseJsonObject(text) {
try {
const parsed = JSON.parse(text);
return parsed && typeof parsed === "object" ? parsed : null;
} catch {
return null;
}
}
function sanitizeDeviceCodeErrorText(value) {
const esc = String.fromCharCode(27);
const ansiCsiRegex = new RegExp(`${esc}\\[[\\u0020-\\u003f]*[\\u0040-\\u007e]`, "g");
const osc8Regex = new RegExp(`${esc}\\]8;;.*?${esc}\\\\|${esc}\\]8;;${esc}\\\\`, "g");
const controlCharsRegex = new RegExp(`[${String.fromCharCode(0)}-${String.fromCharCode(31)}${String.fromCharCode(127)}${String.fromCharCode(128)}-${String.fromCharCode(159)}]`, "g");
return value.replace(osc8Regex, "").replace(ansiCsiRegex, "").replace(controlCharsRegex, " ").replace(/\s+/g, " ").trim();
}
function resolveNextDeviceCodePollDelayMs(intervalMs, deadlineMs) {
const remainingMs = Math.max(0, deadlineMs - Date.now());
return Math.min(Math.max(intervalMs, OPENAI_CODEX_DEVICE_CODE_MIN_INTERVAL_MS), remainingMs);
}
function resolveDeviceCodePollRequestTimeoutMs(deadlineMs) {
return Math.min(OPENAI_CODEX_DEVICE_REQUEST_TIMEOUT_MS, Math.max(0, deadlineMs - Date.now()));
}
function isDeviceCodeOperationTimeoutError(error) {
return error instanceof Error && (error.name === "TimeoutError" || error.name === "AbortError");
}
function rethrowIfDeviceCodeCallerAborted(signal, error) {
if (signal?.aborted) throw signal.reason instanceof Error ? signal.reason : error;
}
function formatDeviceCodeError(params) {
const body = parseJsonObject(params.bodyText);
const error = trimNonEmptyString(body?.error);
const description = trimNonEmptyString(body?.error_description);
const safeError = error ? sanitizeDeviceCodeErrorText(error) : void 0;
const safeDescription = description ? sanitizeDeviceCodeErrorText(description) : void 0;
if (safeError && safeDescription) return `${params.prefix}: ${safeError} (${safeDescription})`;
if (safeError) return `${params.prefix}: ${safeError}`;
const bodyText = sanitizeDeviceCodeErrorText(params.bodyText);
return bodyText ? `${params.prefix}: HTTP ${params.status} ${bodyText}` : `${params.prefix}: HTTP ${params.status}`;
}
async function readOpenAICodexDeviceBody(response, timeoutMs) {
return await readResponseTextLimited(response, response.ok ? OPENAI_CODEX_DEVICE_JSON_BODY_LIMIT_BYTES : OPENAI_CODEX_DEVICE_ERROR_BODY_LIMIT_BYTES, { chunkTimeoutMs: timeoutMs });
}
async function runOpenAICodexDeviceRequest(params) {
const guardedOptions = {
url: params.url,
fetchImpl: params.fetchFn,
init: params.init,
timeoutMs: params.timeoutMs,
...params.signal ? { signal: params.signal } : {},
beforeRequest: params.assertCurrent,
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, params.timeoutMs)
};
} finally {
await release();
}
}
async function fetchOpenAICodexDeviceCode(params) {
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, signal, assertCurrent) {
signal?.throwIfAborted();
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 } : {},
assertCurrent
});
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.");
throw new Error(formatDeviceCodeError({
prefix: "OpenAI device code request failed",
status: result.status,
bodyText: result.bodyText
}));
}
const body = parseJsonObject(result.bodyText);
const deviceAuthId = trimNonEmptyString(body?.device_auth_id);
const userCode = trimNonEmptyString(body?.user_code) ?? trimNonEmptyString(body?.usercode);
if (!deviceAuthId || !userCode) throw new Error("OpenAI device code response was missing the device code or user code.");
return {
deviceAuthId,
userCode,
verificationUrl: `${OPENAI_AUTH_BASE_URL}/codex/device`,
intervalMs: positiveSecondsToSafeMilliseconds(body?.interval) ?? OPENAI_CODEX_DEVICE_CODE_DEFAULT_INTERVAL_MS
};
}
async function pollOpenAICodexDeviceCode(params) {
const deadline = Date.now() + OPENAI_CODEX_DEVICE_CODE_TIMEOUT_MS;
while (Date.now() < deadline) {
params.signal?.throwIfAborted();
const requestTimeoutMs = resolveDeviceCodePollRequestTimeoutMs(deadline);
if (requestTimeoutMs <= 0) break;
let result;
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 } : {},
assertCurrent: params.assertCurrent
});
} catch (error) {
rethrowIfDeviceCodeCallerAborted(params.signal, error);
if (isDeviceCodeOperationTimeoutError(error)) continue;
if (!collectErrorGraphCandidates(error, (candidate) => [candidate.cause]).some((candidate) => classifyTransientNetworkErrorCode(extractErrorCode(candidate)) !== void 0)) throw error;
await waitForDeviceCodePoll(resolveNextDeviceCodePollDelayMs(params.intervalMs, deadline), params.signal);
continue;
}
if (result.ok) {
const body = parseJsonObject(result.bodyText);
const authorizationCode = trimNonEmptyString(body?.authorization_code);
const codeVerifier = trimNonEmptyString(body?.code_verifier);
if (!authorizationCode || !codeVerifier) throw new Error("OpenAI device authorization response was missing the exchange code.");
return {
authorizationCode,
codeVerifier
};
}
if (result.status === 403 || result.status === 404) {
await waitForDeviceCodePoll(resolveNextDeviceCodePollDelayMs(params.intervalMs, deadline), params.signal);
continue;
}
throw new Error(formatDeviceCodeError({
prefix: "OpenAI device authorization failed",
status: result.status,
bodyText: result.bodyText
}));
}
throw new Error("OpenAI device authorization timed out after 15 minutes.");
}
async function exchangeOpenAICodexDeviceCode(params) {
params.signal?.throwIfAborted();
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 } : {},
assertCurrent: params.assertCurrent
});
if (!result.ok) throw new Error(formatDeviceCodeError({
prefix: "OpenAI device token exchange failed",
status: result.status,
bodyText: result.bodyText
}));
const body = parseJsonObject(result.bodyText);
const access = trimNonEmptyString(body?.access_token);
const refresh = trimNonEmptyString(body?.refresh_token);
if (!access || !refresh) throw new Error("OpenAI token exchange succeeded but did not return OAuth tokens.");
return {
access,
refresh,
expires: resolveExpiresAtMsFromDurationSeconds(body?.expires_in) ?? resolveOpenAICodexAccessTokenExpiry(access) ?? Date.now()
};
}
async function loginOpenAICodexDeviceCode(params) {
const fetchFn = params.fetchFn ?? fetch;
params.onProgress?.("Requesting device code…");
const deviceCode = await requestOpenAICodexDeviceCode(fetchFn, params.signal, params.assertCurrent);
await params.onVerification({
verificationUrl: deviceCode.verificationUrl,
userCode: deviceCode.userCode,
expiresInMs: OPENAI_CODEX_DEVICE_CODE_TIMEOUT_MS
});
params.onProgress?.("Waiting for device authorization…");
const authorization = await pollOpenAICodexDeviceCode({
fetchFn,
deviceAuthId: deviceCode.deviceAuthId,
userCode: deviceCode.userCode,
intervalMs: deviceCode.intervalMs,
...params.signal ? { signal: params.signal } : {},
assertCurrent: params.assertCurrent
});
params.onProgress?.("Exchanging device code…");
return await exchangeOpenAICodexDeviceCode({
fetchFn,
authorizationCode: authorization.authorizationCode,
codeVerifier: authorization.codeVerifier,
...params.signal ? { signal: params.signal } : {},
assertCurrent: params.assertCurrent
});
}
function waitForDeviceCodePoll(ms, signal) {
if (!signal) return new Promise((resolve) => {
setTimeout(resolve, ms);
});
signal.throwIfAborted();
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
signal.removeEventListener("abort", onAbort);
resolve();
}, ms);
const onAbort = () => {
clearTimeout(timer);
reject(signal.reason instanceof Error ? signal.reason : /* @__PURE__ */ new Error("Device login cancelled"));
};
signal.addEventListener("abort", onAbort, { once: true });
});
}
//#endregion
export { loginOpenAICodexDeviceCode };