UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

373 lines (372 loc) 17.6 kB
import { o as isRecord } from "./record-coerce-DHZ4bFlT.js"; import { _ as resolveOAuthDir, y as resolveStateDir } from "./paths-mvMm5bYV.js"; import { t as formatCliCommand } from "./command-format-CKGmlpAQ.js"; import { _ as uniqueStrings } from "./string-normalization-WNUDCpXX.js"; import { g as shortenHomePath } from "./utils-CCC-BEJH.js"; import "./agent-scope-MrLta7Pq.js"; import { a as resolveAgentDir, n as listAgentIds, s as resolveDefaultAgentDir } from "./agent-scope-config-CgCYpZfK.js"; import { l as resolveAuthStorePath } from "./runtime-snapshots-CGKcj2Tz.js"; import { H as log, t as clearRuntimeAuthProfileStoreSnapshots, x as isLegacyOAuthRef } from "./store-C8spD0DG.js"; import { n as saveJsonFile, t as loadJsonFile } from "./json-file-CVAOif1i.js"; import { t as note } from "./note-BRSJp0UF.js"; import fs from "node:fs"; import path from "node:path"; import os from "node:os"; import * as childProcess from "node:child_process"; import { createCipheriv, createDecipheriv, hash } from "node:crypto"; //#region src/commands/doctor/shared/legacy-oauth-sidecar.ts const LEGACY_OAUTH_SECRET_DIRNAME$1 = "auth-profiles"; const LEGACY_OAUTH_SECRET_VERSION = 1; const LEGACY_OAUTH_SECRET_ALGORITHM = "aes-256-gcm"; const LEGACY_OAUTH_SECRET_KEY_ENV = "OPENCLAW_AUTH_PROFILE_SECRET_KEY"; const LEGACY_OAUTH_SECRET_KEYCHAIN_SERVICE = "OpenClaw Auth Profile Secrets"; const LEGACY_OAUTH_SECRET_KEYCHAIN_ACCOUNT = "oauth-profile-master-key"; const LEGACY_OAUTH_SECRET_KEY_FILE_NAME = "auth-profile-secret-key"; function readNonEmptyString$1(value) { return typeof value === "string" && value.trim() ? value : void 0; } /** Resolve the legacy OAuth sidecar JSON path for an auth profile ref. */ function resolveLegacyOAuthSidecarPath(ref, env = process.env) { return path.join(resolveOAuthDir(env), LEGACY_OAUTH_SECRET_DIRNAME$1, `${ref.id}.json`); } function normalizeLegacyOAuthSecretMaterial(raw) { if (!isRecord(raw)) return null; const material = { ...readNonEmptyString$1(raw.access) ? { access: readNonEmptyString$1(raw.access) } : {}, ...readNonEmptyString$1(raw.refresh) ? { refresh: readNonEmptyString$1(raw.refresh) } : {}, ...readNonEmptyString$1(raw.idToken) ? { idToken: readNonEmptyString$1(raw.idToken) } : {} }; return Object.keys(material).length > 0 ? material : null; } function coerceLegacyOAuthEncryptedPayload(raw) { if (!isRecord(raw)) return null; return raw.algorithm === LEGACY_OAUTH_SECRET_ALGORITHM && typeof raw.iv === "string" && typeof raw.tag === "string" && typeof raw.ciphertext === "string" ? { algorithm: raw.algorithm, iv: raw.iv, tag: raw.tag, ciphertext: raw.ciphertext } : null; } /** Return true when raw JSON has the legacy OAuth sidecar envelope or plaintext token shape. */ function isLegacyOAuthSidecarPayload(raw) { if (!isRecord(raw)) return false; if (raw.version !== LEGACY_OAUTH_SECRET_VERSION || readNonEmptyString$1(raw.profileId) === void 0 || raw.provider !== "openai-codex") return false; return coerceLegacyOAuthEncryptedPayload(raw.encrypted) !== null || normalizeLegacyOAuthSecretMaterial(raw) !== null; } function buildLegacyOAuthSecretAad(params) { return Buffer.from(`${params.ref.id}\0${params.profileId}\0${params.provider}`, "utf8"); } function buildLegacyOAuthSecretKey(seed) { return hash("sha256", `openclaw:auth-profile-oauth:${seed}`, "buffer"); } function encryptLegacyOAuthMaterialForTest(params) { const iv = Buffer.from("0102030405060708090a0b0c", "hex"); const cipher = createCipheriv(LEGACY_OAUTH_SECRET_ALGORITHM, buildLegacyOAuthSecretKey(params.seed), iv); cipher.setAAD(buildLegacyOAuthSecretAad({ ref: params.ref, profileId: params.profileId, provider: params.provider })); const ciphertext = Buffer.concat([cipher.update(JSON.stringify(params.material), "utf8"), cipher.final()]); return { algorithm: LEGACY_OAUTH_SECRET_ALGORITHM, iv: iv.toString("base64url"), tag: cipher.getAuthTag().toString("base64url"), ciphertext: ciphertext.toString("base64url") }; } function isPathInsideOrEqual(parentDir, candidatePath) { const relative = path.relative(path.resolve(parentDir), path.resolve(candidatePath)); return relative === "" || relative !== "" && !relative.startsWith("..") && !path.isAbsolute(relative); } function uniquePaths(paths) { return uniqueStrings(paths.filter((entry) => Boolean(entry))); } function resolveLegacyOAuthSecretKeyFileCandidates(env) { if (process.platform === "win32") { const home = env.USERPROFILE?.trim() || os.homedir(); const root = env.APPDATA?.trim() || (home ? path.join(home, "AppData", "Roaming") : void 0); return uniquePaths([root ? path.join(root, "OpenClaw", LEGACY_OAUTH_SECRET_KEY_FILE_NAME) : void 0, home ? path.join(home, ".openclaw-auth-profile-secrets", LEGACY_OAUTH_SECRET_KEY_FILE_NAME) : void 0]); } if (process.platform === "darwin") { const home = env.HOME?.trim() || os.homedir(); return uniquePaths([home ? path.join(home, "Library", "Application Support", "OpenClaw", LEGACY_OAUTH_SECRET_KEY_FILE_NAME) : void 0, home ? path.join(home, ".openclaw-auth-profile-secrets", LEGACY_OAUTH_SECRET_KEY_FILE_NAME) : void 0]); } const home = env.HOME?.trim() || os.homedir(); const root = env.XDG_CONFIG_HOME?.trim() || (home ? path.join(home, ".config") : void 0); return uniquePaths([root ? path.join(root, "openclaw", LEGACY_OAUTH_SECRET_KEY_FILE_NAME) : void 0, home ? path.join(home, ".openclaw-auth-profile-secrets", LEGACY_OAUTH_SECRET_KEY_FILE_NAME) : void 0]); } function resolveLegacyOAuthSecretKeyFilePath(env) { const stateDir = resolveStateDir(env); return resolveLegacyOAuthSecretKeyFileCandidates(env).find((candidate) => !isPathInsideOrEqual(stateDir, candidate)); } function readLegacyOAuthSecretKeyFile(env) { const keyPath = resolveLegacyOAuthSecretKeyFilePath(env); if (!keyPath) return; try { return fs.readFileSync(keyPath, "utf8").trim() || void 0; } catch { return; } } function readLegacyMacOAuthSecretKeychainKey(params) { if (process.platform !== "darwin" || params.allowKeychainPrompt === false || params.env.VITEST === "true" || params.env.VITEST_WORKER_ID !== void 0) return; try { return childProcess.execFileSync("security", [ "find-generic-password", "-s", LEGACY_OAUTH_SECRET_KEYCHAIN_SERVICE, "-a", LEGACY_OAUTH_SECRET_KEYCHAIN_ACCOUNT, "-w" ], { encoding: "utf8", timeout: 5e3, stdio: [ "pipe", "pipe", "pipe" ] }).trim(); } catch { return; } } function resolveLegacyOAuthSecretKeySeeds(env) { const seeds = []; const addSeed = (value) => { const trimmed = value?.trim(); if (trimmed && !seeds.includes(trimmed)) seeds.push(trimmed); }; addSeed(env[LEGACY_OAUTH_SECRET_KEY_ENV]); if (env.NODE_ENV === "test" && env.VITEST === "true") addSeed("openclaw-test-oauth-profile-secret-key"); addSeed(readLegacyOAuthSecretKeyFile(env)); return seeds; } function decryptLegacyOAuthSecretMaterialWithSeed(params, seed) { try { const decipher = createDecipheriv(LEGACY_OAUTH_SECRET_ALGORITHM, buildLegacyOAuthSecretKey(seed), Buffer.from(params.encrypted.iv, "base64url")); decipher.setAAD(buildLegacyOAuthSecretAad({ ref: params.ref, profileId: params.profileId, provider: params.provider })); decipher.setAuthTag(Buffer.from(params.encrypted.tag, "base64url")); const plaintext = Buffer.concat([decipher.update(Buffer.from(params.encrypted.ciphertext, "base64url")), decipher.final()]).toString("utf8"); return normalizeLegacyOAuthSecretMaterial(JSON.parse(plaintext)); } catch { return null; } } function decryptLegacyOAuthSecretMaterial(params) { const seeds = resolveLegacyOAuthSecretKeySeeds(params.env); for (const seed of seeds) { const material = decryptLegacyOAuthSecretMaterialWithSeed(params, seed); if (material) return material; } const keychainSeed = readLegacyMacOAuthSecretKeychainKey({ allowKeychainPrompt: params.allowKeychainPrompt, env: params.env }); if (keychainSeed && !seeds.includes(keychainSeed)) return decryptLegacyOAuthSecretMaterialWithSeed(params, keychainSeed); if (process.platform === "darwin" && params.allowKeychainPrompt === false && params.env.VITEST !== "true" && params.env.VITEST_WORKER_ID === void 0) emitKeychainOnlyMigrationHintOnce(params.profileId); return null; } let keychainOnlyMigrationHintEmitted = false; function emitKeychainOnlyMigrationHintOnce(profileId) { if (keychainOnlyMigrationHintEmitted) return; keychainOnlyMigrationHintEmitted = true; log.warn("Legacy Codex OAuth credentials are stored only in macOS Keychain on this host. Headless paths cannot prompt for Keychain access; run `openclaw doctor --fix` from an interactive terminal to migrate them back to inline auth-profiles.json credentials.", { profileId }); } function loadLegacyOAuthSidecarMaterial(params) { const env = params.env ?? process.env; const raw = loadJsonFile(resolveLegacyOAuthSidecarPath(params.ref, env)); if (!isRecord(raw)) return null; if (raw.version !== LEGACY_OAUTH_SECRET_VERSION || raw.profileId !== params.profileId || raw.provider !== params.provider) return null; const encrypted = coerceLegacyOAuthEncryptedPayload(raw.encrypted); if (encrypted) return decryptLegacyOAuthSecretMaterial({ ref: params.ref, profileId: params.profileId, provider: params.provider, encrypted, env, allowKeychainPrompt: params.allowKeychainPrompt }); return normalizeLegacyOAuthSecretMaterial(raw); } const legacyOAuthSidecarTestUtils = { buildLegacyOAuthSecretAad, buildLegacyOAuthSecretKey, encryptLegacyOAuthMaterial: encryptLegacyOAuthMaterialForTest }; //#endregion //#region src/commands/doctor-auth-oauth-sidecar.ts /** Doctor repair for legacy OAuth sidecar files and inline auth profile stores. */ const LEGACY_OAUTH_SECRET_DIRNAME = "auth-profiles"; function readNonEmptyString(value) { return typeof value === "string" && value.trim() ? value : void 0; } function addCandidate(candidates, agentDir) { const authPath = resolveAuthStorePath(agentDir); candidates.set(path.resolve(authPath), { agentDir, authPath }); } function listExistingAgentDirsFromState(env) { const root = path.join(resolveStateDir(env), "agents"); let entries; try { entries = fs.readdirSync(root, { withFileTypes: true }); } catch { return []; } return entries.filter((entry) => entry.isDirectory() || entry.isSymbolicLink()).map((entry) => path.join(root, entry.name, "agent")).filter((agentDir) => { try { return fs.statSync(agentDir).isDirectory(); } catch { return false; } }); } function listAuthProfileRepairCandidates(cfg, env) { const candidates = /* @__PURE__ */ new Map(); addCandidate(candidates, resolveDefaultAgentDir(cfg, env)); const envAgentDir = readNonEmptyString(env.OPENCLAW_AGENT_DIR); if (envAgentDir) addCandidate(candidates, envAgentDir); for (const agentId of listAgentIds(cfg)) addCandidate(candidates, resolveAgentDir(cfg, agentId, env)); for (const agentDir of listExistingAgentDirsFromState(env)) addCandidate(candidates, agentDir); return [...candidates.values()]; } function resolveLegacyOAuthSidecarStore(candidate) { if (!fs.existsSync(candidate.authPath)) return null; const raw = loadJsonFile(candidate.authPath); if (!isRecord(raw) || !isRecord(raw.profiles)) return null; const profiles = []; for (const [profileId, value] of Object.entries(raw.profiles)) { if (!isRecord(value) || value.type !== "oauth") continue; const ref = isLegacyOAuthRef(value.oauthRef) ? value.oauthRef : void 0; if (!ref || readNonEmptyString(value.provider) !== ref.provider) continue; profiles.push({ profileId, provider: ref.provider, ref }); } return profiles.length > 0 ? { ...candidate, raw, profiles } : null; } function listUnreferencedLegacyOAuthSidecars(referencedRefIds, env) { const sidecarDir = path.join(resolveOAuthDir(env), LEGACY_OAUTH_SECRET_DIRNAME); let entries; try { entries = fs.readdirSync(sidecarDir, { withFileTypes: true }); } catch { return []; } return entries.flatMap((entry) => { if (!entry.isFile() || !entry.name.endsWith(".json")) return []; const refId = entry.name.slice(0, -5); if (!/^[a-f0-9]{32}$/.test(refId) || referencedRefIds.has(refId)) return []; const sidecarPath = path.join(sidecarDir, entry.name); return isLegacyOAuthSidecarPayload(loadJsonFile(sidecarPath)) ? [{ sidecarPath }] : []; }); } function applyLegacyOAuthSidecarMaterial(params) { if (!isRecord(params.raw.profiles)) return false; const entry = params.raw.profiles[params.profile.profileId]; if (!isRecord(entry)) return false; delete entry.oauthRef; if (params.material.access) entry.access = params.material.access; if (params.material.refresh) entry.refresh = params.material.refresh; if (params.material.idToken) entry.idToken = params.material.idToken; return true; } function backupLegacyOAuthSidecarStore(authPath, now) { const backupPath = `${authPath}.oauth-ref.${now()}.bak`; fs.copyFileSync(authPath, backupPath); return backupPath; } /** * Migrates legacy Codex OAuth sidecar secrets back into inline auth profile credentials. * * Only sidecar files that were successfully imported and are not referenced by another failed * profile are removed; unreferenced sidecars stay because unknown agent directories may use them. */ async function maybeRepairLegacyOAuthSidecarProfiles(params) { const now = params.now ?? Date.now; const emitNotes = params.emitNotes !== false; const env = params.env ?? process.env; const stores = listAuthProfileRepairCandidates(params.cfg, env).map(resolveLegacyOAuthSidecarStore).filter((entry) => entry !== null); const unreferencedSidecars = listUnreferencedLegacyOAuthSidecars(new Set(stores.flatMap((entry) => entry.profiles.map((p) => p.ref.id))), env); const result = { detected: [...stores.map((entry) => entry.authPath), ...unreferencedSidecars.map((entry) => entry.sidecarPath)], changes: [], warnings: [] }; if (stores.length === 0 && unreferencedSidecars.length === 0) return result; if (emitNotes) note([ ...stores.map((entry) => `- ${shortenHomePath(entry.authPath)} has legacy Codex OAuth profiles to migrate.`), ...unreferencedSidecars.length > 0 ? [`- Found ${unreferencedSidecars.length} unreferenced legacy Codex OAuth sidecar credential file${unreferencedSidecars.length === 1 ? "" : "s"}.`, `- Unreferenced sidecar files are left in place because external agent directories outside this scan may still reference them.`] : [], `- ${formatCliCommand("openclaw doctor --fix")} migrates active profiles back to inline OAuth credentials and removes only sidecar files it successfully migrated.` ].join("\n"), "Auth profiles"); if (!await params.prompter.confirmAutoFix({ message: "Migrate legacy Codex OAuth credentials now?", initialValue: true })) return result; const migratedSidecarsByRefId = /* @__PURE__ */ new Map(); const unresolvedRefIds = /* @__PURE__ */ new Set(); for (const store of stores) { let migratedCount = 0; const storeMigratedSidecarsByRefId = /* @__PURE__ */ new Map(); for (const profile of store.profiles) { const material = loadLegacyOAuthSidecarMaterial({ ...profile, env }); if (!material) { unresolvedRefIds.add(profile.ref.id); result.warnings.push(`Could not decrypt legacy OAuth sidecar for ${profile.profileId} in ${shortenHomePath(store.authPath)}; re-authenticate this profile.`); continue; } if (applyLegacyOAuthSidecarMaterial({ raw: store.raw, profile, material })) { migratedCount += 1; storeMigratedSidecarsByRefId.set(profile.ref.id, resolveLegacyOAuthSidecarPath(profile.ref, env)); } else unresolvedRefIds.add(profile.ref.id); } if (migratedCount === 0) continue; try { const backupPath = backupLegacyOAuthSidecarStore(store.authPath, now); if (!("version" in store.raw)) store.raw.version = 1; saveJsonFile(store.authPath, store.raw); for (const [refId, sidecarPath] of storeMigratedSidecarsByRefId) migratedSidecarsByRefId.set(refId, sidecarPath); result.changes.push(`Migrated ${migratedCount} legacy Codex OAuth profile${migratedCount === 1 ? "" : "s"} in ${shortenHomePath(store.authPath)} to inline credentials (backup: ${shortenHomePath(backupPath)}).`); } catch (err) { for (const refId of storeMigratedSidecarsByRefId.keys()) unresolvedRefIds.add(refId); result.warnings.push(`Failed to migrate legacy OAuth sidecars in ${shortenHomePath(store.authPath)}: ${String(err)}`); } } for (const [refId, sidecarPath] of migratedSidecarsByRefId) { if (unresolvedRefIds.has(refId)) continue; try { fs.rmSync(sidecarPath, { force: true }); } catch (err) { result.warnings.push(`Failed to remove migrated legacy OAuth sidecar ${shortenHomePath(sidecarPath)}: ${String(err)}`); } } if (unreferencedSidecars.length > 0) result.warnings.push(`Found ${unreferencedSidecars.length} unreferenced legacy Codex OAuth sidecar credential file${unreferencedSidecars.length === 1 ? "" : "s"}; left in place because external agent directories outside this scan may still reference ${unreferencedSidecars.length === 1 ? "it" : "them"}.`); if (result.changes.length > 0) clearRuntimeAuthProfileStoreSnapshots(); if (emitNotes && result.changes.length > 0) note(result.changes.map((change) => `- ${change}`).join("\n"), "Doctor changes"); if (emitNotes && result.warnings.length > 0) note(result.warnings.map((warning) => `- ${warning}`).join("\n"), "Doctor warnings"); return result; } legacyOAuthSidecarTestUtils.buildLegacyOAuthSecretAad, legacyOAuthSidecarTestUtils.buildLegacyOAuthSecretKey; //#endregion export { maybeRepairLegacyOAuthSidecarProfiles as t };