Skip to main content
Glama
auth.ts7.63 kB
import { config } from 'dotenv'; import axios from 'axios'; import * as fs from 'fs'; import * as path from 'path'; // Load environment variables config(); interface TokenData { access_token: string; refresh_token?: string; expires_at?: number; token_type: string; } class MediumAuth { private clientId: string; private clientSecret: string; private accessToken: string | null = null; private refreshToken: string | null = null; private tokenExpiresAt: number | null = null; private tokenFilePath: string; constructor() { // Validate credentials from environment this.clientId = this.validateCredential('MEDIUM_CLIENT_ID'); this.clientSecret = this.validateCredential('MEDIUM_CLIENT_SECRET'); // Set token storage path this.tokenFilePath = path.join(process.cwd(), '.medium-tokens.json'); // Load existing tokens if available this.loadStoredTokens(); } private validateCredential(key: string): string { const value = process.env[key]; if (!value) { this.logSecurityAlert(`Missing critical credential: ${key}`); throw new Error(`🚨 Security Alert: Missing ${key} in environment variables`); } return value; } private loadStoredTokens(): void { try { if (fs.existsSync(this.tokenFilePath)) { const data = fs.readFileSync(this.tokenFilePath, 'utf-8'); const tokenData: TokenData = JSON.parse(data); this.accessToken = tokenData.access_token; this.refreshToken = tokenData.refresh_token || null; this.tokenExpiresAt = tokenData.expires_at || null; console.log('✅ Loaded stored authentication tokens'); } } catch (error) { console.error('⚠️ Failed to load stored tokens:', error); } } private saveTokens(tokenData: TokenData): void { try { fs.writeFileSync(this.tokenFilePath, JSON.stringify(tokenData, null, 2), 'utf-8'); console.log('✅ Tokens saved securely'); } catch (error) { console.error('⚠️ Failed to save tokens:', error); } } public async authenticate(): Promise<void> { try { // Check if we have a valid token if (this.isTokenValid()) { this.logAuthSuccess(); return; } // Try to refresh token if available if (this.refreshToken) { await this.refreshAccessToken(); this.logAuthSuccess(); return; } // Otherwise use the access token from environment or request new one this.accessToken = await this.requestAccessToken(); this.logAuthSuccess(); } catch (error) { this.handleAuthenticationFailure(error); } } private isTokenValid(): boolean { if (!this.accessToken) return false; // If no expiry time, assume valid (for self-issued tokens) if (!this.tokenExpiresAt) return true; // Check if token expires in more than 5 minutes const now = Date.now(); const bufferTime = 5 * 60 * 1000; // 5 minutes return this.tokenExpiresAt > (now + bufferTime); } private async requestAccessToken(): Promise<string> { // Check for direct access token in environment (for self-issued tokens) const directToken = process.env.MEDIUM_ACCESS_TOKEN; if (directToken) { console.log('🔑 Using direct access token from environment'); // Save to file for persistence this.saveTokens({ access_token: directToken, token_type: 'Bearer' }); return directToken; } // Check for authorization code (for OAuth flow) const authCode = process.env.MEDIUM_AUTH_CODE; if (authCode) { return await this.exchangeCodeForToken(authCode); } throw new Error( '🚨 No authentication method available. Please set either:\n' + ' - MEDIUM_ACCESS_TOKEN (for self-issued integration tokens)\n' + ' - MEDIUM_AUTH_CODE (for OAuth authorization code)\n' + 'Visit: https://medium.com/me/settings/security to get your integration token' ); } private async exchangeCodeForToken(authCode: string): Promise<string> { try { console.log('🔄 Exchanging authorization code for access token...'); const response = await axios.post('https://api.medium.com/v1/tokens', { code: authCode, client_id: this.clientId, client_secret: this.clientSecret, grant_type: 'authorization_code', redirect_uri: process.env.MEDIUM_REDIRECT_URI || 'http://localhost:3000/callback' }); const tokenData: TokenData = { access_token: response.data.access_token, refresh_token: response.data.refresh_token, expires_at: Date.now() + (response.data.expires_in * 1000), token_type: response.data.token_type || 'Bearer' }; this.accessToken = tokenData.access_token; this.refreshToken = tokenData.refresh_token || null; this.tokenExpiresAt = tokenData.expires_at || null; this.saveTokens(tokenData); return tokenData.access_token; } catch (error: any) { throw new Error(`Failed to exchange authorization code: ${error.message}`); } } private async refreshAccessToken(): Promise<void> { if (!this.refreshToken) { throw new Error('No refresh token available'); } try { console.log('🔄 Refreshing access token...'); const response = await axios.post('https://api.medium.com/v1/tokens', { refresh_token: this.refreshToken, client_id: this.clientId, client_secret: this.clientSecret, grant_type: 'refresh_token' }); const tokenData: TokenData = { access_token: response.data.access_token, refresh_token: response.data.refresh_token || this.refreshToken, expires_at: Date.now() + (response.data.expires_in * 1000), token_type: response.data.token_type || 'Bearer' }; this.accessToken = tokenData.access_token; this.refreshToken = tokenData.refresh_token || null; this.tokenExpiresAt = tokenData.expires_at || null; this.saveTokens(tokenData); } catch (error: any) { console.error('Failed to refresh token:', error.message); // Clear stored tokens and force re-authentication this.accessToken = null; this.refreshToken = null; throw error; } } public getAccessToken(): string { if (!this.accessToken) { this.logSecurityAlert('Unauthorized access token request'); throw new Error('🔒 Authentication Required: Call authenticate() first'); } return this.accessToken; } public isAuthenticated(): boolean { return this.isTokenValid(); } public clearTokens(): void { this.accessToken = null; this.refreshToken = null; this.tokenExpiresAt = null; try { if (fs.existsSync(this.tokenFilePath)) { fs.unlinkSync(this.tokenFilePath); console.log('🗑️ Tokens cleared'); } } catch (error) { console.error('⚠️ Failed to clear tokens:', error); } } private logAuthSuccess() { console.log(` ✅ Medium Authentication Successful 🕒 Timestamp: ${new Date().toISOString()} `); } private logSecurityAlert(message: string) { console.error(` ⚠️ SECURITY ALERT ⚠️ Message: ${message} Timestamp: ${new Date().toISOString()} `); } private handleAuthenticationFailure(error: any) { this.logSecurityAlert(`Authentication Failed: ${error.message}`); throw new Error(`🚫 Medium Authentication Failed: ${error.message}`); } } export default MediumAuth;

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/aliiqbal208/medium-mcp'

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