type KnownProvider = 'github' | 'gitlab' | 'stackoverflow'; const env = (key: string): string | undefined => { // Use globalThis to avoid hard dependency on Node 'process' types/environ try { const g = globalThis as unknown as { process?: { env?: Record } }; return g?.process?.env?.[key]; } catch { return undefined; } }; export type OAuthClientIds = Partial>; const DEFAULT_SCOPES: Record = { github: 'read:user,repo', gitlab: 'read_api read_user read_repository openid profile email', stackoverflow: '', }; export function buildOAuthUrl(params: { providerId: KnownProvider | string; redirectUri: string; state?: string; scopeOverride?: string; clientIds?: OAuthClientIds; }): string { const id = (params.providerId || '').toLowerCase() as KnownProvider; const clientId = (params.clientIds && params.clientIds[id]) || env( id === 'github' ? 'NEXT_PUBLIC_GITHUB_CLIENT_ID' : id === 'gitlab' ? 'NEXT_PUBLIC_GITLAB_CLIENT_ID' : 'NEXT_PUBLIC_STACKOVERFLOW_CLIENT_ID' ); // clientId should be inlined at build-time for NEXT_PUBLIC_* const scope = params.scopeOverride ?? DEFAULT_SCOPES[id as KnownProvider] ?? ''; const redirect = encodeURIComponent(params.redirectUri); const state = params.state ? `&state=${encodeURIComponent(params.state)}` : ''; if (id === 'github') { return `https://github.com/login/oauth/authorize?client_id=${clientId}&redirect_uri=${redirect}${scope ? `&scope=${encodeURIComponent(scope)}` : ''}${state}`; } if (id === 'gitlab') { return `https://gitlab.com/oauth/authorize?client_id=${clientId}&redirect_uri=${redirect}&response_type=code&scope=${encodeURIComponent(scope)}${state}`; } if (id === 'stackoverflow') { return `https://stackoverflow.com/oauth?client_id=${clientId}&redirect_uri=${redirect}${scope ? `&scope=${encodeURIComponent(scope)}` : ''}${state}`; } return ''; }