@karanb192/reddit-buddy-mcp
Version:
Clean, LLM-optimized Reddit MCP server. Browse posts, search content, analyze users. No fluff, just Reddit data.
216 lines ⢠7.51 kB
JavaScript
/**
* Reddit authentication manager
*/
import { homedir, platform } from 'os';
import { join } from 'path';
import { promises as fs } from 'fs';
export class AuthManager {
config = null;
configPath;
constructor() {
this.configPath = this.getConfigPath();
}
/**
* Load authentication configuration
*/
async load() {
try {
const configFile = join(this.configPath, 'auth.json');
const data = await fs.readFile(configFile, 'utf-8');
this.config = JSON.parse(data);
// Validate config
if (this.config && !this.isValidConfig(this.config)) {
console.error('Invalid auth configuration found');
this.config = null;
}
return this.config;
}
catch (error) {
// No auth configured or invalid file
return null;
}
}
/**
* Save authentication configuration
*/
async save(config) {
try {
// Ensure directory exists
await fs.mkdir(this.configPath, { recursive: true });
// Save config
const configFile = join(this.configPath, 'auth.json');
await fs.writeFile(configFile, JSON.stringify(config, null, 2), { mode: 0o600 } // Read/write for owner only
);
this.config = config;
}
catch (error) {
throw new Error(`Failed to save auth configuration: ${error}`);
}
}
/**
* Get current configuration
*/
getConfig() {
return this.config;
}
/**
* Check if authenticated
*/
isAuthenticated() {
return this.config !== null && this.config.clientId !== undefined;
}
/**
* Check if token is expired
*/
isTokenExpired() {
if (!this.config?.expiresAt)
return true;
return Date.now() >= this.config.expiresAt;
}
/**
* Get access token for Reddit OAuth
*/
async getAccessToken() {
if (!this.config)
return null;
// For script apps, we can use app-only auth
if (!this.config.accessToken || this.isTokenExpired()) {
await this.refreshAccessToken();
}
return this.config.accessToken || null;
}
/**
* Refresh access token using client credentials
*/
async refreshAccessToken() {
if (!this.config?.clientId) {
throw new Error('No client ID configured');
}
try {
// Reddit script apps can use device_id flow without secret
const auth = Buffer.from(`${this.config.clientId}:`).toString('base64');
const response = await fetch('https://www.reddit.com/api/v1/access_token', {
method: 'POST',
headers: {
'Authorization': `Basic ${auth}`,
'Content-Type': 'application/x-www-form-urlencoded',
'User-Agent': 'RedditBuddy/1.0.0 by karanb192'
},
body: 'grant_type=https://oauth.reddit.com/grants/installed_client&device_id=DO_NOT_TRACK'
});
if (!response.ok) {
const error = await response.text();
throw new Error(`Failed to get access token: ${response.status} - ${error}`);
}
const data = await response.json();
// Update config
this.config.accessToken = data.access_token;
this.config.expiresAt = Date.now() + (data.expires_in * 1000);
this.config.scope = data.scope;
// Save updated config
await this.save(this.config);
}
catch (error) {
throw new Error(`Failed to refresh access token: ${error}`);
}
}
/**
* Clear authentication
*/
async clear() {
this.config = null;
try {
const configFile = join(this.configPath, 'auth.json');
await fs.unlink(configFile);
}
catch {
// File might not exist
}
}
/**
* Get headers for Reddit API requests
*/
async getHeaders() {
const headers = {
'User-Agent': 'RedditBuddy/1.0 (by /u/karanb192)',
'Accept': 'application/json',
'Accept-Language': 'en-US,en;q=0.9',
'Cache-Control': 'no-cache'
};
const token = await this.getAccessToken();
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
return headers;
}
/**
* Get rate limit based on auth status
*/
getRateLimit() {
return this.isAuthenticated() ? 100 : 10;
}
/**
* Get cache TTL based on auth status (in ms)
*/
getCacheTTL() {
return this.isAuthenticated()
? 5 * 60 * 1000 // 5 minutes for authenticated
: 15 * 60 * 1000; // 15 minutes for unauthenticated
}
/**
* Private: Get configuration directory path based on OS
*/
getConfigPath() {
const home = homedir();
switch (platform()) {
case 'win32':
return join(process.env.APPDATA || join(home, 'AppData', 'Roaming'), 'reddit-buddy');
case 'darwin':
return join(home, 'Library', 'Application Support', 'reddit-buddy');
default: // linux and others
return join(process.env.XDG_CONFIG_HOME || join(home, '.config'), 'reddit-buddy');
}
}
/**
* Private: Validate configuration
*/
isValidConfig(config) {
return config && typeof config.clientId === 'string' && config.clientId.length > 0;
}
/**
* Setup wizard for authentication
*/
static async runSetupWizard() {
console.log('\nš Reddit Buddy Authentication Setup\n');
console.log('This will help you set up authentication for 10x more requests.\n');
console.log('Step 1: Create a Reddit App');
console.log(' 1. Go to: https://www.reddit.com/prefs/apps');
console.log(' 2. Click "Create App" or "Create Another App"');
console.log(' 3. Fill in the following:');
console.log(' - Name: Reddit Buddy (or anything you like)');
console.log(' - App type: Select "script"');
console.log(' - Description: Personal use');
console.log(' - About URL: (leave blank)');
console.log(' - Redirect URI: http://localhost:8080');
console.log(' 4. Click "Create app"\n');
console.log('Step 2: Copy your Client ID');
console.log(' - Find it under "personal use script"');
console.log(' - It looks like: XaBcDeFgHiJkLm\n');
// In a real implementation, we'd use a prompt library here
console.log('Please enter your Client ID and press Enter:');
// This is a placeholder - in real implementation, use readline or a prompt library
const clientId = 'YOUR_CLIENT_ID_HERE';
// Validate client ID format
if (!/^[A-Za-z0-9_-]{10,30}$/.test(clientId)) {
throw new Error('Invalid Client ID format');
}
const config = {
clientId,
deviceId: 'DO_NOT_TRACK'
};
console.log('\nā
Setup complete! Your authentication is configured.');
console.log('You now have access to 100 requests per minute.\n');
return config;
}
}
//# sourceMappingURL=auth.js.map