feat(FN-3434): add Claude OAuth credential interop with verification and st
This merge lands four major features: the desktop app gains shell onboarding with remote mode support via a new `DesktopModeChooser` and `shell-settings` module (FN-3399); the dashboard gains full archived insights support with the `InsightsView` redesign and `useInsights` hook overhaul (FN-3315); C Fusion-Task-Id: FN-3434
This commit is contained in:
@@ -4,7 +4,9 @@ import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import {
|
||||
choosePreferredStoredCredential,
|
||||
extractClaudeCliStoredCredential,
|
||||
extractCodexCliStoredCredential,
|
||||
getClaudeCodeCredentialPaths,
|
||||
readStoredCredentialsFromAuthFile,
|
||||
shouldHydrateStoredCredential,
|
||||
} from "../oauth-credential-interop.js";
|
||||
@@ -86,6 +88,59 @@ describe("oauth credential interop", () => {
|
||||
expect(shouldHydrateStoredCredential({ type: "api_key", key: "sk-live" }, valid)).toBe(false);
|
||||
});
|
||||
|
||||
it("extracts Claude OAuth credentials from .credentials.json payload", () => {
|
||||
const credential = extractClaudeCliStoredCredential({
|
||||
claudeAiOauth: {
|
||||
accessToken: "claude-access",
|
||||
refreshToken: "claude-refresh",
|
||||
expiresAt: Date.now() + 3600_000,
|
||||
},
|
||||
});
|
||||
|
||||
expect(credential).toEqual({
|
||||
type: "oauth",
|
||||
access: "claude-access",
|
||||
refresh: "claude-refresh",
|
||||
expires: expect.any(Number),
|
||||
});
|
||||
});
|
||||
|
||||
it("reads Claude credentials from auth file as anthropic OAuth", () => {
|
||||
const tempDir = mkdtempSync(join(tmpdir(), "fusion-oauth-interop-"));
|
||||
|
||||
try {
|
||||
const authPath = join(tempDir, ".credentials.json");
|
||||
writeFileSync(
|
||||
authPath,
|
||||
JSON.stringify({
|
||||
claudeAiOauth: {
|
||||
accessToken: "claude-access",
|
||||
refreshToken: "claude-refresh",
|
||||
expiresAt: Date.now() + 3_600_000,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(readStoredCredentialsFromAuthFile(authPath)).toEqual({
|
||||
anthropic: {
|
||||
type: "oauth",
|
||||
access: "claude-access",
|
||||
refresh: "claude-refresh",
|
||||
expires: expect.any(Number),
|
||||
},
|
||||
});
|
||||
} finally {
|
||||
rmSync(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("returns both supported Claude credential paths", () => {
|
||||
expect(getClaudeCodeCredentialPaths("/tmp/home")).toEqual([
|
||||
"/tmp/home/.claude/.credentials.json",
|
||||
"/tmp/home/.config/claude/.credentials.json",
|
||||
]);
|
||||
});
|
||||
|
||||
it("gracefully ignores malformed auth files", () => {
|
||||
const tempDir = mkdtempSync(join(tmpdir(), "fusion-oauth-interop-"));
|
||||
|
||||
|
||||
@@ -770,7 +770,9 @@ export { ChatStore } from "./chat-store.js";
|
||||
export type { ChatStoreEvents } from "./chat-store.js";
|
||||
export {
|
||||
choosePreferredStoredCredential,
|
||||
extractClaudeCliStoredCredential,
|
||||
extractCodexCliStoredCredential,
|
||||
getClaudeCodeCredentialPaths,
|
||||
getCodexCliAuthPath,
|
||||
readStoredCredentialsFromAuthFile,
|
||||
shouldHydrateStoredCredential,
|
||||
|
||||
@@ -23,6 +23,13 @@ export function getCodexCliAuthPath(home = getHomeDir()): string {
|
||||
return join(home, ".codex", "auth.json");
|
||||
}
|
||||
|
||||
export function getClaudeCodeCredentialPaths(home = getHomeDir()): string[] {
|
||||
return [
|
||||
join(home, ".claude", ".credentials.json"),
|
||||
join(home, ".config", "claude", ".credentials.json"),
|
||||
];
|
||||
}
|
||||
|
||||
function parseJwtPayload(token: string): Record<string, unknown> | null {
|
||||
try {
|
||||
const [, payload = ""] = token.split(".", 3);
|
||||
@@ -213,6 +220,34 @@ export function extractCodexCliStoredCredential(raw: unknown): StoredAuthCredent
|
||||
};
|
||||
}
|
||||
|
||||
export function extractClaudeCliStoredCredential(raw: unknown): StoredAuthCredential | undefined {
|
||||
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const record = raw as Record<string, unknown>;
|
||||
const oauthRecord =
|
||||
record.claudeAiOauth && typeof record.claudeAiOauth === "object" && !Array.isArray(record.claudeAiOauth)
|
||||
? (record.claudeAiOauth as Record<string, unknown>)
|
||||
: record;
|
||||
|
||||
const access = typeof oauthRecord.accessToken === "string" ? oauthRecord.accessToken : undefined;
|
||||
const refresh = typeof oauthRecord.refreshToken === "string" ? oauthRecord.refreshToken : undefined;
|
||||
const expiresRaw = oauthRecord.expiresAt;
|
||||
const expires = typeof expiresRaw === "number" && Number.isFinite(expiresRaw) ? expiresRaw : undefined;
|
||||
|
||||
if (!access || !refresh || expires === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
type: "oauth",
|
||||
access,
|
||||
refresh,
|
||||
expires,
|
||||
};
|
||||
}
|
||||
|
||||
export function readStoredCredentialsFromAuthFile(authPath: string): Record<string, StoredAuthCredential> {
|
||||
if (!existsSync(authPath)) {
|
||||
return {};
|
||||
@@ -225,6 +260,11 @@ export function readStoredCredentialsFromAuthFile(authPath: string): Record<stri
|
||||
return { "openai-codex": codexCliCredential };
|
||||
}
|
||||
|
||||
const claudeCliCredential = extractClaudeCliStoredCredential(parsed);
|
||||
if (claudeCliCredential) {
|
||||
return { anthropic: claudeCliCredential };
|
||||
}
|
||||
|
||||
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user