fix(auth): harden legacy auth.json cleanup
This commit is contained in:
@@ -1,8 +1,13 @@
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
type AuthProfileStore,
|
||||
calculateAuthProfileCooldownMs,
|
||||
ensureAuthProfileStore,
|
||||
resolveAuthProfileOrder,
|
||||
} from "./auth-profiles.js";
|
||||
|
||||
@@ -280,6 +285,51 @@ describe("resolveAuthProfileOrder", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("ensureAuthProfileStore", () => {
|
||||
it("migrates legacy auth.json and deletes it (PR #368)", () => {
|
||||
const agentDir = fs.mkdtempSync(
|
||||
path.join(os.tmpdir(), "clawdbot-auth-profiles-"),
|
||||
);
|
||||
try {
|
||||
const legacyPath = path.join(agentDir, "auth.json");
|
||||
fs.writeFileSync(
|
||||
legacyPath,
|
||||
`${JSON.stringify(
|
||||
{
|
||||
anthropic: {
|
||||
type: "oauth",
|
||||
provider: "anthropic",
|
||||
access: "access-token",
|
||||
refresh: "refresh-token",
|
||||
expires: Date.now() + 60_000,
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`,
|
||||
"utf8",
|
||||
);
|
||||
|
||||
const store = ensureAuthProfileStore(agentDir);
|
||||
expect(store.profiles["anthropic:default"]).toMatchObject({
|
||||
type: "oauth",
|
||||
provider: "anthropic",
|
||||
});
|
||||
|
||||
const migratedPath = path.join(agentDir, "auth-profiles.json");
|
||||
expect(fs.existsSync(migratedPath)).toBe(true);
|
||||
expect(fs.existsSync(legacyPath)).toBe(false);
|
||||
|
||||
// idempotent
|
||||
const store2 = ensureAuthProfileStore(agentDir);
|
||||
expect(store2.profiles["anthropic:default"]).toBeDefined();
|
||||
expect(fs.existsSync(legacyPath)).toBe(false);
|
||||
} finally {
|
||||
fs.rmSync(agentDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("auth profile cooldowns", () => {
|
||||
it("applies exponential backoff with a 1h cap", () => {
|
||||
expect(calculateAuthProfileCooldownMs(1)).toBe(60_000);
|
||||
|
||||
@@ -11,6 +11,7 @@ import lockfile from "proper-lockfile";
|
||||
import type { ClawdbotConfig } from "../config/config.js";
|
||||
import { resolveOAuthPath } from "../config/paths.js";
|
||||
import type { AuthProfileConfig } from "../config/types.js";
|
||||
import { createSubsystemLogger } from "../logging.js";
|
||||
import { resolveUserPath } from "../utils.js";
|
||||
import { resolveClawdbotAgentDir } from "./agent-paths.js";
|
||||
import { normalizeProviderId } from "./model-selection.js";
|
||||
@@ -29,6 +30,8 @@ const AUTH_STORE_LOCK_OPTIONS = {
|
||||
stale: 30_000,
|
||||
} as const;
|
||||
|
||||
const log = createSubsystemLogger("agents/auth-profiles");
|
||||
|
||||
export type ApiKeyCredential = {
|
||||
type: "api_key";
|
||||
provider: string;
|
||||
@@ -350,14 +353,20 @@ export function ensureAuthProfileStore(agentDir?: string): AuthProfileStore {
|
||||
saveJsonFile(authPath, store);
|
||||
}
|
||||
|
||||
// Delete legacy auth.json after successful migration to prevent stale tokens
|
||||
// from being re-migrated and overwriting fresh credentials (fixes #363)
|
||||
if (legacy !== null) {
|
||||
// PR #368: legacy auth.json could get re-migrated from other agent dirs,
|
||||
// overwriting fresh OAuth creds with stale tokens (fixes #363). Delete only
|
||||
// after we've successfully written auth-profiles.json.
|
||||
if (shouldWrite && legacy !== null) {
|
||||
const legacyPath = resolveLegacyAuthStorePath(agentDir);
|
||||
try {
|
||||
fs.unlinkSync(legacyPath);
|
||||
} catch {
|
||||
// Ignore if already deleted or permission issues
|
||||
} catch (err) {
|
||||
if ((err as NodeJS.ErrnoException)?.code !== "ENOENT") {
|
||||
log.warn("failed to delete legacy auth.json after migration", {
|
||||
err,
|
||||
legacyPath,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user