mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 06:40:44 +00:00
refactor(auth): drop legacy external cli oauth sync path
This commit is contained in:
@@ -31,6 +31,7 @@ Docs: https://docs.openclaw.ai
|
||||
- OpenAI Codex/OAuth: treat the OpenAI TLS prerequisites probe as advisory instead of a hard blocker, so Codex sign-in can still proceed when the speculative Node/OpenSSL precheck fails but the real OAuth flow still works. Thanks @vincentkoc.
|
||||
- Models status/OAuth health: align OAuth health reporting with the same effective credential view runtime uses, so expired refreshable sessions stop showing healthy by default and fresher imported Codex CLI credentials surface correctly in `models status`, doctor, and gateway auth status. Thanks @vincentkoc.
|
||||
- OpenAI Codex/OAuth: keep external CLI OAuth imports runtime-only by overlaying fresher Codex CLI credentials without mutating `auth-profiles.json`, so `.codex` stays a bootstrap/runtime input instead of becoming durable OpenClaw state. Thanks @vincentkoc.
|
||||
- OpenAI Codex/OAuth: drop legacy CLI-manager routing from the remaining bootstrap path so Codex and MiniMax CLI imports are matched by their canonical OpenClaw profile ids instead of stale `managedBy` metadata. Thanks @vincentkoc.
|
||||
- Twitch/setup: load Twitch through the bundled setup-entry discovery path and keep setup/status account detection aligned with runtime config. (#68008) Thanks @gumadeiras.
|
||||
- Feishu/card actions: resolve card-action chat type from the Feishu chat API when stored context is missing, preferring `chat_mode` over `chat_type`, so DM-originated card actions no longer bypass `dmPolicy` by falling through to the group handling path. (#68201)
|
||||
- Cron/isolated-agent: preserve `trusted: false` on isolated cron awareness events mirrored into the main session, and forward the optional `trusted` flag through the gateway cron wrapper so explicit trust downgrades survive session-key scoping. (#68210)
|
||||
|
||||
@@ -6,9 +6,9 @@ const mocks = vi.hoisted(() => ({
|
||||
readMiniMaxCliCredentialsCached: vi.fn<() => OAuthCredential | null>(() => null),
|
||||
}));
|
||||
|
||||
let syncExternalCliCredentials: typeof import("./auth-profiles/external-cli-sync.js").syncExternalCliCredentials;
|
||||
let readManagedExternalCliCredential: typeof import("./auth-profiles/external-cli-sync.js").readManagedExternalCliCredential;
|
||||
let resolveExternalCliAuthProfiles: typeof import("./auth-profiles/external-cli-sync.js").resolveExternalCliAuthProfiles;
|
||||
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 MINIMAX_CLI_PROFILE_ID: typeof import("./auth-profiles/constants.js").MINIMAX_CLI_PROFILE_ID;
|
||||
|
||||
@@ -35,37 +35,21 @@ function makeStore(profileId?: string, credential?: OAuthCredential): AuthProfil
|
||||
};
|
||||
}
|
||||
|
||||
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: "MiniMax",
|
||||
profileId: MINIMAX_CLI_PROFILE_ID,
|
||||
provider: "minimax-portal" as const,
|
||||
readMock: mocks.readMiniMaxCliCredentialsCached,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
describe("syncExternalCliCredentials", () => {
|
||||
describe("external cli oauth resolution", () => {
|
||||
beforeEach(async () => {
|
||||
vi.resetModules();
|
||||
vi.doUnmock("./auth-profiles/external-cli-sync.js");
|
||||
mocks.readCodexCliCredentialsCached.mockReset().mockReturnValue(null);
|
||||
mocks.readMiniMaxCliCredentialsCached.mockReset().mockReturnValue(null);
|
||||
vi.doMock("./cli-credentials.js", () => ({
|
||||
readCodexCliCredentialsCached: mocks.readCodexCliCredentialsCached,
|
||||
readMiniMaxCliCredentialsCached: mocks.readMiniMaxCliCredentialsCached,
|
||||
}));
|
||||
({ syncExternalCliCredentials, shouldReplaceStoredOAuthCredential } =
|
||||
await import("./auth-profiles/external-cli-sync.js"));
|
||||
({ CODEX_CLI_PROFILE_ID, OPENAI_CODEX_DEFAULT_PROFILE_ID, MINIMAX_CLI_PROFILE_ID } =
|
||||
mocks.readCodexCliCredentialsCached.mockReset().mockReturnValue(null);
|
||||
mocks.readMiniMaxCliCredentialsCached.mockReset().mockReturnValue(null);
|
||||
({
|
||||
readManagedExternalCliCredential,
|
||||
resolveExternalCliAuthProfiles,
|
||||
shouldReplaceStoredOAuthCredential,
|
||||
} = await import("./auth-profiles/external-cli-sync.js"));
|
||||
({ OPENAI_CODEX_DEFAULT_PROFILE_ID, MINIMAX_CLI_PROFILE_ID } =
|
||||
await import("./auth-profiles/constants.js"));
|
||||
});
|
||||
|
||||
@@ -120,150 +104,110 @@ describe("syncExternalCliCredentials", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it.each([{ providerLabel: "Codex" }, { 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",
|
||||
managedBy: current.provider === "openai-codex" ? "codex-cli" : ("minimax-cli" as const),
|
||||
});
|
||||
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;
|
||||
it("reads codex external cli credentials by profile id", () => {
|
||||
mocks.readCodexCliCredentialsCached.mockReturnValue(
|
||||
makeOAuthCredential({
|
||||
provider: "openai-codex",
|
||||
access: "new-access-token",
|
||||
refresh: "new-refresh-token",
|
||||
expires: freshExpiry,
|
||||
accountId: "acct_456",
|
||||
access: "codex-access-token",
|
||||
refresh: "codex-refresh-token",
|
||||
}),
|
||||
);
|
||||
|
||||
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 credential = readManagedExternalCliCredential({
|
||||
profileId: OPENAI_CODEX_DEFAULT_PROFILE_ID,
|
||||
credential: makeOAuthCredential({ provider: "openai-codex" }),
|
||||
});
|
||||
|
||||
const mutated = syncExternalCliCredentials(store);
|
||||
|
||||
expect(mutated).toBe(true);
|
||||
expect(store.profiles[OPENAI_CODEX_DEFAULT_PROFILE_ID]).toMatchObject({
|
||||
access: "new-access-token",
|
||||
refresh: "new-refresh-token",
|
||||
expires: freshExpiry,
|
||||
managedBy: "codex-cli",
|
||||
expect(credential).toMatchObject({
|
||||
access: "codex-access-token",
|
||||
refresh: "codex-refresh-token",
|
||||
});
|
||||
});
|
||||
|
||||
it.each([{ providerLabel: "Codex" }, { 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",
|
||||
}),
|
||||
);
|
||||
it("returns null when the profile id/provider do not map to the same external source", () => {
|
||||
mocks.readCodexCliCredentialsCached.mockReturnValue(
|
||||
makeOAuthCredential({ provider: "openai-codex" }),
|
||||
);
|
||||
|
||||
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 credential = readManagedExternalCliCredential({
|
||||
profileId: OPENAI_CODEX_DEFAULT_PROFILE_ID,
|
||||
credential: makeOAuthCredential({ provider: "anthropic" }),
|
||||
});
|
||||
|
||||
const mutated = syncExternalCliCredentials(store);
|
||||
expect(credential).toBeNull();
|
||||
});
|
||||
|
||||
expect(mutated).toBe(false);
|
||||
expect(store.profiles[current.profileId]).toMatchObject({
|
||||
access: `fresh-${current.provider}-access-token`,
|
||||
refresh: `fresh-${current.provider}-refresh-token`,
|
||||
expires: freshExpiry,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it("upgrades matching Codex CLI credentials with external ownership metadata", () => {
|
||||
const expires = Date.now() + 60_000;
|
||||
it("resolves fresher codex and minimax external oauth profiles as runtime overlays", () => {
|
||||
mocks.readCodexCliCredentialsCached.mockReturnValue(
|
||||
makeOAuthCredential({
|
||||
provider: "openai-codex",
|
||||
access: "same-access-token",
|
||||
refresh: "same-refresh-token",
|
||||
expires,
|
||||
access: "codex-fresh-access",
|
||||
refresh: "codex-fresh-refresh",
|
||||
expires: Date.now() + 5 * 24 * 60 * 60_000,
|
||||
}),
|
||||
);
|
||||
mocks.readMiniMaxCliCredentialsCached.mockReturnValue(
|
||||
makeOAuthCredential({
|
||||
provider: "minimax-portal",
|
||||
access: "minimax-fresh-access",
|
||||
refresh: "minimax-fresh-refresh",
|
||||
expires: Date.now() + 5 * 24 * 60 * 60_000,
|
||||
}),
|
||||
);
|
||||
|
||||
const store = makeStore(
|
||||
OPENAI_CODEX_DEFAULT_PROFILE_ID,
|
||||
const profiles = resolveExternalCliAuthProfiles({
|
||||
version: 1,
|
||||
profiles: {
|
||||
[OPENAI_CODEX_DEFAULT_PROFILE_ID]: makeOAuthCredential({
|
||||
provider: "openai-codex",
|
||||
access: "codex-stale-access",
|
||||
refresh: "codex-stale-refresh",
|
||||
expires: Date.now() - 5_000,
|
||||
}),
|
||||
[MINIMAX_CLI_PROFILE_ID]: makeOAuthCredential({
|
||||
provider: "minimax-portal",
|
||||
access: "minimax-stale-access",
|
||||
refresh: "minimax-stale-refresh",
|
||||
expires: Date.now() - 5_000,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
const profilesById = new Map(
|
||||
profiles.map((profile) => [profile.profileId, profile.credential]),
|
||||
);
|
||||
expect(profilesById.get(OPENAI_CODEX_DEFAULT_PROFILE_ID)).toMatchObject({
|
||||
access: "codex-fresh-access",
|
||||
refresh: "codex-fresh-refresh",
|
||||
});
|
||||
expect(profilesById.get(MINIMAX_CLI_PROFILE_ID)).toMatchObject({
|
||||
access: "minimax-fresh-access",
|
||||
refresh: "minimax-fresh-refresh",
|
||||
});
|
||||
});
|
||||
|
||||
it("does not emit runtime overlays when the stored credential is newer", () => {
|
||||
mocks.readCodexCliCredentialsCached.mockReturnValue(
|
||||
makeOAuthCredential({
|
||||
provider: "openai-codex",
|
||||
access: "same-access-token",
|
||||
refresh: "same-refresh-token",
|
||||
expires,
|
||||
access: "stale-external-access",
|
||||
refresh: "stale-external-refresh",
|
||||
expires: Date.now() - 5_000,
|
||||
}),
|
||||
);
|
||||
|
||||
const mutated = syncExternalCliCredentials(store);
|
||||
const profiles = resolveExternalCliAuthProfiles(
|
||||
makeStore(
|
||||
OPENAI_CODEX_DEFAULT_PROFILE_ID,
|
||||
makeOAuthCredential({
|
||||
provider: "openai-codex",
|
||||
access: "fresh-store-access",
|
||||
refresh: "fresh-store-refresh",
|
||||
expires: Date.now() + 5 * 24 * 60 * 60_000,
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
expect(mutated).toBe(true);
|
||||
expect(store.profiles[OPENAI_CODEX_DEFAULT_PROFILE_ID]).toMatchObject({
|
||||
access: "same-access-token",
|
||||
refresh: "same-refresh-token",
|
||||
expires,
|
||||
managedBy: "codex-cli",
|
||||
});
|
||||
expect(profiles).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,20 +3,11 @@ import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { AUTH_STORE_VERSION } from "./auth-profiles/constants.js";
|
||||
import type { AuthProfileStore } from "./auth-profiles/types.js";
|
||||
|
||||
const AUTH_STORE_CACHE_TTL_MS = 15 * 60 * 1000;
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
syncExternalCliCredentials: vi.fn((_: AuthProfileStore) => false),
|
||||
}));
|
||||
|
||||
vi.mock("./auth-profiles/external-cli-sync.js", () => ({
|
||||
syncExternalCliCredentials: mocks.syncExternalCliCredentials,
|
||||
}));
|
||||
const resolveExternalAuthProfilesWithPluginsMock = vi.fn(() => []);
|
||||
|
||||
vi.mock("../plugins/provider-runtime.js", () => ({
|
||||
resolveExternalAuthProfilesWithPlugins: () => [],
|
||||
resolveExternalAuthProfilesWithPlugins: resolveExternalAuthProfilesWithPluginsMock,
|
||||
}));
|
||||
|
||||
let clearRuntimeAuthProfileStoreSnapshots: typeof import("./auth-profiles.js").clearRuntimeAuthProfileStoreSnapshots;
|
||||
@@ -82,23 +73,29 @@ describe("auth profile store cache", () => {
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
clearRuntimeAuthProfileStoreSnapshots();
|
||||
resolveExternalAuthProfilesWithPluginsMock.mockReset();
|
||||
resolveExternalAuthProfilesWithPluginsMock.mockReturnValue([]);
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("reuses the synced auth store while auth-profiles.json is unchanged", async () => {
|
||||
it("reuses the cached auth store while auth-profiles.json is unchanged", async () => {
|
||||
await withAgentDirEnv("openclaw-auth-store-cache-", (agentDir) => {
|
||||
writeAuthStore(agentDir, "sk-test");
|
||||
const authPath = writeAuthStore(agentDir, "sk-test");
|
||||
const readFileSyncSpy = vi.spyOn(fs, "readFileSync");
|
||||
|
||||
ensureAuthProfileStore(agentDir);
|
||||
ensureAuthProfileStore(agentDir);
|
||||
|
||||
expect(mocks.syncExternalCliCredentials).toHaveBeenCalledTimes(1);
|
||||
expect(
|
||||
readFileSyncSpy.mock.calls.filter(([target]) => String(target) === authPath),
|
||||
).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
it("refreshes the cached auth store after auth-profiles.json changes", async () => {
|
||||
await withAgentDirEnv("openclaw-auth-store-refresh-", async (agentDir) => {
|
||||
const authPath = writeAuthStore(agentDir, "sk-test-1");
|
||||
const readFileSyncSpy = vi.spyOn(fs, "readFileSync");
|
||||
|
||||
ensureAuthProfileStore(agentDir);
|
||||
|
||||
@@ -108,30 +105,35 @@ describe("auth profile store cache", () => {
|
||||
|
||||
const reloaded = ensureAuthProfileStore(agentDir);
|
||||
|
||||
expect(mocks.syncExternalCliCredentials).toHaveBeenCalledTimes(2);
|
||||
expect(
|
||||
readFileSyncSpy.mock.calls.filter(([target]) => String(target) === authPath),
|
||||
).toHaveLength(2);
|
||||
expect(reloaded.profiles["openai:default"]).toMatchObject({
|
||||
key: "sk-test-2",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("re-syncs external CLI credentials after the cache ttl when auth-profiles.json is absent", () => {
|
||||
it("reapplies runtime-only external auth overlays over a cached missing auth store", () => {
|
||||
const agentDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-auth-store-missing-"));
|
||||
const previousAgentDir = process.env.OPENCLAW_AGENT_DIR;
|
||||
const previousPiAgentDir = process.env.PI_CODING_AGENT_DIR;
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date("2026-03-21T15:00:00.000Z"));
|
||||
let syncCount = 0;
|
||||
mocks.syncExternalCliCredentials.mockImplementation((store) => {
|
||||
syncCount += 1;
|
||||
store.profiles["openai-codex:default"] = {
|
||||
type: "oauth",
|
||||
provider: "openai-codex",
|
||||
access: `access-${syncCount}`,
|
||||
refresh: `refresh-${syncCount}`,
|
||||
expires: Date.now() + 60_000,
|
||||
};
|
||||
return true;
|
||||
let overlayCount = 0;
|
||||
resolveExternalAuthProfilesWithPluginsMock.mockImplementation(() => {
|
||||
overlayCount += 1;
|
||||
return [
|
||||
{
|
||||
profileId: "openai-codex:default",
|
||||
credential: {
|
||||
type: "oauth" as const,
|
||||
provider: "openai-codex",
|
||||
access: `access-${overlayCount}`,
|
||||
refresh: `refresh-${overlayCount}`,
|
||||
expires: Date.now() + 60_000,
|
||||
},
|
||||
persistence: "runtime-only" as const,
|
||||
},
|
||||
];
|
||||
});
|
||||
try {
|
||||
process.env.OPENCLAW_AGENT_DIR = agentDir;
|
||||
@@ -141,15 +143,8 @@ describe("auth profile store cache", () => {
|
||||
const second = ensureAuthProfileStore(agentDir);
|
||||
|
||||
expect(first.profiles["openai-codex:default"]).toMatchObject({ access: "access-1" });
|
||||
expect(second.profiles["openai-codex:default"]).toMatchObject({ access: "access-1" });
|
||||
expect(mocks.syncExternalCliCredentials).toHaveBeenCalledTimes(1);
|
||||
|
||||
vi.advanceTimersByTime(AUTH_STORE_CACHE_TTL_MS + 1);
|
||||
|
||||
const third = ensureAuthProfileStore(agentDir);
|
||||
|
||||
expect(mocks.syncExternalCliCredentials).toHaveBeenCalledTimes(2);
|
||||
expect(third.profiles["openai-codex:default"]).toMatchObject({ access: "access-2" });
|
||||
expect(second.profiles["openai-codex:default"]).toMatchObject({ access: "access-2" });
|
||||
expect(resolveExternalAuthProfilesWithPluginsMock).toHaveBeenCalledTimes(2);
|
||||
} finally {
|
||||
if (previousAgentDir === undefined) {
|
||||
delete process.env.OPENCLAW_AGENT_DIR;
|
||||
|
||||
@@ -4,15 +4,10 @@ import {
|
||||
} from "../cli-credentials.js";
|
||||
import {
|
||||
EXTERNAL_CLI_SYNC_TTL_MS,
|
||||
OPENAI_CODEX_DEFAULT_PROFILE_ID,
|
||||
MINIMAX_CLI_PROFILE_ID,
|
||||
log,
|
||||
OPENAI_CODEX_DEFAULT_PROFILE_ID,
|
||||
} from "./constants.js";
|
||||
import type { AuthProfileStore, ExternalOAuthManager, OAuthCredential } from "./types.js";
|
||||
|
||||
type ExternalCliSyncOptions = {
|
||||
log?: boolean;
|
||||
};
|
||||
import type { AuthProfileStore, OAuthCredential } from "./types.js";
|
||||
|
||||
export type ExternalCliResolvedProfile = {
|
||||
profileId: string;
|
||||
@@ -22,7 +17,6 @@ export type ExternalCliResolvedProfile = {
|
||||
type ExternalCliSyncProvider = {
|
||||
profileId: string;
|
||||
provider: string;
|
||||
managedBy: ExternalOAuthManager;
|
||||
readCredentials: () => OAuthCredential | null;
|
||||
};
|
||||
|
||||
@@ -44,8 +38,7 @@ export function areOAuthCredentialsEquivalent(
|
||||
a.email === b.email &&
|
||||
a.enterpriseUrl === b.enterpriseUrl &&
|
||||
a.projectId === b.projectId &&
|
||||
a.accountId === b.accountId &&
|
||||
a.managedBy === b.managedBy
|
||||
a.accountId === b.accountId
|
||||
);
|
||||
}
|
||||
|
||||
@@ -78,69 +71,40 @@ const EXTERNAL_CLI_SYNC_PROVIDERS: ExternalCliSyncProvider[] = [
|
||||
{
|
||||
profileId: MINIMAX_CLI_PROFILE_ID,
|
||||
provider: "minimax-portal",
|
||||
managedBy: "minimax-cli",
|
||||
readCredentials: () => readMiniMaxCliCredentialsCached({ ttlMs: EXTERNAL_CLI_SYNC_TTL_MS }),
|
||||
},
|
||||
{
|
||||
profileId: OPENAI_CODEX_DEFAULT_PROFILE_ID,
|
||||
provider: "openai-codex",
|
||||
managedBy: "codex-cli",
|
||||
readCredentials: () => readCodexCliCredentialsCached({ ttlMs: EXTERNAL_CLI_SYNC_TTL_MS }),
|
||||
},
|
||||
];
|
||||
|
||||
function withExternalCliManager(
|
||||
creds: OAuthCredential,
|
||||
managedBy: ExternalOAuthManager,
|
||||
): OAuthCredential {
|
||||
return {
|
||||
...creds,
|
||||
managedBy,
|
||||
};
|
||||
}
|
||||
|
||||
function stripExternalCliManager(creds: OAuthCredential): OAuthCredential {
|
||||
const { managedBy: _managedBy, ...runtimeCredential } = creds;
|
||||
return runtimeCredential;
|
||||
}
|
||||
|
||||
function resolveExternalCliSyncProvider(params: {
|
||||
profileId?: string;
|
||||
profileId: string;
|
||||
credential?: OAuthCredential;
|
||||
}): ExternalCliSyncProvider | null {
|
||||
const byProfileId =
|
||||
typeof params.profileId === "string"
|
||||
? EXTERNAL_CLI_SYNC_PROVIDERS.find((entry) => entry.profileId === params.profileId)
|
||||
: undefined;
|
||||
if (byProfileId) {
|
||||
return byProfileId;
|
||||
}
|
||||
const managedBy = params.credential?.managedBy;
|
||||
if (!managedBy) {
|
||||
const provider = EXTERNAL_CLI_SYNC_PROVIDERS.find(
|
||||
(entry) => entry.profileId === params.profileId,
|
||||
);
|
||||
if (!provider) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
EXTERNAL_CLI_SYNC_PROVIDERS.find(
|
||||
(entry) =>
|
||||
entry.managedBy === managedBy &&
|
||||
(!params.credential || entry.provider === params.credential.provider),
|
||||
) ?? null
|
||||
);
|
||||
if (params.credential && provider.provider !== params.credential.provider) {
|
||||
return null;
|
||||
}
|
||||
return provider;
|
||||
}
|
||||
|
||||
export function readManagedExternalCliCredential(params: {
|
||||
profileId?: string;
|
||||
profileId: string;
|
||||
credential: OAuthCredential;
|
||||
}): OAuthCredential | null {
|
||||
const provider = resolveExternalCliSyncProvider(params);
|
||||
if (!provider) {
|
||||
return null;
|
||||
}
|
||||
const creds = provider.readCredentials();
|
||||
if (!creds) {
|
||||
return null;
|
||||
}
|
||||
return withExternalCliManager(creds, provider.managedBy);
|
||||
return provider.readCredentials();
|
||||
}
|
||||
|
||||
export function resolveExternalCliAuthProfiles(
|
||||
@@ -152,83 +116,18 @@ export function resolveExternalCliAuthProfiles(
|
||||
if (!creds) {
|
||||
continue;
|
||||
}
|
||||
const runtimeCredential = stripExternalCliManager(
|
||||
withExternalCliManager(creds, providerConfig.managedBy),
|
||||
);
|
||||
const existing = store.profiles[providerConfig.profileId];
|
||||
const existingOAuth = existing?.type === "oauth" ? existing : undefined;
|
||||
if (
|
||||
!shouldReplaceStoredOAuthCredential(existingOAuth, runtimeCredential) &&
|
||||
!areOAuthCredentialsEquivalent(existingOAuth, runtimeCredential)
|
||||
!shouldReplaceStoredOAuthCredential(existingOAuth, creds) &&
|
||||
!areOAuthCredentialsEquivalent(existingOAuth, creds)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
profiles.push({
|
||||
profileId: providerConfig.profileId,
|
||||
credential: runtimeCredential,
|
||||
credential: creds,
|
||||
});
|
||||
}
|
||||
return profiles;
|
||||
}
|
||||
|
||||
/** Sync external CLI credentials into the store for a given provider. */
|
||||
function syncExternalCliCredentialsForProvider(
|
||||
store: AuthProfileStore,
|
||||
providerConfig: ExternalCliSyncProvider,
|
||||
options: ExternalCliSyncOptions,
|
||||
): boolean {
|
||||
const { profileId, provider, managedBy, readCredentials } = providerConfig;
|
||||
const existing = store.profiles[profileId];
|
||||
const creds = readCredentials();
|
||||
if (!creds) {
|
||||
return false;
|
||||
}
|
||||
const managedCreds = withExternalCliManager(creds, managedBy);
|
||||
|
||||
const existingOAuth = existing?.type === "oauth" ? existing : undefined;
|
||||
if (!shouldReplaceStoredOAuthCredential(existingOAuth, managedCreds)) {
|
||||
if (options.log !== false) {
|
||||
if (!areOAuthCredentialsEquivalent(existingOAuth, managedCreds) && existingOAuth) {
|
||||
log.debug(`kept newer stored ${provider} credentials over external cli sync`, {
|
||||
profileId,
|
||||
storedExpires: new Date(existingOAuth.expires).toISOString(),
|
||||
externalExpires: Number.isFinite(managedCreds.expires)
|
||||
? new Date(managedCreds.expires).toISOString()
|
||||
: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
store.profiles[profileId] = managedCreds;
|
||||
if (options.log !== false) {
|
||||
log.info(`synced ${provider} credentials from external cli`, {
|
||||
profileId,
|
||||
expires: new Date(managedCreds.expires).toISOString(),
|
||||
managedBy,
|
||||
});
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync OAuth credentials from external CLI tools (MiniMax CLI, Codex CLI)
|
||||
* into the store.
|
||||
*
|
||||
* Returns true if any credentials were updated.
|
||||
*/
|
||||
export function syncExternalCliCredentials(
|
||||
store: AuthProfileStore,
|
||||
options: ExternalCliSyncOptions = {},
|
||||
): boolean {
|
||||
let mutated = false;
|
||||
|
||||
for (const provider of EXTERNAL_CLI_SYNC_PROVIDERS) {
|
||||
if (syncExternalCliCredentialsForProvider(store, provider, options)) {
|
||||
mutated = true;
|
||||
}
|
||||
}
|
||||
|
||||
return mutated;
|
||||
}
|
||||
|
||||
@@ -72,8 +72,8 @@ vi.mock("./external-auth.js", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("./external-cli-sync.js", () => ({
|
||||
syncExternalCliCredentials: () => false,
|
||||
readManagedExternalCliCredential: () => null,
|
||||
resolveExternalCliAuthProfiles: () => [],
|
||||
areOAuthCredentialsEquivalent: (a: unknown, b: unknown) => a === b,
|
||||
}));
|
||||
|
||||
|
||||
@@ -79,8 +79,8 @@ vi.mock("./doctor.js", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("./external-cli-sync.js", () => ({
|
||||
syncExternalCliCredentials: () => false,
|
||||
readManagedExternalCliCredential: () => null,
|
||||
resolveExternalCliAuthProfiles: () => [],
|
||||
areOAuthCredentialsEquivalent: (a: unknown, b: unknown) => a === b,
|
||||
}));
|
||||
|
||||
|
||||
@@ -65,8 +65,8 @@ vi.mock("./doctor.js", () => ({
|
||||
// credential files; it is slow and can pollute test state. Stub it to a no-op
|
||||
// so the suite only exercises in-repo auth-profile logic.
|
||||
vi.mock("./external-cli-sync.js", () => ({
|
||||
syncExternalCliCredentials: () => false,
|
||||
readManagedExternalCliCredential: () => null,
|
||||
resolveExternalCliAuthProfiles: () => [],
|
||||
areOAuthCredentialsEquivalent: (a: unknown, b: unknown) => a === b,
|
||||
}));
|
||||
|
||||
|
||||
@@ -76,8 +76,8 @@ vi.mock("./doctor.js", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("./external-cli-sync.js", () => ({
|
||||
syncExternalCliCredentials: () => false,
|
||||
readManagedExternalCliCredential: () => null,
|
||||
resolveExternalCliAuthProfiles: () => [],
|
||||
areOAuthCredentialsEquivalent: (a: unknown, b: unknown) => a === b,
|
||||
}));
|
||||
|
||||
|
||||
@@ -12,10 +12,6 @@ import {
|
||||
} from "./models-config.e2e-harness.js";
|
||||
import type { ProviderConfig as ModelsProviderConfig } from "./models-config.providers.secrets.js";
|
||||
|
||||
vi.mock("./auth-profiles/external-cli-sync.js", () => ({
|
||||
syncExternalCliCredentials: () => false,
|
||||
}));
|
||||
|
||||
vi.mock("./models-config.providers.js", async () => {
|
||||
function createImplicitProvider(baseUrl: string): ModelsProviderConfig {
|
||||
return {
|
||||
|
||||
@@ -11,7 +11,7 @@ vi.mock("../plugins/provider-runtime.js", () => ({
|
||||
|
||||
vi.mock("./auth-profiles/external-cli-sync.js", () => ({
|
||||
readManagedExternalCliCredential: () => null,
|
||||
syncExternalCliCredentials: () => false,
|
||||
resolveExternalCliAuthProfiles: () => [],
|
||||
}));
|
||||
|
||||
type AuthProfileStore = Parameters<typeof saveAuthProfileStore>[0];
|
||||
|
||||
Reference in New Issue
Block a user