import fs from 'fs/promises';
import path from 'path';
import axios from 'axios';
import dotenv from 'dotenv';
dotenv.config();
const TOKEN_PATH = path.join(process.cwd(), 'spotify-tokens.json');
interface TokenData {
access_token: string;
refresh_token: string;
expires_at: number;
}
/**
* TokenManager handles OAuth token storage, retrieval, and automatic refreshing.
* It persists tokens to disk and ensures they're always valid before use.
*/
export class TokenManager {
private tokenData: TokenData | null = null;
/**
* Load tokens from disk
*/
async loadTokens(): Promise<TokenData | null> {
try {
const data = await fs.readFile(TOKEN_PATH, 'utf-8');
this.tokenData = JSON.parse(data);
return this.tokenData;
} catch (error) {
return null;
}
}
/**
* Save tokens to disk
*/
async saveTokens(data: any) {
const tokens: TokenData = {
access_token: data.access_token,
refresh_token: data.refresh_token || this.tokenData?.refresh_token,
// Calculate expiry (current time + expires_in seconds - buffer)
expires_at: Date.now() + (data.expires_in * 1000)
};
this.tokenData = tokens;
await fs.writeFile(TOKEN_PATH, JSON.stringify(tokens, null, 2));
}
/**
* Get a valid access token, refreshing if necessary
*/
async getValidAccessToken(): Promise<string> {
if (!this.tokenData) await this.loadTokens();
if (!this.tokenData) {
throw new Error("No tokens found. Run 'npm run auth' first to authenticate with Spotify.");
}
// Check if expired (with 1 minute buffer)
if (Date.now() > this.tokenData.expires_at - 60000) {
await this.refreshAccessToken();
}
return this.tokenData.access_token;
}
/**
* Refresh the access token using the refresh token
*/
private async refreshAccessToken() {
console.error("Refreshing Spotify Access Token...");
try {
const params = new URLSearchParams();
params.append('grant_type', 'refresh_token');
params.append('refresh_token', this.tokenData!.refresh_token);
const authHeader = Buffer.from(
`${process.env.SPOTIFY_CLIENT_ID}:${process.env.SPOTIFY_CLIENT_SECRET}`
).toString('base64');
const response = await axios.post('https://accounts.spotify.com/api/token', params, {
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Authorization': `Basic ${authHeader}`
}
});
await this.saveTokens(response.data);
console.error("Token refresh successful");
} catch (error) {
console.error("Failed to refresh token. You may need to re-authenticate.");
throw error;
}
}
}
export const tokenManager = new TokenManager();