mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-27 04:47:03 -06:00
fix(doctor): rewrite non-canonical api_key auth profiles
Rewrites non-canonical api_key fields in auth-profiles.json to canonical key via openclaw doctor --fix, with backups, while preserving canonical key/keyRef credentials and active-agent auth stores. Fixes #57389. Co-authored-by: alkor2000 <200923177@qq.com>
This commit is contained in:
@@ -0,0 +1,326 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { clearRuntimeAuthProfileStoreSnapshots } from "../agents/auth-profiles/store.js";
|
||||
import {
|
||||
createOpenClawTestState,
|
||||
type OpenClawTestState,
|
||||
} from "../test-utils/openclaw-test-state.js";
|
||||
import { maybeRepairCanonicalApiKeyFieldAlias } from "./doctor-auth-flat-profiles.js";
|
||||
import type { DoctorPrompter } from "./doctor-prompter.js";
|
||||
|
||||
const states: OpenClawTestState[] = [];
|
||||
|
||||
function makePrompter(shouldRepair: boolean): DoctorPrompter {
|
||||
return {
|
||||
confirm: vi.fn(async () => shouldRepair),
|
||||
confirmAutoFix: vi.fn(async () => shouldRepair),
|
||||
confirmAggressiveAutoFix: vi.fn(async () => shouldRepair),
|
||||
confirmRuntimeRepair: vi.fn(async () => shouldRepair),
|
||||
select: vi.fn(async (_params, fallback) => fallback),
|
||||
shouldRepair,
|
||||
shouldForce: false,
|
||||
repairMode: {
|
||||
shouldRepair,
|
||||
shouldForce: false,
|
||||
nonInteractive: false,
|
||||
canPrompt: true,
|
||||
updateInProgress: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function makeTestState(): Promise<OpenClawTestState> {
|
||||
const state = await createOpenClawTestState({
|
||||
layout: "state-only",
|
||||
prefix: "openclaw-doctor-canonical-api-key-",
|
||||
env: {
|
||||
OPENCLAW_AGENT_DIR: undefined,
|
||||
},
|
||||
});
|
||||
states.push(state);
|
||||
return state;
|
||||
}
|
||||
|
||||
afterEach(async () => {
|
||||
clearRuntimeAuthProfileStoreSnapshots();
|
||||
for (const state of states.splice(0)) {
|
||||
await state.cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
describe("maybeRepairCanonicalApiKeyFieldAlias", () => {
|
||||
it('rewrites the non-canonical "api_key" field to "key" with a backup (57389)', async () => {
|
||||
const state = await makeTestState();
|
||||
const canonical = {
|
||||
version: 1,
|
||||
profiles: {
|
||||
"my-key": {
|
||||
type: "api_key",
|
||||
provider: "my-provider",
|
||||
api_key: "sk-snake-case-key",
|
||||
},
|
||||
},
|
||||
order: {
|
||||
"my-provider": ["my-key"],
|
||||
},
|
||||
};
|
||||
const authPath = await state.writeAuthProfiles(canonical);
|
||||
|
||||
const result = await maybeRepairCanonicalApiKeyFieldAlias({
|
||||
cfg: {},
|
||||
prompter: makePrompter(true),
|
||||
now: () => 123,
|
||||
});
|
||||
|
||||
expect(result.detected).toEqual([authPath]);
|
||||
expect(result.changes).toStrictEqual([
|
||||
`Rewrote 1 "api_key" field(s) to "key" in ${authPath} (backup: ${authPath}.api-key-alias.123.bak).`,
|
||||
]);
|
||||
expect(result.warnings).toStrictEqual([]);
|
||||
// After the fix: api_key is aliased to the canonical key, other fields untouched.
|
||||
expect(JSON.parse(fs.readFileSync(authPath, "utf8"))).toEqual({
|
||||
version: 1,
|
||||
profiles: {
|
||||
"my-key": {
|
||||
type: "api_key",
|
||||
provider: "my-provider",
|
||||
key: "sk-snake-case-key",
|
||||
},
|
||||
},
|
||||
order: {
|
||||
"my-provider": ["my-key"],
|
||||
},
|
||||
});
|
||||
// The backup preserves the original non-canonical shape.
|
||||
expect(JSON.parse(fs.readFileSync(`${authPath}.api-key-alias.123.bak`, "utf8"))).toEqual(
|
||||
canonical,
|
||||
);
|
||||
});
|
||||
|
||||
it('rewrites non-canonical SecretRef "api_key" fields to canonical "key"', async () => {
|
||||
const state = await makeTestState();
|
||||
const canonical = {
|
||||
version: 1,
|
||||
profiles: {
|
||||
"my-key": {
|
||||
type: "api_key",
|
||||
provider: "my-provider",
|
||||
api_key: { source: "env", provider: "default", id: "MY_PROVIDER_API_KEY" },
|
||||
},
|
||||
},
|
||||
};
|
||||
const authPath = await state.writeAuthProfiles(canonical);
|
||||
|
||||
const result = await maybeRepairCanonicalApiKeyFieldAlias({
|
||||
cfg: {},
|
||||
prompter: makePrompter(true),
|
||||
now: () => 123,
|
||||
});
|
||||
|
||||
expect(result.detected).toEqual([authPath]);
|
||||
expect(result.changes).toStrictEqual([
|
||||
`Rewrote 1 "api_key" field(s) to "key" in ${authPath} (backup: ${authPath}.api-key-alias.123.bak).`,
|
||||
]);
|
||||
expect(result.warnings).toStrictEqual([]);
|
||||
expect(JSON.parse(fs.readFileSync(authPath, "utf8"))).toEqual({
|
||||
version: 1,
|
||||
profiles: {
|
||||
"my-key": {
|
||||
type: "api_key",
|
||||
provider: "my-provider",
|
||||
key: { source: "env", provider: "default", id: "MY_PROVIDER_API_KEY" },
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(JSON.parse(fs.readFileSync(`${authPath}.api-key-alias.123.bak`, "utf8"))).toEqual(
|
||||
canonical,
|
||||
);
|
||||
});
|
||||
|
||||
it("repairs auth profiles from OPENCLAW_AGENT_DIR", async () => {
|
||||
const state = await makeTestState();
|
||||
const agentDir = state.path("external-agent");
|
||||
const authPath = path.join(agentDir, "auth-profiles.json");
|
||||
const canonical = {
|
||||
version: 1,
|
||||
profiles: {
|
||||
"my-key": {
|
||||
type: "api_key",
|
||||
provider: "my-provider",
|
||||
api_key: "sk-snake-case-key",
|
||||
},
|
||||
},
|
||||
};
|
||||
fs.mkdirSync(agentDir, { recursive: true });
|
||||
fs.writeFileSync(authPath, `${JSON.stringify(canonical, null, 2)}\n`, "utf8");
|
||||
|
||||
const result = await maybeRepairCanonicalApiKeyFieldAlias({
|
||||
cfg: {},
|
||||
prompter: makePrompter(true),
|
||||
now: () => 123,
|
||||
env: {
|
||||
...state.env,
|
||||
OPENCLAW_AGENT_DIR: agentDir,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.detected).toEqual([authPath]);
|
||||
expect(result.changes).toStrictEqual([
|
||||
`Rewrote 1 "api_key" field(s) to "key" in ${authPath} (backup: ${authPath}.api-key-alias.123.bak).`,
|
||||
]);
|
||||
expect(result.warnings).toStrictEqual([]);
|
||||
expect(JSON.parse(fs.readFileSync(authPath, "utf8"))).toEqual({
|
||||
version: 1,
|
||||
profiles: {
|
||||
"my-key": {
|
||||
type: "api_key",
|
||||
provider: "my-provider",
|
||||
key: "sk-snake-case-key",
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("repairs auth profiles from PI_CODING_AGENT_DIR", async () => {
|
||||
const state = await makeTestState();
|
||||
const agentDir = state.path("legacy-external-agent");
|
||||
const authPath = path.join(agentDir, "auth-profiles.json");
|
||||
const canonical = {
|
||||
version: 1,
|
||||
profiles: {
|
||||
"my-key": {
|
||||
type: "api_key",
|
||||
provider: "my-provider",
|
||||
api_key: "sk-snake-case-key",
|
||||
},
|
||||
},
|
||||
};
|
||||
fs.mkdirSync(agentDir, { recursive: true });
|
||||
fs.writeFileSync(authPath, `${JSON.stringify(canonical, null, 2)}\n`, "utf8");
|
||||
|
||||
const result = await maybeRepairCanonicalApiKeyFieldAlias({
|
||||
cfg: {},
|
||||
prompter: makePrompter(true),
|
||||
now: () => 123,
|
||||
env: {
|
||||
...state.env,
|
||||
OPENCLAW_AGENT_DIR: undefined,
|
||||
PI_CODING_AGENT_DIR: agentDir,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.detected).toEqual([authPath]);
|
||||
expect(JSON.parse(fs.readFileSync(authPath, "utf8")).profiles["my-key"]).toEqual({
|
||||
type: "api_key",
|
||||
provider: "my-provider",
|
||||
key: "sk-snake-case-key",
|
||||
});
|
||||
});
|
||||
|
||||
it('does not touch profiles that already have the canonical "key" field', async () => {
|
||||
const state = await makeTestState();
|
||||
const canonical = {
|
||||
version: 1,
|
||||
profiles: {
|
||||
"good-key": {
|
||||
type: "api_key",
|
||||
provider: "my-provider",
|
||||
key: "sk-already-canonical",
|
||||
},
|
||||
},
|
||||
};
|
||||
const authPath = await state.writeAuthProfiles(canonical);
|
||||
|
||||
const result = await maybeRepairCanonicalApiKeyFieldAlias({
|
||||
cfg: {},
|
||||
prompter: makePrompter(true),
|
||||
now: () => 123,
|
||||
});
|
||||
|
||||
expect(result.detected).toStrictEqual([]);
|
||||
expect(result.changes).toStrictEqual([]);
|
||||
expect(JSON.parse(fs.readFileSync(authPath, "utf8"))).toEqual(canonical);
|
||||
expect(fs.existsSync(`${authPath}.api-key-alias.123.bak`)).toBe(false);
|
||||
});
|
||||
|
||||
it('does not replace canonical "keyRef" credentials with stale "api_key" fields', async () => {
|
||||
const state = await makeTestState();
|
||||
const canonical = {
|
||||
version: 1,
|
||||
profiles: {
|
||||
"ref-key": {
|
||||
type: "api_key",
|
||||
provider: "my-provider",
|
||||
keyRef: { source: "env", provider: "default", id: "MY_PROVIDER_API_KEY" },
|
||||
api_key: "stale-inline-key",
|
||||
},
|
||||
},
|
||||
};
|
||||
const authPath = await state.writeAuthProfiles(canonical);
|
||||
|
||||
const result = await maybeRepairCanonicalApiKeyFieldAlias({
|
||||
cfg: {},
|
||||
prompter: makePrompter(true),
|
||||
now: () => 123,
|
||||
});
|
||||
|
||||
expect(result.detected).toStrictEqual([]);
|
||||
expect(result.changes).toStrictEqual([]);
|
||||
expect(JSON.parse(fs.readFileSync(authPath, "utf8"))).toEqual(canonical);
|
||||
expect(fs.existsSync(`${authPath}.api-key-alias.123.bak`)).toBe(false);
|
||||
});
|
||||
|
||||
it('does not replace inline canonical SecretRef "key" credentials with stale "api_key" fields', async () => {
|
||||
const state = await makeTestState();
|
||||
const canonical = {
|
||||
version: 1,
|
||||
profiles: {
|
||||
"inline-ref-key": {
|
||||
type: "api_key",
|
||||
provider: "my-provider",
|
||||
key: { source: "env", provider: "default", id: "MY_PROVIDER_API_KEY" },
|
||||
api_key: "stale-inline-key",
|
||||
},
|
||||
},
|
||||
};
|
||||
const authPath = await state.writeAuthProfiles(canonical);
|
||||
|
||||
const result = await maybeRepairCanonicalApiKeyFieldAlias({
|
||||
cfg: {},
|
||||
prompter: makePrompter(true),
|
||||
now: () => 123,
|
||||
});
|
||||
|
||||
expect(result.detected).toStrictEqual([]);
|
||||
expect(result.changes).toStrictEqual([]);
|
||||
expect(JSON.parse(fs.readFileSync(authPath, "utf8"))).toEqual(canonical);
|
||||
expect(fs.existsSync(`${authPath}.api-key-alias.123.bak`)).toBe(false);
|
||||
});
|
||||
|
||||
it('reports the non-canonical "api_key" field without rewriting when repair is declined', async () => {
|
||||
const state = await makeTestState();
|
||||
const canonical = {
|
||||
version: 1,
|
||||
profiles: {
|
||||
"my-key": {
|
||||
type: "api_key",
|
||||
provider: "my-provider",
|
||||
api_key: "sk-snake-case-key",
|
||||
},
|
||||
},
|
||||
};
|
||||
const authPath = await state.writeAuthProfiles(canonical);
|
||||
|
||||
const result = await maybeRepairCanonicalApiKeyFieldAlias({
|
||||
cfg: {},
|
||||
prompter: makePrompter(false),
|
||||
now: () => 123,
|
||||
});
|
||||
|
||||
expect(result.detected).toEqual([authPath]);
|
||||
expect(result.changes).toStrictEqual([]);
|
||||
expect(JSON.parse(fs.readFileSync(authPath, "utf8"))).toEqual(canonical);
|
||||
expect(fs.existsSync(`${authPath}.api-key-alias.123.bak`)).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -12,6 +12,7 @@ import { formatCliCommand } from "../cli/command-format.js";
|
||||
import { resolveStateDir } from "../config/paths.js";
|
||||
import type { AuthProfileConfig } from "../config/types.auth.js";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import { coerceSecretRef } from "../config/types.secrets.js";
|
||||
import { loadJsonFile } from "../infra/json-file.js";
|
||||
import { isRecord } from "../shared/record-coerce.js";
|
||||
import { note } from "../terminal/note.js";
|
||||
@@ -173,8 +174,8 @@ function addCandidate(
|
||||
candidates.set(path.resolve(authPath), { agentDir, authPath });
|
||||
}
|
||||
|
||||
function listExistingAgentDirsFromState(): string[] {
|
||||
const root = path.join(resolveStateDir(), "agents");
|
||||
function listExistingAgentDirsFromState(env: NodeJS.ProcessEnv): string[] {
|
||||
const root = path.join(resolveStateDir(env), "agents");
|
||||
let entries: fs.Dirent[];
|
||||
try {
|
||||
entries = fs.readdirSync(root, { withFileTypes: true });
|
||||
@@ -193,13 +194,21 @@ function listExistingAgentDirsFromState(): string[] {
|
||||
});
|
||||
}
|
||||
|
||||
function listAuthProfileRepairCandidates(cfg: OpenClawConfig): AuthProfileRepairCandidate[] {
|
||||
function listAuthProfileRepairCandidates(
|
||||
cfg: OpenClawConfig,
|
||||
env: NodeJS.ProcessEnv,
|
||||
): AuthProfileRepairCandidate[] {
|
||||
const candidates = new Map<string, AuthProfileRepairCandidate>();
|
||||
addCandidate(candidates, resolveDefaultAgentDir(cfg));
|
||||
for (const agentId of listAgentIds(cfg)) {
|
||||
addCandidate(candidates, resolveAgentDir(cfg, agentId));
|
||||
addCandidate(candidates, resolveDefaultAgentDir(cfg, env));
|
||||
const envAgentDir =
|
||||
readNonEmptyString(env.OPENCLAW_AGENT_DIR) ?? readNonEmptyString(env.PI_CODING_AGENT_DIR);
|
||||
if (envAgentDir) {
|
||||
addCandidate(candidates, envAgentDir);
|
||||
}
|
||||
for (const agentDir of listExistingAgentDirsFromState()) {
|
||||
for (const agentId of listAgentIds(cfg)) {
|
||||
addCandidate(candidates, resolveAgentDir(cfg, agentId, env));
|
||||
}
|
||||
for (const agentDir of listExistingAgentDirsFromState(env)) {
|
||||
addCandidate(candidates, agentDir);
|
||||
}
|
||||
return [...candidates.values()];
|
||||
@@ -303,12 +312,14 @@ export async function maybeRepairLegacyFlatAuthProfileStores(params: {
|
||||
cfg: OpenClawConfig;
|
||||
prompter: DoctorPrompter;
|
||||
now?: () => number;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
}): Promise<LegacyFlatAuthProfileRepairResult> {
|
||||
const now = params.now ?? Date.now;
|
||||
const legacyStores = listAuthProfileRepairCandidates(params.cfg)
|
||||
const env = params.env ?? process.env;
|
||||
const legacyStores = listAuthProfileRepairCandidates(params.cfg, env)
|
||||
.map(resolveLegacyFlatStore)
|
||||
.filter((entry): entry is LegacyFlatAuthProfileStore => entry !== null);
|
||||
const awsSdkMarkerStores = listAuthProfileRepairCandidates(params.cfg)
|
||||
const awsSdkMarkerStores = listAuthProfileRepairCandidates(params.cfg, env)
|
||||
.map(resolveAwsSdkAuthProfileMarkerStore)
|
||||
.filter((entry): entry is AwsSdkAuthProfileMarkerStore => entry !== null);
|
||||
|
||||
@@ -399,3 +410,114 @@ export async function maybeRepairLegacyFlatAuthProfileStores(params: {
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
type CanonicalApiKeyAliasRepair = {
|
||||
authPath: string;
|
||||
raw: Record<string, unknown>;
|
||||
profileIds: string[];
|
||||
};
|
||||
|
||||
function resolveCanonicalApiKeyAliasRepair(
|
||||
candidate: AuthProfileRepairCandidate,
|
||||
): CanonicalApiKeyAliasRepair | null {
|
||||
if (!fs.existsSync(candidate.authPath)) {
|
||||
return null;
|
||||
}
|
||||
const raw = loadJsonFile(candidate.authPath);
|
||||
if (!isRecord(raw) || !isRecord(raw.profiles)) {
|
||||
return null;
|
||||
}
|
||||
const profileIds: string[] = [];
|
||||
for (const [profileId, value] of Object.entries(raw.profiles)) {
|
||||
if (!isRecord(value)) {
|
||||
continue;
|
||||
}
|
||||
const type = readNonEmptyString(value.type) ?? readNonEmptyString(value.mode);
|
||||
const hasApiKeyField =
|
||||
readNonEmptyString(value["api_key"]) !== undefined ||
|
||||
coerceSecretRef(value["api_key"]) !== null;
|
||||
const hasCanonicalKey =
|
||||
readNonEmptyString(value.key) !== undefined || coerceSecretRef(value.key) !== null;
|
||||
const hasCanonicalKeyRef = coerceSecretRef(value.keyRef) !== null;
|
||||
if (type === "api_key" && hasApiKeyField && !hasCanonicalKey && !hasCanonicalKeyRef) {
|
||||
profileIds.push(profileId);
|
||||
}
|
||||
}
|
||||
return profileIds.length > 0 ? { authPath: candidate.authPath, raw, profileIds } : null;
|
||||
}
|
||||
|
||||
function backupCanonicalApiKeyAlias(authPath: string, now: () => number): string {
|
||||
const backupPath = `${authPath}.api-key-alias.${now()}.bak`;
|
||||
fs.copyFileSync(authPath, backupPath);
|
||||
return backupPath;
|
||||
}
|
||||
|
||||
export async function maybeRepairCanonicalApiKeyFieldAlias(params: {
|
||||
cfg: OpenClawConfig;
|
||||
prompter: DoctorPrompter;
|
||||
now?: () => number;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
}): Promise<LegacyFlatAuthProfileRepairResult> {
|
||||
const now = params.now ?? Date.now;
|
||||
const env = params.env ?? process.env;
|
||||
const repairs = listAuthProfileRepairCandidates(params.cfg, env)
|
||||
.map(resolveCanonicalApiKeyAliasRepair)
|
||||
.filter((entry): entry is CanonicalApiKeyAliasRepair => entry !== null);
|
||||
|
||||
const result: LegacyFlatAuthProfileRepairResult = {
|
||||
detected: repairs.map((entry) => entry.authPath),
|
||||
changes: [],
|
||||
warnings: [],
|
||||
};
|
||||
if (repairs.length === 0) {
|
||||
return result;
|
||||
}
|
||||
|
||||
const noteLines = repairs.map(
|
||||
(entry) =>
|
||||
`- ${shortenHomePath(entry.authPath)} has ${entry.profileIds.length} profile(s) using the non-canonical "api_key" field; the canonical field is "key".`,
|
||||
);
|
||||
noteLines.push(
|
||||
`- Runtime auth parsing only reads canonical "key" and "keyRef" fields, so these profiles are silently skipped; ${formatCliCommand("openclaw doctor --fix")} rewrites "api_key" to "key" with a backup.`,
|
||||
);
|
||||
note(noteLines.join("\n"), "Auth profiles");
|
||||
|
||||
const shouldRepair = await params.prompter.confirmAutoFix({
|
||||
message: 'Rewrite non-canonical "api_key" fields to "key" now?',
|
||||
initialValue: true,
|
||||
});
|
||||
if (!shouldRepair) {
|
||||
return result;
|
||||
}
|
||||
|
||||
for (const entry of repairs) {
|
||||
try {
|
||||
const backupPath = backupCanonicalApiKeyAlias(entry.authPath, now);
|
||||
const profiles = entry.raw.profiles as Record<string, Record<string, unknown>>;
|
||||
for (const profileId of entry.profileIds) {
|
||||
const profile = profiles[profileId];
|
||||
if (!isRecord(profile)) {
|
||||
continue;
|
||||
}
|
||||
profile.key = profile["api_key"];
|
||||
delete profile["api_key"];
|
||||
}
|
||||
fs.writeFileSync(entry.authPath, `${JSON.stringify(entry.raw, null, 2)}\n`);
|
||||
result.changes.push(
|
||||
`Rewrote ${entry.profileIds.length} "api_key" field(s) to "key" in ${shortenHomePath(entry.authPath)} (backup: ${shortenHomePath(backupPath)}).`,
|
||||
);
|
||||
} catch (err) {
|
||||
result.warnings.push(
|
||||
`Failed to rewrite "api_key" fields in ${shortenHomePath(entry.authPath)}: ${String(err)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
clearRuntimeAuthProfileStoreSnapshots();
|
||||
if (result.changes.length > 0) {
|
||||
note(result.changes.map((change) => `- ${change}`).join("\n"), "Doctor changes");
|
||||
}
|
||||
if (result.warnings.length > 0) {
|
||||
note(result.warnings.map((warning) => `- ${warning}`).join("\n"), "Doctor warnings");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -133,7 +133,7 @@ async function runGatewayConfigHealth(ctx: DoctorHealthFlowContext): Promise<voi
|
||||
}
|
||||
|
||||
async function runAuthProfileHealth(ctx: DoctorHealthFlowContext): Promise<void> {
|
||||
const { maybeRepairLegacyFlatAuthProfileStores } =
|
||||
const { maybeRepairLegacyFlatAuthProfileStores, maybeRepairCanonicalApiKeyFieldAlias } =
|
||||
await import("../commands/doctor-auth-flat-profiles.js");
|
||||
const { maybeRepairLegacyOAuthProfileIds } =
|
||||
await import("../commands/doctor-auth-legacy-oauth.js");
|
||||
@@ -147,6 +147,10 @@ async function runAuthProfileHealth(ctx: DoctorHealthFlowContext): Promise<void>
|
||||
cfg: ctx.cfg,
|
||||
prompter: ctx.prompter,
|
||||
});
|
||||
await maybeRepairCanonicalApiKeyFieldAlias({
|
||||
cfg: ctx.cfg,
|
||||
prompter: ctx.prompter,
|
||||
});
|
||||
await maybeRepairLegacyOAuthSidecarProfiles({
|
||||
cfg: ctx.cfg,
|
||||
prompter: ctx.prompter,
|
||||
|
||||
Reference in New Issue
Block a user