import path from "path" import fs from "fs/promises" import z from "zod" import { Global } from "../global" export namespace McpAuth { export const Tokens = z.object({ accessToken: z.string(), refreshToken: z.string().optional(), expiresAt: z.number().optional(), scope: z.string().optional(), }) export type Tokens = z.infer export const ClientInfo = z.object({ clientId: z.string(), clientSecret: z.string().optional(), clientIdIssuedAt: z.number().optional(), clientSecretExpiresAt: z.number().optional(), }) export type ClientInfo = z.infer export const Entry = z.object({ tokens: Tokens.optional(), clientInfo: ClientInfo.optional(), codeVerifier: z.string().optional(), oauthState: z.string().optional(), serverUrl: z.string().optional(), // Track the URL these credentials are for }) export type Entry = z.infer const filepath = path.join(Global.Path.data, "mcp-auth.json") export async function get(mcpName: string): Promise { const data = await all() return data[mcpName] } /** * Get auth entry and validate it's for the correct URL. * Returns undefined if URL has changed (credentials are invalid). */ export async function getForUrl(mcpName: string, serverUrl: string): Promise { const entry = await get(mcpName) if (!entry) return undefined // If no serverUrl is stored, this is from an old version - consider it invalid if (!entry.serverUrl) return undefined // If URL has changed, credentials are invalid if (entry.serverUrl !== serverUrl) return undefined return entry } export async function all(): Promise> { const file = Bun.file(filepath) return file.json().catch(() => ({})) } export async function set(mcpName: string, entry: Entry, serverUrl?: string): Promise { const file = Bun.file(filepath) const data = await all() // Always update serverUrl if provided if (serverUrl) { entry.serverUrl = serverUrl } await Bun.write(file, JSON.stringify({ ...data, [mcpName]: entry }, null, 2)) await fs.chmod(file.name!, 0o600) } export async function remove(mcpName: string): Promise { const file = Bun.file(filepath) const data = await all() delete data[mcpName] await Bun.write(file, JSON.stringify(data, null, 2)) await fs.chmod(file.name!, 0o600) } export async function updateTokens(mcpName: string, tokens: Tokens, serverUrl?: string): Promise { const entry = (await get(mcpName)) ?? {} entry.tokens = tokens await set(mcpName, entry, serverUrl) } export async function updateClientInfo(mcpName: string, clientInfo: ClientInfo, serverUrl?: string): Promise { const entry = (await get(mcpName)) ?? {} entry.clientInfo = clientInfo await set(mcpName, entry, serverUrl) } export async function updateCodeVerifier(mcpName: string, codeVerifier: string): Promise { const entry = (await get(mcpName)) ?? {} entry.codeVerifier = codeVerifier await set(mcpName, entry) } export async function clearCodeVerifier(mcpName: string): Promise { const entry = await get(mcpName) if (entry) { delete entry.codeVerifier await set(mcpName, entry) } } export async function updateOAuthState(mcpName: string, oauthState: string): Promise { const entry = (await get(mcpName)) ?? {} entry.oauthState = oauthState await set(mcpName, entry) } export async function getOAuthState(mcpName: string): Promise { const entry = await get(mcpName) return entry?.oauthState } export async function clearOAuthState(mcpName: string): Promise { const entry = await get(mcpName) if (entry) { delete entry.oauthState await set(mcpName, entry) } } /** * Check if stored tokens are expired. * Returns null if no tokens exist, false if no expiry or not expired, true if expired. */ export async function isTokenExpired(mcpName: string): Promise { const entry = await get(mcpName) if (!entry?.tokens) return null if (!entry.tokens.expiresAt) return false return entry.tokens.expiresAt < Date.now() / 1000 } }