Skip to main content
Glama
vsoneji
by vsoneji
fileTokenStore.ts4.59 kB
import { promises as fs } from 'node:fs' import { join, dirname } from 'node:path' import { fileURLToPath } from 'node:url' import { type TokenData } from '@sudowealth/schwab-api' import { LOGGER_CONTEXTS } from './constants.js' import { logger } from './log.js' const storeLogger = logger.child(LOGGER_CONTEXTS.TOKEN_STORE) const __filename = fileURLToPath(import.meta.url) const __dirname = dirname(__filename) const TOKEN_DIR = join(__dirname, '../../.auth') const TOKEN_FILE = join(TOKEN_DIR, 'tokens.json') /** * Token identifiers for determining storage key * Prefer schwabUserId when available, fall back to clientId */ export type TokenIdentifiers = { schwabUserId?: string clientId?: string } /** * File-based token store interface */ export interface FileTokenStore { /** * Save token data to file storage */ save: (ids: TokenIdentifiers, tokenData: TokenData) => Promise<void> /** * Load token data from file storage */ load: (ids: TokenIdentifiers) => Promise<TokenData | null> /** * Migrate token from old key to new key */ migrateIfNeeded: ( oldIds: TokenIdentifiers, newIds: TokenIdentifiers, ) => Promise<void> /** * Get the key for a token */ tokenKey: (ids: TokenIdentifiers) => string } /** * Ensure the token directory exists */ async function ensureTokenDir(): Promise<void> { try { await fs.mkdir(TOKEN_DIR, { recursive: true }) } catch (error) { storeLogger.error('Failed to create token directory', { error }) throw error } } /** * Read all tokens from file */ async function readTokens(): Promise<Record<string, TokenData>> { try { await ensureTokenDir() const data = await fs.readFile(TOKEN_FILE, 'utf-8') return JSON.parse(data) } catch (error: any) { if (error.code === 'ENOENT') { // File doesn't exist yet return {} } storeLogger.error('Failed to read tokens file', { error }) return {} } } /** * Write all tokens to file */ async function writeTokens(tokens: Record<string, TokenData>): Promise<void> { try { await ensureTokenDir() await fs.writeFile(TOKEN_FILE, JSON.stringify(tokens, null, 2), 'utf-8') } catch (error) { storeLogger.error('Failed to write tokens file', { error }) throw error } } /** * Create a file-based token store */ export function makeFileTokenStore(): FileTokenStore { const tokenKey = (ids: TokenIdentifiers): string => { if (ids.schwabUserId) { return `schwab:${ids.schwabUserId}` } if (ids.clientId) { return `client:${ids.clientId}` } throw new Error('Either schwabUserId or clientId must be provided') } return { tokenKey, save: async (ids: TokenIdentifiers, tokenData: TokenData) => { const key = tokenKey(ids) storeLogger.debug('Saving token to file storage', { key: key.substring(0, 20) + '...', }) const tokens = await readTokens() tokens[key] = tokenData await writeTokens(tokens) storeLogger.debug('Token saved to file storage', { key: key.substring(0, 20) + '...', }) }, load: async (ids: TokenIdentifiers): Promise<TokenData | null> => { const key = tokenKey(ids) storeLogger.debug('Loading token from file storage', { key: key.substring(0, 20) + '...', }) const tokens = await readTokens() const tokenData = tokens[key] || null if (tokenData) { storeLogger.debug('Token loaded from file storage', { key: key.substring(0, 20) + '...', }) } else { storeLogger.debug('No token found in file storage', { key: key.substring(0, 20) + '...', }) } return tokenData }, migrateIfNeeded: async ( oldIds: TokenIdentifiers, newIds: TokenIdentifiers, ) => { const oldKey = tokenKey(oldIds) const newKey = tokenKey(newIds) if (oldKey === newKey) { storeLogger.debug('Token migration not needed, keys are identical') return } storeLogger.info('Attempting token migration', { oldKey: oldKey.substring(0, 20) + '...', newKey: newKey.substring(0, 20) + '...', }) const tokens = await readTokens() const oldToken = tokens[oldKey] const newToken = tokens[newKey] if (oldToken && !newToken) { // Migrate from old to new tokens[newKey] = oldToken delete tokens[oldKey] await writeTokens(tokens) storeLogger.info('Token migration complete', { oldKey: oldKey.substring(0, 20) + '...', newKey: newKey.substring(0, 20) + '...', }) } else if (newToken) { storeLogger.debug( 'Token already exists at new key, no migration needed', ) } else { storeLogger.debug('No token at old key to migrate') } }, } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/vsoneji/schwab-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server