utils.ts•2.15 kB
import crypto from "crypto";
import path from "path";
import fs from "fs/promises";
import { exec } from "child_process";
import { promisify } from "util";
import type { PKCEPair } from "./types.js";
const execAsync = promisify(exec);
/**
* Generate PKCE code verifier and challenge
*/
export function generatePKCE(): PKCEPair {
// Generate 128-character random string for verifier
const verifier = base64URLEncode(crypto.randomBytes(96));
// Create SHA256 hash of verifier for challenge
const challenge = base64URLEncode(
crypto.createHash("sha256").update(verifier).digest()
);
return {
verifier,
challenge,
};
}
/**
* Base64 URL encode (RFC 4648)
*/
function base64URLEncode(buffer: Buffer): string {
return buffer
.toString("base64")
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=/g, "");
}
/**
* Get the config directory path (~/.mcp-spotify)
*/
export function getConfigDir(): string {
const home = process.env.HOME || process.env.USERPROFILE || ".";
return path.join(home, ".mcp-spotify");
}
/**
* Ensure config directory exists
*/
export async function ensureConfigDir(): Promise<string> {
const dir = getConfigDir();
await fs.mkdir(dir, { recursive: true });
return dir;
}
/**
* Get the tokens file path
*/
export function getTokensPath(): string {
return path.join(getConfigDir(), "tokens.json");
}
/**
* Open a URL in the default browser (cross-platform)
*/
export async function openBrowser(url: string): Promise<void> {
const platform = process.platform;
try {
if (platform === "win32") {
// Windows
await execAsync(`start "" "${url}"`);
} else if (platform === "darwin") {
// macOS
await execAsync(`open "${url}"`);
} else {
// Linux/Unix
await execAsync(`xdg-open "${url}"`);
}
} catch (error) {
// If automatic opening fails, log the URL for manual opening
console.error(`Please open this URL manually: ${url}`);
throw error;
}
}
/**
* Generate a random state parameter for OAuth
*/
export function generateState(): string {
return base64URLEncode(crypto.randomBytes(32));
}