/**
* Antigravity OAuth flow (Gemini 3, Claude, GPT-OSS via Google Cloud).
* Copied from oh-my-pi's google-antigravity + google-oauth-shared modules.
*/
import type { Server } from "node:http";
import type { OAuthCredentials, OAuthLoginCallbacks, OAuthProviderInterface } from "@earendil-works/pi-ai";
import { getAntigravityUserAgent } from "./wire.js";
const CLIENT_ID = atob(
"MTA3MTAwNjA2MDU5MS10bWhzc2luMmgyMWxjcmUyMzV2dG9sb2poNGc0MDNlcC5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbQ==",
);
const CLIENT_SECRET = atob("R09DU1BYLUs1OEZXUjQ4NkxkTEoxbUxCOHNYQzR6NnFEQWY=");
const CALLBACK_PORT = 51121;
const CALLBACK_PATH = "/oauth-callback";
const SCOPES = [
"https://www.googleapis.com/auth/cloud-platform",
"https://www.googleapis.com/auth/userinfo.email",
"https://www.googleapis.com/auth/userinfo.profile",
"https://www.googleapis.com/auth/cclog",
"https://www.googleapis.com/auth/experimentsandconfigs",
];
const AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
const TOKEN_URL = "https://oauth2.googleapis.com/token";
const CLOUD_CODE_ASSIST_ENDPOINT = "https://daily-cloudcode-pa.googleapis.com";
const LOAD_CODE_ASSIST_URL = `${CLOUD_CODE_ASSIST_ENDPOINT}/v1internal:loadCodeAssist`;
const ONBOARD_USER_URL = `${CLOUD_CODE_ASSIST_ENDPOINT}/v1internal:onboardUser`;
const OPERATIONS_URL = `${CLOUD_CODE_ASSIST_ENDPOINT}/v1internal`;
const FREE_TIER_ID = "free-tier";
const ONBOARD_TIMEOUT_MS = 30_000;
const ONBOARD_POLL_INTERVAL_MS = 1_000;
const REQUEST_TIMEOUT_MS = 30_000;
export const ANTIGRAVITY_LOAD_CODE_ASSIST_METADATA = Object.freeze({
ideType: "ANTIGRAVITY",
});
type AntigravityCredentials = OAuthCredentials & { projectId: string };
interface LoadCodeAssistResponse {
currentTier?: { id?: string } | null;
paidTier?: { id?: string } | null;
allowedTiers?: { id?: string }[];
ineligibleTiers?: { tierId?: string; reasonMessage?: string; validationUrl?: string }[];
cloudaicompanionProject?: string;
}
interface OnboardOperation {
name?: string;
done?: boolean;
error?: { code?: number; message?: string } | null;
response?: { "@type"?: string; cloudaicompanionProject?: string } | null;
}
/** Marker for provisioning/flow errors that should surface as-is rather than be wrapped. */
class OAuthError extends Error {
constructor(message: string) {
super(message);
this.name = "OAuthError";
}
}
/** Extract the Google "verify your account" URL from an error body, matching oh-my-pi. */
function extractGoogleValidationUrl(errorBody: string): string | undefined {
if (!errorBody.includes("VALIDATION_REQUIRED")) return undefined;
const start = errorBody.indexOf("{");
if (start === -1) return undefined;
try {
const parsed = JSON.parse(errorBody.slice(start)) as {
error?: { details?: Array<{ reason?: string; metadata?: { validation_url?: string } }> };
};
const detail = parsed.error?.details?.find(
(d) => d.reason === "VALIDATION_REQUIRED" && typeof d.metadata?.validation_url === "string",
);
return detail?.metadata?.validation_url;
} catch {
return undefined;
}
}
function formatGoogleValidationRequiredMessage(validationUrl: string, nextAction: string, email?: string): string {
const account = email ? ` for ${email}` : "";
return `Account verification required${account}. Visit ${validationUrl} to continue, then ${nextAction}.`;
}
function throwIfLoginCancelled(signal: AbortSignal | undefined): void {
if (signal?.aborted) {
throw new Error("Login cancelled");
}
}
async function oauthFetch(
url: string,
init: RequestInit,
signal: AbortSignal | undefined,
timeoutMs = REQUEST_TIMEOUT_MS,
): Promise<Response> {
const timeoutSignal = AbortSignal.timeout(timeoutMs);
const requestSignal = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
try {
return await fetch(url, { ...init, signal: requestSignal });
} catch (err) {
if (signal?.aborted) {
throw new Error("Login cancelled");
}
if (timeoutSignal.aborted) {
throw new OAuthError(`Timed out after ${timeoutMs}ms waiting for ${url}`);
}
throw err;
}
}
function parseLoadCodeAssistResponse(payload: unknown): LoadCodeAssistResponse {
if (typeof payload !== "object" || payload === null) {
throw new OAuthError("failed to unmarshal LoadCodeAssistResponse");
}
return payload as LoadCodeAssistResponse;
}
function parseOnboardOperation(payload: unknown): OnboardOperation {
if (typeof payload !== "object" || payload === null) {
throw new OAuthError("failed to unmarshal OnboardUser operation");
}
return payload as OnboardOperation;
}
function extractProjectId(payload: LoadCodeAssistResponse): string | undefined {
const projectId = payload.cloudaicompanionProject;
return projectId && projectId.length > 0 ? projectId : undefined;
}
function hasMessageField(payload: LoadCodeAssistResponse, field: "currentTier" | "paidTier"): boolean {
return payload[field] !== undefined && payload[field] !== null;
}
function isFreeTierAllowed(payload: LoadCodeAssistResponse): boolean {
return payload.allowedTiers?.some((tier) => tier.id === FREE_TIER_ID) === true;
}
function getFreeTierIneligibility(
payload: LoadCodeAssistResponse,
): { reasonMessage: string; validationUrl: string | undefined } | undefined {
const tier = payload.ineligibleTiers?.find((candidate) => candidate.tierId === FREE_TIER_ID);
if (!tier?.reasonMessage) return undefined;
return {
reasonMessage: tier.reasonMessage,
validationUrl: tier.validationUrl && tier.validationUrl.length > 0 ? tier.validationUrl : undefined,
};
}
function assertFreeTierEligible(payload: LoadCodeAssistResponse): void {
if (isFreeTierAllowed(payload)) return;
const ineligibility = getFreeTierIneligibility(payload);
if (!ineligibility) return;
const validation = ineligibility.validationUrl ? `\n${ineligibility.validationUrl}` : "";
throw new OAuthError(`${ineligibility.reasonMessage}${validation}`);
}
async function requestCloudCodeAssist({
label,
url,
init,
signal,
timeoutMs,
}: {
label: string;
url: string;
init: RequestInit;
signal?: AbortSignal;
timeoutMs?: number;
}): Promise<unknown> {
throwIfLoginCancelled(signal);
const response = await oauthFetch(url, init, signal, timeoutMs);
if (response.status !== 200) {
const errorText = await response.text();
throw new OAuthError(`${label} failed: ${response.status} ${response.statusText}: ${errorText}`);
}
return response.json();
}
function cloudCodeAssistHeaders(accessToken: string): Record<string, string> {
return {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json",
"User-Agent": getAntigravityUserAgent(),
};
}
async function postLoadCodeAssist(
accessToken: string,
body: Record<string, unknown>,
signal?: AbortSignal,
): Promise<LoadCodeAssistResponse> {
const payload = await requestCloudCodeAssist({
label: "loadCodeAssist",
url: LOAD_CODE_ASSIST_URL,
init: {
method: "POST",
headers: cloudCodeAssistHeaders(accessToken),
body: JSON.stringify(body),
},
signal,
});
return parseLoadCodeAssistResponse(payload);
}
async function loadCodeAssist(accessToken: string, signal?: AbortSignal): Promise<LoadCodeAssistResponse> {
let payload = await postLoadCodeAssist(accessToken, { metadata: ANTIGRAVITY_LOAD_CODE_ASSIST_METADATA }, signal);
const projectId = extractProjectId(payload);
if (!hasMessageField(payload, "paidTier") && projectId) {
payload = await postLoadCodeAssist(
accessToken,
{ cloudaicompanionProject: projectId, metadata: ANTIGRAVITY_LOAD_CODE_ASSIST_METADATA },
signal,
);
}
return payload;
}
function remainingOnboardTime(deadline: number): number {
const remaining = deadline - Date.now();
if (remaining > 0) return remaining;
throw new OAuthError(`onboardUser timed out after ${ONBOARD_TIMEOUT_MS}ms`);
}
function describeOperationError(error: OnboardOperation["error"]): string {
if (error?.message) {
return typeof error.code === "number" ? `${error.code}: ${error.message}` : error.message;
}
return JSON.stringify(error) ?? String(error);
}
async function onboardUser(accessToken: string, signal?: AbortSignal): Promise<void> {
const deadline = Date.now() + ONBOARD_TIMEOUT_MS;
let operation = parseOnboardOperation(
await requestCloudCodeAssist({
label: "onboardUser",
url: ONBOARD_USER_URL,
init: {
method: "POST",
headers: cloudCodeAssistHeaders(accessToken),
body: JSON.stringify({ tierId: FREE_TIER_ID, metadata: ANTIGRAVITY_LOAD_CODE_ASSIST_METADATA }),
},
signal,
timeoutMs: remainingOnboardTime(deadline),
}),
);
while (true) {
if (operation.done === true) {
if (operation.error !== undefined && operation.error !== null) {
throw new OAuthError(`OnboardUser operation failed: ${describeOperationError(operation.error)}`);
}
if (operation.response === undefined || operation.response === null) {
throw new OAuthError("failed to unmarshal OnboardUserResponse");
}
return;
}
const waitMs = Math.min(ONBOARD_POLL_INTERVAL_MS, remainingOnboardTime(deadline));
await new Promise((resolve) => setTimeout(resolve, waitMs));
throwIfLoginCancelled(signal);
const operationName = operation.name ?? "";
if (operationName.length === 0) {
throw new OAuthError("onboardUser returned an operation without a name");
}
operation = parseOnboardOperation(
await requestCloudCodeAssist({
label: "onboardUser operation",
url: `${OPERATIONS_URL}/${operationName}`,
init: { method: "GET", headers: cloudCodeAssistHeaders(accessToken) },
signal,
timeoutMs: remainingOnboardTime(deadline),
}),
);
}
}
async function discoverProject(
accessToken: string,
onProgress?: (message: string) => void,
signal?: AbortSignal,
): Promise<string> {
onProgress?.("Checking Cloud Code Assist account status...");
try {
const initial = await loadCodeAssist(accessToken, signal);
assertFreeTierEligible(initial);
if (!hasMessageField(initial, "currentTier")) {
onProgress?.("Provisioning the Antigravity free tier...");
await onboardUser(accessToken, signal);
}
onProgress?.("Refreshing Cloud Code Assist project...");
const refreshed = await loadCodeAssist(accessToken, signal);
const projectId = extractProjectId(refreshed);
if (projectId) return projectId;
throw new OAuthError("loadCodeAssist did not return a cloudaicompanionProject");
} catch (error) {
throwIfLoginCancelled(signal);
if (error instanceof OAuthError || (error instanceof Error && error.message === "Login cancelled")) {
throw error;
}
throw new Error(
`Could not discover an Antigravity project. ${error instanceof Error ? error.message : String(error)}`,
);
}
}
async function getUserEmail(accessToken: string, signal?: AbortSignal): Promise<string | undefined> {
try {
const response = await oauthFetch(
"https://www.googleapis.com/oauth2/v1/userinfo?alt=json",
{ headers: { Authorization: `Bearer ${accessToken}` } },
signal,
);
if (response.ok) {
const data = (await response.json()) as { email?: string };
return data.email;
}
} catch {
// Email is optional.
}
return undefined;
}
export async function refreshAntigravityToken(refreshToken: string, projectId: string): Promise<OAuthCredentials> {
const response = await fetch(TOKEN_URL, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET,
refresh_token: refreshToken,
grant_type: "refresh_token",
}),
});
if (!response.ok) {
const error = await response.text();
throw new Error(`Antigravity token refresh failed: ${error}`);
}
const data = (await response.json()) as {
access_token: string;
expires_in: number;
refresh_token?: string;
};
return {
refresh: data.refresh_token || refreshToken,
access: data.access_token,
expires: Date.now() + data.expires_in * 1000 - 5 * 60 * 1000,
projectId,
};
}
const OAUTH_SUCCESS_HTML = `<!doctype html><html><head><meta charset="utf-8"><title>Antigravity</title></head>
<body style="font-family: system-ui, sans-serif; padding: 40px;">
<h2>Authentication complete</h2><p>You can close this window and return to Prime.</p>
</body></html>`;
const OAUTH_ERROR_HTML = (
message: string,
) => `<!doctype html><html><head><meta charset="utf-8"><title>Antigravity</title></head>
<body style="font-family: system-ui, sans-serif; padding: 40px;">
<h2>Authentication failed</h2><p>${message.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">")}</p>
</body></html>`;
interface CallbackResult {
code: string;
state: string;
}
type CallbackOutcome = CallbackResult | { error: string } | null;
const CALLBACK_WAIT_TIMEOUT_MS = 300_000;
async function startCallbackServer(
expectedState: string,
signal?: AbortSignal,
): Promise<{
redirectUri: string;
server: Server;
waitForCallback: Promise<CallbackOutcome>;
cancelWait: () => void;
}> {
const { createServer } = await import("node:http");
const host = "127.0.0.1";
const bind = (port: number): Promise<Server> =>
new Promise((resolve, reject) => {
const server = createServer();
server.once("error", reject);
server.listen(port, host, () => {
server.removeListener("error", reject);
resolve(server);
});
});
let server: Server;
let port: number;
try {
server = await bind(CALLBACK_PORT);
port = CALLBACK_PORT;
} catch (error) {
const code = (error as { code?: string })?.code;
if (code !== "EADDRINUSE") throw error;
server = await bind(0);
const address = server.address();
if (address === null || typeof address === "string") throw new Error("Failed to bind OAuth callback server");
port = address.port;
}
let settleWait: ((value: CallbackOutcome) => void) | undefined;
const waitForCallback = new Promise<CallbackOutcome>((resolve) => {
let settled = false;
settleWait = (value) => {
if (settled) return;
settled = true;
resolve(value);
};
});
server.on("request", (req, res) => {
const url = new URL(req.url || "", `http://${host}:${port}`);
if (url.pathname !== CALLBACK_PATH) {
res.writeHead(404, { "Content-Type": "text/html; charset=utf-8" });
res.end(OAUTH_ERROR_HTML("Callback route not found."));
return;
}
const code = url.searchParams.get("code");
const state = url.searchParams.get("state") ?? "";
const error = url.searchParams.get("error") ?? "";
const errorDescription = url.searchParams.get("error_description") ?? error;
if (error) {
res.writeHead(500, { "Content-Type": "text/html; charset=utf-8" });
res.end(OAUTH_ERROR_HTML(`Authorization failed: ${errorDescription}`));
// The redirect carries our state nonce, so it came from the genuine
// authorization flow (e.g. the user denied consent). Surface it now
// instead of leaving the login waiting for the callback timeout.
if (!expectedState || state === expectedState) {
settleWait?.({ error: `Authorization failed: ${errorDescription}` });
}
return;
}
if (!code) {
res.writeHead(500, { "Content-Type": "text/html; charset=utf-8" });
res.end(OAUTH_ERROR_HTML("Missing authorization code"));
return;
}
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
res.end(OAUTH_SUCCESS_HTML);
settleWait?.({ code, state });
});
const cancelWait = () => settleWait?.(null);
const timeout = setTimeout(() => {
settleWait?.({ error: "OAuth login timed out waiting for browser authentication" });
server.close();
}, CALLBACK_WAIT_TIMEOUT_MS);
timeout.unref();
if (signal) {
signal.addEventListener(
"abort",
() => {
clearTimeout(timeout);
cancelWait();
server.close();
},
{ once: true },
);
}
return { redirectUri: `http://${host}:${port}${CALLBACK_PATH}`, server, waitForCallback, cancelWait };
}
function generateState(): string {
const bytes = new Uint8Array(16);
crypto.getRandomValues(bytes);
return Array.from(bytes)
.map((value) => value.toString(16).padStart(2, "0"))
.join("");
}
function parseCallbackInput(input: string): { code?: string; state?: string } {
const value = input.trim();
if (!value) return {};
try {
const url = new URL(value);
return { code: url.searchParams.get("code") ?? undefined, state: url.searchParams.get("state") ?? undefined };
} catch {
// not a URL
}
if (value.includes("code=")) {
const params = new URLSearchParams(value.replace(/^[?#]/, ""));
return { code: params.get("code") ?? undefined, state: params.get("state") ?? undefined };
}
const [code, state] = value.split("#", 2);
return { code, state };
}
async function loginAntigravity(callbacks: OAuthLoginCallbacks): Promise<OAuthCredentials> {
const signal = callbacks.signal;
const state = generateState();
throwIfLoginCancelled(signal);
callbacks.onProgress?.("Starting local server for OAuth callback...");
const { redirectUri, server, waitForCallback, cancelWait } = await startCallbackServer(state, signal);
try {
throwIfLoginCancelled(signal);
const authParams = new URLSearchParams({
client_id: CLIENT_ID,
response_type: "code",
redirect_uri: redirectUri,
scope: SCOPES.join(" "),
state,
access_type: "offline",
prompt: "consent",
});
const authUrl = `${AUTH_URL}?${authParams.toString()}`;
callbacks.onAuth({ url: authUrl, instructions: "Complete the sign-in in your browser." });
callbacks.onProgress?.("Waiting for browser authentication...");
let code: string | undefined;
const manualInput = callbacks.onManualCodeInput;
if (manualInput) {
let manualResult: CallbackResult | undefined;
void manualInput()
.then((input) => {
const parsed = parseCallbackInput(input);
if (parsed.code) {
manualResult = { code: parsed.code, state: parsed.state ?? "" };
}
cancelWait();
})
.catch(() => {
cancelWait();
});
const result = await waitForCallback;
if (result && "error" in result) {
throw new OAuthError(result.error);
}
if (result?.code) {
if (result.state !== state) throw new OAuthError("OAuth state mismatch - possible CSRF attack");
code = result.code;
} else if (manualResult?.code) {
if (manualResult.state && manualResult.state !== state) {
throw new OAuthError("OAuth state mismatch - possible CSRF attack");
}
code = manualResult.code;
}
} else {
const result = await waitForCallback;
if (result && "error" in result) {
throw new OAuthError(result.error);
}
if (result?.code) {
if (result.state !== state) throw new OAuthError("OAuth state mismatch - possible CSRF attack");
code = result.code;
}
}
if (!code) {
throwIfLoginCancelled(signal);
throw new Error("No authorization code received");
}
callbacks.onProgress?.("Exchanging authorization code for tokens...");
const tokenResponse = await oauthFetch(
TOKEN_URL,
{
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET,
code,
grant_type: "authorization_code",
redirect_uri: redirectUri,
}),
},
signal,
);
if (!tokenResponse.ok) {
const error = await tokenResponse.text();
throw new Error(`Token exchange failed: ${error}`);
}
const tokenData = (await tokenResponse.json()) as {
access_token: string;
refresh_token: string;
expires_in: number;
};
if (!tokenData.refresh_token) {
throw new Error("No refresh token received. Please try again.");
}
throwIfLoginCancelled(signal);
callbacks.onProgress?.("Getting user info...");
const email = await getUserEmail(tokenData.access_token, signal);
let projectId: string;
try {
projectId = await discoverProject(tokenData.access_token, callbacks.onProgress, signal);
} catch (error) {
const validationUrl = extractGoogleValidationUrl(error instanceof Error ? error.message : String(error));
if (!validationUrl) throw error;
throw new OAuthError(formatGoogleValidationRequiredMessage(validationUrl, "sign in again", email));
}
return {
refresh: tokenData.refresh_token,
access: tokenData.access_token,
expires: Date.now() + tokenData.expires_in * 1000 - 5 * 60 * 1000,
projectId,
email,
};
} finally {
server.close();
}
}
export const antigravityOAuth: Omit<OAuthProviderInterface, "id"> = {
name: "Antigravity (Gemini 3, Claude, GPT-OSS)",
usesCallbackServer: true,
async login(callbacks: OAuthLoginCallbacks): Promise<OAuthCredentials> {
return loginAntigravity(callbacks);
},
async refreshToken(credentials: OAuthCredentials): Promise<OAuthCredentials> {
const creds = credentials as AntigravityCredentials;
if (!creds.projectId) {
throw new Error("Antigravity credentials missing projectId");
}
return refreshAntigravityToken(creds.refresh, creds.projectId);
},
getApiKey(credentials: OAuthCredentials): string {
const creds = credentials as AntigravityCredentials;
return JSON.stringify({ token: creds.access, projectId: creds.projectId, email: creds.email });
},
};