openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
1,816 lines • 96.5 kB
JavaScript
import { m as readNonBlankString } from "./string-coerce-1VuiLaaR.mjs";
import { c as isRecord, l as isStringRecord } from "./record-coerce-TIsquoVR.mjs";
import { t as isBlockedObjectKey } from "./prototype-keys-CuYw53fZ.mjs";
import { a as coerceSecretRef } from "./types.secrets-DsPRn9Dz.mjs";
import { p as shortenHomePath } from "./utils-DgZn2dLu.mjs";
import { t as formatCliCommand } from "./command-format-DwEdrDKL.mjs";
import { a as getNodeSqliteKysely, i as executeSqliteQueryTakeFirstSync, r as executeSqliteQuerySync } from "./kysely-sync-K7ftY-g4.mjs";
import { n as assertExistingDatabaseIdentity, u as readDatabasePathIdentitySync } from "./sqlite-worker-identity-KSR14F6C.mjs";
import { t as openNodeSqliteDatabase } from "./node-sqlite-XIcwmOrl.mjs";
import { l as getOpenClawDatabaseMaintenanceScope } from "./openclaw-state-db-async-lifecycle-DLKxSPCk.mjs";
import { Fn as object, Jn as string, Nt as array, xn as literal } from "./schemas-BOYIvvln.mjs";
import { y as requireOpenClawStateDatabaseIdentity } from "./openclaw-state-db-cache-BKCnj4of.mjs";
import { v as readOpenClawAgentDatabaseIdentity } from "./openclaw-agent-db-validation-cache-_HDeZNQr.mjs";
import { f as withExistingOpenClawStateDatabaseReadOnly } from "./openclaw-state-db-readonly-CC57AD_q.mjs";
import "./agent-scope-config-CDYpzA0r.mjs";
import { n as listAgentEntries } from "./agent-roster-BXD1uMJD.mjs";
import { l as runOpenClawStateWriteTransaction } from "./openclaw-state-db-D1jWDzK3.mjs";
import { c as recordLegacyMigrationSource, i as readLegacyMigrationReceiptFromDatabase, o as recordLegacyMigrationReceipt, r as readLegacyMigrationReceipt, s as recordLegacyMigrationRun } from "./state-migrations.receipts-gEcgM3Aq.mjs";
import { t as splitTrailingAuthProfile } from "./model-ref-profile-BNjB2hED.mjs";
import { l as acquireFileLockSyncWithRetry } from "./update-managed-service-handoff-database-BzJlW6Xp.mjs";
import { t as isChannelConfigMetadataKey } from "./config-metadata-aX1D2IMg.mjs";
import { M as collectConfiguredModelRefs } from "./runtime-snapshot-DYptB0Cf.mjs";
import { n as OPENAI_PROVIDER_ID } from "./openai-routing-CI5rYN6I.mjs";
import { t as listMutableCodexRouteAgentEntries } from "./codex-route-agent-entries-B6LYojZK.mjs";
import { c as resolveSharedAuthStorePath, l as resolveSharedMainAuthAgentDir, o as resolveSharedAuthStoreOwnership } from "./path-resolve-CIRhdX6A.mjs";
import { a as readAuthProfileJsonCellText, i as inspectAuthProfileJsonCell } from "./sqlite-json-P6pbG3re.mjs";
import { O as listLegacyAuthProfileArchives, a as inspectPersistedAuthProfileStoreRaw, b as writePersistedAuthProfileStateRaw, d as readPersistedAuthProfileStoreRaw, f as readPersistedSharedAuthProfileStateRaw, g as resolveAuthProfileDatabasePath, i as inspectPersistedAuthProfileStateRaw, j as resolveLegacyOAuthPath, l as readAuthProfileStateJsonTextReadOnly, o as inspectPersistedSharedAuthProfileStateRaw, p as readPersistedSharedAuthProfileStoreRaw, s as inspectPersistedSharedAuthProfileStoreRaw, u as readPersistedAuthProfileStateRaw, v as runAuthProfileWriteTransaction, x as writePersistedAuthProfileStoreRaw } from "./sqlite-CW8wNN4G.mjs";
import { n as normalizeSecretInput } from "./normalize-secret-input-DBJvdk-L.mjs";
import { a as loadPersistedSharedAuthProfileStore, f as coerceAuthProfileState, r as loadPersistedAuthProfileStore } from "./persisted-BVyWmvfF.mjs";
import { t as loadJsonFileThroughSymlink } from "./json-file-5C1aYJVp.mjs";
import { n as hasMatchingOAuthIdentity, t as areOAuthCredentialsEquivalent } from "./oauth-shared-CFtD6sCU.mjs";
import { d as renameUserProfileAuthLinks } from "./user-model-accounts-sYn17idc.mjs";
import { i as clearRuntimeAuthProfileStoreSnapshots } from "./runtime-snapshots-kS-hnXTd.mjs";
import { _ as normalizeLegacyAuthProfileFields, f as applyLegacyAuthStore, g as hasUsableAuthProfileCredential, h as coerceLegacyFlatCredential, m as coerceLegacyAuthStore, o as clearAuthProfileMigrationDiagnostics, p as coerceLegacyAuthProfileStore, u as readLegacyAuthProfileProviders, v as normalizeLegacyCredentialFields, y as parseLegacyCredentialEntry } from "./legacy-source-diagnostic-Cum2WXHU.mjs";
import { x as isInheritedMainOAuthCredentialFromStores } from "./runtime-snapshot-owner-BFahRX4y.mjs";
import { f as saveAuthProfileStoreWithPreparedOwner } from "./store-runtime-DZk7O5wt.mjs";
import { r as resolveLegacyInheritedAuthAgentDir } from "./legacy-inherited-auth-dir-DmZI0WMx.mjs";
import { r as stripImportedConfigAuthProfileCredentials, t as ensureConfigAuthProfiles } from "./doctor-auth-profile-config-GX4pYfLQ.mjs";
import { t as note } from "./note-Byxgt4MB.mjs";
import { t as createVerifiedSqliteSnapshot } from "./sqlite-snapshot-c6hBDowh.mjs";
import { n as assertNoRetiredStateFiles } from "./state-migrations.retired-files-Dlsbs5FH.mjs";
import { a as resolveLegacyAuthStatePath, n as listAuthProfileRepairCandidates, o as resolveLegacyFlatAuthPath, r as listReferencedLegacyOAuthSidecarPaths } from "./doctor-auth-legacy-paths-D2U_pugg.mjs";
import { n as resolveLegacyRuntimeModelProviderAlias } from "./legacy-runtime-model-providers-DkyokuxH.mjs";
import { n as repairModelRefAuthProfile, r as repairRetiredConfigModelRefs } from "./retired-model-ref-repair-Bb8dugZD.mjs";
import { t as inspectAuthDatabaseFiles } from "./stale-auth-order-store-l15QP_P7.mjs";
import fs from "node:fs";
import path from "node:path";
import { isDeepStrictEqual } from "node:util";
import { createHash, randomUUID } from "node:crypto";
//#region src/plugins/auth-profile-config-refs.ts
/** Installed plugins can predate Doctor's rename map; these fields reference host credentials. */
function rewritePluginAuthProfileRefs(config, profileIdMap) {
let changed = false;
const rewrite = (value) => {
if (Array.isArray(value)) {
value.forEach(rewrite);
return;
}
if (!isRecord(value)) return;
for (const [key, entry] of Object.entries(value)) if ((key === "authProfileId" || key === "defaultAuthProfileId") && typeof entry === "string") {
const replacement = profileIdMap.get(entry.trim());
if (replacement && replacement !== entry) {
value[key] = replacement;
changed = true;
}
} else rewrite(entry);
};
for (const entry of Object.values(config.plugins?.entries ?? {})) rewrite(entry.config);
for (const [channel, settings] of Object.entries(config.channels ?? {})) if (!isChannelConfigMetadataKey(channel)) rewrite(settings);
return changed;
}
//#endregion
//#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 = [];
const releaseAll = () => {
for (const release of releases.toReversed()) release();
};
try {
for (const sourcePath of [...new Set(sourcePaths.map((entry) => path.resolve(entry)))].toSorted()) releases.push(acquireFileLockSyncWithRetry(sourcePath));
} catch (error) {
releaseAll();
throw error;
}
return releaseAll;
}
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 && 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 rows = withExistingOpenClawStateDatabaseReadOnly(({ db }) => executeSqliteQuerySync(db, getNodeSqliteKysely(db).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, { env }) ?? [];
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 row = withExistingOpenClawStateDatabaseReadOnly(({ db }) => executeSqliteQueryTakeFirstSync(db, getNodeSqliteKysely(db).selectFrom("migration_sources").select("status").where("source_key", "=", sourceKey)), { env });
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-alias-receipt.ts
const SOURCE_KEY = "auth-profile-sqlite-alias-map:v1";
const receiptSchema = object({
format: literal(SOURCE_KEY),
mappings: array(object({
from: string(),
to: string(),
credentials: array(object({
databasePath: string(),
beforeSha256: string().nullable(),
afterSha256: string(),
beforeCanonicalFieldsSha256: string().optional(),
afterCanonicalFieldsSha256: string().optional()
})).min(1),
sources: array(object({
path: string(),
sha256: string()
})).optional()
}))
});
function profiles(raw) {
return isRecord(raw) && isRecord(raw.profiles) ? raw.profiles : {};
}
/** Persist the collision decision before independently committed owners can lose its source. */
function recordAuthAliasMigration(params) {
const previousReceipt = readLegacyMigrationReceipt(SOURCE_KEY, params.env);
const previousMappings = previousReceipt ? receiptSchema.parse(JSON.parse(previousReceipt.reportJson)).mappings : [];
if (params.importedProfileIds) {
for (const { from } of previousMappings) if (params.importedProfileIds.has(from) && !params.profileIdMap.has(from)) throw new Error(`Recorded auth account ${from} could not be verified; its import source was preserved.`);
}
const mappings = [];
for (const [from, to] of params.profileIdMap) {
if (from === to) continue;
const credentials = params.stores.flatMap(({ databasePath, store, migratedStore }) => {
const sourceProfiles = profiles(store);
const before = sourceProfiles[from];
const after = profiles(migratedStore)[to];
const current = before ?? sourceProfiles[to];
const historical = previousMappings.filter((entry) => entry.from === from && entry.to === to).flatMap((entry) => entry.credentials).find((expected) => expected.databasePath === databasePath && current !== void 0 && (before === void 0 ? [expected.afterSha256, expected.afterCanonicalFieldsSha256] : [expected.beforeSha256, expected.beforeCanonicalFieldsSha256]).includes(digestAuthProfileMigrationValue(current)));
if (after === void 0 || before === void 0 && !historical && !(params.importedProfileIds?.has(from) && params.sources?.length)) return [];
if (params.importedProfileIds?.has(from) && before !== void 0 && (!isRecord(before) || !isRecord(after) || digestAuthProfileMigrationValue({
...before,
provider: after.provider
}) !== digestAuthProfileMigrationValue(after))) throw new Error(`Legacy auth input conflicts with the existing account ${from}; its source was preserved.`);
if (historical) {
if (before === void 0 && (!isRecord(current) || digestAuthProfileMigrationValue(normalizeLegacyCredentialFields(current)) !== digestAuthProfileMigrationValue(after))) throw new Error(`Recorded auth account ${from} changed during migration; its source was preserved.`);
return [{
...historical,
...before !== void 0 ? { beforeCanonicalFieldsSha256: digestAuthProfileMigrationValue(before) } : {},
afterCanonicalFieldsSha256: digestAuthProfileMigrationValue(after)
}];
}
return [{
databasePath,
beforeSha256: before === void 0 ? null : digestAuthProfileMigrationValue(before),
afterSha256: digestAuthProfileMigrationValue(after)
}];
});
if (credentials.length > 0) mappings.push({
from,
to,
credentials,
...params.sources?.length ? { sources: [...params.sources] } : {}
});
}
if (mappings.length === 0 && (!previousReceipt || !params.normalizeCredentialFields)) return previousReceipt?.sourceSha256 ?? void 0;
return runOpenClawStateWriteTransaction(({ db, path }) => {
const prior = readLegacyMigrationReceiptFromDatabase(db, SOURCE_KEY);
const previous = prior ? receiptSchema.parse(JSON.parse(prior.reportJson)).mappings : [];
const records = new Map(params.normalizeCredentialFields ? [] : previous.map((entry) => [digestAuthProfileMigrationValue(entry), entry]));
for (const entry of params.normalizeCredentialFields ? previous : []) {
const normalized = structuredClone(entry);
for (const expected of normalized.credentials) {
const store = params.stores.find((candidate) => candidate.databasePath === expected.databasePath);
if (!store) continue;
const before = profiles(store.store);
const after = profiles(store.migratedStore);
const normalizedFingerprint = (id, fingerprints) => {
const value = before[id];
return isRecord(value) && after[id] !== void 0 && fingerprints.includes(digestAuthProfileMigrationValue(value)) && digestAuthProfileMigrationValue(normalizeLegacyCredentialFields(value)) === digestAuthProfileMigrationValue(after[id]) ? digestAuthProfileMigrationValue(after[id]) : void 0;
};
const beforeCanonicalFieldsSha256 = normalizedFingerprint(entry.from, [expected.beforeSha256, expected.beforeCanonicalFieldsSha256]);
const afterCanonicalFieldsSha256 = normalizedFingerprint(entry.to, [expected.afterSha256, expected.afterCanonicalFieldsSha256]);
if (beforeCanonicalFieldsSha256) expected.beforeCanonicalFieldsSha256 = beforeCanonicalFieldsSha256;
if (afterCanonicalFieldsSha256) expected.afterCanonicalFieldsSha256 = afterCanonicalFieldsSha256;
}
records.set(digestAuthProfileMigrationValue(normalized), normalized);
}
for (const entry of mappings) records.set(digestAuthProfileMigrationValue(entry), entry);
const report = {
format: SOURCE_KEY,
mappings: [...records.values()]
};
const reportJson = JSON.stringify(report);
const sourceSha256 = digestAuthProfileMigrationValue(report);
if (prior?.sourceSha256 === sourceSha256 && prior.reportJson === reportJson) return sourceSha256;
recordLegacyMigrationReceipt(db, {
sourceKey: SOURCE_KEY,
migrationKind: "auth-profile-sqlite-alias-map",
sourcePath: path,
targetTable: "migration_sources",
sourceSha256,
sourceSizeBytes: null,
sourceRecordCount: report.mappings.length,
runId: `${SOURCE_KEY}:${sourceSha256}`,
now: Date.now(),
reportJson,
upsert: true
});
return sourceSha256;
}, { env: params.env });
}
function runWithAuthAliasMigrationReceipt(expectedSha256, env, operation) {
if (expectedSha256 === void 0) return operation();
return runOpenClawStateWriteTransaction((database) => {
if (readLegacyMigrationReceiptFromDatabase(database.db, SOURCE_KEY)?.sourceSha256 !== expectedSha256) throw new Error("Auth alias migration receipt changed before repair; rerun Doctor.");
return operation(database);
}, { env });
}
/** Recover only a recorded account, never a same-suffix credential or a newly reused source ID. */
function recoverAuthAliasMigration(params) {
const recovered = /* @__PURE__ */ new Map();
const blocked = /* @__PURE__ */ new Set();
const receipt = readLegacyMigrationReceipt(SOURCE_KEY, params.env);
if (!receipt) return {
recovered,
blocked
};
const report = receiptSchema.parse(JSON.parse(receipt.reportJson));
const stores = new Map(params.stores.map((entry) => [entry.databasePath, profiles(entry.store)]));
const matches = /* @__PURE__ */ new Map();
for (const mapping of report.mappings) {
const sourcePaths = mapping.sources?.map((source) => source.path) ?? [];
if (sourcePaths.length > 0 && sourcePaths.every((source) => !fs.existsSync(source))) {
const archive = params.archivedMappings?.get(mapping.from);
const recordedSources = report.mappings.filter((entry) => entry.from === mapping.from && entry.to === mapping.to).flatMap((entry) => entry.sources ?? []);
const verifiedStores = new Set(archive?.origins.filter((origin) => recordedSources.some((source) => source.path === origin.sourcePath && source.sha256 === origin.sourceSha256)).map((origin) => origin.databasePath));
if (archive?.profileId === mapping.to && verifiedStores.size > 0 && [...stores].every(([databasePath, entries]) => entries[mapping.from] === void 0 && (entries[mapping.to] === void 0 || verifiedStores.has(databasePath)))) {
const targets = matches.get(mapping.from) ?? /* @__PURE__ */ new Set();
targets.add(mapping.to);
matches.set(mapping.from, targets);
}
continue;
}
if (mapping.credentials.every((expected) => {
const entries = stores.get(expected.databasePath);
if (!entries) return false;
const before = entries[mapping.from];
const after = entries[mapping.to];
return before !== void 0 && after === void 0 && [expected.beforeSha256, expected.beforeCanonicalFieldsSha256].includes(digestAuthProfileMigrationValue(before)) || before === void 0 && after !== void 0 && [expected.afterSha256, expected.afterCanonicalFieldsSha256].includes(digestAuthProfileMigrationValue(after)) || before === void 0 && after === void 0 && expected.beforeSha256 === null && mapping.sources !== void 0 && mapping.sources.length > 0 && mapping.sources.every((source) => {
try {
return createHash("sha256").update(fs.readFileSync(source.path)).digest("hex") === source.sha256;
} catch (error) {
if (isRecord(error) && error.code === "ENOENT") return false;
throw error;
}
});
}) && [...stores].every(([databasePath, entries]) => mapping.credentials.some((entry) => entry.databasePath === databasePath) || entries[mapping.from] === void 0 && entries[mapping.to] === void 0)) {
const targets = matches.get(mapping.from) ?? /* @__PURE__ */ new Set();
targets.add(mapping.to);
matches.set(mapping.from, targets);
}
}
for (const from of new Set(report.mappings.map((entry) => entry.from))) {
const targets = matches.get(from);
if (targets?.size === 1) for (const to of targets) recovered.set(from, to);
else blocked.add(from);
}
return {
recovered,
blocked
};
}
//#endregion
//#region src/commands/doctor/auth-alias-transforms.ts
const LEGACY_OPENAI_CODEX_PROVIDER_ID = "openai-codex";
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 canonicalLegacyAuthProvider(provider) {
const normalized = provider.trim().toLowerCase();
if (isLegacyOpenAICodexProvider(normalized)) return OPENAI_PROVIDER_ID;
const legacy = resolveLegacyRuntimeModelProviderAlias(normalized);
return legacy?.legacyProvider === normalized ? legacy.provider : normalized;
}
function legacyAuthProfileTarget(profileId) {
if (profileId === "openai:codex-cli") return {
provider: OPENAI_PROVIDER_ID,
suffix: "default"
};
const separator = profileId.indexOf(":");
if (separator <= 0) return null;
const provider = profileId.slice(0, separator).trim().toLowerCase();
const canonical = canonicalLegacyAuthProvider(provider);
return canonical === provider ? null : {
provider: canonical,
suffix: profileId.slice(separator + 1).trim() || "default"
};
}
function isLegacyAuthProfileId(profileId) {
return legacyAuthProfileTarget(profileId) !== null;
}
function collectRawAuthRotationProfileIds(raw) {
if (!isRecord(raw)) return [];
const ids = [];
for (const field of [
"order",
"lastGood",
"usageStats"
]) {
const entries = raw[field];
if (!isRecord(entries)) continue;
if (field === "usageStats") {
ids.push(...Object.keys(entries));
continue;
}
for (const value of Object.values(entries)) for (const id of Array.isArray(value) ? value : [value]) if (typeof id === "string") ids.push(id);
}
return ids;
}
function isReadableAuthAliasState(raw) {
if (!isRecord(raw)) return false;
return (raw.order === void 0 || isRecord(raw.order) && Object.values(raw.order).every((ids) => Array.isArray(ids) && ids.every((id) => typeof id === "string"))) && (raw.lastGood === void 0 || isRecord(raw.lastGood) && Object.values(raw.lastGood).every((id) => typeof id === "string")) && (raw.usageStats === void 0 || isRecord(raw.usageStats) && Object.values(raw.usageStats).every(isRecord));
}
function isReadableAuthAliasStore(raw) {
if (!isRecord(raw) || !isRecord(raw.profiles) || !isReadableAuthAliasState(raw)) return false;
return Object.values(raw.profiles).every((value) => parseLegacyCredentialEntry(value) !== null);
}
function allocateLegacyAuthProfileId(legacyProfileId, occupied) {
const target = legacyAuthProfileTarget(legacyProfileId);
if (!target) throw new Error(`Not a retired auth profile id: ${legacyProfileId}`);
const { provider, suffix } = target;
const direct = `${provider}:${suffix}`;
if (!occupied.has(direct)) {
occupied.add(direct);
return direct;
}
const chatgpt = `${provider}:${isLegacyOpenAICodexProfileId(legacyProfileId) ? "chatgpt" : "cli"}-${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 canonicalizeLegacyAuthProfileEntries(profiles, options) {
const occupied = new Set(Object.keys(profiles).filter((id) => !isLegacyAuthProfileId(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 target = legacyAuthProfileTarget(profileId);
const legacyId = target !== null;
const provider = typeof rawProfile.provider === "string" ? canonicalLegacyAuthProvider(rawProfile.provider) : target?.provider;
const legacyProvider = typeof rawProfile.provider === "string" && provider !== rawProfile.provider.trim().toLowerCase();
if (!legacyId && !legacyProvider) continue;
if (target && provider !== target.provider) continue;
if (options?.preserveUnmappedLegacyIds && !options.profileIdMap?.has(profileId)) continue;
const mappedProfileId = options?.profileIdMap?.get(profileId);
const nextProfileId = mappedProfileId && !occupied.has(mappedProfileId) ? mappedProfileId : legacyId ? allocateLegacyAuthProfileId(profileId, /* @__PURE__ */ new Set([...occupied, ...reservedMappedIds])) : profileId;
occupied.add(nextProfileId);
const nextProfile = {
...rawProfile,
provider
};
if (nextProfileId !== profileId) {
delete profiles[profileId];
profileIdMap.set(profileId, nextProfileId);
}
profiles[nextProfileId] = nextProfile;
changed = true;
}
return {
profileIdMap,
changed
};
}
function canonicalizeLegacyAuthOrder(auth, profileIdMap, options) {
if (!isRecord(auth.order)) return false;
const order = auth.order;
const before = structuredClone(order);
const occupied = /* @__PURE__ */ new Set([...Object.values(order).flatMap((entries) => Array.isArray(entries) ? entries.filter((entry) => typeof entry === "string" && !isLegacyAuthProfileId(entry)) : []), ...profileIdMap.values()]);
const unresolved = (entry) => typeof entry !== "string" || Boolean(options?.preserveUnmappedLegacyIds && !profileIdMap.has(entry));
const rewrite = (entry) => {
if (typeof entry !== "string") return entry;
const mapped = profileIdMap.get(entry);
if (mapped) return mapped;
if (!isLegacyAuthProfileId(entry) || options?.preserveUnmappedLegacyIds) return entry;
const allocated = allocateLegacyAuthProfileId(entry, occupied);
profileIdMap.set(entry, allocated);
return allocated;
};
const aliases = /* @__PURE__ */ new Map();
for (const provider of Object.keys(order)) {
const canonical = canonicalLegacyAuthProvider(provider);
if (canonical !== provider && Array.isArray(order[provider])) {
const group = aliases.get(canonical) ?? [];
group.push(provider);
aliases.set(canonical, group);
}
}
for (const [provider, entries] of Object.entries(order)) if (Array.isArray(entries) && ![...aliases.values()].some((group) => group.includes(provider))) order[provider] = entries.map(rewrite);
for (const [canonical, providers] of aliases) {
const canonicalEntries = order[canonical];
if (canonicalEntries !== void 0 && !Array.isArray(canonicalEntries)) continue;
const moved = [];
let hasResolvedAlias = false;
for (const provider of providers) {
const entries = order[provider];
if (!Array.isArray(entries)) continue;
const retained = entries.filter(unresolved);
const resolved = entries.filter((entry) => !unresolved(entry));
if (resolved.length > 0 || entries.length === 0) {
hasResolvedAlias = true;
moved.push(...resolved.map(rewrite));
}
if (retained.length > 0) order[provider] = retained;
else delete order[provider];
}
if (hasResolvedAlias) {
const combined = Array.isArray(canonicalEntries) && canonicalEntries.length === 0 ? [] : [...moved, ...canonicalEntries ?? []];
order[canonical] = [...new Set(combined)];
}
}
return !isDeepStrictEqual(before, order);
}
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 canonicalizeLegacyAuthLastGood(record, profileIdMap, options) {
const before = structuredClone(record);
for (const [provider, value] of Object.entries(before)) {
const canonical = canonicalLegacyAuthProvider(provider);
const mapped = typeof value === "string" ? profileIdMap.get(value) : void 0;
if (canonical !== provider && options?.preserveUnmappedLegacyIds && mapped === void 0) continue;
if (canonical !== provider) {
delete record[provider];
if (Object.hasOwn(before, canonical)) continue;
}
record[canonical] = mapped ?? value;
}
return !isDeepStrictEqual(before, record);
}
function canonicalizeLegacyAuthStore(raw, stateRaw, profileIdMap) {
if (!isRecord(raw) || !isRecord(raw.profiles)) {
if (isRecord(stateRaw)) canonicalizeLegacyAuthRotationState(stateRaw, new Map(profileIdMap));
return null;
}
const rewrite = canonicalizeLegacyAuthProfileEntries(raw.profiles, {
profileIdMap,
preserveUnmappedLegacyIds: true
});
const effectiveProfileIdMap = new Map([...profileIdMap, ...rewrite.profileIdMap]);
const rotation = canonicalizeLegacyAuthRotationState(raw, effectiveProfileIdMap);
if (isRecord(stateRaw)) canonicalizeLegacyAuthRotationState(stateRaw, effectiveProfileIdMap);
return rewrite.changed || rotation ? rewrite.profileIdMap.size : null;
}
function canonicalizeLegacyAuthRotationState(auth, profileIdMap) {
const options = { preserveUnmappedLegacyIds: true };
const orderChanged = canonicalizeLegacyAuthOrder(auth, profileIdMap, options);
const usageChanged = isRecord(auth.usageStats) ? renameMappedProfileIdKeys(auth.usageStats, profileIdMap) : false;
const lastGoodChanged = isRecord(auth.lastGood) ? canonicalizeLegacyAuthLastGood(auth.lastGood, profileIdMap, options) : false;
return orderChanged || usageChanged || lastGoodChanged;
}
//#endregion
//#region src/commands/doctor-auth-flat-profiles.ts
function resolveMigrationTargetDatabasePath(agentDir, env = process.env) {
return agentDir ? resolveAuthProfileDatabasePath(agentDir) : resolveSharedAuthStorePath(env);
}
function listAuthProfileMigrationTargets(candidates, env) {
const targets = /* @__PURE__ */ new Map([[resolveSharedAuthStorePath(env), void 0]]);
for (const agentDir of [resolveSharedMainAuthAgentDir(env), ...candidates.flatMap((candidate) => candidate.agentDir ? [candidate.agentDir] : [])]) {
const databasePath = resolveAuthProfileDatabasePath(agentDir);
if (!targets.has(databasePath)) targets.set(databasePath, agentDir);
}
return targets;
}
var AuthProfileMigrationVerificationError = class extends Error {
constructor(detail) {
super("auth profile SQLite verification failed");
this.detail = detail;
this.name = "AuthProfileMigrationVerificationError";
}
};
function isSafeLegacyProviderKey(key) {
return key.trim().length > 0 && !isBlockedObjectKey(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);
for (const agent of listAgentEntries(cfg)) addModelHints(agent.models);
return hints;
}
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 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) 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 = coerceLegacyAuthProfileStore(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 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 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))
};
}
/**
* 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;
assertNoRetiredStateFiles("OAuth credential sidecars", listReferencedLegacyOAuthSidecarPaths(env, params.cfg));
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].includes(receipt.sourcePath));
if (!candidate) return false;
const raw = parseAuthProfileMigrationSource(receipt);
normalizeLegacyAuthProfileFields(raw);
const imported = coerceLegacyAuthProfileStore(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"))) return false;
recoverableSources.add(receipt.sourcePath);
return recoveryApproved;
};
const warnings = [];
const migratedProfileIds = /* @__PURE__ */ new Set();
const blockedProfileIds = /* @__PURE__ */ new Set();
const resume = () => {
try {
return resumePendingAuthProfileMigrationArchives(env, recoverCompleted);
} catch (err) {
params.openAICodexAuthProfileIdMap?.forEach((_target, id) => blockedProfileIds.add(id));
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 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),
changes: resumedChanges,
migratedProfileIds,
blockedProfileIds,
warnings
};
if (warnings.length > 0) return result;
if (fs.existsSync(oauthPath)) {
result.detected.push(oauthPath);
warnings.push(`Pre-June OAuth credentials at ${shortenHomePath(oauthPath)} were left unchanged. Install OpenClaw 2026.9.5 and run ${formatCliCommand("openclaw doctor --fix")} before upgrading to latest; see https://docs.openclaw.ai/install/updating#upgrading-very-old-versions.`);
}
if (detected.length === 0) return result;
note([...detected.map((candidate) => {
const hasCredentials = fs.existsSync(candidate.authPath) || fs.existsSync(candidate.legacyPath);
const providers = readLegacyAuthProfileProviders([{
kind: "auth-profiles",
path: candidate.authPath
}, ...fs.existsSync(candidate.legacyPath) ? [{
kind: "legacy-auth",
path: candidate.legacyPath
}] : []]);
return `- ${shortenHomePath(candidate.authPath)} / ${shortenHomePath(candidate.statePath)}${hasCredentials ? ` (affected providers: ${providers?.join(", ") ?? "unknown; provider scope unavailable"})` : ""}`;
}), `- ${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;
const warningCount = warnings.length;
result.changes.push(...resume());
if (warnings.length > warningCount) return result;
}
const requestedProfileIdMap = params.openAICodexAuthProfileIdMap ?? collectOpenAICodexAuthProfileStoreIdMap({
cfg: params.cfg,
env
});
const repairedFields = await maybeRepairLegacyAuthProfileStores({
cfg: params.cfg,
env,
profileIdMap: requestedProfileIdMap,
renameProfileIds: false
});
result.changes.push(...repairedFields.changes);
warnings.push(...repairedFields.warnings);
const openAIProfileIdMap = repairedFields.profileIdMap;
for (const [from, to] of requestedProfileIdMap) if (openAIProfileIdMap.get(from) !== to) {
blockedProfileIds.add(from);
blockedProfileIds.add(to);
}
for (const candidate of detected) {
const configOwnerCandidate = isDefaultAgentCandidate(candidate, params.cfg, env);
const candidateProfileIds = /* @__PURE__ */ new Set();
let completed = false;
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 importedAliasProfileIds = new Set(isRecord(rawStore) && isRecord(rawStore.profiles) ? Object.keys(rawStore.profiles) : []);
const rawState = parseAuthProfileMigrationSource(receiptByPath.get(path.resolve(candidate.statePath)));
for (const id of [
...importedAliasProfileIds,
...collectRawAuthRotationProfileIds(rawStore),
...collectRawAuthRotationProfileIds(rawState),
...configOwnerCandidate && configStore ? Object.keys(configStore.profiles) : []
]) candidateProfileIds.add(id);
const openAIProviderRepair = canonicalizeLegacyAuthStore(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 awsSdkMarkers = isRecord(rawStore) && isRecord(rawStore.profiles) ? readAwsSdkAuthProfileMarkers(candidate) : null;
if (awsSdkMarkers && isRecord(rawStore)) removeAwsSdkProfileMarkers(rawStore, awsSdkMarkers.map((profile) => profile.profileId));
const canonicalizedSecretRefs = normalizeLegacyAuthProfileFields(rawStore);
const maybeCanonicalStore = coerceLegacyAuthProfileStore(rawStore) ?? coerceLegacyFlatAuthProfileStore(rawStore) ?? null;
const canonicalStore = hasImportableAuthProfileStore(maybeCanonicalStore) ? maybeCanonicalStore : null;
const configCanonicalStore = configStore && configOwnerCandidate ? structuredClone(configStore) : null;
if (configCanonicalStore) {
Object.keys(configCanonicalStore.profiles).forEach((id) => importedAliasProfileIds.add(id));
canonicalizeLegacyAuthStore(configCanonicalStore, null, openAIProfileIdMap);
}
const legacyStore = coerceLegacyAuthStore(parseAuthProfileMigrationSource(receiptByPath.get(path.resolve(candidate.legacyPath))));
const state = coerceAuthProfileState(rawState);
if (!canonicalStore && !configCanonicalStore && !legacyStore && !hasAuthProfileState(state) && !awsSdkMarkers) {
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);
Object.keys(legacyAsStore.profiles).forEach((id) => importedAliasProfileIds.add(id));
Object.keys(legacyAsStore.profiles).forEach((id) => candidateProfileIds.add(id));
canonicalizeLegacyAuthStore(legacyAsStore, null, openAIProfileIdMap);
}
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 = runWithAuthAliasMigrationReceipt(recordAuthAliasMigration({
profileIdMap: openAIProfileIdMap,
stores: [{
databasePath: targetDatabasePath,
store: existing,
migratedStore: next
}],
importedProfileIds: importedAliasProfileIds,
sources: sourceReceipts.filter((receipt) => receipt.sourcePath !== path.resolve(candidate.statePath)).map((receipt) => ({
path: receipt.sourcePath,
sha256: receipt.sourceSha256
})),
env
}), env, () => 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;
}
}
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;
});
for (const id of [
...importedAliasProfileIds,
...importedProfileIds,
...collectAuthProfileStateProfileIds(state),
...collectAuthProfileStateProfileIds(coerceAuthProfileState(canonicalStore)),
...collectAuthProfileStateProfileIds(coerceAuthProfileState(configCanonicalStore))
]) migratedProfileIds.add(id);
if (configStore && configOwnerCandidate && stripImportedConfigAuthProfileCredentials(params.cfg, configStore)) result.configChanged = true;
if (awsSdkMarkers) {
const configProfiles = ensureConfigAuthProfiles(params.cfg);
for (const marker of awsSdkMarkers) configProfiles[marker.profileId] = {
provider: marker.provider,
mode: "aws-sdk",
...marker.email ? { email: marker.email } : {},
...marker.displayName ? { displayName: marker.displayName } : {}
};
result.configChanged = true;
}
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 (canonicalizedSecretRefs > 0) result.changes.push(`Canonicalized ${canonicalizedSecretRefs} auth SecretRef(s) to source/provider/id; unsupported fields are preserved in the verified source archives (${archiveText}).`);
completed = true;
if (unresolvedSidecarWarning) result.warnings.push(unresolvedSidecarWarning);
if (openAIProviderRepair !== null) result.changes.push(`Migrated retired auth profile identifiers in ${shortenHomePath(candidate.authPath)}.`);
if (awsSdkMarkers) result.changes.push(`Moved aws-sdk profile metadata from ${shortenHomePath(candidate.authPath)} to auth.profiles before removing the legacy auth profile JSON.`);
} catch (err) {
if (candidateProfileIds.size === 0) openAIProfileIdMap.forEach((_target, id) => blockedProfileIds.add(id));
result.warnings.push(`Failed to migrate auth profile JSON for ${shortenHomePath(candidate.authPath)}: ${String(err)}`);
} finally {
if (!completed) candidateProfileIds.forEach((id) => blockedProfileIds.add(id));
releaseSources?.();
}
}
clearRuntimeAuthProfileStoreSnapshots();
clearAuthProfileMigrationDiagnostics();
return result;
}
function readAwsSdkAuthProfileMarkers(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 ? markers : null;
}
function removeAwsSdkProfileMarkers(raw, profileIds) {
if (!isRecord(raw.profiles)) return;
for (const profileId of profileIds) delete raw.profiles[profileId];
}
function rewriteMappedAuthProfileRefs(config, profileIdMap) {
let changed = false;
const rewrite = (owner, key) => {
if (!isRecord(owner) || typeof owner[key] !== "string") return;
let profileId = owner[key];
if (key === "apiKey") profileId = normalizeSecretInput(profileId);
else if (key === "authProfileId") profileId = profileId.trim();
const replacement = profileIdMap.get(profileId);
if (replacement && replacement !== owner[key]) {
owner[key] = replacement;
changed = true;
}
};
for (const provider of Object.values(config.models?.providers ?? {})) rewrite(provider, "apiKey");
for (const model of config.tools?.media?.models ?? []) {
rewrite(model, "profile");
rewrite(model, "preferredProfile");
}
for (const server of Object.values(config.mcp?.servers ?? {})) rewrite(server.oauth, "authProfileId");
const agents = [config.agents?.defaults, ...listMutableCodexRouteAgentEntries(config).map(({ agent }) => agent)];
for (const agent of agents) {
if (!isRecord(agent) || !isRecord(agent.models)) continue;
for (const model of Object.values(agent.models)) rewrite(isRecord(model) ? model.agentRuntime : void 0, "authProfileId");
}
return changed;
}
/**
* Canonicalizes config references for retired provider and profile identifiers.
*
* The optional map lets config and store repairs share deterministic profile ids when both surfaces
* contain the same legacy profile.
*/
function maybeRepairOpenAICodexAuthConfig(cfg, options) {
let 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 = canonicalizeLegacyAuthProfileEntries(auth.profiles, {
profileIdMap,
preserveUnmappedLegacyIds: options?.profileIdMap !== void 0
});
for (const [from, to] of rewrite.profileIdMap) profileIdMap.set(from, to);
changed ||= rewrite.changed;
}
if (auth) {
const orderChanged = canonicalizeLegacyAuthOrder(auth, profileIdMap, { preserveUnmappedLegacyIds: options?.profileIdMap !== void 0 });
changed ||= orderChanged;
}
if (profileIdMap.size > 0 && rewriteMappedAuthProfileRefs(config, profileIdMap)) changed = true;
if (profileIdMap.size > 0) {
const models = repairRetiredConfigModelRefs(config, ({ modelRef }) => repairModelRefAuthProfile(modelRef, profileIdMap));
config = models.config;
changed ||= models.changes.length > 0;
const pluginsChanged = rewritePluginAuthProfileRefs(config, profileIdMap);
changed ||= pluginsChanged;
}
if (!changed) return {
config,
changes: [],
warnings: []
};
return {
config,
changes: ["Migrated legacy auth profile config to canonical providers."],
warnings: []
};
}
/** Normalize already-SQLite stores under their current owners after legacy import. */
async function maybeRepairLegacyAuthProfileStores(params) {
const env = params.env ?? process.env;
const warnings = [];
let incompleteCensus = false;
const candidates = listAuthProfileRepairCandidates(params.cfg, env, (pathname) => {
incompleteCensus = true;
warnings.push(`Skipped auth-profile alias migration because ${shortenHomePath(pathname)} is unavailable.`);
});
const stores = [];
const planned = [];
for (const [databasePath, agentDir] of listAuthProfileMigrationTargets(candidates, env)) {
let identity;
try {
identity = readDatabasePathIdentitySync(databasePath);
} catch {
incompleteCensus = true;
warnings.push(`Skipped auth-profile alias migration because ${shortenHomePath(databasePath)} is unavailable.`);
continue;
}
const store = agentDir ? inspectPersistedAuthProfileStoreRaw(agentDir) : inspectPersistedSharedAuthProfileStoreRaw(env);
const state = agentDir ? inspectPersistedAuthProfileStateRaw(agentDir) : inspectPersistedSharedAuthProfileStateRaw(env);
if (store.status === "unreadable" || agentDir !== void 0 && inspectAuthDatabaseFiles(agentDir) === "unreadable" || store.status === "readable" && (!isRecord(store.raw) || !isRecord(store.raw.profiles))) {
incompleteCensus = true;
warnings.push(`Skipped auth-profile alias migration because ${shortenHomePath(databasePath)} is unreadable or invalid.`);
continue;
}
stores.push({
databasePath,
store: store.status === "readable" ? store.raw : null
});
if (store.status === "readable" || state.status === "readable" || state.status === "unreadable") {
assertExistingDatabaseIdentity(databasePath, identity.key, identity.birthtime);
const canReadRotationState = state.status === "missing" || state.status === "readable" && isReadableAuthAliasState(state.raw);
const canRenameAliases = (store.status !== "readable" || isReadableAuthAliasStore(store.raw)) && canReadRotationState;
if (!canRenameAliases) warnings.push(`Skipped auth-profile alias renames because ${shortenHomePath(databasePath)} is unreadable or invalid; supported credential fields can still be repaired.`);
const unreadableStateJson = state.status === "unreadable" ? readAuthProfileStateJsonTextReadOnly({
path: databasePath,
kind: agentDir === void 0 && resolveSharedAuthStoreOwnership(env).location === "state-db" ? "shared-state" : "agent",
env
}) : void 0;
assertExistingDatabaseIdentity(databasePath, identity.key, identity.birthtime);
if (state.status === "unreadable" && unreadableStateJson === void 0) {
incompleteCensus = true;
warnings.push(`Skipped auth-profile field repair because ${shortenHomePath(databasePath)} has no stable rotation-state source.`);
continue;
}
planned.push({
databasePath,
identity,
canRenameAliases,
canReadRotationState,
agentDir,
store: store.status === "readable" ? store.raw : null,
state: state.status === "readable" ? state.raw : null,
...unreadableStateJson !== void 0 ? { unreadableStateJson } : {}
});
}
}
const profileIdMap = new Map(incompleteCensus ? [] : params.profileIdMap);
if (incompleteCensus && params.profileIdMap.size > 0) warnings.push("Deferred auth profile aliases because an owner could not be inspected; repair that owner and rerun Doctor.");
for (const target of planned) {
if (target.canRenameAliases) continue;
const profiles = isRecord(target.store) && isRecord(target.store.profiles) ? target.store.profiles : {};
const references = /* @__PURE__ */ new Set([
...Object.keys(profiles),
...collectRawAuthRotationProfileIds(target.store),
...collectRawAuthRotationProfileIds(target.state)
]);
for (const [from, to] of profileIdMap) if (!target.canReadRotationState || references.has(from) || references.has(to)) {
profileIdMap.delete(from);
warnings.push(`Deferred auth profile alias ${from} because ${shortenHomePath(target.databasePath)} cannot safely rename its credential owner.`);
}
}
const recovery = recoverAuthAliasMigration({
stores,
env,
archivedMappings: recoverArchivedAuthProfileMappings({
candidates,
env
})
});
const migrationMap = params.renameProfileIds === false ? /* @__PURE__ */ new Map() : profileIdMap;
const unresolvedProfileIds = new Set(Object.entries(params.cfg.auth?.profiles ?? {}).filter(([id, profile]) => !profileIdMap.has(id) && (isLegacyAuthProfileId(id) || typeof profile.provider === "string" && canonicalLegacyAuthProvider(profile.provider) !== profile.provider)).map(([id]) => id));
for (const target of planned) {
for (const id of [...collectRawAuthRotationProfileIds(target.store), ...collectRawAuthRotationProfileIds(target.state)]) if (isLegacyAuthProfileId(id) && !profileIdMap.has(id)) unresolvedProfileIds.add(id);
if (!isRecord(target.store) || !isRecord(target.store.profiles)) continue;
for (const [profileId, profile] of Object.entries(target.store.profiles)) {
const provider = isRecord(profile) && typeof profile.provider === "string" ? profile.provider.trim().toLowerCase() : void 0;
if (!profileIdMap.has(profileId) && (isLegacyAuthProfileId(profileId) || provider !== void 0 && canonicalLegacyAuthProvider(provider) !== provider)) unresolvedProfileIds.add(profileId);
}
}
for (const profileId of unresolvedProfileIds) warnings.push(`Kept auth profile ${profileId} unchanged because its provider realm or identity is unresolved.`);
for (const target of planned) {
const profiles = isRecord(target.store) && isRecord(target.store.profiles) ? target.store.profiles : {};
const occupied = /* @__PURE__ */ new Set([
...Object.keys(profiles),
...collectRawAuthRotationProfileIds(target.store),
...collectRawAuthRotationProfileIds(target.state)
]);
for (const [from, to] of profileIdMap) if (from !== to && occupied.has(to) && (occupied.has(from) || recovery.recovered.get(from) !== to)) {
profileIdMap.delete(from);
warnings.push(`Deferred stale auth profile alias ${from}; the target is occupied.`);
}
}
for (const from of profileIdMap.keys()) if (recovery.blocked.has(from)) {
profileIdMap.delete(from);
warnings.push(`Kept auth profile ${from} unchanged because its recorded account changed; reconcile the migration before retrying.`);
}
const migrated = planned.map((target) => {
const migratedStore = structuredClone(target.store);
const migratedState = structuredClone(target.state);
if (target.canRenameAliases) canonicalizeLegacyAuthStore(migratedStore, migratedState, migrationMap);
const canonicalizedSecretRefs = normalizeLegacyAuthProfileFields(migratedStore);
return Object.assign(target, {
migratedStore,
migratedState,
canonicalizedSecretRefs
});
});
const changed = migrated.filter((target) => !isDeepStrictEqual(target.store, target.migratedStore) || !isDeepStrictEqual(target.state, target.migratedState));
if (changed.length === 0 && migrationMap.size === 0) return {
changes: [],
warnings,
profileIdMap
};
const maintenance = getOpenClawDatabaseMaintenanceScope();
const assertTargetCurrent = (target, database) => {
maintenance?.assertOwnerCurrent();
assertExistingDatabaseIdentity(target.databasePath, target.identity.key, target.identity.birthtime);
if (database) {
const native = "agentId" in database ? readOpenClawAgentDatabaseIdentity(database) : void 0;
const identity = native ? {
key: `file:${String(native.identity)}`,
birthtime: native.birthtime
} : requireOpenClawStateDatabaseIdentity(database);
if (identity.key !== target.identity.key) throw new Error("Auth profile database generation changed after backup; retry Doctor.");
assertExistingDatabaseIdentity(database.path, identity.key, identity.birthtime);
}
};
const changes = [];
const backupId = randomUUID();
for (const target of changed) {
assertTargetCurrent(target);
const kind = target.agentDir === void 0 && resolveSharedAuthStoreOwnership(env).location === "state-db" ? "shared-state" : "agent";
const backup = await createVerifiedSqliteSnapshot({
sourcePath: target.databasePath,
targetPath: `${target.databasePath}.auth-profile-migration-${backupId}.bak`,
preserveRowIds: true,
beforePublish: () => assertTargetCurrent(target),
validate: (database) => {
for (const field of ["store", "state"]) {
if (field === "state" && target.unreadableStateJson !== void 0) {
if (readAuthProfileJsonCellText(database, "state", kind) !== target.unreadableStateJson) throw new Error("Auth profile backup differs from the planned rotation-state source; retry Doctor.");
continue;
}
const row = inspectAuthProfileJsonCell(database, field, kind);
if (row.status === "unreadable" || !isDeepStrictEqual(row.status === "readable" ? row.raw : null, target[field])) throw new Error("Auth profile backup differs from the planned source; retry Doctor.");
}
}
});
assertTargetCurrent(target);
changes.push(`Saved auth profile migration backup: ${shortenHomePath(backup.path)}.`);
}
const assertSourceCurrent = (target, database) => {
assertTargetCurrent(target, database);
if (target.unreadableStateJson !== void 0 && readAuthProfileJsonCellText(database.db, "state", "agentId" in database ? "agent" : "shared-state") !== target.unreadableStateJson) throw new Error("Auth profile rotation-state source changed during migration; retry Doctor.");
if (!isDeepStrictEqual(readPersistedAuthProfileStoreRaw(target.agentDir, database), target.store) || !isDeepStrictEqual(readPersistedAuthProfileStateRaw(target.agentDir, database), target.state)) throw new Error("auth profile store or rotation state changed during alias migration");
};
for (const target of migrated) runAuthProfileWriteTransaction(target.agentDir, (database) => assertSourceCurrent(target, database), { env });
maintenance?.assertOwnerCurrent();
const receiptSha256 = recordAuthAliasMigration({
profileIdMap: migrationMap,
stores: migrated,
env,
normalizeCredentialFields: true
});
const locked = [];
const migrate = (index, sharedDatabase) => {
const nextTarget = migrated[index];
if (nextTarget) {
runAuthProfileWriteTransaction(nextTarget.agentDir, (database) => {
assertSourceCurrent(nextTarget, database);
locked.push({
database,
target: nextTarget
});
migrate(index + 1, sharedDatabase);
}, { env });
return;
}
maintenance?.assertOwnerCurrent();
const renamedLinks = renameUserProfileAuthLinks(migrationMap, {
env,
database: sharedDatabase
});
if (renamedLinks > 0) changes.push(`Updated renamed auth profiles in ${renamedLinks} personal account selection record(s).`);
for (const { database, target } of locked) {
const store = target.migratedStore;
const state = target.migratedState;
const storeChanged = !isDeepStrictEqual(store, target.store);
const stateChanged = !isDeepStrictEqual(state, target.state);
if (storeChanged) {
writePersistedAuthProfileStoreRaw(store, target.agentDir, database);
if (target.canonicalizedSecretRefs > 0) changes.push(`Canonicalized ${target.canonicalizedSecretRefs} auth SecretRef(s) in ${shortenHomePath(target.databasePath)} to source/provider/id; unsupported fields are preserved in the verified migration backup.`);
}
if (stateChanged) writePersistedAuthProfileStateRaw(state, target.agentDir, database);
if (storeChanged || stateChanged) changes.push(`Migrated stored auth profile aliases in ${shortenHomePath(target.databasePath)}.`);
}
};
runWithAuthAliasMigrationReceipt(receiptSha256, env, (database) => migrate(0, database));
if (changes.length > 0) clearRuntimeAuthProfileStoreSnapshots();
return {
changes,
warnings,
profileIdMap
};
}
function recoverArchivedAuthProfileMappings(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" || archive.kind === "legacy-auth");
for (const candidate of params.candidates) {
const canonicalProfiles = coerceLegacyAuthProfileStore(candidate.agentDir ? readPersistedAuthProfileStoreRaw(candidate.agentDir) : readPersistedSharedAuthProfileStoreRaw(params.env))?.profiles;
if (!canonicalProfiles) continue;
const sourcePaths = [candidate.authPath, resolveLegacyFlatAuthPath(path.dirname(candidate.authPath))];
for (const archive of archives) {
const sourcePath = sourcePaths.find((source) => archive.path.startsWith(`${source}.migrated-`));
if (!sourcePath) continue;
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(sourcePath)}\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"));
const sourceStore = coerceLegacyAuthProfileStore(archivedStore) ?? coerceLegacyFlatAuthProfileStore(archivedStore);
if (!sourceStore) continue;
for (const [legacyProfileId, rawCredential] of Object.entries(sourceStore.profiles)) {
const target = legacyAuthProfileTarget(legacyProfileId);
if (!target) continue;
const archivedCredential = parseLegacyCredentialEntry({
...rawCredential,
provider: target.provider
}, target.provider);
if (!archivedCredential) continue;
const matches = Object.entries(report.expectedProfileSha256).flatMap(([canonicalProfileId, expectedSha256]) => {
const credential = canonicalProfiles[canonicalProfileId];
return typeof expectedSha256 === "string" && credential?.provider === target.provider && (archivedCredential.type === "oauth" && credential.type === "oauth" ? hasMatchingOAuthIdentity(archivedCredential, credential) || areOAuthCredentialsEquivalent(archivedCredential, credential) : isDeepStrictEqual(archivedCredential, credential)) ? [canonicalProfileId] : [];
});
if (matches.length !== 1) continue;
const canonicalProfileId = matches[0];
const previous = recovered.get(legacyProfileId);
if (previous && previous.profileId !== canonicalProfileId) {
recovered.delete(legacyProfileId);
ambiguous.add(legacyProfileId);
} else if (!ambiguous.has(legacyProfileId)) recovered.set(legacyProfileId, {
profileId: canonicalProfileId,
origins: [...previous?.origins ?? [], {
sourcePath: path.resolve(sourcePath),
sourceSha256,
databasePath: resolveMigrationTargetDatabasePath(candidate.agentDir, params.env)
}]
});
}
} catch {}
}
}
return recovered;
}
/** Collects collision-safe retired profile ids across config, SQLite, and legacy agent stores. */
function collectOpenAICodexAuthProfileStoreIdMap(params) {
const env = params.env ?? process.env;
const occupied = /* @__PURE__ */ new Set(["openai:codex-cli"]);
const eligible = /* @__PURE__ */ new Set();
const blocked = /* @__PURE__ */ new Set();
const profileIdMap = /* @__PURE__ */ new Map();
const sqliteStores = [];
let incompleteCensus = false;
const candidates = listAuthProfileRepairCandidates(params.cfg, env, () => {
incompleteCensus = true;
});
if (incompleteCensus) return profileIdMap;
const configuredProviders = new Set(Object.keys(params.cfg.models?.providers ?? {}).map((provider) => provider.trim().toLowerCase()));
const collectReferences = (raw) => {
for (const id of collectRawAuthRotationProfileIds(raw)) occupied.add(id);
};
const collectProfiles = (raw) => {
if (!isRecord(raw) || !isRecord(raw.profiles)) return false;
for (const [profileId, value] of Object.entries(raw.profiles)) {
occupied.add(profileId);
const target = legacyAuthProfileTarget(profileId);
const provider = isRecord(value) && typeof value.provider === "string" ? value.provider.trim().toLowerCase() : void 0;
const canonical = provider === void 0 ? void 0 : canonicalLegacyAuthProvider(provider);
if (!target && canonical === provider) continue;
const legacyPrefix = profileId.slice(0, profileId.indexOf(":")).trim().toLowerCase();
const changesRealm = provider !== canonical;
const explicitRealm = isRecord(value) && changesRealm && [
"enterpriseUrl",
"tokenEndpoint",
"deviceAuthorizationEndpoint",
"issuer"
].some((field) => value[field] !== void 0);
if (provider === void 0 || canonical === void 0 || target !== null && target.provider !== canonical || target !== null && configuredProviders.has(legacyPrefix) || changesRealm && configuredProviders.has(provider) || explicitRealm) blocked.add(profileId);
else eligible.add(profileId);
}
collectReferences(raw);
return true;
};
collectProfiles({
profiles: params.cfg.auth?.profiles ?? {},
order: params.cfg.auth?.order
});
for (const [databasePath, agentDir] of listAuthProfileMigrationTargets(candidates, env)) {
if (agentDir && inspectAuthDatabaseFiles(agentDir) === "unreadable") return profileIdMap;
const inspection = agentDir ? inspectPersistedAuthProfileStoreRaw(agentDir) : inspectPersistedSharedAuthProfileStoreRaw(env);
const state = agentDir ? inspectPersistedAuthProfileStateRaw(agentDir) : inspectPersistedSharedAuthProfileStateRaw(env);
if (inspection.status === "unreadable" || state.status === "unreadable") return profileIdMap;
if (inspection.status === "missing") sqliteStores.push({
databasePath,
store: null
});
if (inspection.status === "readable") {
sqliteStores.push({
databasePath,
store: inspection.raw
});
if (!collectProfiles(inspection.raw)) return profileIdMap;
if (!isReadableAuthAliasStore(inspection.raw) && isRecord(inspection.raw) && isRecord(inspection.raw.profiles)) Object.keys(inspection.raw.profiles).forEach((id) => blocked.add(id));
}
if (state.status === "readable") {
if (!isRecord(state.raw)) return profileIdMap;
collectReferences(state.raw);
if (!isReadableAuthAliasState(state.raw) && inspection.status === "readable" && isRecord(inspection.raw) && isRecord(inspection.raw.profiles)) Object.keys(inspection.raw.profiles).forEach((id) => blocked.add(id));
}
}
for (const candidate of candidates) {
if (fs.existsSync(candidate.authPath) && !collectProfiles(loadJsonFileThroughSymlink(candidate.authPath))) return profileIdMap;
const legacyPath = resolveLegacyFlatAuthPath(path.dirname(candidate.authPath));
if (fs.existsSync(legacyPath)) {
const legacy = coerceLegacyAuthStore(loadJsonFileThroughSymlink(legacyPath));
if (!legacy) return profileIdMap;
const store = {
version: 1,
profiles: {}
};
applyLegacyAuthStore(store, legacy);
collectProfiles(store);
}
const statePath = resolveLegacyAuthStatePath(path.dirname(candidate.authPath));
if (fs.existsSync(statePath)) {
const raw = loadJsonFileThroughSymlink(statePath);
if (!isRecord(raw)) return profileIdMap;
collectReferences(raw);
}
}
const archivedMappings = recoverArchivedAuthProfileMappings({
candidates,
env
});
const recovery = recoverAuthAliasMigration({
stores: sqliteStores,
env,
archivedMappings
});
for (const profileId of recovery.blocked) blocked.add(profileId);
for (const [from, to] of recovery.recovered) if (!blocked.has(from)) {
profileIdMap.set(from, to);
if (archivedMappings.get(from)?.profileId === to) params.recoveredProfileIds?.add(from);
}
for (const profileId of [...eligible].toSorted((left, right) => left.localeCompare(right))) if (!blocked.has(profileId) && !profileIdMap.has(profileId)) profileIdMap.set(profileId, isLegacyAuthProfileId(profileId) ? allocateLegacyAuthProfileId(profileId, occupied) : profileId);
for (const [legacyProfileId, archive] of archivedMappings) if (sqliteStores.every(({ databasePath, store }) => !isRecord(store) || !isRecord(store.profiles) || store.profiles[archive.profileId] === void 0 || archive.origins.some((origin) => origin.databasePath === databasePath)) && !profileIdMap.has(legacyProfileId) && !blocked.has(legacyProfileId)) {
profileIdMap.set(legacyProfileId, archive.profileId);
params.recoveredProfileIds?.add(legacyProfileId);
}
return profileIdMap;
}
//#endregion
//#region src/commands/doctor/auth-profile-repair.ts
/** Complete only verified imports or explicitly requested auth repairs through the same owners. */
async function repairAuthProfileMigration(params) {
const env = params.env ?? process.env;
const planned = params.profileIdMap ?? collectOpenAICodexAuthProfileStoreIdMap({
cfg: params.cfg,
env
});
const config = structuredClone(params.cfg);
const imported = await maybeMigrateAuthProfileJsonStoresToSqlite({
cfg: config,
env,
prompter: { confirmAutoFix: params.prompter.confirmAutoFix },
openAICodexAuthProfileIdMap: planned
});
const recoveredProfileIds = /* @__PURE__ */ new Set();
const recovered = imported.detected.length === 0 ? collectOpenAICodexAuthProfileStoreIdMap({
cfg: config,
env,
recoveredProfileIds
}) : /* @__PURE__ */ new Map();
const authorized = new Map([...planned, ...[...recovered].filter(([from]) => recoveredProfileIds.has(from))].filter(([from, to]) => !imported.blockedProfileIds.has(from) && !imported.blockedProfileIds.has(to) && (params.prompter.shouldRepair || recoveredProfileIds.has(from) && imported.detected.length === 0 || imported.migratedProfileIds.has(from) || imported.migratedProfileIds.has(to))));
const aliases = params.prompter.shouldRepair || authorized.size > 0 ? await maybeRepairLegacyAuthProfileStores({
cfg: config,
env,
profileIdMap: authorized
}) : {
profileIdMap: authorized,
changes: [],
warnings: []
};
const repaired = maybeRepairOpenAICodexAuthConfig(config, { profileIdMap: aliases.profileIdMap });
return {
config: repaired.config,
changes: [...repaired.changes, ...imported.configChanged ? ["Auth profile SQLite migration updated auth.profiles."] : []],
storeChanges: [...imported.changes, ...aliases.changes],
warnings: [
...imported.warnings,
...aliases.warnings,
...repaired.warnings
],
profileIdMap: aliases.profileIdMap
};
}
//#endregion
export { collectOpenAICodexAuthProfileStoreIdMap as n, maybeRepairLegacyAuthProfileStores as r, repairAuthProfileMigration as t };