|
1 | 1 | import { createHash } from "node:crypto"; |
2 | 2 | import { mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises"; |
| 3 | +import { readFileSync } from "node:fs"; |
3 | 4 | import { homedir } from "node:os"; |
4 | 5 | import { dirname, join } from "node:path"; |
5 | 6 | import type { |
@@ -41,40 +42,88 @@ export function normalizeMCPServerURL(serverURL: string): string { |
41 | 42 | return url.toString(); |
42 | 43 | } |
43 | 44 |
|
44 | | -function authFilePath(identity: MCPAuthIdentity, home: string): string { |
| 45 | +export function authFilePath(identity: MCPAuthIdentity, home: string = homedir()): string { |
45 | 46 | const normalizedURL = normalizeMCPServerURL(identity.serverURL); |
46 | 47 | const digest = createHash("sha256") |
47 | 48 | .update(JSON.stringify([identity.serverName, normalizedURL])) |
48 | 49 | .digest("hex"); |
49 | 50 | return join(mcpAuthDir(home), `${serverDisplaySlug(identity.serverName)}-${digest}.json`); |
50 | 51 | } |
51 | 52 |
|
52 | | -export async function loadAuthState( |
53 | | - identity: MCPAuthIdentity, |
54 | | - home: string = homedir(), |
55 | | -): Promise<MCPAuthState> { |
56 | | - let raw: string; |
| 53 | +function isEnoent(err: unknown): boolean { |
| 54 | + return ( |
| 55 | + typeof err === "object" && |
| 56 | + err !== null && |
| 57 | + "code" in err && |
| 58 | + (err as { code?: unknown }).code === "ENOENT" |
| 59 | + ); |
| 60 | +} |
| 61 | + |
| 62 | +function parseAuthState(raw: string): MCPAuthState | undefined { |
| 63 | + try { |
| 64 | + const parsed: unknown = JSON.parse(raw); |
| 65 | + if (typeof parsed === "object" && parsed !== null) return parsed as MCPAuthState; |
| 66 | + } catch { |
| 67 | + // A corrupt auth file should not wedge the session; treat it as no state and |
| 68 | + // let a fresh authorization overwrite it. |
| 69 | + } |
| 70 | + return undefined; |
| 71 | +} |
| 72 | + |
| 73 | +function stateFromRaw(raw: string | undefined): MCPAuthState { |
| 74 | + if (raw === undefined) return {}; |
| 75 | + return parseAuthState(raw) ?? {}; |
| 76 | +} |
| 77 | + |
| 78 | +function readAuthFileSync(path: string): string | undefined { |
57 | 79 | try { |
58 | | - raw = await readFile(authFilePath(identity, home), "utf8"); |
| 80 | + return readFileSync(path, "utf8"); |
59 | 81 | } catch (err) { |
60 | | - if ( |
61 | | - typeof err === "object" && |
62 | | - err !== null && |
63 | | - "code" in err && |
64 | | - (err as { code?: unknown }).code === "ENOENT" |
65 | | - ) { |
66 | | - return {}; |
67 | | - } |
| 82 | + if (isEnoent(err)) return undefined; |
68 | 83 | throw err; |
69 | 84 | } |
| 85 | +} |
| 86 | + |
| 87 | +async function readAuthFile(path: string): Promise<string | undefined> { |
70 | 88 | try { |
71 | | - const parsed = JSON.parse(raw); |
72 | | - if (typeof parsed === "object" && parsed !== null) return parsed as MCPAuthState; |
| 89 | + return await readFile(path, "utf8"); |
| 90 | + } catch (err) { |
| 91 | + if (isEnoent(err)) return undefined; |
| 92 | + throw err; |
| 93 | + } |
| 94 | +} |
| 95 | + |
| 96 | +// Synchronous connect-contract load. Tolerates a missing (ENOENT) or corrupt |
| 97 | +// file with empty state, matching loadAuthState; other read errors propagate. |
| 98 | +export function loadAuthStateSync( |
| 99 | + identity: MCPAuthIdentity, |
| 100 | + home: string = homedir(), |
| 101 | +): MCPAuthState { |
| 102 | + return stateFromRaw(readAuthFileSync(authFilePath(identity, home))); |
| 103 | +} |
| 104 | + |
| 105 | +export async function loadAuthState( |
| 106 | + identity: MCPAuthIdentity, |
| 107 | + home: string = homedir(), |
| 108 | +): Promise<MCPAuthState> { |
| 109 | + return stateFromRaw(await readAuthFile(authFilePath(identity, home))); |
| 110 | +} |
| 111 | + |
| 112 | +// Cache refresh for a live provider: missing, unreadable, or corrupt files |
| 113 | +// return undefined so the caller keeps its in-memory mirror. Empty-on-corrupt |
| 114 | +// is loadAuthState's connect contract, not cache invalidation. |
| 115 | +export function tryLoadAuthStateSync( |
| 116 | + identity: MCPAuthIdentity, |
| 117 | + home: string = homedir(), |
| 118 | +): MCPAuthState | undefined { |
| 119 | + let raw: string | undefined; |
| 120 | + try { |
| 121 | + raw = readAuthFileSync(authFilePath(identity, home)); |
73 | 122 | } catch { |
74 | | - // A corrupt auth file should not wedge the session; treat it as no state and |
75 | | - // let a fresh authorization overwrite it. |
| 123 | + return undefined; |
76 | 124 | } |
77 | | - return {}; |
| 125 | + if (raw === undefined) return undefined; |
| 126 | + return parseAuthState(raw); |
78 | 127 | } |
79 | 128 |
|
80 | 129 | // pid alone is not unique per call — concurrent saves in one process must not |
@@ -174,14 +223,7 @@ async function unlinkAuthFile(path: string): Promise<void> { |
174 | 223 | try { |
175 | 224 | await unlink(path); |
176 | 225 | } catch (err) { |
177 | | - if ( |
178 | | - typeof err === "object" && |
179 | | - err !== null && |
180 | | - "code" in err && |
181 | | - (err as { code?: unknown }).code === "ENOENT" |
182 | | - ) { |
183 | | - return; |
184 | | - } |
| 226 | + if (isEnoent(err)) return; |
185 | 227 | throw err; |
186 | 228 | } |
187 | 229 | } |
0 commit comments