Skip to main content
Glama
vsoneji
by vsoneji
oauth.ts6.56 kB
import { promises as fs } from 'node:fs' import https from 'node:https' import { join, dirname } from 'node:path' import { fileURLToPath } from 'node:url' import { createApiClient, type EnhancedTokenManager, } from '@sudowealth/schwab-api' import express, { type Request, type Response } from 'express' import open from 'open' import { type ValidatedEnv } from '../../types/env.js' import { LOGGER_CONTEXTS } from '../shared/constants.js' import { type FileTokenStore } from '../shared/fileTokenStore.js' import { logger } from '../shared/log.js' import { generateCertificates } from './certificates.js' const oauthLogger = logger.child(LOGGER_CONTEXTS.OAUTH_HANDLER) const __filename = fileURLToPath(import.meta.url) const __dirname = dirname(__filename) const CERT_DIR = join(__dirname, '../../.certs') export interface OAuthResult { schwabUserId: string clientId: string } /** * Start the OAuth server and handle the authentication flow */ export async function startOAuthServer( config: ValidatedEnv, tokenManager: EnhancedTokenManager, fileTokenStore: FileTokenStore, ): Promise<OAuthResult> { return new Promise(async (resolve, reject) => { const app = express() const port = config.PORT // Ensure certificates exist await generateCertificates() // Load certificates const certPath = join(CERT_DIR, 'cert.pem') const keyPath = join(CERT_DIR, 'key.pem') let credentials: { key: Buffer; cert: Buffer } try { const [key, cert] = await Promise.all([ fs.readFile(keyPath), fs.readFile(certPath), ]) credentials = { key, cert } } catch (error) { oauthLogger.error('Failed to load certificates', { error }) reject(error) return } // Callback endpoint app.get('/callback', async (req: Request, res: Response) => { try { const code = req.query.code as string const state = req.query.state as string if (!code || !state) { oauthLogger.error('Missing code or state in callback') res.status(400).send('Missing code or state parameter') return } oauthLogger.info('OAuth callback received, exchanging code for tokens') // Exchange the code for tokens try { await tokenManager.exchangeCode(code, state) } catch (error: any) { oauthLogger.error('Token exchange failed', { error: error.message, }) res.status(500).send('Token exchange failed: ' + error.message) return } oauthLogger.info('Token exchange successful') // Create API client to get user info const client = createApiClient({ config: { environment: 'PRODUCTION' }, auth: tokenManager, }) // Fetch user preferences to get Schwab user ID oauthLogger.info('Fetching user preferences to get Schwab user ID') let userPreferences try { userPreferences = await client.trader.userPreference.getUserPreference() } catch (error: any) { oauthLogger.error('Failed to fetch user preferences', { error: error.message, }) res.status(500).send('Failed to fetch user info: ' + error.message) return } const schwabUserId = userPreferences?.streamerInfo?.[0]?.schwabClientCorrelId if (!schwabUserId) { oauthLogger.error('Failed to get Schwab user ID from preferences') res.status(500).send('Failed to get Schwab user ID') return } // Migrate token from clientId-based key to schwabUserId-based key const clientId = config.SCHWAB_CLIENT_ID try { const currentTokenData = await fileTokenStore.load({ clientId }) if (currentTokenData) { await fileTokenStore.save({ schwabUserId }, currentTokenData) oauthLogger.info('Token migrated to schwabUserId key') } } catch (error: any) { oauthLogger.warn('Token migration failed, continuing', { error: error.message, }) } oauthLogger.info('OAuth flow completed successfully', { schwabUserId: schwabUserId.substring(0, 8) + '...', }) res.send(` <html> <head> <title>Authentication Successful</title> <style> body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); } .container { background: white; padding: 40px; border-radius: 10px; box-shadow: 0 10px 25px rgba(0,0,0,0.2); text-align: center; max-width: 500px; } h1 { color: #4CAF50; margin-bottom: 20px; } p { color: #666; line-height: 1.6; } .success-icon { font-size: 64px; margin-bottom: 20px; } </style> </head> <body> <div class="container"> <div class="success-icon">✓</div> <h1>Authentication Successful!</h1> <p>You have successfully authenticated with Schwab.</p> <p>You can close this window and return to the terminal.</p> </div> </body> </html> `) // Shutdown server after successful authentication setTimeout(() => { server.close(() => { oauthLogger.info('OAuth server shut down') resolve({ schwabUserId, clientId, }) }) }, 1000) } catch (error: any) { oauthLogger.error('Error in OAuth callback', { error: error.message, }) res.status(500).send('Internal server error: ' + error.message) } }) // Health check endpoint app.get('/health', (req: Request, res: Response) => { res.json({ status: 'ok' }) }) // Start HTTPS server const server = https.createServer(credentials, app) server.listen(port, async () => { oauthLogger.info(`OAuth server listening on https://localhost:${port}`) // Generate authorization URL const { authUrl } = await tokenManager.getAuthorizationUrl() oauthLogger.info('Please visit this URL to authorize the application:') console.log(`\n${authUrl}\n`) console.log('Opening browser automatically...\n') // Open browser automatically try { await open(authUrl) } catch (error) { oauthLogger.warn('Failed to open browser automatically', { error }) console.log('Please open the URL manually in your browser.') } }) server.on('error', (error) => { oauthLogger.error('OAuth server error', { error }) reject(error) }) }) }

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