pi-mcp-adapter 2.2.1 → 2.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +36 -0
- package/README.md +1 -2
- package/commands.ts +48 -20
- package/direct-tools.ts +11 -3
- package/index.ts +96 -38
- package/init.ts +19 -3
- package/mcp-auth-flow.ts +378 -0
- package/mcp-auth.ts +277 -0
- package/mcp-callback-server.ts +251 -0
- package/mcp-oauth-provider.ts +265 -0
- package/package.json +14 -6
- package/proxy-modes.ts +31 -0
- package/server-manager.ts +66 -24
- package/types.ts +27 -1
package/mcp-auth-flow.ts
ADDED
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Auth Flow
|
|
3
|
+
*
|
|
4
|
+
* High-level OAuth flow management using the MCP SDK's built-in auth functions.
|
|
5
|
+
* Follows the OpenCode pattern: let the SDK handle discovery internally via transport.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
UnauthorizedError,
|
|
10
|
+
} from "@modelcontextprotocol/sdk/client/auth.js"
|
|
11
|
+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"
|
|
12
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js"
|
|
13
|
+
import open from "open"
|
|
14
|
+
import { McpOAuthProvider, type McpOAuthConfig } from "./mcp-oauth-provider.js"
|
|
15
|
+
import {
|
|
16
|
+
ensureCallbackServer,
|
|
17
|
+
waitForCallback,
|
|
18
|
+
cancelPendingCallback,
|
|
19
|
+
stopCallbackServer,
|
|
20
|
+
} from "./mcp-callback-server.js"
|
|
21
|
+
import {
|
|
22
|
+
getAuthForUrl,
|
|
23
|
+
isTokenExpired,
|
|
24
|
+
hasStoredTokens,
|
|
25
|
+
clearAllCredentials,
|
|
26
|
+
updateOAuthState,
|
|
27
|
+
getOAuthState,
|
|
28
|
+
clearOAuthState,
|
|
29
|
+
type StoredTokens,
|
|
30
|
+
} from "./mcp-auth.js"
|
|
31
|
+
import type { ServerEntry } from "./types.js"
|
|
32
|
+
|
|
33
|
+
/** Auth status for a server */
|
|
34
|
+
export type AuthStatus = "authenticated" | "expired" | "not_authenticated"
|
|
35
|
+
|
|
36
|
+
// Track pending transports for auth completion
|
|
37
|
+
const pendingTransports = new Map<string, StreamableHTTPClientTransport>()
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Generate a cryptographically secure random state parameter.
|
|
41
|
+
*/
|
|
42
|
+
function generateState(): string {
|
|
43
|
+
return Array.from(crypto.getRandomValues(new Uint8Array(32)))
|
|
44
|
+
.map((b) => b.toString(16).padStart(2, "0"))
|
|
45
|
+
.join("")
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Extract OAuth configuration from a ServerEntry.
|
|
50
|
+
*/
|
|
51
|
+
function extractOAuthConfig(definition: ServerEntry): McpOAuthConfig {
|
|
52
|
+
// If oauth is explicitly false, return empty config
|
|
53
|
+
if (definition.oauth === false) {
|
|
54
|
+
return {}
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
grantType: definition.oauth?.grantType,
|
|
58
|
+
clientId: definition.oauth?.clientId,
|
|
59
|
+
clientSecret: definition.oauth?.clientSecret,
|
|
60
|
+
scope: definition.oauth?.scope,
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Start OAuth authentication flow for a server.
|
|
66
|
+
* Returns the authorization URL that should be opened in a browser.
|
|
67
|
+
*
|
|
68
|
+
* This follows the OpenCode pattern:
|
|
69
|
+
* 1. Create transport with auth provider
|
|
70
|
+
* 2. Try to connect - SDK handles discovery internally
|
|
71
|
+
* 3. If UnauthorizedError, capture the auth URL from onRedirect
|
|
72
|
+
*/
|
|
73
|
+
export async function startAuth(
|
|
74
|
+
serverName: string,
|
|
75
|
+
serverUrl: string,
|
|
76
|
+
definition?: ServerEntry
|
|
77
|
+
): Promise<{ authorizationUrl: string; transport: StreamableHTTPClientTransport }> {
|
|
78
|
+
const config = definition ? extractOAuthConfig(definition) : {}
|
|
79
|
+
|
|
80
|
+
if (config.grantType === "client_credentials") {
|
|
81
|
+
const authProvider = new McpOAuthProvider(serverName, serverUrl, config, {
|
|
82
|
+
onRedirect: async () => {
|
|
83
|
+
throw new Error("Browser redirect is not used for client_credentials flow")
|
|
84
|
+
},
|
|
85
|
+
})
|
|
86
|
+
const transport = new StreamableHTTPClientTransport(new URL(serverUrl), {
|
|
87
|
+
authProvider,
|
|
88
|
+
})
|
|
89
|
+
const client = new Client({
|
|
90
|
+
name: "pi-mcp",
|
|
91
|
+
version: "3.0.0",
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
await client.connect(transport)
|
|
96
|
+
return { authorizationUrl: "", transport }
|
|
97
|
+
} finally {
|
|
98
|
+
await client.close().catch(() => {})
|
|
99
|
+
await transport.close().catch(() => {})
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Start the callback server
|
|
104
|
+
await ensureCallbackServer()
|
|
105
|
+
|
|
106
|
+
// Generate and store OAuth state BEFORE creating the provider
|
|
107
|
+
// The SDK will call provider.state() to read this value
|
|
108
|
+
const oauthState = generateState()
|
|
109
|
+
await updateOAuthState(serverName, oauthState)
|
|
110
|
+
|
|
111
|
+
// Create the auth provider
|
|
112
|
+
let capturedUrl: URL | undefined
|
|
113
|
+
const authProvider = new McpOAuthProvider(serverName, serverUrl, config, {
|
|
114
|
+
onRedirect: async (url) => {
|
|
115
|
+
capturedUrl = url
|
|
116
|
+
},
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
// Create transport with auth provider
|
|
120
|
+
// The SDK handles OAuth discovery internally when connecting
|
|
121
|
+
const transport = new StreamableHTTPClientTransport(new URL(serverUrl), {
|
|
122
|
+
authProvider,
|
|
123
|
+
})
|
|
124
|
+
const client = new Client({
|
|
125
|
+
name: "pi-mcp",
|
|
126
|
+
version: "3.0.0",
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
// Try to connect - this triggers the OAuth flow
|
|
130
|
+
try {
|
|
131
|
+
await client.connect(transport)
|
|
132
|
+
// If we get here, we're already authenticated
|
|
133
|
+
await client.close().catch(() => {})
|
|
134
|
+
await transport.close().catch(() => {})
|
|
135
|
+
return { authorizationUrl: "", transport }
|
|
136
|
+
} catch (error) {
|
|
137
|
+
if (error instanceof UnauthorizedError && capturedUrl) {
|
|
138
|
+
await client.close().catch(() => {})
|
|
139
|
+
// Store transport for later finishAuth
|
|
140
|
+
pendingTransports.set(serverName, transport)
|
|
141
|
+
return { authorizationUrl: capturedUrl.toString(), transport }
|
|
142
|
+
}
|
|
143
|
+
await client.close().catch(() => {})
|
|
144
|
+
await transport.close().catch(() => {})
|
|
145
|
+
throw error
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Complete OAuth authentication with the authorization code.
|
|
151
|
+
*/
|
|
152
|
+
export async function completeAuth(
|
|
153
|
+
serverName: string,
|
|
154
|
+
authorizationCode: string
|
|
155
|
+
): Promise<AuthStatus> {
|
|
156
|
+
const transport = pendingTransports.get(serverName)
|
|
157
|
+
if (!transport) {
|
|
158
|
+
throw new Error(`No pending OAuth flow for server: ${serverName}`)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
try {
|
|
162
|
+
// Complete the auth using the transport's finishAuth method
|
|
163
|
+
await transport.finishAuth(authorizationCode)
|
|
164
|
+
return "authenticated"
|
|
165
|
+
} finally {
|
|
166
|
+
pendingTransports.delete(serverName)
|
|
167
|
+
await transport.close().catch(() => {})
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Perform the complete OAuth authentication flow for a server.
|
|
173
|
+
*
|
|
174
|
+
* @param serverName - The name of the MCP server
|
|
175
|
+
* @param serverUrl - The URL of the MCP server
|
|
176
|
+
* @param definition - The server definition (optional)
|
|
177
|
+
* @returns The final auth status
|
|
178
|
+
*/
|
|
179
|
+
export async function authenticate(
|
|
180
|
+
serverName: string,
|
|
181
|
+
serverUrl: string,
|
|
182
|
+
definition?: ServerEntry,
|
|
183
|
+
): Promise<AuthStatus> {
|
|
184
|
+
// Start auth flow
|
|
185
|
+
const { authorizationUrl } = await startAuth(serverName, serverUrl, definition)
|
|
186
|
+
|
|
187
|
+
// If no auth URL needed, already authenticated
|
|
188
|
+
if (!authorizationUrl) {
|
|
189
|
+
return "authenticated"
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Get the state that was already generated and stored in startAuth()
|
|
193
|
+
const oauthState = await getOAuthState(serverName)
|
|
194
|
+
if (!oauthState) {
|
|
195
|
+
throw new Error("OAuth state not found - this should not happen")
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Register the callback BEFORE opening the browser
|
|
199
|
+
const callbackPromise = waitForCallback(oauthState)
|
|
200
|
+
|
|
201
|
+
// Open browser
|
|
202
|
+
console.log(`MCP Auth: Opening browser for ${serverName}`)
|
|
203
|
+
try {
|
|
204
|
+
await open(authorizationUrl)
|
|
205
|
+
} catch (error) {
|
|
206
|
+
console.warn(`MCP Auth: Failed to open browser for ${serverName}`, { error })
|
|
207
|
+
throw new Error(
|
|
208
|
+
`Could not open browser. Please open this URL manually: ${authorizationUrl}`
|
|
209
|
+
)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
try {
|
|
213
|
+
// Wait for callback
|
|
214
|
+
const code = await callbackPromise
|
|
215
|
+
|
|
216
|
+
// Validate state
|
|
217
|
+
const storedState = await getOAuthState(serverName)
|
|
218
|
+
if (storedState !== oauthState) {
|
|
219
|
+
await clearOAuthState(serverName)
|
|
220
|
+
throw new Error("OAuth state mismatch - potential CSRF attack")
|
|
221
|
+
}
|
|
222
|
+
await clearOAuthState(serverName)
|
|
223
|
+
|
|
224
|
+
// Complete the auth
|
|
225
|
+
return await completeAuth(serverName, code)
|
|
226
|
+
} catch (error) {
|
|
227
|
+
cancelPendingCallback(oauthState)
|
|
228
|
+
const pendingTransport = pendingTransports.get(serverName)
|
|
229
|
+
if (pendingTransport) {
|
|
230
|
+
pendingTransports.delete(serverName)
|
|
231
|
+
await pendingTransport.close().catch(() => {})
|
|
232
|
+
}
|
|
233
|
+
throw error
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Get a valid access token for a server, refreshing if necessary.
|
|
239
|
+
*
|
|
240
|
+
* @param serverName - The name of the MCP server
|
|
241
|
+
* @param serverUrl - The URL of the MCP server
|
|
242
|
+
* @returns The valid tokens or null if not authenticated
|
|
243
|
+
*/
|
|
244
|
+
export async function getValidToken(
|
|
245
|
+
serverName: string,
|
|
246
|
+
serverUrl: string,
|
|
247
|
+
): Promise<StoredTokens | null> {
|
|
248
|
+
// Check if we have valid tokens
|
|
249
|
+
const entry = await getAuthForUrl(serverName, serverUrl)
|
|
250
|
+
if (!entry?.tokens) {
|
|
251
|
+
return null
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Check expiration
|
|
255
|
+
const expired = await isTokenExpired(serverName)
|
|
256
|
+
if (expired === false) {
|
|
257
|
+
return entry.tokens
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (expired === true && entry.tokens.refreshToken) {
|
|
261
|
+
// Token is expired, try to refresh
|
|
262
|
+
console.log(`MCP Auth: Token expired for ${serverName}, attempting refresh`)
|
|
263
|
+
|
|
264
|
+
try {
|
|
265
|
+
// Create auth provider for token refresh
|
|
266
|
+
const authProvider = new McpOAuthProvider(serverName, serverUrl, {}, {
|
|
267
|
+
onRedirect: async () => {},
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
const clientInfo = await authProvider.clientInformation()
|
|
271
|
+
if (!clientInfo) {
|
|
272
|
+
console.log(`MCP Auth: No client info for refresh for ${serverName}`)
|
|
273
|
+
return null
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Try to get tokens to find the token endpoint
|
|
277
|
+
const existingTokens = await authProvider.tokens()
|
|
278
|
+
if (!existingTokens) {
|
|
279
|
+
return null
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Create transport to trigger refresh
|
|
283
|
+
const transport = new StreamableHTTPClientTransport(new URL(serverUrl), {
|
|
284
|
+
authProvider,
|
|
285
|
+
})
|
|
286
|
+
|
|
287
|
+
// Try to connect - SDK will attempt token refresh internally
|
|
288
|
+
const client = new Client({ name: "pi-mcp", version: "3.0.0" })
|
|
289
|
+
try {
|
|
290
|
+
await client.connect(transport)
|
|
291
|
+
// Get refreshed tokens
|
|
292
|
+
const refreshed = await getAuthForUrl(serverName, serverUrl)
|
|
293
|
+
return refreshed?.tokens ?? null
|
|
294
|
+
} catch (error) {
|
|
295
|
+
console.error(`MCP Auth: Token refresh failed for ${serverName}`, { error })
|
|
296
|
+
return null
|
|
297
|
+
} finally {
|
|
298
|
+
await client.close().catch(() => {})
|
|
299
|
+
await transport.close().catch(() => {})
|
|
300
|
+
}
|
|
301
|
+
} catch (error) {
|
|
302
|
+
console.error(`MCP Auth: Token refresh failed for ${serverName}`, { error })
|
|
303
|
+
return null
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// No expiration info or no refresh token, assume valid
|
|
308
|
+
return entry.tokens
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Check the authentication status for a server.
|
|
313
|
+
*
|
|
314
|
+
* @param serverName - The name of the MCP server
|
|
315
|
+
* @returns The current auth status
|
|
316
|
+
*/
|
|
317
|
+
export async function getAuthStatus(serverName: string): Promise<AuthStatus> {
|
|
318
|
+
const hasTokens = await hasStoredTokens(serverName)
|
|
319
|
+
if (!hasTokens) return "not_authenticated"
|
|
320
|
+
|
|
321
|
+
const expired = await isTokenExpired(serverName)
|
|
322
|
+
return expired ? "expired" : "authenticated"
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Remove all OAuth credentials for a server.
|
|
327
|
+
*
|
|
328
|
+
* @param serverName - The name of the MCP server
|
|
329
|
+
*/
|
|
330
|
+
export async function removeAuth(serverName: string): Promise<void> {
|
|
331
|
+
const oauthState = await getOAuthState(serverName)
|
|
332
|
+
if (oauthState) {
|
|
333
|
+
cancelPendingCallback(oauthState)
|
|
334
|
+
}
|
|
335
|
+
const pendingTransport = pendingTransports.get(serverName)
|
|
336
|
+
if (pendingTransport) {
|
|
337
|
+
pendingTransports.delete(serverName)
|
|
338
|
+
await pendingTransport.close().catch(() => {})
|
|
339
|
+
}
|
|
340
|
+
clearAllCredentials(serverName)
|
|
341
|
+
await clearOAuthState(serverName)
|
|
342
|
+
console.log(`MCP Auth: Removed credentials for ${serverName}`)
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Check if OAuth is supported for a server configuration.
|
|
347
|
+
* OAuth is supported for HTTP servers unless explicitly disabled.
|
|
348
|
+
*
|
|
349
|
+
* @param definition - The server definition
|
|
350
|
+
* @returns True if OAuth is supported
|
|
351
|
+
*/
|
|
352
|
+
export function supportsOAuth(definition: ServerEntry): boolean {
|
|
353
|
+
// OAuth requires a URL
|
|
354
|
+
if (!definition.url) return false
|
|
355
|
+
|
|
356
|
+
// Explicitly disabled via auth: false or oauth: false
|
|
357
|
+
if (definition.auth === false) return false
|
|
358
|
+
if (definition.oauth === false) return false
|
|
359
|
+
|
|
360
|
+
// OAuth is enabled if auth is 'oauth' or not specified (auto-detect)
|
|
361
|
+
return definition.auth === "oauth" || definition.auth === undefined
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Initialize the OAuth system on startup.
|
|
366
|
+
* Starts the callback server if there are any OAuth servers configured.
|
|
367
|
+
*/
|
|
368
|
+
export async function initializeOAuth(): Promise<void> {
|
|
369
|
+
await ensureCallbackServer()
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Shutdown the OAuth system.
|
|
374
|
+
* Stops the callback server and cancels pending auths.
|
|
375
|
+
*/
|
|
376
|
+
export async function shutdownOAuth(): Promise<void> {
|
|
377
|
+
await stopCallbackServer()
|
|
378
|
+
}
|
package/mcp-auth.ts
ADDED
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Auth Storage Module
|
|
3
|
+
*
|
|
4
|
+
* Handles secure storage of OAuth credentials, tokens, client information,
|
|
5
|
+
* and PKCE state for MCP servers. Maintains backward compatibility with
|
|
6
|
+
* per-server directory structure.
|
|
7
|
+
*
|
|
8
|
+
* Token storage location: ~/.pi/agent/mcp-oauth/<server>/tokens.json
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { mkdirSync, readFileSync, writeFileSync, existsSync, rmSync } from 'fs';
|
|
12
|
+
import { join } from 'path';
|
|
13
|
+
import { homedir } from 'os';
|
|
14
|
+
|
|
15
|
+
/** OAuth token storage format */
|
|
16
|
+
export interface StoredTokens {
|
|
17
|
+
accessToken: string;
|
|
18
|
+
refreshToken?: string;
|
|
19
|
+
expiresAt?: number; // Unix timestamp in seconds
|
|
20
|
+
scope?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** OAuth client information from dynamic or static registration */
|
|
24
|
+
export interface StoredClientInfo {
|
|
25
|
+
clientId: string;
|
|
26
|
+
clientSecret?: string;
|
|
27
|
+
clientIdIssuedAt?: number;
|
|
28
|
+
clientSecretExpiresAt?: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Complete auth entry for a server */
|
|
32
|
+
export interface AuthEntry {
|
|
33
|
+
tokens?: StoredTokens;
|
|
34
|
+
clientInfo?: StoredClientInfo;
|
|
35
|
+
codeVerifier?: string;
|
|
36
|
+
oauthState?: string;
|
|
37
|
+
serverUrl?: string; // Track the URL these credentials are for
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Base directory for auth storage - can be overridden via env var for testing
|
|
41
|
+
function getAuthBaseDir(): string {
|
|
42
|
+
return process.env.MCP_OAUTH_DIR
|
|
43
|
+
? process.env.MCP_OAUTH_DIR
|
|
44
|
+
: join(homedir(), '.pi', 'agent', 'mcp-oauth');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Get the server-specific directory path.
|
|
49
|
+
*/
|
|
50
|
+
function getServerDir(serverName: string): string {
|
|
51
|
+
return join(getAuthBaseDir(), serverName);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Get the tokens file path for a server.
|
|
56
|
+
*/
|
|
57
|
+
function getTokensFilePath(serverName: string): string {
|
|
58
|
+
return join(getServerDir(serverName), 'tokens.json');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Ensure the server directory exists with secure permissions.
|
|
63
|
+
*/
|
|
64
|
+
function ensureServerDir(serverName: string): void {
|
|
65
|
+
const dir = getServerDir(serverName);
|
|
66
|
+
if (!existsSync(dir)) {
|
|
67
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Read the auth entry for a server from disk.
|
|
73
|
+
* Returns undefined if file doesn't exist.
|
|
74
|
+
*/
|
|
75
|
+
function readAuthEntry(serverName: string): AuthEntry | undefined {
|
|
76
|
+
try {
|
|
77
|
+
const filePath = getTokensFilePath(serverName);
|
|
78
|
+
if (!existsSync(filePath)) {
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
const data = readFileSync(filePath, 'utf-8');
|
|
82
|
+
return JSON.parse(data) as AuthEntry;
|
|
83
|
+
} catch (error) {
|
|
84
|
+
console.error(`Failed to read auth entry for ${serverName}:`, error);
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Write the auth entry for a server to disk with secure permissions.
|
|
91
|
+
*/
|
|
92
|
+
function writeAuthEntry(serverName: string, entry: AuthEntry): void {
|
|
93
|
+
ensureServerDir(serverName);
|
|
94
|
+
const filePath = getTokensFilePath(serverName);
|
|
95
|
+
writeFileSync(filePath, JSON.stringify(entry, null, 2), { mode: 0o600 });
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Get auth entry for a server.
|
|
100
|
+
*/
|
|
101
|
+
export function getAuthEntry(serverName: string): AuthEntry | undefined {
|
|
102
|
+
return readAuthEntry(serverName);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Get auth entry and validate it's for the correct URL.
|
|
107
|
+
* Returns undefined if URL has changed (credentials are invalid).
|
|
108
|
+
*/
|
|
109
|
+
export function getAuthForUrl(serverName: string, serverUrl: string): AuthEntry | undefined {
|
|
110
|
+
const entry = getAuthEntry(serverName);
|
|
111
|
+
if (!entry) return undefined;
|
|
112
|
+
|
|
113
|
+
// If no serverUrl is stored, this is from an old version - consider it invalid
|
|
114
|
+
if (!entry.serverUrl) return undefined;
|
|
115
|
+
|
|
116
|
+
// If URL has changed, credentials are invalid
|
|
117
|
+
if (entry.serverUrl !== serverUrl) return undefined;
|
|
118
|
+
|
|
119
|
+
return entry;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Save auth entry for a server.
|
|
124
|
+
*/
|
|
125
|
+
export function saveAuthEntry(serverName: string, entry: AuthEntry, serverUrl?: string): void {
|
|
126
|
+
// Always update serverUrl if provided
|
|
127
|
+
if (serverUrl) {
|
|
128
|
+
entry.serverUrl = serverUrl;
|
|
129
|
+
}
|
|
130
|
+
writeAuthEntry(serverName, entry);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Remove auth entry for a server.
|
|
135
|
+
* Also removes the server directory if empty.
|
|
136
|
+
*/
|
|
137
|
+
export function removeAuthEntry(serverName: string): void {
|
|
138
|
+
try {
|
|
139
|
+
const filePath = getTokensFilePath(serverName);
|
|
140
|
+
if (existsSync(filePath)) {
|
|
141
|
+
writeFileSync(filePath, '{}', { mode: 0o600 });
|
|
142
|
+
}
|
|
143
|
+
// Try to remove the directory
|
|
144
|
+
const dir = getServerDir(serverName);
|
|
145
|
+
if (existsSync(dir)) {
|
|
146
|
+
try {
|
|
147
|
+
rmSync(dir, { recursive: true });
|
|
148
|
+
} catch {
|
|
149
|
+
// Directory may not be empty, ignore
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
} catch (error) {
|
|
153
|
+
console.error(`Failed to remove auth entry for ${serverName}:`, error);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Update tokens for a server.
|
|
159
|
+
*/
|
|
160
|
+
export function updateTokens(
|
|
161
|
+
serverName: string,
|
|
162
|
+
tokens: StoredTokens,
|
|
163
|
+
serverUrl?: string
|
|
164
|
+
): void {
|
|
165
|
+
const entry = getAuthEntry(serverName) ?? {};
|
|
166
|
+
entry.tokens = tokens;
|
|
167
|
+
saveAuthEntry(serverName, entry, serverUrl);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Update client info for a server.
|
|
172
|
+
*/
|
|
173
|
+
export function updateClientInfo(
|
|
174
|
+
serverName: string,
|
|
175
|
+
clientInfo: StoredClientInfo,
|
|
176
|
+
serverUrl?: string
|
|
177
|
+
): void {
|
|
178
|
+
const entry = getAuthEntry(serverName) ?? {};
|
|
179
|
+
entry.clientInfo = clientInfo;
|
|
180
|
+
saveAuthEntry(serverName, entry, serverUrl);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Update code verifier for a server.
|
|
185
|
+
*/
|
|
186
|
+
export function updateCodeVerifier(serverName: string, codeVerifier: string): void {
|
|
187
|
+
const entry = getAuthEntry(serverName) ?? {};
|
|
188
|
+
entry.codeVerifier = codeVerifier;
|
|
189
|
+
saveAuthEntry(serverName, entry);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Clear code verifier for a server.
|
|
194
|
+
*/
|
|
195
|
+
export function clearCodeVerifier(serverName: string): void {
|
|
196
|
+
const entry = getAuthEntry(serverName);
|
|
197
|
+
if (entry) {
|
|
198
|
+
delete entry.codeVerifier;
|
|
199
|
+
saveAuthEntry(serverName, entry);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Update OAuth state for a server.
|
|
205
|
+
*/
|
|
206
|
+
export function updateOAuthState(serverName: string, state: string): void {
|
|
207
|
+
const entry = getAuthEntry(serverName) ?? {};
|
|
208
|
+
entry.oauthState = state;
|
|
209
|
+
saveAuthEntry(serverName, entry);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Get OAuth state for a server.
|
|
214
|
+
*/
|
|
215
|
+
export function getOAuthState(serverName: string): string | undefined {
|
|
216
|
+
const entry = getAuthEntry(serverName);
|
|
217
|
+
return entry?.oauthState;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Clear OAuth state for a server.
|
|
222
|
+
*/
|
|
223
|
+
export function clearOAuthState(serverName: string): void {
|
|
224
|
+
const entry = getAuthEntry(serverName);
|
|
225
|
+
if (entry) {
|
|
226
|
+
delete entry.oauthState;
|
|
227
|
+
saveAuthEntry(serverName, entry);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Check if stored tokens are expired.
|
|
233
|
+
* Returns null if no tokens exist, false if no expiry or not expired, true if expired.
|
|
234
|
+
*/
|
|
235
|
+
export function isTokenExpired(serverName: string): boolean | null {
|
|
236
|
+
const entry = getAuthEntry(serverName);
|
|
237
|
+
if (!entry?.tokens) return null;
|
|
238
|
+
if (!entry.tokens.expiresAt) return false;
|
|
239
|
+
return entry.tokens.expiresAt < Date.now() / 1000;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Check if a server has stored tokens.
|
|
244
|
+
*/
|
|
245
|
+
export function hasStoredTokens(serverName: string): boolean {
|
|
246
|
+
const entry = getAuthEntry(serverName);
|
|
247
|
+
return !!entry?.tokens;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Clear all credentials for a server.
|
|
252
|
+
*/
|
|
253
|
+
export function clearAllCredentials(serverName: string): void {
|
|
254
|
+
removeAuthEntry(serverName);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Clear only client info for a server.
|
|
259
|
+
*/
|
|
260
|
+
export function clearClientInfo(serverName: string): void {
|
|
261
|
+
const entry = getAuthEntry(serverName);
|
|
262
|
+
if (entry) {
|
|
263
|
+
delete entry.clientInfo;
|
|
264
|
+
saveAuthEntry(serverName, entry);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Clear only tokens for a server.
|
|
270
|
+
*/
|
|
271
|
+
export function clearTokens(serverName: string): void {
|
|
272
|
+
const entry = getAuthEntry(serverName);
|
|
273
|
+
if (entry) {
|
|
274
|
+
delete entry.tokens;
|
|
275
|
+
saveAuthEntry(serverName, entry);
|
|
276
|
+
}
|
|
277
|
+
}
|