export interface GitHubProfile { id: number login: string name: string | null avatarUrl: string } export class GitHubProfileError extends Error { constructor(public readonly code: 'invalid_username' | 'not_found' | 'rate_limited' | 'request_failed') { super(code) } } export function normalizeGitHubUsername(username: string) { return username.trim() } export async function getGitHubProfile(username: string): Promise { const normalizedUsername = normalizeGitHubUsername(username) if (!/^(?:[a-z\d]|[a-z\d](?:[a-z\d]|-(?=[a-z\d])){0,37}[a-z\d])$/i.test(normalizedUsername)) throw new GitHubProfileError('invalid_username') let timeout: ReturnType | undefined const signal = typeof AbortSignal.timeout === 'function' ? AbortSignal.timeout(10_000) : (() => { const controller = new AbortController() timeout = setTimeout(() => controller.abort(), 10_000) return controller.signal })() try { const response = await fetch(`https://api.github.com/users/${encodeURIComponent(normalizedUsername)}`, { headers: { accept: 'application/vnd.github+json', }, signal, }) if (response.status === 404) throw new GitHubProfileError('not_found') if (response.status === 429 || (response.status === 403 && (response.headers.get('x-ratelimit-remaining') === '0' || response.headers.has('retry-after')))) throw new GitHubProfileError('rate_limited') if (!response.ok) throw new GitHubProfileError('request_failed') const profile = await response.json() as { id?: unknown login?: unknown name?: unknown avatar_url?: unknown } if (!profile || typeof profile !== 'object' || typeof profile.id !== 'number' || typeof profile.login !== 'string' || typeof profile.avatar_url !== 'string') throw new GitHubProfileError('request_failed') return { id: profile.id, login: profile.login, name: typeof profile.name === 'string' ? profile.name : null, avatarUrl: profile.avatar_url, } } catch (error) { if (error instanceof GitHubProfileError) throw error throw new GitHubProfileError('request_failed') } finally { if (timeout) clearTimeout(timeout) } }