mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 06:30:42 +00:00
fix(cli): key oauth session epochs on identity
This commit is contained in:
@@ -30,21 +30,23 @@ describe("resolveCliAuthEpoch", () => {
|
||||
).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it("keeps claude cli oauth epochs stable across access-token refreshes", async () => {
|
||||
it("keeps identity-less claude cli oauth epochs stable across token changes", async () => {
|
||||
let access = "access-a";
|
||||
let refresh = "refresh-a";
|
||||
let expires = 1;
|
||||
setCliAuthEpochTestDeps({
|
||||
readClaudeCliCredentialsCached: () => ({
|
||||
type: "oauth",
|
||||
provider: "anthropic",
|
||||
access,
|
||||
refresh: "refresh",
|
||||
refresh,
|
||||
expires,
|
||||
}),
|
||||
});
|
||||
|
||||
const first = await resolveCliAuthEpoch({ provider: "claude-cli" });
|
||||
access = "access-b";
|
||||
refresh = "refresh-b";
|
||||
expires = 2;
|
||||
const second = await resolveCliAuthEpoch({ provider: "claude-cli" });
|
||||
|
||||
@@ -52,20 +54,19 @@ describe("resolveCliAuthEpoch", () => {
|
||||
expect(second).toBe(first);
|
||||
});
|
||||
|
||||
it("changes claude cli oauth epochs when the refresh token changes", async () => {
|
||||
let refresh = "refresh-a";
|
||||
it("changes claude cli token epochs when the static token changes", async () => {
|
||||
let token = "token-a";
|
||||
setCliAuthEpochTestDeps({
|
||||
readClaudeCliCredentialsCached: () => ({
|
||||
type: "oauth",
|
||||
type: "token",
|
||||
provider: "anthropic",
|
||||
access: "access",
|
||||
refresh,
|
||||
token,
|
||||
expires: 1,
|
||||
}),
|
||||
});
|
||||
|
||||
const first = await resolveCliAuthEpoch({ provider: "claude-cli" });
|
||||
refresh = "refresh-b";
|
||||
token = "token-b";
|
||||
const second = await resolveCliAuthEpoch({ provider: "claude-cli" });
|
||||
|
||||
expect(first).toBeDefined();
|
||||
@@ -73,7 +74,7 @@ describe("resolveCliAuthEpoch", () => {
|
||||
expect(second).not.toBe(first);
|
||||
});
|
||||
|
||||
it("keeps oauth auth-profile epochs stable across access-token refreshes", async () => {
|
||||
it("keeps oauth auth-profile epochs stable across token refreshes", async () => {
|
||||
let store: AuthProfileStore = {
|
||||
version: 1,
|
||||
profiles: {
|
||||
@@ -81,8 +82,9 @@ describe("resolveCliAuthEpoch", () => {
|
||||
type: "oauth",
|
||||
provider: "anthropic",
|
||||
access: "access-a",
|
||||
refresh: "refresh",
|
||||
refresh: "refresh-a",
|
||||
expires: 1,
|
||||
email: "user@example.com",
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -101,8 +103,9 @@ describe("resolveCliAuthEpoch", () => {
|
||||
type: "oauth",
|
||||
provider: "anthropic",
|
||||
access: "access-b",
|
||||
refresh: "refresh",
|
||||
refresh: "refresh-b",
|
||||
expires: 2,
|
||||
email: "user@example.com",
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -115,7 +118,7 @@ describe("resolveCliAuthEpoch", () => {
|
||||
expect(second).toBe(first);
|
||||
});
|
||||
|
||||
it("changes oauth auth-profile epochs when the refresh token changes", async () => {
|
||||
it("changes oauth auth-profile epochs when the account identity changes", async () => {
|
||||
let store: AuthProfileStore = {
|
||||
version: 1,
|
||||
profiles: {
|
||||
@@ -123,8 +126,9 @@ describe("resolveCliAuthEpoch", () => {
|
||||
type: "oauth",
|
||||
provider: "anthropic",
|
||||
access: "access",
|
||||
refresh: "refresh-a",
|
||||
refresh: "refresh",
|
||||
expires: 1,
|
||||
email: "user-a@example.com",
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -143,8 +147,9 @@ describe("resolveCliAuthEpoch", () => {
|
||||
type: "oauth",
|
||||
provider: "anthropic",
|
||||
access: "access",
|
||||
refresh: "refresh-b",
|
||||
refresh: "refresh",
|
||||
expires: 1,
|
||||
email: "user-b@example.com",
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -162,6 +167,8 @@ describe("resolveCliAuthEpoch", () => {
|
||||
let access = "local-access-a";
|
||||
let localRefresh = "local-refresh-a";
|
||||
let refresh = "profile-refresh-a";
|
||||
let accountId = "acct-1";
|
||||
let email = "user-a@example.com";
|
||||
setCliAuthEpochTestDeps({
|
||||
readCodexCliCredentialsCached: () => ({
|
||||
type: "oauth",
|
||||
@@ -169,7 +176,7 @@ describe("resolveCliAuthEpoch", () => {
|
||||
access,
|
||||
refresh: localRefresh,
|
||||
expires: 1,
|
||||
accountId: "acct-1",
|
||||
accountId,
|
||||
}),
|
||||
loadAuthProfileStoreForRuntime: () => ({
|
||||
version: 1,
|
||||
@@ -180,6 +187,7 @@ describe("resolveCliAuthEpoch", () => {
|
||||
access: "profile-access",
|
||||
refresh,
|
||||
expires: 1,
|
||||
email,
|
||||
},
|
||||
},
|
||||
}),
|
||||
@@ -204,18 +212,31 @@ describe("resolveCliAuthEpoch", () => {
|
||||
provider: "codex-cli",
|
||||
authProfileId: "openai:work",
|
||||
});
|
||||
accountId = "acct-2";
|
||||
const fifth = await resolveCliAuthEpoch({
|
||||
provider: "codex-cli",
|
||||
authProfileId: "openai:work",
|
||||
});
|
||||
email = "user-b@example.com";
|
||||
const sixth = await resolveCliAuthEpoch({
|
||||
provider: "codex-cli",
|
||||
authProfileId: "openai:work",
|
||||
});
|
||||
|
||||
expect(first).toBeDefined();
|
||||
expect(third).toBeDefined();
|
||||
expect(fourth).toBeDefined();
|
||||
expect(second).toBe(first);
|
||||
expect(third).not.toBe(second);
|
||||
expect(fourth).not.toBe(third);
|
||||
expect(third).toBe(second);
|
||||
expect(fourth).toBe(third);
|
||||
expect(fifth).toBeDefined();
|
||||
expect(sixth).toBeDefined();
|
||||
expect(fifth).not.toBe(fourth);
|
||||
expect(sixth).not.toBe(fifth);
|
||||
});
|
||||
|
||||
it("can ignore local codex state when the backend is profile-owned", async () => {
|
||||
let localAccess = "local-access-a";
|
||||
let profileRefresh = "profile-refresh-a";
|
||||
let profileAccountId = "acct-1";
|
||||
setCliAuthEpochTestDeps({
|
||||
readCodexCliCredentialsCached: () => ({
|
||||
type: "oauth",
|
||||
@@ -234,7 +255,7 @@ describe("resolveCliAuthEpoch", () => {
|
||||
access: "profile-access",
|
||||
refresh: profileRefresh,
|
||||
expires: 1,
|
||||
accountId: "acct-1",
|
||||
accountId: profileAccountId,
|
||||
},
|
||||
},
|
||||
}),
|
||||
@@ -257,10 +278,17 @@ describe("resolveCliAuthEpoch", () => {
|
||||
authProfileId: "openai-codex:default",
|
||||
skipLocalCredential: true,
|
||||
});
|
||||
profileAccountId = "acct-2";
|
||||
const fourth = await resolveCliAuthEpoch({
|
||||
provider: "codex-cli",
|
||||
authProfileId: "openai-codex:default",
|
||||
skipLocalCredential: true,
|
||||
});
|
||||
|
||||
expect(first).toBeDefined();
|
||||
expect(second).toBe(first);
|
||||
expect(third).toBeDefined();
|
||||
expect(third).not.toBe(second);
|
||||
expect(third).toBe(second);
|
||||
expect(fourth).toBeDefined();
|
||||
expect(fourth).not.toBe(third);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -23,7 +23,7 @@ const defaultCliAuthEpochDeps: CliAuthEpochDeps = {
|
||||
|
||||
const cliAuthEpochDeps: CliAuthEpochDeps = { ...defaultCliAuthEpochDeps };
|
||||
|
||||
export const CLI_AUTH_EPOCH_VERSION = 2;
|
||||
export const CLI_AUTH_EPOCH_VERSION = 3;
|
||||
|
||||
export function setCliAuthEpochTestDeps(overrides: Partial<CliAuthEpochDeps>): void {
|
||||
Object.assign(cliAuthEpochDeps, overrides);
|
||||
@@ -41,20 +41,35 @@ function encodeUnknown(value: unknown): string {
|
||||
return JSON.stringify(value ?? null);
|
||||
}
|
||||
|
||||
function encodeOAuthIdentity(credential: {
|
||||
type: "oauth";
|
||||
provider: string;
|
||||
clientId?: string;
|
||||
email?: string;
|
||||
enterpriseUrl?: string;
|
||||
projectId?: string;
|
||||
accountId?: string;
|
||||
}): string {
|
||||
return JSON.stringify([
|
||||
"oauth",
|
||||
credential.provider,
|
||||
credential.clientId ?? null,
|
||||
credential.email ?? null,
|
||||
credential.enterpriseUrl ?? null,
|
||||
credential.projectId ?? null,
|
||||
credential.accountId ?? null,
|
||||
]);
|
||||
}
|
||||
|
||||
function encodeClaudeCredential(credential: ClaudeCliCredential): string {
|
||||
if (credential.type === "oauth") {
|
||||
return JSON.stringify(["oauth", credential.provider, credential.refresh]);
|
||||
return encodeOAuthIdentity(credential);
|
||||
}
|
||||
return JSON.stringify(["token", credential.provider, credential.token]);
|
||||
}
|
||||
|
||||
function encodeCodexCredential(credential: CodexCliCredential): string {
|
||||
return JSON.stringify([
|
||||
credential.type,
|
||||
credential.provider,
|
||||
credential.refresh,
|
||||
credential.accountId ?? null,
|
||||
]);
|
||||
return encodeOAuthIdentity(credential);
|
||||
}
|
||||
|
||||
function encodeAuthProfileCredential(credential: AuthProfileCredential): string {
|
||||
@@ -79,16 +94,7 @@ function encodeAuthProfileCredential(credential: AuthProfileCredential): string
|
||||
credential.displayName ?? null,
|
||||
]);
|
||||
case "oauth":
|
||||
return JSON.stringify([
|
||||
"oauth",
|
||||
credential.provider,
|
||||
credential.refresh,
|
||||
credential.clientId ?? null,
|
||||
credential.email ?? null,
|
||||
credential.enterpriseUrl ?? null,
|
||||
credential.projectId ?? null,
|
||||
credential.accountId ?? null,
|
||||
]);
|
||||
return encodeOAuthIdentity(credential);
|
||||
}
|
||||
throw new Error("Unsupported auth profile credential type");
|
||||
}
|
||||
|
||||
@@ -160,6 +160,28 @@ describe("cli-session helpers", () => {
|
||||
).toEqual({ sessionId: "cli-session-1" });
|
||||
});
|
||||
|
||||
it("accepts older auth epoch versions for binding upgrades", () => {
|
||||
const binding = {
|
||||
sessionId: "cli-session-1",
|
||||
authProfileId: "anthropic:work",
|
||||
authEpoch: "refresh-token-auth-epoch",
|
||||
authEpochVersion: 2,
|
||||
extraSystemPromptHash: "prompt-a",
|
||||
mcpConfigHash: "mcp-a",
|
||||
};
|
||||
|
||||
expect(
|
||||
resolveCliSessionReuse({
|
||||
binding,
|
||||
authProfileId: "anthropic:work",
|
||||
authEpoch: "identity-auth-epoch",
|
||||
authEpochVersion: 3,
|
||||
extraSystemPromptHash: "prompt-a",
|
||||
mcpConfigHash: "mcp-a",
|
||||
}),
|
||||
).toEqual({ sessionId: "cli-session-1" });
|
||||
});
|
||||
|
||||
it("does not treat model changes as a session mismatch", () => {
|
||||
const binding = {
|
||||
sessionId: "cli-session-1",
|
||||
|
||||
Reference in New Issue
Block a user