import crypto from "node:crypto"; import type { Db } from "@paperclipai/db"; import { secretService } from "./secrets.js"; const GOOGLE_AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth"; const GOOGLE_TOKEN_URL = "https://oauth2.googleapis.com/token"; const GEMINI_CLIENT_ID = "812546505895-ag9nvbqvf8cpqk3mfem1glig0jtl5i31.apps.googleusercontent.com"; const GEMINI_SCOPES = "openid https://www.googleapis.com/auth/cloud-platform https://www.googleapis.com/auth/generative-language.retriever"; const GOOGLE_TOKEN_SECRET_NAME = "google_gemini_oauth_token"; export interface GoogleTokens { accessToken: string; refreshToken?: string; } export function googleOAuthService(db: Db) { const svc = secretService(db); function generatePkce(): { verifier: string; challenge: string } { const verifier = crypto.randomBytes(32).toString("base64url"); const challenge = crypto.createHash("sha256").update(verifier).digest("base64url"); return { verifier, challenge }; } function generateAuthUrl( redirectUri: string, state: string, ): { url: string; verifier: string } { const { verifier, challenge } = generatePkce(); const params = new URLSearchParams({ client_id: GEMINI_CLIENT_ID, redirect_uri: redirectUri, response_type: "code", scope: GEMINI_SCOPES, code_challenge: challenge, code_challenge_method: "S256", state, access_type: "offline", prompt: "consent", }); const url = `${GOOGLE_AUTH_URL}?${params.toString()}`; return { url, verifier }; } async function exchangeCode( code: string, redirectUri: string, verifier: string, ): Promise<{ accessToken: string; refreshToken?: string; expiresIn?: number }> { const body = new URLSearchParams({ grant_type: "authorization_code", client_id: GEMINI_CLIENT_ID, code, redirect_uri: redirectUri, code_verifier: verifier, }); const response = await fetch(GOOGLE_TOKEN_URL, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: body.toString(), }); if (!response.ok) { const errorText = await response.text(); throw new Error(`Google token exchange failed: ${response.status} ${errorText}`); } const data = (await response.json()) as { access_token: string; refresh_token?: string; expires_in?: number; }; return { accessToken: data.access_token, refreshToken: data.refresh_token, expiresIn: data.expires_in, }; } async function storeTokens(companyId: string, tokens: GoogleTokens): Promise { const value = JSON.stringify(tokens); const existing = await svc.getByName(companyId, GOOGLE_TOKEN_SECRET_NAME); if (existing) { await svc.rotate(existing.id, { value }); } else { await svc.create(companyId, { name: GOOGLE_TOKEN_SECRET_NAME, provider: "local_encrypted", value, description: "Google OAuth tokens for Gemini API access", }); } } async function resolveTokens(companyId: string): Promise { const secret = await svc.getByName(companyId, GOOGLE_TOKEN_SECRET_NAME); if (!secret) { throw new Error("Google OAuth tokens not found for this company"); } const value = await svc.resolveSecretValue(companyId, secret.id, "latest"); return JSON.parse(value) as GoogleTokens; } return { generatePkce, generateAuthUrl, exchangeCode, storeTokens, resolveTokens, }; }