UNPKG

@coretext-ai/qa-google-contacts-a58500d5-8331-4ce9-a140-d204a9fae815

Version:
185 lines 7.18 kB
import { OAuth2Client } from 'google-auth-library'; import { TokenManager } from './token-manager.js'; import { CredentialLoader } from './credential-loader.js'; import open from 'open'; import http from 'http'; import url from 'url'; export class GoogleOAuthClient { constructor() { this.scopes = [ "https://www.googleapis.com/auth/contacts", "https://www.googleapis.com/auth/contacts.other.readonly", "https://www.googleapis.com/auth/contacts.readonly", "https://www.googleapis.com/auth/directory.readonly", "https://www.googleapis.com/auth/user.addresses.read", "https://www.googleapis.com/auth/user.emails.read", "https://www.googleapis.com/auth/user.phonenumbers.read", "https://www.googleapis.com/auth/userinfo.profile" ]; this.tokenManager = new TokenManager(); this.credentialLoader = new CredentialLoader(); } /** * Initialize OAuth client */ async initialize() { const credentialPath = process.env.GOOGLE_OAUTH_CREDENTIALS; if (!credentialPath) { throw new Error('GOOGLE_OAUTH_CREDENTIALS environment variable is required'); } const credentials = await this.credentialLoader.loadCredentials(credentialPath); this.oauth2Client = new OAuth2Client(credentials.client_id, credentials.client_secret, 'http://localhost:3000/oauth/callback'); console.error('[GOOGLE_OAUTH] OAuth client initialized'); } /** * Ensure we have a valid access token */ async getValidAccessToken() { const tokens = await this.tokenManager.getTokens(); if (!tokens) { console.error('[GOOGLE_OAUTH] No OAuth tokens found. Starting authorization flow...'); await this.authorize(); return this.getValidAccessToken(); } if (this.isTokenExpired(tokens)) { console.error('[GOOGLE_OAUTH] Token expired, refreshing...'); if (!tokens.refresh_token) { throw new Error('No refresh token available for token refresh'); } const refreshedTokens = await this.refreshTokens(tokens.refresh_token); if (!refreshedTokens.access_token) { throw new Error('Failed to refresh access token'); } return refreshedTokens.access_token; } if (!tokens.access_token) { throw new Error('No access token available'); } return tokens.access_token; } /** * Start OAuth authorization flow */ async authorize() { const authUrl = this.oauth2Client.generateAuthUrl({ access_type: 'offline', scope: this.scopes, prompt: 'consent' }); console.error('[GOOGLE_OAUTH] Starting OAuth authorization flow...'); console.error('[GOOGLE_OAUTH] Opening browser for authorization...'); console.error('[GOOGLE_OAUTH] If browser doesn\'t open, visit this URL:'); console.error(`[GOOGLE_OAUTH] ${authUrl}`); // Start callback server and open browser in parallel const [authCode] = await Promise.all([ this.startCallbackServer(), open(authUrl) ]); // Exchange the authorization code for tokens await this.exchangeCodeForTokens(authCode); } /** * Exchange authorization code for tokens */ async exchangeCodeForTokens(authCode) { const { tokens } = await this.oauth2Client.getToken(authCode); await this.tokenManager.storeTokens(tokens); console.error('[GOOGLE_OAUTH] Authorization successful! Tokens stored.'); } /** * Refresh expired access token */ async refreshTokens(refreshToken) { this.oauth2Client.setCredentials({ refresh_token: refreshToken }); const { credentials } = await this.oauth2Client.refreshAccessToken(); await this.tokenManager.storeTokens(credentials); return credentials; } /** * Check if token is expired (with 5-minute buffer) */ isTokenExpired(tokens) { const buffer = 5 * 60 * 1000; // 5 minutes const expiryTime = tokens.expiry_date || 0; return Date.now() + buffer >= expiryTime; } /** * Start a local HTTP server to handle OAuth callback */ async startCallbackServer() { return new Promise((resolve, reject) => { const server = http.createServer((req, res) => { const queryObject = url.parse(req.url, true).query; if (queryObject.code) { res.writeHead(200, { 'Content-Type': 'text/html' }); res.end(` <html> <body> <h1>Authorization successful!</h1> <p>You can close this window and return to your application.</p> </body> </html> `); server.close(); resolve(queryObject.code); } else if (queryObject.error) { res.writeHead(400, { 'Content-Type': 'text/html' }); res.end(` <html> <body> <h1>Authorization failed</h1> <p>Error: ${queryObject.error}</p> </body> </html> `); server.close(); reject(new Error(`OAuth authorization failed: ${queryObject.error}`)); } else { res.writeHead(400, { 'Content-Type': 'text/plain' }); res.end('Invalid callback request'); } }); // Listen on port 3000 (matching the redirect URI) server.listen(3000, () => { console.error('[GOOGLE_OAUTH] Callback server listening on http://localhost:3000'); }); // Timeout after 5 minutes setTimeout(() => { server.close(); reject(new Error('OAuth authorization timeout')); }, 5 * 60 * 1000); }); } /** * Revoke tokens and clear stored credentials */ async revokeTokens() { const tokens = await this.tokenManager.getTokens(); if (tokens?.access_token) { try { await this.oauth2Client.revokeToken(tokens.access_token); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); console.warn(`Failed to revoke token: ${errorMessage}`); } } await this.tokenManager.deleteTokens(); console.log('OAuth tokens revoked and deleted'); } /** * Check if user has valid authentication */ async isAuthenticated() { try { const tokens = await this.tokenManager.getTokens(); return !!tokens?.access_token && !this.isTokenExpired(tokens); } catch (error) { return false; } } } //# sourceMappingURL=google-oauth-client.js.map