import { generateCodeChallenge, generateCodeVerifier, generateState } from "@/lib/auth/pkce"; import type { TokenSet } from "@/lib/types/model"; const AUTHORIZE_URL = "https://accounts.spotify.com/authorize"; const TOKEN_URL = "https://accounts.spotify.com/api/token"; export const SPOTIFY_SCOPES = [ "user-library-read", "user-library-modify", "playlist-read-private", "playlist-modify-private", "playlist-modify-public", "user-follow-read", "user-follow-modify", ].join(" "); function clientId(): string { const id = process.env.SPOTIFY_CLIENT_ID; if (!id) throw new Error("SPOTIFY_CLIENT_ID is not set"); return id; } export function buildSpotifyAuthUrl(redirectUri: string): { url: string; codeVerifier: string; state: string; } { const codeVerifier = generateCodeVerifier(); const codeChallenge = generateCodeChallenge(codeVerifier); const state = generateState(); const params = new URLSearchParams({ response_type: "code", client_id: clientId(), scope: SPOTIFY_SCOPES, redirect_uri: redirectUri, state, code_challenge_method: "S256", code_challenge: codeChallenge, }); return { url: `${AUTHORIZE_URL}?${params.toString()}`, codeVerifier, state }; } interface SpotifyTokenResponse { access_token: string; refresh_token?: string; expires_in: number; scope: string; token_type: string; } export async function exchangeSpotifyCode( code: string, codeVerifier: string, redirectUri: string ): Promise { const body = new URLSearchParams({ grant_type: "authorization_code", code, redirect_uri: redirectUri, client_id: clientId(), code_verifier: codeVerifier, }); const res = await fetch(TOKEN_URL, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body, }); if (!res.ok) { throw new Error(`Spotify token exchange failed: ${res.status} ${await res.text()}`); } const data = (await res.json()) as SpotifyTokenResponse; return { accessToken: data.access_token, refreshToken: data.refresh_token, expiresAt: Date.now() + data.expires_in * 1000, scope: data.scope, }; } export async function refreshSpotifyToken(tokens: TokenSet): Promise { if (!tokens.refreshToken) { throw new Error("No refresh token available for Spotify account"); } const body = new URLSearchParams({ grant_type: "refresh_token", refresh_token: tokens.refreshToken, client_id: clientId(), }); const res = await fetch(TOKEN_URL, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body, }); if (!res.ok) { throw new Error(`Spotify token refresh failed: ${res.status} ${await res.text()}`); } const data = (await res.json()) as SpotifyTokenResponse; return { accessToken: data.access_token, refreshToken: data.refresh_token ?? tokens.refreshToken, expiresAt: Date.now() + data.expires_in * 1000, scope: data.scope, }; }