openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
373 lines (372 loc) • 17.6 kB
JavaScript
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 };