import { google } from 'googleapis' /** * @param {{ clientId: string, clientSecret: string|null }} config * @returns {import('@drama/auth').OauthPort} */ export function createGoogleOauthPort({ clientId, clientSecret }) { if (!clientId) throw new Error('createGoogleOauthPort: clientId is required') const client = new google.auth.OAuth2(clientId, clientSecret || undefined) return { async exchange({ provider, code, oauth_flow, redirect_uri }) { if (provider !== 'google') throw new Error(`Unsupported OAuth provider: ${provider}`) if (oauth_flow === 'authorization_code') { if (!clientSecret) throw new Error('Google OAuth authorization code flow requires GOOGLE_OAUTH_CLIENT_SECRET') if (!redirect_uri) throw new Error('Google OAuth authorization code flow requires redirect_uri') const { tokens } = await client.getToken({ code, redirect_uri }) if (!tokens.id_token) throw new Error('Google OAuth token response is missing id_token') const ticket = await client.verifyIdToken({ idToken: tokens.id_token, audience: clientId }) const payload = ticket.getPayload() if (!payload?.sub || !payload.email) throw new Error('Google ID token is missing account identity') return { provider_user_id: payload.sub, email: payload.email, email_verified: payload.email_verified === true, } } if (oauth_flow !== 'access_token') throw new Error(`Unsupported Google OAuth flow: ${oauth_flow}`) const tokenInfo = await client.getTokenInfo(code) if (tokenInfo.aud !== clientId) throw new Error('Google access token audience does not match client ID') const response = await fetch('https://openidconnect.googleapis.com/v1/userinfo', { headers: { authorization: `Bearer ${code}` }, }) if (!response.ok) throw new Error(`Google userinfo request failed: HTTP ${response.status}`) const payload = await response.json() if (!payload.sub || !payload.email) throw new Error('Google userinfo is missing account identity') return { provider_user_id: payload.sub, email: payload.email, email_verified: payload.email_verified === true, } }, } }