refactor(auth): unify external CLI credential sync

This commit is contained in:
Peter Steinberger 2026-03-24 10:01:46 -07:00
parent 27448c3113
commit 8e9e2d2f4e
No known key found for this signature in database
5 changed files with 401 additions and 216 deletions

View File

@ -1,21 +1,70 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { AuthProfileStore } from "./auth-profiles/types.js";
import type { AuthProfileStore, OAuthCredential } from "./auth-profiles/types.js";
const mocks = vi.hoisted(() => ({
readCodexCliCredentialsCached: vi.fn(),
readQwenCliCredentialsCached: vi.fn(() => null),
readMiniMaxCliCredentialsCached: vi.fn(() => null),
readCodexCliCredentialsCached: vi.fn<() => OAuthCredential | null>(() => null),
readQwenCliCredentialsCached: vi.fn<() => OAuthCredential | null>(() => null),
readMiniMaxCliCredentialsCached: vi.fn<() => OAuthCredential | null>(() => null),
}));
let syncExternalCliCredentials: typeof import("./auth-profiles/external-cli-sync.js").syncExternalCliCredentials;
let shouldReplaceStoredOAuthCredential: typeof import("./auth-profiles/external-cli-sync.js").shouldReplaceStoredOAuthCredential;
let CODEX_CLI_PROFILE_ID: typeof import("./auth-profiles/constants.js").CODEX_CLI_PROFILE_ID;
let OPENAI_CODEX_DEFAULT_PROFILE_ID: typeof import("./auth-profiles/constants.js").OPENAI_CODEX_DEFAULT_PROFILE_ID;
let QWEN_CLI_PROFILE_ID: typeof import("./auth-profiles/constants.js").QWEN_CLI_PROFILE_ID;
let MINIMAX_CLI_PROFILE_ID: typeof import("./auth-profiles/constants.js").MINIMAX_CLI_PROFILE_ID;
const OPENAI_CODEX_DEFAULT_PROFILE_ID = "openai-codex:default";
function makeOAuthCredential(
overrides: Partial<OAuthCredential> & Pick<OAuthCredential, "provider">,
) {
return {
type: "oauth" as const,
provider: overrides.provider,
access: overrides.access ?? `${overrides.provider}-access`,
refresh: overrides.refresh ?? `${overrides.provider}-refresh`,
expires: overrides.expires ?? Date.now() + 60_000,
accountId: overrides.accountId,
email: overrides.email,
enterpriseUrl: overrides.enterpriseUrl,
projectId: overrides.projectId,
};
}
function makeStore(profileId?: string, credential?: OAuthCredential): AuthProfileStore {
return {
version: 1,
profiles: profileId && credential ? { [profileId]: credential } : {},
};
}
function getProviderCases() {
return [
{
label: "Codex",
profileId: OPENAI_CODEX_DEFAULT_PROFILE_ID,
provider: "openai-codex" as const,
readMock: mocks.readCodexCliCredentialsCached,
legacyProfileId: CODEX_CLI_PROFILE_ID,
},
{
label: "Qwen",
profileId: QWEN_CLI_PROFILE_ID,
provider: "qwen-portal" as const,
readMock: mocks.readQwenCliCredentialsCached,
},
{
label: "MiniMax",
profileId: MINIMAX_CLI_PROFILE_ID,
provider: "minimax-portal" as const,
readMock: mocks.readMiniMaxCliCredentialsCached,
},
];
}
describe("syncExternalCliCredentials", () => {
beforeEach(async () => {
vi.resetModules();
mocks.readCodexCliCredentialsCached.mockReset();
mocks.readCodexCliCredentialsCached.mockReset().mockReturnValue(null);
mocks.readQwenCliCredentialsCached.mockReset().mockReturnValue(null);
mocks.readMiniMaxCliCredentialsCached.mockReset().mockReturnValue(null);
vi.doMock("./cli-credentials.js", () => ({
@ -23,68 +72,118 @@ describe("syncExternalCliCredentials", () => {
readQwenCliCredentialsCached: mocks.readQwenCliCredentialsCached,
readMiniMaxCliCredentialsCached: mocks.readMiniMaxCliCredentialsCached,
}));
({ syncExternalCliCredentials } = await import("./auth-profiles/external-cli-sync.js"));
({ CODEX_CLI_PROFILE_ID } = await import("./auth-profiles/constants.js"));
({ syncExternalCliCredentials, shouldReplaceStoredOAuthCredential } =
await import("./auth-profiles/external-cli-sync.js"));
({
CODEX_CLI_PROFILE_ID,
OPENAI_CODEX_DEFAULT_PROFILE_ID,
QWEN_CLI_PROFILE_ID,
MINIMAX_CLI_PROFILE_ID,
} = await import("./auth-profiles/constants.js"));
});
it("syncs Codex CLI credentials into the supported default auth profile", () => {
const expires = Date.now() + 60_000;
mocks.readCodexCliCredentialsCached.mockReturnValue({
type: "oauth",
provider: "openai-codex",
access: "access-token",
refresh: "refresh-token",
expires,
accountId: "acct_123",
describe("shouldReplaceStoredOAuthCredential", () => {
it("keeps equivalent stored credentials", () => {
const stored = makeOAuthCredential({ provider: "openai-codex", access: "a", refresh: "r" });
const incoming = makeOAuthCredential({ provider: "openai-codex", access: "a", refresh: "r" });
expect(shouldReplaceStoredOAuthCredential(stored, incoming)).toBe(false);
});
const store: AuthProfileStore = {
version: 1,
profiles: {},
};
it("keeps the newer stored credential", () => {
const incoming = makeOAuthCredential({
provider: "openai-codex",
expires: Date.now() + 60_000,
});
const stored = makeOAuthCredential({
provider: "openai-codex",
access: "fresh-access",
refresh: "fresh-refresh",
expires: Date.now() + 5 * 24 * 60 * 60_000,
});
const mutated = syncExternalCliCredentials(store);
expect(mutated).toBe(true);
expect(mocks.readCodexCliCredentialsCached).toHaveBeenCalledWith(
expect.objectContaining({ ttlMs: expect.any(Number) }),
);
expect(store.profiles[OPENAI_CODEX_DEFAULT_PROFILE_ID]).toMatchObject({
type: "oauth",
provider: "openai-codex",
access: "access-token",
refresh: "refresh-token",
expires,
accountId: "acct_123",
expect(shouldReplaceStoredOAuthCredential(stored, incoming)).toBe(false);
});
it("replaces when incoming credentials are fresher", () => {
const stored = makeOAuthCredential({
provider: "openai-codex",
expires: Date.now() + 60_000,
});
const incoming = makeOAuthCredential({
provider: "openai-codex",
access: "new-access",
refresh: "new-refresh",
expires: Date.now() + 5 * 24 * 60 * 60_000,
});
expect(shouldReplaceStoredOAuthCredential(stored, incoming)).toBe(true);
expect(shouldReplaceStoredOAuthCredential(undefined, incoming)).toBe(true);
});
expect(store.profiles[CODEX_CLI_PROFILE_ID]).toBeUndefined();
});
it.each([{ providerLabel: "Codex" }, { providerLabel: "Qwen" }, { providerLabel: "MiniMax" }])(
"syncs $providerLabel CLI credentials into the target auth profile",
({ providerLabel }) => {
const providerCase = getProviderCases().find((entry) => entry.label === providerLabel);
expect(providerCase).toBeDefined();
const current = providerCase!;
const expires = Date.now() + 60_000;
current.readMock.mockReturnValue(
makeOAuthCredential({
provider: current.provider,
access: `${current.provider}-access-token`,
refresh: `${current.provider}-refresh-token`,
expires,
accountId: "acct_123",
}),
);
const store = makeStore();
const mutated = syncExternalCliCredentials(store);
expect(mutated).toBe(true);
expect(current.readMock).toHaveBeenCalledWith(
expect.objectContaining({ ttlMs: expect.any(Number) }),
);
expect(store.profiles[current.profileId]).toMatchObject({
type: "oauth",
provider: current.provider,
access: `${current.provider}-access-token`,
refresh: `${current.provider}-refresh-token`,
expires,
accountId: "acct_123",
});
if (current.legacyProfileId) {
expect(store.profiles[current.legacyProfileId]).toBeUndefined();
}
},
);
it("refreshes stored Codex expiry from external CLI even when the cached profile looks fresh", () => {
const staleExpiry = Date.now() + 30 * 60_000;
const freshExpiry = Date.now() + 5 * 24 * 60 * 60_000;
mocks.readCodexCliCredentialsCached.mockReturnValue({
type: "oauth",
provider: "openai-codex",
access: "new-access-token",
refresh: "new-refresh-token",
expires: freshExpiry,
accountId: "acct_456",
});
mocks.readCodexCliCredentialsCached.mockReturnValue(
makeOAuthCredential({
provider: "openai-codex",
access: "new-access-token",
refresh: "new-refresh-token",
expires: freshExpiry,
accountId: "acct_456",
}),
);
const store: AuthProfileStore = {
version: 1,
profiles: {
[OPENAI_CODEX_DEFAULT_PROFILE_ID]: {
type: "oauth",
provider: "openai-codex",
access: "old-access-token",
refresh: "old-refresh-token",
expires: staleExpiry,
accountId: "acct_456",
},
},
};
const store = makeStore(
OPENAI_CODEX_DEFAULT_PROFILE_ID,
makeOAuthCredential({
provider: "openai-codex",
access: "old-access-token",
refresh: "old-refresh-token",
expires: staleExpiry,
accountId: "acct_456",
}),
);
const mutated = syncExternalCliCredentials(store);
@ -96,39 +195,43 @@ describe("syncExternalCliCredentials", () => {
});
});
it("does not overwrite newer stored Codex credentials with older external CLI credentials", () => {
const staleExpiry = Date.now() + 30 * 60_000;
const freshExpiry = Date.now() + 5 * 24 * 60 * 60_000;
mocks.readCodexCliCredentialsCached.mockReturnValue({
type: "oauth",
provider: "openai-codex",
access: "stale-access-token",
refresh: "stale-refresh-token",
expires: staleExpiry,
accountId: "acct_789",
});
it.each([{ providerLabel: "Codex" }, { providerLabel: "Qwen" }, { providerLabel: "MiniMax" }])(
"does not overwrite newer stored $providerLabel credentials",
({ providerLabel }) => {
const providerCase = getProviderCases().find((entry) => entry.label === providerLabel);
expect(providerCase).toBeDefined();
const current = providerCase!;
const staleExpiry = Date.now() + 30 * 60_000;
const freshExpiry = Date.now() + 5 * 24 * 60 * 60_000;
current.readMock.mockReturnValue(
makeOAuthCredential({
provider: current.provider,
access: `stale-${current.provider}-access-token`,
refresh: `stale-${current.provider}-refresh-token`,
expires: staleExpiry,
accountId: "acct_789",
}),
);
const store: AuthProfileStore = {
version: 1,
profiles: {
[OPENAI_CODEX_DEFAULT_PROFILE_ID]: {
type: "oauth",
provider: "openai-codex",
access: "fresh-access-token",
refresh: "fresh-refresh-token",
const store = makeStore(
current.profileId,
makeOAuthCredential({
provider: current.provider,
access: `fresh-${current.provider}-access-token`,
refresh: `fresh-${current.provider}-refresh-token`,
expires: freshExpiry,
accountId: "acct_789",
},
},
};
}),
);
const mutated = syncExternalCliCredentials(store);
const mutated = syncExternalCliCredentials(store);
expect(mutated).toBe(false);
expect(store.profiles[OPENAI_CODEX_DEFAULT_PROFILE_ID]).toMatchObject({
access: "fresh-access-token",
refresh: "fresh-refresh-token",
expires: freshExpiry,
});
});
expect(mutated).toBe(false);
expect(store.profiles[current.profileId]).toMatchObject({
access: `fresh-${current.provider}-access-token`,
refresh: `fresh-${current.provider}-refresh-token`,
expires: freshExpiry,
});
},
);
});

View File

@ -6,6 +6,7 @@ export const LEGACY_AUTH_FILENAME = "auth.json";
export const CLAUDE_CLI_PROFILE_ID = "anthropic:claude-cli";
export const CODEX_CLI_PROFILE_ID = "openai-codex:codex-cli";
export const OPENAI_CODEX_DEFAULT_PROFILE_ID = "openai-codex:default";
export const QWEN_CLI_PROFILE_ID = "qwen-portal:qwen-cli";
export const MINIMAX_CLI_PROFILE_ID = "minimax-portal:minimax-cli";

View File

@ -5,19 +5,27 @@ import {
} from "../cli-credentials.js";
import {
EXTERNAL_CLI_SYNC_TTL_MS,
OPENAI_CODEX_DEFAULT_PROFILE_ID,
QWEN_CLI_PROFILE_ID,
MINIMAX_CLI_PROFILE_ID,
log,
} from "./constants.js";
import type { AuthProfileStore, OAuthCredential } from "./types.js";
const OPENAI_CODEX_DEFAULT_PROFILE_ID = "openai-codex:default";
type ExternalCliSyncOptions = {
log?: boolean;
};
function shallowEqualOAuthCredentials(a: OAuthCredential | undefined, b: OAuthCredential): boolean {
type ExternalCliSyncProvider = {
profileId: string;
provider: string;
readCredentials: () => OAuthCredential | null;
};
function areOAuthCredentialsEquivalent(
a: OAuthCredential | undefined,
b: OAuthCredential,
): boolean {
if (!a) {
return false;
}
@ -48,14 +56,44 @@ function hasNewerStoredOAuthCredential(
);
}
export function shouldReplaceStoredOAuthCredential(
existing: OAuthCredential | undefined,
incoming: OAuthCredential,
): boolean {
if (!existing || existing.type !== "oauth") {
return true;
}
if (areOAuthCredentialsEquivalent(existing, incoming)) {
return false;
}
return !hasNewerStoredOAuthCredential(existing, incoming);
}
const EXTERNAL_CLI_SYNC_PROVIDERS: ExternalCliSyncProvider[] = [
{
profileId: QWEN_CLI_PROFILE_ID,
provider: "qwen-portal",
readCredentials: () => readQwenCliCredentialsCached({ ttlMs: EXTERNAL_CLI_SYNC_TTL_MS }),
},
{
profileId: MINIMAX_CLI_PROFILE_ID,
provider: "minimax-portal",
readCredentials: () => readMiniMaxCliCredentialsCached({ ttlMs: EXTERNAL_CLI_SYNC_TTL_MS }),
},
{
profileId: OPENAI_CODEX_DEFAULT_PROFILE_ID,
provider: "openai-codex",
readCredentials: () => readCodexCliCredentialsCached({ ttlMs: EXTERNAL_CLI_SYNC_TTL_MS }),
},
];
/** Sync external CLI credentials into the store for a given provider. */
function syncExternalCliCredentialsForProvider(
store: AuthProfileStore,
profileId: string,
provider: string,
readCredentials: () => OAuthCredential | null,
providerConfig: ExternalCliSyncProvider,
options: ExternalCliSyncOptions,
): boolean {
const { profileId, provider, readCredentials } = providerConfig;
const existing = store.profiles[profileId];
const creds = readCredentials();
if (!creds) {
@ -63,18 +101,17 @@ function syncExternalCliCredentialsForProvider(
}
const existingOAuth = existing?.type === "oauth" ? existing : undefined;
if (shallowEqualOAuthCredentials(existingOAuth, creds)) {
return false;
}
if (hasNewerStoredOAuthCredential(existingOAuth, creds)) {
if (!shouldReplaceStoredOAuthCredential(existingOAuth, creds)) {
if (options.log !== false) {
log.debug(`kept newer stored ${provider} credentials over external cli sync`, {
profileId,
storedExpires: new Date(existingOAuth!.expires).toISOString(),
externalExpires: Number.isFinite(creds.expires)
? new Date(creds.expires).toISOString()
: null,
});
if (!areOAuthCredentialsEquivalent(existingOAuth, creds) && existingOAuth) {
log.debug(`kept newer stored ${provider} credentials over external cli sync`, {
profileId,
storedExpires: new Date(existingOAuth.expires).toISOString(),
externalExpires: Number.isFinite(creds.expires)
? new Date(creds.expires).toISOString()
: null,
});
}
}
return false;
}
@ -101,38 +138,10 @@ export function syncExternalCliCredentials(
): boolean {
let mutated = false;
if (
syncExternalCliCredentialsForProvider(
store,
QWEN_CLI_PROFILE_ID,
"qwen-portal",
() => readQwenCliCredentialsCached({ ttlMs: EXTERNAL_CLI_SYNC_TTL_MS }),
options,
)
) {
mutated = true;
}
if (
syncExternalCliCredentialsForProvider(
store,
MINIMAX_CLI_PROFILE_ID,
"minimax-portal",
() => readMiniMaxCliCredentialsCached({ ttlMs: EXTERNAL_CLI_SYNC_TTL_MS }),
options,
)
) {
mutated = true;
}
if (
syncExternalCliCredentialsForProvider(
store,
OPENAI_CODEX_DEFAULT_PROFILE_ID,
"openai-codex",
() => readCodexCliCredentialsCached({ ttlMs: EXTERNAL_CLI_SYNC_TTL_MS }),
options,
)
) {
mutated = true;
for (const provider of EXTERNAL_CLI_SYNC_PROVIDERS) {
if (syncExternalCliCredentialsForProvider(store, provider, options)) {
mutated = true;
}
}
return mutated;

View File

@ -8,6 +8,7 @@ const execFileSyncMock = vi.fn();
const CLI_CREDENTIALS_CACHE_TTL_MS = 15 * 60 * 1000;
let readClaudeCliCredentialsCached: typeof import("./cli-credentials.js").readClaudeCliCredentialsCached;
let readCodexCliCredentialsCached: typeof import("./cli-credentials.js").readCodexCliCredentialsCached;
let readQwenCliCredentialsCached: typeof import("./cli-credentials.js").readQwenCliCredentialsCached;
let resetCliCredentialCachesForTest: typeof import("./cli-credentials.js").resetCliCredentialCachesForTest;
let writeClaudeCliKeychainCredentials: typeof import("./cli-credentials.js").writeClaudeCliKeychainCredentials;
let writeClaudeCliCredentials: typeof import("./cli-credentials.js").writeClaudeCliCredentials;
@ -53,11 +54,28 @@ function createJwtWithExp(expSeconds: number): string {
return `${encode({ alg: "RS256", typ: "JWT" })}.${encode({ exp: expSeconds })}.signature`;
}
function writePortalCliCredentialFile(
filePath: string,
options: { access: string; refresh: string; expires: number },
) {
fs.mkdirSync(path.dirname(filePath), { recursive: true, mode: 0o700 });
fs.writeFileSync(
filePath,
JSON.stringify({
access_token: options.access,
refresh_token: options.refresh,
expiry_date: options.expires,
}),
"utf8",
);
}
describe("cli credentials", () => {
beforeAll(async () => {
({
readClaudeCliCredentialsCached,
readCodexCliCredentialsCached,
readQwenCliCredentialsCached,
resetCliCredentialCachesForTest,
writeClaudeCliKeychainCredentials,
writeClaudeCliCredentials,
@ -354,4 +372,50 @@ describe("cli credentials", () => {
fs.rmSync(tempHome, { recursive: true, force: true });
}
});
it("invalidates cached Qwen credentials when oauth_creds.json changes within the TTL window", () => {
const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-qwen-cache-"));
const credPath = path.join(tempHome, ".qwen", "oauth_creds.json");
try {
writePortalCliCredentialFile(credPath, {
access: "stale-access",
refresh: "stale-refresh",
expires: 1_000,
});
fs.utimesSync(credPath, new Date("2026-03-24T10:00:00Z"), new Date("2026-03-24T10:00:00Z"));
vi.setSystemTime(new Date("2026-03-24T10:00:00Z"));
const first = readQwenCliCredentialsCached({
ttlMs: CLI_CREDENTIALS_CACHE_TTL_MS,
homeDir: tempHome,
});
expect(first).toMatchObject({
access: "stale-access",
refresh: "stale-refresh",
expires: 1_000,
});
writePortalCliCredentialFile(credPath, {
access: "fresh-access",
refresh: "fresh-refresh",
expires: 2_000,
});
fs.utimesSync(credPath, new Date("2026-03-24T10:05:00Z"), new Date("2026-03-24T10:05:00Z"));
vi.advanceTimersByTime(60_000);
const second = readQwenCliCredentialsCached({
ttlMs: CLI_CREDENTIALS_CACHE_TTL_MS,
homeDir: tempHome,
});
expect(second).toMatchObject({
access: "fresh-access",
refresh: "fresh-refresh",
expires: 2_000,
});
} finally {
fs.rmSync(tempHome, { recursive: true, force: true });
}
});
});

View File

@ -21,7 +21,7 @@ type CachedValue<T> = {
value: T | null;
readAt: number;
cacheKey: string;
sourceMtimeMs?: number | null;
sourceFingerprint?: number | string | null;
};
let claudeCliCache: CachedValue<ClaudeCliCredential> | null = null;
@ -157,6 +157,45 @@ function readFileMtimeMs(filePath: string): number | null {
}
}
function readCachedCliCredential<T>(options: {
ttlMs: number;
cache: CachedValue<T> | null;
cacheKey: string;
read: () => T | null;
setCache: (next: CachedValue<T> | null) => void;
readSourceFingerprint?: () => number | string | null;
}): T | null {
const { ttlMs, cache, cacheKey, read, setCache, readSourceFingerprint } = options;
if (ttlMs <= 0) {
return read();
}
const now = Date.now();
const sourceFingerprint = readSourceFingerprint?.();
if (
cache &&
cache.cacheKey === cacheKey &&
cache.sourceFingerprint === sourceFingerprint &&
now - cache.readAt < ttlMs
) {
return cache.value;
}
const value = read();
const cachedSourceFingerprint = readSourceFingerprint?.();
if (!readSourceFingerprint || cachedSourceFingerprint === sourceFingerprint) {
setCache({
value,
readAt: now,
cacheKey,
sourceFingerprint: cachedSourceFingerprint,
});
} else {
setCache(null);
}
return value;
}
function computeCodexKeychainAccount(codexHome: string) {
const hash = createHash("sha256").update(codexHome).digest("hex");
return `cli|${hash.slice(0, 16)}`;
@ -334,27 +373,21 @@ export function readClaudeCliCredentialsCached(options?: {
homeDir?: string;
execSync?: ExecSyncFn;
}): ClaudeCliCredential | null {
const ttlMs = options?.ttlMs ?? 0;
const now = Date.now();
const cacheKey = resolveClaudeCliCredentialsPath(options?.homeDir);
if (
ttlMs > 0 &&
claudeCliCache &&
claudeCliCache.cacheKey === cacheKey &&
now - claudeCliCache.readAt < ttlMs
) {
return claudeCliCache.value;
}
const value = readClaudeCliCredentials({
allowKeychainPrompt: options?.allowKeychainPrompt,
platform: options?.platform,
homeDir: options?.homeDir,
execSync: options?.execSync,
return readCachedCliCredential({
ttlMs: options?.ttlMs ?? 0,
cache: claudeCliCache,
cacheKey: resolveClaudeCliCredentialsPath(options?.homeDir),
read: () =>
readClaudeCliCredentials({
allowKeychainPrompt: options?.allowKeychainPrompt,
platform: options?.platform,
homeDir: options?.homeDir,
execSync: options?.execSync,
}),
setCache: (next) => {
claudeCliCache = next;
},
});
if (ttlMs > 0) {
claudeCliCache = { value, readAt: now, cacheKey };
}
return value;
}
export function writeClaudeCliKeychainCredentials(
@ -533,78 +566,53 @@ export function readCodexCliCredentialsCached(options?: {
platform?: NodeJS.Platform;
execSync?: ExecSyncFn;
}): CodexCliCredential | null {
const ttlMs = options?.ttlMs ?? 0;
const now = Date.now();
const authPath = resolveCodexCliAuthPath();
const cacheKey = `${options?.platform ?? process.platform}|${authPath}`;
const sourceMtimeMs = readFileMtimeMs(authPath);
if (
ttlMs > 0 &&
codexCliCache &&
codexCliCache.cacheKey === cacheKey &&
codexCliCache.sourceMtimeMs === sourceMtimeMs &&
now - codexCliCache.readAt < ttlMs
) {
return codexCliCache.value;
}
const value = readCodexCliCredentials({
platform: options?.platform,
execSync: options?.execSync,
return readCachedCliCredential({
ttlMs: options?.ttlMs ?? 0,
cache: codexCliCache,
cacheKey: `${options?.platform ?? process.platform}|${authPath}`,
read: () =>
readCodexCliCredentials({
platform: options?.platform,
execSync: options?.execSync,
}),
setCache: (next) => {
codexCliCache = next;
},
readSourceFingerprint: () => readFileMtimeMs(authPath),
});
const cachedSourceMtimeMs = readFileMtimeMs(authPath);
if (ttlMs > 0 && cachedSourceMtimeMs === sourceMtimeMs) {
codexCliCache = {
value,
readAt: now,
cacheKey,
sourceMtimeMs: cachedSourceMtimeMs,
};
} else if (ttlMs > 0) {
codexCliCache = null;
}
return value;
}
export function readQwenCliCredentialsCached(options?: {
ttlMs?: number;
homeDir?: string;
}): QwenCliCredential | null {
const ttlMs = options?.ttlMs ?? 0;
const now = Date.now();
const cacheKey = resolveQwenCliCredentialsPath(options?.homeDir);
if (
ttlMs > 0 &&
qwenCliCache &&
qwenCliCache.cacheKey === cacheKey &&
now - qwenCliCache.readAt < ttlMs
) {
return qwenCliCache.value;
}
const value = readQwenCliCredentials({ homeDir: options?.homeDir });
if (ttlMs > 0) {
qwenCliCache = { value, readAt: now, cacheKey };
}
return value;
const credPath = resolveQwenCliCredentialsPath(options?.homeDir);
return readCachedCliCredential({
ttlMs: options?.ttlMs ?? 0,
cache: qwenCliCache,
cacheKey: credPath,
read: () => readQwenCliCredentials({ homeDir: options?.homeDir }),
setCache: (next) => {
qwenCliCache = next;
},
readSourceFingerprint: () => readFileMtimeMs(credPath),
});
}
export function readMiniMaxCliCredentialsCached(options?: {
ttlMs?: number;
homeDir?: string;
}): MiniMaxCliCredential | null {
const ttlMs = options?.ttlMs ?? 0;
const now = Date.now();
const cacheKey = resolveMiniMaxCliCredentialsPath(options?.homeDir);
if (
ttlMs > 0 &&
minimaxCliCache &&
minimaxCliCache.cacheKey === cacheKey &&
now - minimaxCliCache.readAt < ttlMs
) {
return minimaxCliCache.value;
}
const value = readMiniMaxCliCredentials({ homeDir: options?.homeDir });
if (ttlMs > 0) {
minimaxCliCache = { value, readAt: now, cacheKey };
}
return value;
const credPath = resolveMiniMaxCliCredentialsPath(options?.homeDir);
return readCachedCliCredential({
ttlMs: options?.ttlMs ?? 0,
cache: minimaxCliCache,
cacheKey: credPath,
read: () => readMiniMaxCliCredentials({ homeDir: options?.homeDir }),
setCache: (next) => {
minimaxCliCache = next;
},
readSourceFingerprint: () => readFileMtimeMs(credPath),
});
}