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:
Fusion
2026-05-04 23:23:50 -07:00
committed by gsxdsm
parent 20fc4f8420
commit d91780d171
18 changed files with 179 additions and 28 deletions

View File

@@ -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-"));

View File

@@ -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,

View File

@@ -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 {};
}