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;