mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-23 16:01:17 +00:00
165 lines
4.9 KiB
TypeScript
165 lines
4.9 KiB
TypeScript
import path from "node:path";
|
|
import { formatCliCommand } from "../cli/command-format.js";
|
|
import type { OpenClawConfig } from "../config/config.js";
|
|
import { note } from "../terminal/note.js";
|
|
|
|
const TLS_CERT_ERROR_CODES = new Set([
|
|
"UNABLE_TO_GET_ISSUER_CERT_LOCALLY",
|
|
"UNABLE_TO_VERIFY_LEAF_SIGNATURE",
|
|
"CERT_HAS_EXPIRED",
|
|
"DEPTH_ZERO_SELF_SIGNED_CERT",
|
|
"SELF_SIGNED_CERT_IN_CHAIN",
|
|
"ERR_TLS_CERT_ALTNAME_INVALID",
|
|
]);
|
|
|
|
const TLS_CERT_ERROR_PATTERNS = [
|
|
/unable to get local issuer certificate/i,
|
|
/unable to verify the first certificate/i,
|
|
/self[- ]signed certificate/i,
|
|
/certificate has expired/i,
|
|
];
|
|
|
|
const OPENAI_AUTH_PROBE_URL =
|
|
"https://auth.openai.com/oauth/authorize?response_type=code&client_id=openclaw-preflight&redirect_uri=http%3A%2F%2Flocalhost%3A1455%2Fauth%2Fcallback&scope=openid+profile+email";
|
|
|
|
type PreflightFailureKind = "tls-cert" | "network";
|
|
|
|
export type OpenAIOAuthTlsPreflightResult =
|
|
| { ok: true }
|
|
| {
|
|
ok: false;
|
|
kind: PreflightFailureKind;
|
|
code?: string;
|
|
message: string;
|
|
};
|
|
|
|
function asRecord(value: unknown): Record<string, unknown> | null {
|
|
return value && typeof value === "object" ? (value as Record<string, unknown>) : null;
|
|
}
|
|
|
|
function extractFailure(error: unknown): {
|
|
code?: string;
|
|
message: string;
|
|
kind: PreflightFailureKind;
|
|
} {
|
|
const root = asRecord(error);
|
|
const rootCause = asRecord(root?.cause);
|
|
const code = typeof rootCause?.code === "string" ? rootCause.code : undefined;
|
|
const message =
|
|
typeof rootCause?.message === "string"
|
|
? rootCause.message
|
|
: typeof root?.message === "string"
|
|
? root.message
|
|
: String(error);
|
|
const isTlsCertError =
|
|
(code ? TLS_CERT_ERROR_CODES.has(code) : false) ||
|
|
TLS_CERT_ERROR_PATTERNS.some((pattern) => pattern.test(message));
|
|
return {
|
|
code,
|
|
message,
|
|
kind: isTlsCertError ? "tls-cert" : "network",
|
|
};
|
|
}
|
|
|
|
function resolveHomebrewPrefixFromExecPath(execPath: string): string | null {
|
|
const marker = `${path.sep}Cellar${path.sep}`;
|
|
const idx = execPath.indexOf(marker);
|
|
if (idx > 0) {
|
|
return execPath.slice(0, idx);
|
|
}
|
|
const envPrefix = process.env.HOMEBREW_PREFIX?.trim();
|
|
return envPrefix ? envPrefix : null;
|
|
}
|
|
|
|
function resolveCertBundlePath(): string | null {
|
|
const prefix = resolveHomebrewPrefixFromExecPath(process.execPath);
|
|
if (!prefix) {
|
|
return null;
|
|
}
|
|
return path.join(prefix, "etc", "openssl@3", "cert.pem");
|
|
}
|
|
|
|
function hasOpenAICodexOAuthProfile(cfg: OpenClawConfig): boolean {
|
|
const profiles = cfg.auth?.profiles;
|
|
if (!profiles) {
|
|
return false;
|
|
}
|
|
return Object.values(profiles).some(
|
|
(profile) => profile.provider === "openai-codex" && profile.mode === "oauth",
|
|
);
|
|
}
|
|
|
|
function shouldRunOpenAIOAuthTlsPrerequisites(params: {
|
|
cfg: OpenClawConfig;
|
|
deep?: boolean;
|
|
}): boolean {
|
|
if (params.deep === true) {
|
|
return true;
|
|
}
|
|
return hasOpenAICodexOAuthProfile(params.cfg);
|
|
}
|
|
|
|
export async function runOpenAIOAuthTlsPreflight(options?: {
|
|
timeoutMs?: number;
|
|
fetchImpl?: typeof fetch;
|
|
}): Promise<OpenAIOAuthTlsPreflightResult> {
|
|
const timeoutMs = options?.timeoutMs ?? 5000;
|
|
const fetchImpl = options?.fetchImpl ?? fetch;
|
|
try {
|
|
await fetchImpl(OPENAI_AUTH_PROBE_URL, {
|
|
method: "GET",
|
|
redirect: "manual",
|
|
signal: AbortSignal.timeout(timeoutMs),
|
|
});
|
|
return { ok: true };
|
|
} catch (error) {
|
|
const failure = extractFailure(error);
|
|
return {
|
|
ok: false,
|
|
kind: failure.kind,
|
|
code: failure.code,
|
|
message: failure.message,
|
|
};
|
|
}
|
|
}
|
|
|
|
export function formatOpenAIOAuthTlsPreflightFix(
|
|
result: Exclude<OpenAIOAuthTlsPreflightResult, { ok: true }>,
|
|
): string {
|
|
if (result.kind !== "tls-cert") {
|
|
return [
|
|
"OpenAI OAuth prerequisites check failed due to a network error before the browser flow.",
|
|
`Cause: ${result.message}`,
|
|
"Verify DNS/firewall/proxy access to auth.openai.com and retry.",
|
|
].join("\n");
|
|
}
|
|
const certBundlePath = resolveCertBundlePath();
|
|
const lines = [
|
|
"OpenAI OAuth prerequisites check failed: Node/OpenSSL cannot validate TLS certificates.",
|
|
`Cause: ${result.code ? `${result.code} (${result.message})` : result.message}`,
|
|
"",
|
|
"Fix (Homebrew Node/OpenSSL):",
|
|
`- ${formatCliCommand("brew postinstall ca-certificates")}`,
|
|
`- ${formatCliCommand("brew postinstall openssl@3")}`,
|
|
];
|
|
if (certBundlePath) {
|
|
lines.push(`- Verify cert bundle exists: ${certBundlePath}`);
|
|
}
|
|
lines.push("- Retry the OAuth login flow.");
|
|
return lines.join("\n");
|
|
}
|
|
|
|
export async function noteOpenAIOAuthTlsPrerequisites(params: {
|
|
cfg: OpenClawConfig;
|
|
deep?: boolean;
|
|
}): Promise<void> {
|
|
if (!shouldRunOpenAIOAuthTlsPrerequisites(params)) {
|
|
return;
|
|
}
|
|
const result = await runOpenAIOAuthTlsPreflight({ timeoutMs: 4000 });
|
|
if (result.ok || result.kind !== "tls-cert") {
|
|
return;
|
|
}
|
|
note(formatOpenAIOAuthTlsPreflightFix(result), "OAuth TLS prerequisites");
|
|
}
|