openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
1,275 lines • 66.3 kB
JavaScript
import { c as isRecord, l as isStringRecord } from "./record-coerce-DItp3I4t.js";
import { m as readNonBlankString } from "./string-coerce-CIXf7egm.js";
import { m as shortenHomePath } from "./utils-P__uGsPB.js";
import { t as formatCliCommand } from "./command-format-C7YfyMTd.js";
import { s as coerceSecretRef } from "./types.secrets-kC0nOetj.js";
import { t as openNodeSqliteDatabase } from "./node-sqlite-BpQX3W0e.js";
import { a as getNodeSqliteKysely, i as executeSqliteQueryTakeFirstSync, r as executeSqliteQuerySync } from "./kysely-sync-COmh4HWh.js";
import { i as openOpenClawStateDatabase, s as runOpenClawStateWriteTransaction } from "./openclaw-state-db-BRTnL-D8.js";
import { r as collectConfiguredModelRefs } from "./configured-model-refs-DEXTV_K3.js";
import { t as splitTrailingAuthProfile } from "./model-ref-profile-BIKs-96s.js";
import { c as resolveSharedMainAuthAgentDir, o as resolveSharedAuthStoreOwnership, s as resolveSharedAuthStorePath } from "./path-resolve-oRkRBkQd.js";
import { O as resolveLegacyOAuthPath, T as listLegacyAuthProfileArchives, _ as runAuthProfileWriteTransaction, a as inspectPersistedAuthProfileStateRaw, c as inspectPersistedSharedAuthProfileStoreRaw, d as readPersistedSharedAuthProfileStateRaw, h as resolveAuthProfileDatabasePath, l as readPersistedAuthProfileStateRaw, o as inspectPersistedAuthProfileStoreRaw, s as inspectPersistedSharedAuthProfileStateRaw } from "./sqlite-MN_7y26V.js";
import { S as hasMatchingOAuthIdentity, a as loadPersistedAuthProfileStore, d as coerceAuthProfileState, i as coercePersistedAuthProfileStore, l as parseLegacyCredentialEntry, r as coerceLegacyAuthStore, s as loadPersistedSharedAuthProfileStore, t as applyLegacyAuthStore, x as areOAuthCredentialsEquivalent } from "./persisted-B_qhhBlh.js";
import { t as loadJsonFileThroughSymlink } from "./json-file-CHTTn1IF.js";
import { o as clearAuthProfileMigrationDiagnostics } from "./legacy-source-diagnostic-BNaogmw1.js";
import { U as isInheritedMainOAuthCredentialFromStores, r as clearRuntimeAuthProfileStoreSnapshots } from "./runtime-snapshots-CHAErv1O.js";
import { x as saveAuthProfileStoreWithPreparedOwner } from "./store-F1B2duCT.js";
import { r as resolveLegacyInheritedAuthAgentDir } from "./legacy-inherited-auth-dir-CMGSmv-F.js";
import { t as acquireFileLockSyncWithRetry } from "./file-lock-sync-lfYqrUDF.js";
import { t as note } from "./note-DqHk3fA1.js";
import { i as resolveLegacyFlatAuthPath, n as resolveLegacyAuthProfilesPath, r as resolveLegacyAuthStatePath, t as listAuthProfileRepairCandidates } from "./doctor-auth-legacy-paths-CCOCSqnd.js";
import { a as recordLegacyMigrationRun, n as readLegacyMigrationReceipt, o as recordLegacyMigrationSource } from "./state-migrations.receipts-BIGn8ljb.js";
import fs from "node:fs";
import { isDeepStrictEqual } from "node:util";
import path from "node:path";
import { createHash, randomUUID } from "node:crypto";
//#region src/commands/doctor-auth-migration-receipts.ts
const MIGRATION_KIND = "auth-profile-json-to-sqlite-v2";
function digestBytes(bytes) {
return createHash("sha256").update(bytes).digest("hex");
}
function createAuthProfileMigrationSourceReceipt(params) {
const sourcePath = path.resolve(params.sourcePath);
const sourceSha256 = digestBytes(params.sourceBytes);
const sourceKey = `auth-profile-v2:${digestBytes(Buffer.from(`${sourcePath}\0${sourceSha256}`))}`;
const stamp = (params.now ?? /* @__PURE__ */ new Date()).toISOString().replaceAll(":", "-");
return {
sourceKey,
runId: `${sourceKey}:${randomUUID()}`,
sourcePath,
sourceSha256,
sourceSizeBytes: params.sourceBytes.byteLength,
sourceRecordCount: params.sourceRecordCount,
sourceBytes: Buffer.from(params.sourceBytes),
targetDatabasePath: path.resolve(params.targetDatabasePath),
targetTable: params.targetTable,
...params.targetStoreKey ? { targetStoreKey: params.targetStoreKey } : {},
archivePath: `${sourcePath}.migrated-${stamp}-${randomUUID()}`,
...params.env ? { env: params.env } : {}
};
}
function reportJson(receipt) {
return JSON.stringify({
format: MIGRATION_KIND,
archivePath: receipt.archivePath,
targetDatabasePath: receipt.targetDatabasePath,
targetTable: receipt.targetTable,
targetStoreKey: receipt.targetStoreKey ?? "primary",
expectedProfileSha256: receipt.expectedProfileSha256,
expectedStateSha256: receipt.expectedStateSha256,
completionStatus: receipt.completionStatus ?? "completed"
});
}
function digestAuthProfileMigrationValue(value) {
return digestBytes(Buffer.from(JSON.stringify(value) ?? "<undefined>"));
}
function recordAuthProfileMigrationImported(receipt, now = Date.now()) {
runOpenClawStateWriteTransaction(({ db }) => {
const kysely = getNodeSqliteKysely(db);
const existing = executeSqliteQueryTakeFirstSync(db, kysely.selectFrom("migration_sources").select(["last_run_id", "status"]).where("source_key", "=", receipt.sourceKey));
if (existing && existing.last_run_id !== receipt.runId && existing.status !== "retryable" && existing.status !== "superseded") throw new Error(`auth profile migration source already owned by ${existing.status} receipt`);
const report = reportJson(receipt);
recordLegacyMigrationRun(db, {
runId: receipt.runId,
startedAt: now,
finishedAt: null,
status: "imported",
reportJson: report,
upsert: true
});
recordLegacyMigrationSource(db, {
sourceKey: receipt.sourceKey,
migrationKind: MIGRATION_KIND,
sourcePath: receipt.sourcePath,
targetTable: receipt.targetTable,
sourceSha256: receipt.sourceSha256,
sourceSizeBytes: receipt.sourceSizeBytes,
sourceRecordCount: receipt.sourceRecordCount,
runId: receipt.runId,
status: "imported",
importedAt: now,
reportJson: report,
upsert: true
});
}, { env: receipt.env });
}
function retirePendingAuthProfileMigrationReceipt(receipt, status, previousStatus = "imported", now = Date.now()) {
runOpenClawStateWriteTransaction(({ db }) => {
const kysely = getNodeSqliteKysely(db);
executeSqliteQuerySync(db, kysely.updateTable("migration_runs").set({
status,
finished_at: now
}).where("id", "=", receipt.runId).where("status", "=", previousStatus));
executeSqliteQuerySync(db, kysely.updateTable("migration_sources").set({ status }).where("source_key", "=", receipt.sourceKey).where("last_run_id", "=", receipt.runId).where("status", "=", previousStatus));
}, { env: receipt.env });
}
function restoreAuthProfileMigrationArchiveNoClobber(receipt) {
try {
fs.linkSync(receipt.archivePath, receipt.sourcePath);
} catch (error) {
if (error.code === "EEXIST") return "source-exists";
throw error;
}
fs.unlinkSync(receipt.archivePath);
return "restored";
}
function recordAuthProfileMigrationCompleted(receipt, now = Date.now(), status = "completed") {
runOpenClawStateWriteTransaction(({ db }) => {
const kysely = getNodeSqliteKysely(db);
executeSqliteQuerySync(db, kysely.updateTable("migration_runs").set({
status,
finished_at: now
}).where("id", "=", receipt.runId));
executeSqliteQuerySync(db, kysely.updateTable("migration_sources").set({
status,
removed_source: 1
}).where("source_key", "=", receipt.sourceKey).where("last_run_id", "=", receipt.runId));
}, { env: receipt.env });
}
function archiveAuthProfileMigrationSource(receipt) {
if (fs.existsSync(receipt.sourcePath)) {
if (digestBytes(fs.readFileSync(receipt.sourcePath)) !== receipt.sourceSha256) throw new Error("legacy auth source changed after verification");
fs.renameSync(receipt.sourcePath, receipt.archivePath);
}
if (digestBytes(fs.readFileSync(receipt.archivePath)) !== receipt.sourceSha256) throw new Error("legacy auth archive verification failed");
}
function acquireAuthProfileMigrationSourceLocks(sourcePaths) {
const releases = [];
try {
for (const sourcePath of [...new Set(sourcePaths.map((entry) => path.resolve(entry)))].toSorted()) releases.push(acquireFileLockSyncWithRetry(sourcePath));
} catch (error) {
for (const release of releases.toReversed()) release();
throw error;
}
return () => {
for (const release of releases.toReversed()) release();
};
}
function verifyAuthProfileMigrationTarget(receipt) {
const expectedProfiles = Object.entries(receipt.expectedProfileSha256 ?? {});
if (expectedProfiles.length === 0 && !receipt.expectedStateSha256) return;
const db = openNodeSqliteDatabase(receipt.targetDatabasePath, { readOnly: true });
try {
const kysely = getNodeSqliteKysely(db);
const readTarget = (kind) => {
const query = receipt.targetStoreKey === "shared" ? kysely.selectFrom("config_machine_state").select("value_json as json").where("state_key", "=", `authProfiles.${kind}`) : kind === "store" ? kysely.selectFrom("auth_profile_store").select("store_json as json").where("store_key", "=", "primary") : kysely.selectFrom("auth_profile_state").select("state_json as json").where("state_key", "=", "primary");
const row = executeSqliteQueryTakeFirstSync(db, query);
return typeof row?.json === "string" ? JSON.parse(row.json) : null;
};
const store = expectedProfiles.length > 0 ? readTarget("store") : null;
for (const [profileId, expectedSha256] of expectedProfiles) if (digestAuthProfileMigrationValue(store?.profiles?.[profileId]) !== expectedSha256) throw new Error("auth profile migration target verification failed");
if (receipt.expectedStateSha256) {
if (digestAuthProfileMigrationValue(readTarget("state")) !== receipt.expectedStateSha256) throw new Error("auth profile migration target verification failed");
}
} finally {
db.close();
}
}
function finalizeAuthProfileMigrationSource(receipt, status = "completed", options = {}) {
receipt.completionStatus = status;
const release = options.sourceLocked ? void 0 : acquireFileLockSyncWithRetry(receipt.sourcePath);
try {
recordAuthProfileMigrationImported(receipt);
verifyAuthProfileMigrationTarget(receipt);
archiveAuthProfileMigrationSource(receipt);
recordAuthProfileMigrationCompleted(receipt, Date.now(), status);
} finally {
release?.();
}
}
function resumePendingAuthProfileMigrationArchives(env, recoverCompleted) {
const changes = [];
const database = openOpenClawStateDatabase({ env });
const kysely = getNodeSqliteKysely(database.db);
const rows = executeSqliteQuerySync(database.db, kysely.selectFrom("migration_sources as source").innerJoin("migration_runs as run", "run.id", "source.last_run_id").select([
"source.source_key",
"source.source_path",
"source.source_sha256",
"source.source_size_bytes",
"source.source_record_count",
"source.target_table",
"source.last_run_id",
"source.report_json",
"source.status"
]).where("source.migration_kind", "=", MIGRATION_KIND).where((eb) => eb.or([eb.and([eb("source.status", "=", "imported"), eb("source.removed_source", "=", 0)]), eb.and([eb("source.status", "=", "completed"), eb("source.removed_source", "=", 1)])]))).rows;
for (const row of rows) {
const report = JSON.parse(row.report_json);
const completed = row.status === "completed";
if (completed && (!recoverCompleted || Object.hasOwn(report, "expectedProfileSha256") || row.target_table === "auth_profile_state" || typeof report.archivePath !== "string" || !fs.existsSync(report.archivePath))) continue;
if (typeof row.source_sha256 !== "string" || typeof row.source_size_bytes !== "number" || typeof row.source_record_count !== "number" || typeof report.archivePath !== "string" || typeof report.targetDatabasePath !== "string" || row.target_table !== "auth_profile_store" && row.target_table !== "auth_profile_stores" && row.target_table !== "auth_profile_state") throw new Error("invalid pending auth profile migration receipt");
const receipt = {
sourceKey: row.source_key,
runId: row.last_run_id,
sourcePath: row.source_path,
sourceSha256: row.source_sha256,
sourceSizeBytes: row.source_size_bytes,
sourceRecordCount: row.source_record_count,
targetDatabasePath: report.targetDatabasePath,
targetTable: row.target_table,
targetStoreKey: report.targetStoreKey === "shared" ? "shared" : "primary",
archivePath: report.archivePath,
...isStringRecord(report.expectedProfileSha256) ? { expectedProfileSha256: report.expectedProfileSha256 } : {},
...typeof report.expectedStateSha256 === "string" ? { expectedStateSha256: report.expectedStateSha256 } : {},
completionStatus: report.completionStatus === "archived-unparsed" ? "archived-unparsed" : "completed",
...env ? { env } : {}
};
if (!fs.existsSync(receipt.sourcePath) && !fs.existsSync(receipt.archivePath)) throw new Error("pending auth profile migration has neither source nor archive");
const lockTarget = fs.existsSync(receipt.sourcePath) ? receipt.sourcePath : receipt.archivePath;
const release = acquireFileLockSyncWithRetry(lockTarget);
try {
const sourceExists = fs.existsSync(receipt.sourcePath);
if (completed) {
receipt.sourceBytes = fs.readFileSync(receipt.archivePath);
if (digestBytes(receipt.sourceBytes) !== receipt.sourceSha256 || sourceExists && digestBytes(fs.readFileSync(receipt.sourcePath)) !== receipt.sourceSha256 || !recoverCompleted?.(receipt)) continue;
if (!sourceExists) fs.linkSync(receipt.archivePath, receipt.sourcePath);
retirePendingAuthProfileMigrationReceipt(receipt, "retryable", "completed");
changes.push("Reset an inconsistent completed auth migration receipt for retry.");
continue;
}
if (digestBytes(fs.readFileSync(sourceExists ? receipt.sourcePath : receipt.archivePath)) !== receipt.sourceSha256) {
if (!sourceExists) throw new Error("legacy auth archive verification failed");
retirePendingAuthProfileMigrationReceipt(receipt, "superseded");
changes.push("Retired an interrupted auth migration receipt for a changed source.");
continue;
}
try {
verifyAuthProfileMigrationTarget(receipt);
} catch {
if (!sourceExists) {
if (restoreAuthProfileMigrationArchiveNoClobber(receipt) === "source-exists") {
const status = digestBytes(fs.readFileSync(receipt.sourcePath)) === receipt.sourceSha256 ? "retryable" : "superseded";
retirePendingAuthProfileMigrationReceipt(receipt, status);
changes.push(status === "retryable" ? "Reset an interrupted auth migration receipt for retry." : "Retired an interrupted auth migration receipt for a changed source.");
continue;
}
}
retirePendingAuthProfileMigrationReceipt(receipt, "retryable");
changes.push("Reset an interrupted auth migration receipt for retry.");
continue;
}
archiveAuthProfileMigrationSource(receipt);
recordAuthProfileMigrationCompleted(receipt, Date.now(), receipt.completionStatus);
} finally {
release();
}
changes.push(`Finalized interrupted auth profile archive -> ${receipt.archivePath}`);
}
return changes;
}
function hasTerminalAuthProfileMigrationReceipt(sourceKey, env) {
const database = openOpenClawStateDatabase({ env });
const row = executeSqliteQueryTakeFirstSync(database.db, getNodeSqliteKysely(database.db).selectFrom("migration_sources").select("status").where("source_key", "=", sourceKey));
return row?.status === "completed" || row?.status === "archived-unparsed";
}
if (process.env.VITEST || false) globalThis[Symbol.for("openclaw.authProfileMigrationReceiptsTestApi")] = {
recordAuthProfileMigrationImported,
recordAuthProfileMigrationCompleted,
restoreAuthProfileMigrationArchiveNoClobber
};
//#endregion
//#region src/commands/doctor-auth-flat-profiles.ts
/** Doctor repairs for legacy auth profile JSON stores and OpenAI provider-id migrations. */
function resolveMigrationTargetDatabasePath(agentDir, env = process.env) {
return agentDir ? resolveAuthProfileDatabasePath(agentDir) : resolveSharedAuthStorePath(env);
}
var AuthProfileMigrationVerificationError = class extends Error {
constructor(detail) {
super("auth profile SQLite verification failed");
this.detail = detail;
this.name = "AuthProfileMigrationVerificationError";
}
};
const UNSAFE_LEGACY_AUTH_PROFILE_KEYS = /* @__PURE__ */ new Set([
"__proto__",
"constructor",
"prototype"
]);
function isSafeLegacyProviderKey(key) {
return key.trim().length > 0 && !UNSAFE_LEGACY_AUTH_PROFILE_KEYS.has(key);
}
function extractProviderFromProfileId(profileId) {
const colon = profileId.indexOf(":");
if (colon <= 0) return;
return readNonBlankString(profileId.slice(0, colon));
}
function extractProviderFromModelRef(modelRef) {
const { model } = splitTrailingAuthProfile(modelRef);
const slash = model.indexOf("/");
if (slash <= 0) return;
return readNonBlankString(model.slice(0, slash));
}
function collectLegacyConfigAuthProfileProviderHints(cfg) {
const hints = /* @__PURE__ */ new Map();
const conflicted = /* @__PURE__ */ new Set();
const addHint = (profileId, provider) => {
const existing = hints.get(profileId);
if (existing && existing !== provider) {
hints.delete(profileId);
conflicted.add(profileId);
return;
}
if (!conflicted.has(profileId)) hints.set(profileId, provider);
};
const addModelHints = (models) => {
if (!isRecord(models)) return;
for (const [modelRef, rawModel] of Object.entries(models)) {
const provider = extractProviderFromModelRef(modelRef);
if (!provider || !isSafeLegacyProviderKey(provider) || !isRecord(rawModel)) continue;
const agentRuntime = isRecord(rawModel.agentRuntime) ? rawModel.agentRuntime : null;
const authProfileId = agentRuntime ? readNonBlankString(agentRuntime.authProfileId) : void 0;
if (authProfileId) addHint(authProfileId, provider);
}
};
for (const { value } of collectConfiguredModelRefs(cfg)) {
const { profile } = splitTrailingAuthProfile(value);
const provider = extractProviderFromModelRef(value);
if (profile && provider && isSafeLegacyProviderKey(provider)) addHint(profile, provider);
}
const root = cfg;
const auth = isRecord(root.auth) ? root.auth : null;
const order = auth && isRecord(auth.order) ? auth.order : null;
if (order) for (const [provider, profileIds] of Object.entries(order)) {
if (!isSafeLegacyProviderKey(provider) || !Array.isArray(profileIds)) continue;
for (const profileId of profileIds) {
const normalizedProfileId = readNonBlankString(profileId);
if (normalizedProfileId) addHint(normalizedProfileId, provider);
}
}
const agents = isRecord(root.agents) ? root.agents : null;
addModelHints((agents && isRecord(agents.defaults) ? agents.defaults : null)?.models);
const agentList = agents && Array.isArray(agents.list) ? agents.list : [];
for (const agent of agentList) if (isRecord(agent)) addModelHints(agent.models);
return hints;
}
function inferLegacyCredentialType(record) {
const explicit = readNonBlankString(record.type) ?? readNonBlankString(record.mode);
if (explicit === "api_key" || explicit === "token" || explicit === "oauth") return explicit;
if (readNonBlankString(record.key) ?? readNonBlankString(record.apiKey)) return "api_key";
if (coerceSecretRef(record.keyRef)) return "api_key";
if (readNonBlankString(record.token)) return "token";
if (coerceSecretRef(record.tokenRef)) return "token";
if (readNonBlankString(record.access) && readNonBlankString(record.refresh) && typeof record.expires === "number") return "oauth";
}
function coerceLegacyFlatCredential(providerId, raw) {
if (!isRecord(raw)) return null;
const type = inferLegacyCredentialType(raw);
if (!type) return null;
const provider = readNonBlankString(raw.provider) ?? providerId;
const credential = parseLegacyCredentialEntry({
...raw,
type,
provider
}, providerId);
if (!credential || !hasUsableAuthProfileCredential(credential)) return null;
return credential;
}
function coerceLegacyFlatAuthProfileStore(raw) {
if (!isRecord(raw) || "profiles" in raw) return null;
const store = {
version: 1,
profiles: {}
};
for (const [key, value] of Object.entries(raw)) {
const providerId = key.trim();
if (!isSafeLegacyProviderKey(providerId)) continue;
const credential = coerceLegacyFlatCredential(providerId, value);
if (!credential) continue;
store.profiles[`${providerId}:default`] = credential;
}
return Object.keys(store.profiles).length > 0 ? store : null;
}
function listAuthProfileSqliteMigrationCandidates(cfg, env) {
return listAuthProfileRepairCandidates(cfg, env).map((candidate) => ({
agentDir: candidate.agentDir,
authPath: candidate.authPath,
statePath: resolveLegacyAuthStatePath(path.dirname(candidate.authPath)),
legacyPath: resolveLegacyFlatAuthPath(path.dirname(candidate.authPath))
}));
}
function hasAuthProfileState(state) {
return Boolean(state.order || state.lastGood || state.usageStats);
}
function normalizeLegacyApiKeyAliasesForImport(raw) {
if (!isRecord(raw) || !isRecord(raw.profiles)) return;
for (const profile of Object.values(raw.profiles)) {
if (!isRecord(profile)) continue;
if ((readNonBlankString(profile.type) ?? readNonBlankString(profile.mode)) !== "api_key") continue;
if (readNonBlankString(profile.key) !== void 0 || coerceSecretRef(profile.key) !== null || coerceSecretRef(profile.keyRef) !== null || profile["api_key"] === void 0) continue;
profile.key = profile["api_key"];
}
}
function collectAuthProfileStateProfileIds(state) {
return [.../* @__PURE__ */ new Set([
...Object.values(state.order ?? {}).flat(),
...Object.values(state.lastGood ?? {}),
...Object.keys(state.usageStats ?? {})
])];
}
function inferLegacyConfigAuthProfileMode(raw) {
const explicit = readNonBlankString(raw.mode) ?? readNonBlankString(raw.type);
if (explicit === "api_key" || explicit === "token" || explicit === "oauth") return explicit;
if (readNonBlankString(raw.key) || readNonBlankString(raw.apiKey) || readNonBlankString(raw["api_key"]) || coerceSecretRef(raw.keyRef) || coerceSecretRef(raw.key) || coerceSecretRef(raw.apiKey) || coerceSecretRef(raw["api_key"])) return "api_key";
if (readNonBlankString(raw.token) || coerceSecretRef(raw.tokenRef) || coerceSecretRef(raw.token)) return "token";
if (readNonBlankString(raw.access) && readNonBlankString(raw.refresh) && typeof raw.expires === "number") return "oauth";
}
function coerceLegacyConfigAuthProfileStore(cfg) {
const cfgRecord = cfg;
const auth = isRecord(cfgRecord.auth) ? cfgRecord.auth : null;
const profiles = auth && isRecord(auth.profiles) ? auth.profiles : null;
if (!profiles) return null;
const providerHints = collectLegacyConfigAuthProfileProviderHints(cfg);
const store = {
version: 1,
profiles: {}
};
for (const [profileId, raw] of Object.entries(profiles)) {
if (!isRecord(raw)) continue;
const mode = inferLegacyConfigAuthProfileMode(raw);
if (mode !== "api_key" && mode !== "token" && mode !== "oauth") continue;
const provider = readNonBlankString(raw.provider) ?? extractProviderFromProfileId(profileId) ?? providerHints.get(profileId);
if (!provider || !isSafeLegacyProviderKey(provider)) continue;
const next = {
...raw,
provider,
mode
};
if (mode === "api_key") {
const keyRef = coerceSecretRef(raw.keyRef) ?? coerceSecretRef(raw.key) ?? coerceSecretRef(raw.apiKey) ?? coerceSecretRef(raw["api_key"]);
const key = readNonBlankString(raw.key) ?? readNonBlankString(raw.apiKey) ?? readNonBlankString(raw["api_key"]);
if (keyRef) {
next.keyRef = keyRef;
delete next.key;
delete next.apiKey;
delete next["api_key"];
} else if (key) {
next.key = key;
delete next.keyRef;
} else continue;
} else if (mode === "token") {
const tokenRef = coerceSecretRef(raw.tokenRef) ?? coerceSecretRef(raw.token);
const token = readNonBlankString(raw.token);
if (tokenRef) {
next.tokenRef = tokenRef;
delete next.token;
} else if (token) {
next.token = token;
delete next.tokenRef;
} else continue;
} else if (!readNonBlankString(raw.access) || !readNonBlankString(raw.refresh) || typeof raw.expires !== "number") continue;
store.profiles[profileId] = next;
}
const canonicalStore = coercePersistedAuthProfileStore(store);
return canonicalStore && Object.keys(canonicalStore.profiles).length > 0 ? canonicalStore : null;
}
function isDefaultAgentCandidate(candidate, cfg, env) {
return candidate.agentDir === void 0 || path.resolve(candidate.agentDir) === path.resolve(resolveLegacyInheritedAuthAgentDir(cfg, env));
}
function stripImportedConfigAuthProfileCredentials(cfg, store) {
const profiles = ensureConfigAuthProfiles(cfg);
let changed = false;
for (const [profileId, credential] of Object.entries(store.profiles)) {
const current = profiles[profileId];
if (!current) continue;
profiles[profileId] = {
provider: current.provider || credential.provider,
mode: credential.type,
...current.email ? { email: current.email } : {},
...current.displayName ? { displayName: current.displayName } : {}
};
changed = true;
}
return changed;
}
function hasUsableAuthProfileCredential(credential) {
if (credential.type === "api_key") return Boolean(readNonBlankString(credential.key) || credential.keyRef);
if (credential.type === "token") return Boolean(readNonBlankString(credential.token) || credential.tokenRef);
return Boolean(readNonBlankString(credential.access)) && Boolean(readNonBlankString(credential.refresh)) && typeof credential.expires === "number";
}
function mergeImportedAuthProfiles(params) {
const profiles = { ...params.store.profiles };
for (const [profileId, credential] of Object.entries(params.profiles)) {
const existing = profiles[profileId];
if (!params.existingProfileIds.has(profileId) || params.replaceExistingWithoutCredential && existing && !hasUsableAuthProfileCredential(existing) && hasUsableAuthProfileCredential(credential)) profiles[profileId] = credential;
}
return {
...params.store,
profiles
};
}
function mergeImportedAuthProfileState(params) {
const next = { ...params.store };
for (const field of [
"order",
"lastGood",
"usageStats"
]) {
const incoming = params.state[field];
if (!incoming) continue;
const existing = params.existingState[field] ?? {};
Object.assign(next, { [field]: {
...params.store[field],
...Object.fromEntries(Object.entries(incoming).filter(([key]) => !Object.hasOwn(existing, key)))
} });
}
return next;
}
function formatMissingAuthProfileSqliteVerification(params) {
const missingProfileIds = [...params.importedProfileIds].filter((profileId) => !params.loaded?.profiles[profileId]);
const missingStateFields = [];
for (const field of ["order", "lastGood"]) for (const [provider, expected] of Object.entries(params.expected[field] ?? {})) if (!isDeepStrictEqual(params.loaded?.[field]?.[provider], expected)) missingStateFields.push(`${field}.${provider}`);
for (const profileId of Object.keys(params.expected.usageStats ?? {})) if (!params.loaded?.usageStats?.[profileId]) missingStateFields.push(`usageStats.${profileId}`);
const parts = [];
if (missingProfileIds.length > 0) parts.push(`imported profile(s): ${missingProfileIds.toSorted().join(", ")}`);
if (missingStateFields.length > 0) parts.push(`auth state field(s): ${missingStateFields.toSorted().join(", ")}`);
return parts.length > 0 ? parts.join("; ") : null;
}
function collectUnresolvedLegacyOAuthSidecarProfileIds(raw) {
if (!isRecord(raw) || !isRecord(raw.profiles)) return [];
const profileIds = [];
for (const [profileId, profile] of Object.entries(raw.profiles)) {
if (!isRecord(profile) || profile.type !== "oauth" || !isRecord(profile.oauthRef)) continue;
if (readNonBlankString(profile.oauthRef.id) && readNonBlankString(profile.oauthRef.provider) && (!readNonBlankString(profile.access) || !readNonBlankString(profile.refresh))) profileIds.push(profileId);
}
return profileIds;
}
function hasImportableAuthProfileStore(store) {
return Boolean(store && (Object.keys(store.profiles).length > 0 || hasAuthProfileState(store)));
}
function prepareAuthProfileSourceReceipt(params) {
const sourceBytes = fs.readFileSync(params.pathname);
let sourceRecordCount = 0;
try {
const parsed = JSON.parse(sourceBytes.toString("utf8"));
sourceRecordCount = isRecord(parsed) ? Object.keys(parsed).length : 0;
} catch {}
return createAuthProfileMigrationSourceReceipt({
sourcePath: params.pathname,
sourceBytes,
sourceRecordCount,
targetDatabasePath: params.targetDatabasePath,
targetTable: params.targetTable,
...params.targetStoreKey ? { targetStoreKey: params.targetStoreKey } : {},
now: new Date(params.now()),
...params.env ? { env: params.env } : {}
});
}
function assertAuthProfileMigrationSourcesUnchanged(candidate, receipts) {
const receiptByPath = new Map(receipts.map((receipt) => [receipt.sourcePath, receipt]));
for (const pathname of [
candidate.authPath,
candidate.statePath,
candidate.legacyPath
]) {
const receipt = receiptByPath.get(path.resolve(pathname));
if (fs.existsSync(pathname) !== Boolean(receipt)) throw new Error("legacy auth source set changed during migration; retry Doctor");
if (!receipt) continue;
if (createHash("sha256").update(fs.readFileSync(pathname)).digest("hex") !== receipt.sourceSha256) throw new Error("legacy auth source changed during migration; retry Doctor");
}
}
function parseAuthProfileMigrationSource(receipt) {
if (!receipt?.sourceBytes) return null;
try {
return JSON.parse(receipt.sourceBytes.toString("utf8"));
} catch {
return null;
}
}
function archivePreviouslyMigratedAuthProfileSource(receipt, result) {
if (!hasTerminalAuthProfileMigrationReceipt(receipt.sourceKey, receipt.env)) return false;
archiveAuthProfileMigrationSource(receipt);
result.changes.push(`Archived a previously migrated legacy auth source without replaying credentials (${shortenHomePath(receipt.archivePath)}).`);
return true;
}
function coerceLegacyOAuthFile(raw) {
if (!isRecord(raw)) return {
store: null,
rejectedEntries: 1
};
const profiles = {};
let rejectedEntries = 0;
for (const [provider, value] of Object.entries(raw)) {
if (!isRecord(value)) {
rejectedEntries += 1;
continue;
}
const credential = parseLegacyCredentialEntry({
...value,
type: "oauth",
provider
}, provider);
if (credential?.type === "oauth") profiles[`${provider}:default`] = credential;
else rejectedEntries += 1;
}
return {
store: Object.keys(profiles).length > 0 ? {
version: 1,
profiles
} : null,
rejectedEntries
};
}
function loadAuthProfileMigrationTargetStore(agentDir, loadStore = loadPersistedAuthProfileStore, database, env = process.env) {
const explicitSharedRead = agentDir === void 0 && database === void 0;
const inspection = explicitSharedRead ? inspectPersistedSharedAuthProfileStoreRaw(env) : inspectPersistedAuthProfileStoreRaw(agentDir, database);
const store = explicitSharedRead && loadStore === loadPersistedAuthProfileStore ? loadPersistedSharedAuthProfileStore(env) : loadStore(agentDir, database ? { database } : void 0);
if (store) return store;
if (inspection.status !== "missing") throw new Error("canonical auth profile store is unreadable; legacy source left in place");
if ((explicitSharedRead ? inspectPersistedSharedAuthProfileStateRaw(env) : inspectPersistedAuthProfileStateRaw(agentDir, database)).status === "unreadable") throw new Error("canonical auth profile state is unreadable; legacy source left in place");
return {
version: 1,
profiles: {},
...coerceAuthProfileState(explicitSharedRead ? readPersistedSharedAuthProfileStateRaw(env) : readPersistedAuthProfileStateRaw(agentDir, database))
};
}
function migrateLegacyOAuthFile(params) {
if (!fs.existsSync(params.oauthPath)) return;
const releaseSource = acquireAuthProfileMigrationSourceLocks([params.oauthPath]);
try {
migrateLockedLegacyOAuthFile(params);
} finally {
releaseSource();
}
}
function migrateLockedLegacyOAuthFile(params) {
const targetDatabasePath = resolveSharedAuthStorePath(params.env);
const sharedStateTarget = resolveSharedAuthStoreOwnership(params.env).location === "state-db";
const receipt = prepareAuthProfileSourceReceipt({
pathname: params.oauthPath,
targetDatabasePath,
targetTable: sharedStateTarget ? "auth_profile_stores" : "auth_profile_store",
targetStoreKey: sharedStateTarget ? "shared" : "primary",
now: params.now,
env: params.env
});
if (archivePreviouslyMigratedAuthProfileSource(receipt, params.result)) return;
const parsed = coerceLegacyOAuthFile(loadJsonFileThroughSymlink(params.oauthPath));
const imported = parsed.store;
if (!imported) {
finalizeAuthProfileMigrationSource(receipt, "archived-unparsed", { sourceLocked: true });
params.result.warnings.push(`Archived an unreadable legacy OAuth source without import; re-authenticate or recover it from ${shortenHomePath(receipt.archivePath)}.`);
return;
}
const existing = loadAuthProfileMigrationTargetStore(void 0, loadPersistedAuthProfileStore, void 0, params.env);
const importedProfileIds = new Set(Object.keys(imported.profiles));
const next = mergeImportedAuthProfiles({
store: existing,
profiles: imported.profiles,
existingProfileIds: new Set(Object.keys(existing.profiles))
});
const loaded = runAuthProfileWriteTransaction(void 0, (database, owner) => {
const authoritative = loadAuthProfileMigrationTargetStore(void 0, loadPersistedAuthProfileStore, database);
if (!isDeepStrictEqual(authoritative, existing)) throw new Error("canonical auth profile store changed during legacy OAuth migration");
saveAuthProfileStoreWithPreparedOwner(next, void 0, {
filterExternalAuthProfiles: false,
preserveStateProfileIds: collectAuthProfileStateProfileIds(coerceAuthProfileState(existing)),
syncExternalCli: false
}, database, owner);
const verified = loadPersistedAuthProfileStore(void 0, { database });
const verificationFailure = formatMissingAuthProfileSqliteVerification({
expected: next,
importedProfileIds,
loaded: verified
});
const mismatched = [...importedProfileIds].filter((profileId) => {
if (existing.profiles[profileId]) return false;
return !isDeepStrictEqual(verified?.profiles[profileId], imported.profiles[profileId]);
});
if (verificationFailure || mismatched.length > 0 || !verified) throw new Error("legacy OAuth import verification failed");
return verified;
}, { env: params.env });
receipt.expectedProfileSha256 = Object.fromEntries([...importedProfileIds].map((profileId) => [profileId, digestAuthProfileMigrationValue(loaded.profiles[profileId])]));
finalizeAuthProfileMigrationSource(receipt, parsed.rejectedEntries > 0 ? "archived-unparsed" : "completed", { sourceLocked: true });
if (parsed.rejectedEntries > 0) params.result.warnings.push(`Imported valid shared OAuth entries and archived ${parsed.rejectedEntries} rejected entr${parsed.rejectedEntries === 1 ? "y" : "ies"} for manual recovery.`);
params.result.changes.push(`Migrated shared legacy OAuth credentials into the shared-main SQLite owner (archive: ${shortenHomePath(receipt.archivePath)}).`);
}
/**
* Imports legacy auth profile JSON and state files into the per-agent SQLite store.
*
* JSON files are verified and atomically renamed to timestamped archives only after import.
* OAuth profiles that still depend on missing sidecar secrets migrate as unavailable ref-only rows.
*/
async function maybeMigrateAuthProfileJsonStoresToSqlite(params) {
const now = params.now ?? Date.now;
const env = params.env ?? process.env;
const loadMigratedStore = params.deps?.loadPersistedAuthProfileStore ?? loadPersistedAuthProfileStore;
const candidates = listAuthProfileSqliteMigrationCandidates(params.cfg, env);
const oauthPath = resolveLegacyOAuthPath(env);
const recoverableSources = /* @__PURE__ */ new Set();
let recoveryApproved = false;
const recoverCompleted = (receipt) => {
const candidate = candidates.find((entry) => [
entry.authPath,
entry.legacyPath,
...entry.agentDir === void 0 ? [oauthPath] : []
].includes(receipt.sourcePath));
if (!candidate) return false;
const raw = parseAuthProfileMigrationSource(receipt);
normalizeLegacyApiKeyAliasesForImport(raw);
const imported = receipt.sourcePath === oauthPath ? coerceLegacyOAuthFile(raw).store : coercePersistedAuthProfileStore(raw) ?? coerceLegacyFlatAuthProfileStore(raw);
if (!imported || !Object.values(imported.profiles).some(hasUsableAuthProfileCredential)) return false;
const inspection = candidate.agentDir ? inspectPersistedAuthProfileStoreRaw(candidate.agentDir) : inspectPersistedSharedAuthProfileStoreRaw(env);
const rawTarget = inspection.status === "missing" ? { profiles: {} } : inspection.status === "readable" ? inspection.raw : null;
if (!isRecord(rawTarget) || !isRecord(rawTarget.profiles)) return false;
const existing = rawTarget.profiles;
if (Object.entries(imported.profiles).some(([id, credential]) => Object.hasOwn(existing, id) || (isLegacyOpenAICodexProfileId(id) || isLegacyOpenAICodexProvider(credential.provider)) && Object.entries(existing).some(([key, entry]) => key.startsWith("openai:") || isRecord(entry) && entry.provider === OPENAI_PROVIDER_ID))) return false;
recoverableSources.add(receipt.sourcePath);
return recoveryApproved;
};
const warnings = [];
const resume = () => {
try {
return resumePendingAuthProfileMigrationArchives(env, recoverCompleted);
} catch (err) {
warnings.push(`Could not finalize an interrupted auth profile archive; legacy sources were left for recovery: ${String(err)}`);
return [];
}
};
const resumedChanges = resume();
const configStore = coerceLegacyConfigAuthProfileStore(params.cfg);
const hasLegacyOAuth = fs.existsSync(oauthPath) || recoverableSources.has(oauthPath);
const candidateSources = (candidate) => [
candidate.authPath,
candidate.statePath,
candidate.legacyPath
].filter((pathname) => fs.existsSync(pathname) || recoverableSources.has(pathname) || configStore && isDefaultAgentCandidate(candidate, params.cfg, env) && pathname === candidate.authPath);
const detected = candidates.filter((candidate) => candidateSources(candidate).length > 0);
const result = {
detected: [...detected.flatMap(candidateSources), ...hasLegacyOAuth ? [oauthPath] : []],
changes: resumedChanges,
configOwnerMigrationApplied: false,
warnings
};
if (warnings.length > 0 || detected.length === 0 && !hasLegacyOAuth) return result;
note([
...detected.map((candidate) => `- ${shortenHomePath(candidate.authPath)} / ${shortenHomePath(candidate.statePath)}`),
...hasLegacyOAuth ? [`- ${shortenHomePath(oauthPath)} (shared-main owner)`] : [],
`- ${formatCliCommand("openclaw doctor --fix")} imports legacy auth profile JSON into SQLite, verifies it, records a receipt, and archives the original bytes.`
].join("\n"), "Auth profile SQLite migration");
if (!await params.prompter.confirmAutoFix({
message: "Migrate auth profile JSON files into SQLite now?",
initialValue: true
})) return result;
if (recoverableSources.size > 0) {
recoveryApproved = true;
result.changes.push(...resume());
if (warnings.length > 0) return result;
}
const openAIProfileIdMap = params.openAICodexAuthProfileIdMap ?? collectOpenAICodexAuthProfileStoreIdMap({
cfg: params.cfg,
env
});
for (const candidate of detected) {
const configOwnerCandidate = isDefaultAgentCandidate(candidate, params.cfg, env);
let releaseSources;
try {
const candidateSourcePaths = [
candidate.authPath,
candidate.statePath,
candidate.legacyPath
];
for (const pathname of candidateSourcePaths) fs.mkdirSync(path.dirname(pathname), { recursive: true });
releaseSources = acquireAuthProfileMigrationSourceLocks(candidateSourcePaths);
const targetDatabasePath = resolveMigrationTargetDatabasePath(candidate.agentDir, env);
const sharedStateTarget = candidate.agentDir === void 0 && resolveSharedAuthStoreOwnership(env).location === "state-db";
const transactionAgentDir = sharedStateTarget || candidate.agentDir !== void 0 ? candidate.agentDir : resolveSharedMainAuthAgentDir(env);
let sourceReceipts = candidateSourcePaths.filter(fs.existsSync).map((pathname) => prepareAuthProfileSourceReceipt({
pathname,
targetDatabasePath,
targetTable: pathname === candidate.statePath ? "auth_profile_state" : sharedStateTarget ? "auth_profile_stores" : "auth_profile_store",
targetStoreKey: sharedStateTarget ? "shared" : "primary",
now,
env
}));
sourceReceipts = sourceReceipts.filter((receipt) => !archivePreviouslyMigratedAuthProfileSource(receipt, result));
assertAuthProfileMigrationSourcesUnchanged(candidate, sourceReceipts);
if (sourceReceipts.length === 0 && !configStore) continue;
const receiptByPath = new Map(sourceReceipts.map((receipt) => [receipt.sourcePath, receipt]));
const rawStore = parseAuthProfileMigrationSource(receiptByPath.get(path.resolve(candidate.authPath)));
const rawState = parseAuthProfileMigrationSource(receiptByPath.get(path.resolve(candidate.statePath)));
const openAIProviderRepair = canonicalizeLegacyOpenAIAuthStore(rawStore, rawState, openAIProfileIdMap);
const unresolvedSidecarProfileIds = new Set(collectUnresolvedLegacyOAuthSidecarProfileIds(rawStore));
const unresolvedSidecarWarning = unresolvedSidecarProfileIds.size > 0 ? `Migrated ${unresolvedSidecarProfileIds.size} legacy OAuth sidecar profile${unresolvedSidecarProfileIds.size === 1 ? "" : "s"} from ${shortenHomePath(candidate.authPath)} into SQLite as configured-unavailable without credentials; re-authenticate ${unresolvedSidecarProfileIds.size === 1 ? "this profile" : "these profiles"} to restore access.` : void 0;
const awsSdkMarkerStore = isRecord(rawStore) && isRecord(rawStore.profiles) ? resolveAwsSdkAuthProfileMarkerStore(candidate) : null;
if (awsSdkMarkerStore && isRecord(rawStore)) {
const configProfiles = ensureConfigAuthProfiles(params.cfg);
for (const marker of awsSdkMarkerStore.profiles) configProfiles[marker.profileId] = {
provider: marker.provider,
mode: "aws-sdk",
...marker.email ? { email: marker.email } : {},
...marker.displayName ? { displayName: marker.displayName } : {}
};
removeAwsSdkProfileMarkers(rawStore, awsSdkMarkerStore.profiles.map((profile) => profile.profileId));
result.configChanged = true;
}
normalizeLegacyApiKeyAliasesForImport(rawStore);
const maybeCanonicalStore = coercePersistedAuthProfileStore(rawStore) ?? coerceLegacyFlatAuthProfileStore(rawStore) ?? null;
const canonicalStore = hasImportableAuthProfileStore(maybeCanonicalStore) ? maybeCanonicalStore : null;
const configCanonicalStore = configStore && configOwnerCandidate ? configStore : null;
const legacyStore = coerceLegacyAuthStore(parseAuthProfileMigrationSource(receiptByPath.get(path.resolve(candidate.legacyPath))));
const state = coerceAuthProfileState(rawState);
if (!canonicalStore && !configCanonicalStore && !legacyStore && !hasAuthProfileState(state) && !awsSdkMarkerStore) {
if (sourceReceipts.length > 0) {
const archived = sourceReceipts.map((receipt) => {
finalizeAuthProfileMigrationSource(receipt, "archived-unparsed", { sourceLocked: true });
return receipt.archivePath;
});
result.warnings.push(unresolvedSidecarWarning ?? `Archived unparseable auth profile input without import for ${shortenHomePath(candidate.authPath)} (${archived.map(shortenHomePath).join(", ")}).`);
continue;
}
result.warnings.push(`Left auth profile JSON in place for ${shortenHomePath(candidate.authPath)} because no importable auth profiles or state were found.`);
continue;
}
const existing = loadAuthProfileMigrationTargetStore(candidate.agentDir, loadMigratedStore, void 0, env);
const existingProfileIds = new Set(Object.keys(existing.profiles));
const existingState = coerceAuthProfileState(existing);
let next = { ...existing };
let verifiedStore = existing;
const importedProfileIds = /* @__PURE__ */ new Set();
const legacyAsStore = {
version: 1,
profiles: {}
};
if (legacyStore) applyLegacyAuthStore(legacyAsStore, legacyStore);
for (const imported of [
legacyAsStore,
canonicalStore,
configCanonicalStore
]) {
if (!imported) continue;
Object.keys(imported.profiles).forEach((id) => importedProfileIds.add(id));
const fromConfig = imported === configCanonicalStore;
next = mergeImportedAuthProfiles({
store: next,
profiles: imported.profiles,
existingProfileIds: fromConfig ? new Set(Object.keys(next.profiles)) : existingProfileIds,
replaceExistingWithoutCredential: fromConfig
});
if (imported === canonicalStore) {
next.version = Math.max(next.version, imported.version);
next = mergeImportedAuthProfileState({
store: next,
state: coerceAuthProfileState(imported),
existingState
});
}
}
if (hasAuthProfileState(state)) next = mergeImportedAuthProfileState({
store: next,
state,
existingState
});
if (canonicalStore || configCanonicalStore || legacyStore || hasAuthProfileState(state)) {
const stateProfileIds = [
state,
canonicalStore,
configCanonicalStore
].flatMap((store) => store ? collectAuthProfileStateProfileIds(coerceAuthProfileState(store)) : []);
try {
assertAuthProfileMigrationSourcesUnchanged(candidate, sourceReceipts);
verifiedStore = runAuthProfileWriteTransaction(transactionAgentDir, (database, owner) => {
const authoritative = loadAuthProfileMigrationTargetStore(candidate.agentDir, loadMigratedStore, database);
if (!isDeepStrictEqual(authoritative, existing)) throw new Error("canonical auth profile store changed during legacy migration");
saveAuthProfileStoreWithPreparedOwner(next, candidate.agentDir, {
filterExternalAuthProfiles: false,
preserveStateProfileIds: stateProfileIds,
syncExternalCli: false
}, database, owner);
const loaded = loadMigratedStore(candidate.agentDir, { database });
const persistedStores = {
isMainStore: resolveMigrationTargetDatabasePath(candidate.agentDir, env) === resolveSharedAuthStorePath(env),
localStore: loaded,
mainStore: resolveMigrationTargetDatabasePath(candidate.agentDir, env) === resolveSharedAuthStorePath(env) ? loaded : loadPersistedSharedAuthProfileStore(env)
};
const dedupedToMainProfileIds = new Set([...importedProfileIds].filter((profileId) => {
const credential = next.profiles[profileId];
return credential !== void 0 && !loaded?.profiles[profileId] && isInheritedMainOAuthCredentialFromStores({
profileId,
credential,
persistedStores
});
}));
const verifiableProfileIds = new Set([...importedProfileIds].filter((profileId) => !dedupedToMainProfileIds.has(profileId)));
const verificationFailure = formatMissingAuthProfileSqliteVerification({
expected: next,
importedProfileIds: verifiableProfileIds,
loaded
});
const mismatchedCredential = [...verifiableProfileIds].some((profileId) => {
if (existingProfileIds.has(profileId)) return false;
return !isDeepStrictEqual(loaded?.profiles[profileId], next.profiles[profileId]);
});
if (verificationFailure || mismatchedCredential || !loaded) throw new AuthProfileMigrationVerificationError(verificationFailure);
return loaded;
}, { env });
} catch (error) {
if (!(error instanceof AuthProfileMigrationVerificationError)) throw error;
result.warnings.push(`Left auth profile JSON in place for ${shortenHomePath(candidate.authPath)} because SQLite verification failed${error.detail ? ` (${error.detail})` : ""}.`);
continue;
}
if (configCanonicalStore && stripImportedConfigAuthProfileCredentials(params.cfg, configCanonicalStore)) result.configChanged = true;
}
const expectedProfileSha256 = Object.fromEntries([...importedProfileIds].flatMap((profileId) => {
const profileValue = verifiedStore.profiles[profileId];
return profileValue ? [[profileId, digestAuthProfileMigrationValue(profileValue)]] : [];
}));
const expectedStateSha256 = digestAuthProfileMigrationValue(candidate.agentDir ? readPersistedAuthProfileStateRaw(candidate.agentDir) : readPersistedSharedAuthProfileStateRaw(env));
const canonicalSourceCarriesState = canonicalStore ? hasAuthProfileState(coerceAuthProfileState(canonicalStore)) : false;
for (const receipt of sourceReceipts) {
if (receipt.targetTable !== "auth_profile_state") receipt.expectedProfileSha256 = expectedProfileSha256;
if (receipt.targetTable === "auth_profile_state" || receipt.sourcePath === candidate.authPath && canonicalSourceCarriesState) receipt.expectedStateSha256 = expectedStateSha256;
}
assertAuthProfileMigrationSourcesUnchanged(candidate, sourceReceipts);
const archives = sourceReceipts.map((receipt) => {
finalizeAuthProfileMigrationSource(receipt, "completed", { sourceLocked: true });
return receipt.archivePath;
});
const archiveText = archives.length > 0 ? `archive${archives.length === 1 ? "" : "s"}: ${archives.map(shortenHomePath).join(", ")}` : "no legacy JSON backup needed";
result.changes.push(`Migrated auth profile JSON for ${shortenHomePath(candidate.authPath)} into SQLite (${archiveText}).`);
if (configOwnerCandidate) result.configOwnerMigrationApplied = true;
if (unresolvedSidecarWarning) result.warnings.push(unresolvedSidecarWarning);
if (openAIProviderRepair !== null) result.changes.push(`Migrated ${openAIProviderRepair} OpenAI Codex auth profile(s) in ${shortenHomePath(candidate.authPath)} to provider "openai".`);
if (awsSdkMarkerStore) result.changes.push(`Moved aws-sdk profile metadata from ${shortenHomePath(candidate.authPath)} to auth.profiles before removing the legacy auth profile JSON.`);
} catch (err) {
result.warnings.push(`Failed to migrate auth profile JSON for ${shortenHomePath(candidate.authPath)}: ${String(err)}`);
} finally {
releaseSources?.();
}
}
const sharedMainAgentDir = resolveSharedMainAuthAgentDir(env);
const sharedMainCredentialSourceRemains = [resolveLegacyAuthProfilesPath(sharedMainAgentDir), resolveLegacyFlatAuthPath(sharedMainAgentDir)].some((pathname) => fs.existsSync(pathname));
if (hasLegacyOAuth && sharedMainCredentialSourceRemains) result.warnings.push(`Deferred shared legacy OAuth migration until higher-priority shared-main credential sources are resolved by ${formatCliCommand("openclaw doctor --fix")}.`);
else if (hasLegacyOAuth) try {
migrateLegacyOAuthFile({
oauthPath,
env,
now,
result
});
} catch (err) {
result.warnings.push(`Failed to migrate shared legacy OAuth credentials; the source was left in place: ${String(err)}`);
}
clearRuntimeAuthProfileStoreSnapshots();
clearAuthProfileMigrationDiagnostics();
return result;
}
function resolveAwsSdkAuthProfileMarkerStore(candidate) {
if (!fs.existsSync(candidate.authPath)) return null;
const raw = loadJsonFileThroughSymlink(candidate.authPath);
if (!isRecord(raw) || !isRecord(raw.profiles)) return null;
const markers = [];
for (const [profileId, value] of Object.entries(raw.profiles)) {
if (!isRecord(value)) continue;
if ((readNonBlankString(value.type) ?? readNonBlankString(value.mode)) !== "aws-sdk") continue;
const provider = readNonBlankString(value.provider) ?? extractProviderFromProfileId(profileId);
if (!provider || !isSafeLegacyProviderKey(provider)) continue;
markers.push({
profileId,
provider,
...readNonBlankString(value.email) ? { email: readNonBlankString(value.email) } : {},
...readNonBlankString(value.displayName) ? { displayName: readNonBlankString(value.displayName) } : {}
});
}
return markers.length > 0 ? {
...candidate,
raw,
profiles: markers
} : null;
}
function ensureConfigAuthProfiles(config) {
const root = config;
const auth = isRecord(root.auth) ? root.auth : {};
if (root.auth !== auth) root.auth = auth;
if (!isRecord(auth.profiles)) auth.profiles = {};
return auth.profiles;
}
function removeAwsSdkProfileMarkers(raw, profileIds) {
if (!isRecord(raw.profiles)) return;
for (const profileId of profileIds) delete raw.profiles[profileId];
}
const LEGACY_OPENAI_CODEX_PROVIDER_ID = "openai-codex";
const OPENAI_PROVIDER_ID = "openai";
function isLegacyOpenAICodexProvider(value) {
return typeof value === "string" && value.trim().toLowerCase() === LEGACY_OPENAI_CODEX_PROVIDER_ID;
}
function isLegacyOpenAICodexProfileId(profileId) {
return profileId.trim().toLowerCase().startsWith(`${LEGACY_OPENAI_CODEX_PROVIDER_ID}:`);
}
function canonicalOpenAIProfileSuffix(profileId) {
return profileId.slice(profileId.indexOf(":") + 1).trim() || "default";
}
function allocateOpenAIProfileId(legacyProfileId, occupied) {
const suffix = canonicalOpenAIProfileSuffix(legacyProfileId);
const direct = `${OPENAI_PROVIDER_ID}:${suffix}`;
if (!occupied.has(direct)) {
occupied.add(direct);
return direct;
}
const chatgpt = `${OPENAI_PROVIDER_ID}:chatgpt-${suffix}`;
if (!occupied.has(chatgpt)) {
occupied.add(chatgpt);
return chatgpt;
}
for (let index = 2;; index += 1) {
const candidate = `${chatgpt}-${index}`;
if (!occupied.has(candidate)) {
occupied.add(candidate);
return candidate;
}
}
}
function canonicalizeOpenAIProfileEntries(profiles, options) {
const occupied = new Set(Object.keys(profiles).filter((id) => !isLegacyOpenAICodexProfileId(id)));
const reservedMappedIds = new Set(options?.profileIdMap?.values() ?? []);
const profileIdMap = /* @__PURE__ */ new Map();
let changed = false;
for (const [profileId, rawProfile] of Object.entries({ ...profiles })) {
if (!isRecord(rawProfile)) continue;
const legacyId = isLegacyOpenAICodexProfileId(profileId);
const legacyProvider = isLegacyOpenAICodexProvider(rawProfile.provider);
if (!legacyId && !legacyProvider) continue;
const mappedProfileId = legacyId ? options?.profileIdMap?.get(profileId) : void 0;
const nextProfileId = mappedProfileId && !occupied.has(mappedProfileId) ? mappedProfileId : legacyId ? allocateOpenAIProfileId(profileId, /* @__PURE__ */ new Set([...occupied, ...reservedMappedIds])) : profileId;
occupied.add(nextProfileId);
const nextProfile = {
...rawProfile,
provider: OPENAI_PROVIDER_ID
};
if (nextProfileId !== profileId) {
delete profiles[profileId];
profileIdMap.set(profileId, nextProfileId);
}
profiles[nextProfileId] = nextProfile;
changed = true;
}
return {
profileIdMap,
changed
};
}
function replaceMappedProfileId(value, profileIdMap) {
if (typeof value === "string") return profileIdMap.get(value) ?? value;
if (Array.isArray(value)) {
let changed = false;
const next = value.map((entry) => {
const replaced = replaceMappedProfileId(entry, profileIdMap);
changed ||= replaced !== entry;
return replaced;
});
return changed ? next : value;
}
if (!isRecord(value)) return value;
let changed = false;
for (const [key, entry] of Object.entries(value)) {
const replaced = replaceMappedProfileId(entry, profileIdMap);
if (replaced !== entry) {
value[key] = replaced;
changed = true;
}
}
return changed ? value : value;
}
const AUTH_PROFILE_REF_KEYS = /* @__PURE__ */ new Set(["authProfileId"]);
function rewriteMappedAuthProfileRefs(value, profileIdMap) {
if (Array.isArray(value)) return value.reduce((changed, entry) => rewriteMappedAuthProfileRefs(entry, profileIdMap) || changed, false);
if (!isRecord(value)) return false;
let changed = false;
for (const [key, entry] of Object.entries(value)) {
if (AUTH_PROFILE_REF_KEYS.has(key) && typeof entry === "string") {
const replaced = profileIdMap.get(entry);
if (replaced && replaced !== entry) {
value[key] = replaced;
changed = true;
}
continue;
}
changed = rewriteMappedAuthProfileRefs(entry, profileIdMap) || changed;
}
return changed;
}
function canonicalizeOpenAIAuthOrder(auth, profileIdMap, options) {
if (!isRecord(auth.order)) return false;
const order = auth.order;
let changed = false;
const existingCanonicalOrder = Array.isArray(order[OPENAI_PROVIDER_ID]) ? [...order[OPENAI_PROVIDER_ID]] : [];
const legacyOrder = Array.isArray(order[LEGACY_OPENAI_CODEX_PROVIDER_ID]) ? order[LEGACY_OPENAI_CODEX_PROVIDER_ID] : [];
const unresolvedLegacyOrder = options?.preserveUnmappedLegacyIds ? legacyOrder.filter((entry) => typeof entry !== "string" || isLegacyOpenAICodexProfileId(entry) && !profileIdMap.has(entry)) : [];
const canonicalOrder = [...legacyOrder.filter((entry) => !unresolvedLegacyOrder.includes(entry)), ...existingCanonicalOrder];
const occupiedProfileIds = new Set(canonicalOrder.filter((entry) => typeof entry === "string" && !isLegacyOpenAICodexProfileId(entry)));
for (const profileId of profileIdMap.values()) occupiedProfileIds.add(profileId);
if (legacyOrder.length > unresolvedLegacyOrder.length) {
if (unresolvedLegacyOrder.length > 0) order[LEGACY_OPENAI_CODEX_PROVIDER_ID] = unresolvedLegacyOrder;
else delete order[LEGACY_OPENAI_CODEX_PROVIDER_ID];
changed = true;
}
const rewritten = canonicalOrder.map((entry) => {
if (typeof entry !== "string") return entry;
const mapped = profileIdMap.get(entry);
if (mapped) return mapped;
if (!isLegacyOpenAICodexProfileId(entry)) return entry;
if (options?.preserveUnmappedLegacyIds) return entry;
const canonicalProfileId = allocateOpenAIProfileId(entry, occupiedProfileIds);
profileIdMap.set(entry, canonicalProfileId);
return canonicalProfileId;
}).filter((entry, index, entries) => typeof entry !== "string" || entries.indexOf(entry) === index);
if (rewritten.length > 0) order[OPENAI_PROVIDER_ID] = rewritten;
else if (OPENAI_PROVIDER_ID in order) delete order[OPENAI_PROVIDER_ID];
return changed || rewritten.some((entry, index) => entry !== canonicalOrder[index]);
}
function renameMappedProfileIdKeys(record, profileIdMap) {
let changed = false;
for (const [key, value] of Object.entries({ ...record })) {
const nextKey = profileIdMap.get(key);
if (!nextKey || nextKey === key) continue;
delete record[key];
record[nextKey] = value;
changed = true;
}
return changed;
}
function canonicalizeOpenAILastGood(record, profileIdMap, options) {
let changed = false;
const legacyValue = record[LEGACY_OPENAI_CODEX_PROVIDER_ID];
const canonicalValue = record[OPENAI_PROVIDER_ID];
const mappedLegacyValue = typeof legacyValue === "string" ? profileIdMap.get(legacyValue) : void 0;
if (legacyValue !== void 0 && (!options?.preserveUnmappedLegacyIds || mappedLegacyValue !== void 0)) {
delete record[LEGACY_OPENAI_CODEX_PROVIDER_ID];
changed = true;
if (canonicalValue === void 0 && typeof legacyValue === "string") record[OPENAI_PROVIDER_ID] = mappedLegacyValue ?? legacyValue;
}
if (typeof record[OPENAI_PROVIDER_ID] === "string") {
const mapped = profileIdMap.get(record[OPENAI_PROVIDER_ID]);
if (mapped) {
record[OPENAI_PROVIDER_ID] = mapped;
changed = true;
}
}
return changed;
}
/**
* Canonicalizes config references from the legacy OpenAI Codex provider id to OpenAI.
*
* The optional map lets config and store repairs share deterministic profile ids when both surfaces
* contain the same legacy profile.
*/
function maybeRepairOpenAICodexAuthConfig(cfg, options) {
const config = structuredClone(cfg);
const root = config;
const auth = isRecord(root.auth) ? root.auth : void 0;
const profileIdMap = new Map(options?.profileIdMap);
let changed = false;
if (isRecord(auth?.profiles)) {
const rewrite = canonicalizeOpenAIProfileEntries(auth.profiles, { profileIdMap });
for (const [from, to] of rewrite.profileIdMap) profileIdMap.set(from, to);
changed ||= rewrite.changed;
}
if (auth) {
const orderChanged = canonicalizeOpenAIAuthOrder(auth, profileIdMap);
changed ||= orderChanged;
}
if (profileIdMap.size > 0 && rewriteMappedAuthProfileRefs(config, profileIdMap)) changed = true;
if (!changed) return {
config,
changes: [],
warnings: []
};
return {
config,
changes: ["Migrated legacy OpenAI Codex auth profile config to the canonical OpenAI provider."],
warnings: []
};
}
function canonicalizeLegacyOpenAIAuthStore(raw, stateRaw, profileIdMap) {
if (!isRecord(raw) || !isRecord(raw.profiles)) {
if (isRecord(stateRaw)) canonicalizeOpenAIAuthRotationState(stateRaw, new Map(profileIdMap));
return null;
}
const rewrite = canonicalizeOpenAIProfileEntries(raw.profiles, { profileIdMap });
const effectiveProfileIdMap = new Map([...profileIdMap, ...rewrite.profileIdMap]);
const rotation = canonicalizeOpenAIAuthRotationState(raw, effectiveProfileIdMap);
if (isRecord(stateRaw)) canonicalizeOpenAIAuthRotationState(stateRaw, effectiveProfileIdMap);
if (rewrite.profileIdMap.size > 0) replaceMappedProfileId(raw, rewrite.profileIdMap);
return rewrite.changed || rotation ? rewrite.profileIdMap.size : null;
}
function canonicalizeOpenAIAuthRotationState(auth, profileIdMap) {
const options = { preserveUnmappedLegacyIds: true };
const orderChanged = canonicalizeOpenAIAuthOrder(auth, profileIdMap, options);
const usageChanged = isRecord(auth.usageStats) ? renameMappedProfileIdKeys(auth.usageStats, profileIdMap) : false;
const lastGoodChanged = isRecord(auth.lastGood) ? canonicalizeOpenAILastGood(auth.lastGood, profileIdMap, options) : false;
return orderChanged || usageChanged || lastGoodChanged;
}
function recoverArchivedOpenAICodexAuthProfileIdMap(params) {
const recovered = /* @__PURE__ */ new Map();
const ambiguous = /* @__PURE__ */ new Set();
const agentDirs = [resolveSharedMainAuthAgentDir(params.env), ...params.candidates.flatMap((candidate) => candidate.agentDir ? [candidate.agentDir] : [])];
const archives = listLegacyAuthProfileArchives({
agentDirs,
env: params.env
}).filter((archive) => archive.kind === "auth-profiles");
for (const candidate of params.candidates) {
const canonicalProfiles = (candidate.agentDir ? loadPersistedAuthProfileStore(candidate.agentDir) : loadPersistedSharedAuthProfileStore(params.env))?.profiles;
if (!canonicalProfiles) continue;
for (const archive of archives.filter((entry) => entry.path.startsWith(`${candidate.authPath}.migrated-`))) try {
const sourceBytes = fs.readFileSync(archive.path);
const sourceSha256 = createHash("sha256").update(sourceBytes).digest("hex");
const sourceKey = `auth-profile-v2:${createHash("sha256").update(`${path.resolve(candidate.authPath)}\0${sourceSha256}`).digest("hex")}`;
const receipt = readLegacyMigrationReceipt(sourceKey, params.env);
if (!receipt?.removedSource || receipt.sourceSha256 !== sourceSha256) continue;
const report = JSON.parse(receipt.reportJson);
const sharedStateTarget = candidate.agentDir === void 0 && resolveSharedAuthStoreOwnership(params.env).location === "state-db";
if (!isRecord(report) || report.format !== "auth-profile-json-to-sqlite-v2" || report.completionStatus !== "completed" || report.targetTable !== (sharedStateTarget ? "auth_profile_stores" : "auth_profile_store") || typeof report.archivePath !== "string" || path.resolve(report.archivePath) !== path.resolve(archive.path) || typeof report.targetDatabasePath !== "string" || path.resolve(report.targetDatabasePath) !== path.resolve(resolveMigrationTargetDatabasePath(candidate.agentDir, params.env)) || !isRecord(report.expectedProfileSha256)) continue;
const archivedStore = JSON.parse(sourceBytes.toString("utf8"));
if (!isRecord(archivedStore) || !isRecord(archivedStore.profiles)) continue;
for (const [legacyProfileId, rawCredential] of Object.entries(archivedStore.profiles)) {
if (!isLegacyOpenAICodexProfileId(legacyProfileId) || !isRecord(rawCredential)) continue;
const archivedCredential = parseLegacyCredentialEntry({
...rawCredential,
provider: "openai"
}, "openai");
if (archivedCredential?.type !== "oauth") continue;
const matches = Object.entries(report.expectedProfileSha256).flatMap(([canonicalProfileId, expectedSha256]) => {
const credential = canonicalProfiles[canonicalProfileId];
return typeof expectedSha256 === "string" && credential?.type === "oauth" && credential.provider === "openai" && (hasMatchingOAuthIdentity(archivedCredential, credential) || areOAuthCredentialsEquivalent(archivedCredential, credential)) ? [canonicalProfileId] : [];
});
if (matches.length !== 1) continue;
const canonicalProfileId = matches[0];
const previous = recovered.get(legacyProfileId);
if (previous && previous !== canonicalProfileId) {
recovered.delete(legacyProfileId);
ambiguous.add(legacyProfileId);
} else if (!ambiguous.has(legacyProfileId)) recovered.set(legacyProfileId, canonicalProfileId);
}
} catch {}
}
return recovered;
}
/** Collects collision-safe OpenAI profile ids across config, SQLite, and legacy agent stores. */
function collectOpenAICodexAuthProfileStoreIdMap(params) {
const env = params.env ?? process.env;
const occupiedProfileIds = /* @__PURE__ */ new Set();
const legacyProfileIds = /* @__PURE__ */ new Set();
const profileIdMap = /* @__PURE__ */ new Map();
const candidates = listAuthProfileRepairCandidates(params.cfg, env);
const addProfileIds = (profileIds) => {
for (const profileId of profileIds) if (isLegacyOpenAICodexProfileId(profileId)) legacyProfileIds.add(profileId);
else occupiedProfileIds.add(profileId);
};
addProfileIds(Object.keys(params.cfg.auth?.profiles ?? {}));
for (const candidate of candidates) {
const persistedStore = candidate.agentDir ? loadPersistedAuthProfileStore(candidate.agentDir) : loadPersistedSharedAuthProfileStore(env);
addProfileIds(Object.keys(persistedStore?.profiles ?? {}));
if (!fs.existsSync(candidate.authPath)) continue;
const raw = loadJsonFileThroughSymlink(candidate.authPath);
if (!isRecord(raw) || !isRecord(raw.profiles)) continue;
addProfileIds(Object.keys(raw.profiles));
}
for (const profileId of [...legacyProfileIds].toSorted((a, b) => a.localeCompare(b))) profileIdMap.set(profileId, allocateOpenAIProfileId(profileId, occupiedProfileIds));
for (const [legacyProfileId, canonicalProfileId] of recoverArchivedOpenAICodexAuthProfileIdMap({
candidates,
env
})) if (!profileIdMap.has(legacyProfileId)) profileIdMap.set(legacyProfileId, canonicalProfileId);
return profileIdMap;
}
//#endregion
export { maybeMigrateAuthProfileJsonStoresToSqlite as n, maybeRepairOpenAICodexAuthConfig as r, collectOpenAICodexAuthProfileStoreIdMap as t };