Files
openclaw/extensions/google/oauth.ts
Peter Steinberger 8310c565e0 feat(onboarding): add provider sign-in (#104502)
* feat(onboarding): add provider sign-in flows

* fix(oauth): keep callback compatibility

* fix(onboarding): reconcile lost auth outcomes

* fix(onboarding): lock auth cancellation at commit

* fix(onboarding): close provider auth lifecycle gaps

* fix(onboarding): make terminal auth failures dismissable

* fix(onboarding): satisfy native app checks

* fix(onboarding): reconcile absent auth sessions

* fix(onboarding): bound provider auth sessions

* fix(onboarding): open provider auth links safely

* test(onboarding): use scanner-safe auth fixtures

* revert: keep established onboarding auth fixtures

* fix(onboarding): close provider auth cancellation gaps

* fix(gateway): retain uncollected wizard results

* fix(onboarding): bind provider reconciliation attempt

* fix(i18n): avoid guessing moved string identities

* style(onboarding): normalize remote auth choices efficiently

* fix(protocol): refresh optional provider auth choices

* test(gateway): cover provider auth dispatch order

* refactor(macos): split onboarding setup support

* fix(macos): refresh merged native checks
2026-07-11 13:00:17 -07:00

110 lines
3.8 KiB
TypeScript

// Google plugin module implements oauth behavior.
import type { OAuthCredential } from "openclaw/plugin-sdk/provider-auth";
import { clearCredentialsCache, extractGeminiCliCredentials } from "./oauth.credentials.js";
import {
buildAuthUrl,
generateOAuthState,
generatePkce,
parseCallbackInput,
shouldUseManualOAuthFlow,
waitForLocalCallback,
} from "./oauth.flow.js";
import type { GeminiCliOAuthContext, GeminiCliOAuthCredentials } from "./oauth.shared.js";
import { exchangeCodeForTokens, refreshTokensForGeminiCli } from "./oauth.token.js";
export { clearCredentialsCache, extractGeminiCliCredentials };
export type { GeminiCliOAuthContext, GeminiCliOAuthCredentials };
export async function loginGeminiCliOAuth(
ctx: GeminiCliOAuthContext,
): Promise<GeminiCliOAuthCredentials> {
const needsManual = shouldUseManualOAuthFlow(ctx.isRemote);
await ctx.note(
needsManual
? [
"You are running in a remote/VPS environment.",
"A URL will be shown for you to open in your LOCAL browser.",
"After signing in, copy the redirect URL and paste it back here.",
].join("\n")
: [
"Browser will open for Google authentication.",
"Sign in with your Google account for Gemini CLI access.",
"The callback will be captured automatically on localhost:8085.",
].join("\n"),
"Gemini CLI OAuth",
);
const { verifier, challenge } = generatePkce();
const state = generateOAuthState();
const authUrl = buildAuthUrl(challenge, state);
if (needsManual) {
return manualFlow(ctx, authUrl, state, verifier);
}
ctx.progress.update("Complete sign-in in browser...");
ctx.log(`\nOpen this URL in your browser:\n\n${authUrl}\n`);
try {
await ctx.openUrl(authUrl);
} catch {
// The URL is already visible; browser launch is best-effort.
}
try {
const { code } = await waitForLocalCallback({
expectedState: state,
timeoutMs: 5 * 60 * 1000,
onProgress: (msg) => ctx.progress.update(msg),
...(ctx.signal ? { signal: ctx.signal } : {}),
});
ctx.progress.update("Exchanging authorization code for tokens...");
return await exchangeCodeForTokens(code, verifier, ctx.signal);
} catch (err) {
if (
err instanceof Error &&
(err.message.includes("EADDRINUSE") ||
err.message.includes("port") ||
err.message.includes("listen"))
) {
ctx.progress.update("Local callback server failed. Switching to manual mode...");
return manualFlow(ctx, authUrl, state, verifier, err);
}
throw err;
}
}
async function manualFlow(
ctx: GeminiCliOAuthContext,
authUrl: string,
state: string,
verifier: string,
cause?: Error,
): Promise<GeminiCliOAuthCredentials> {
ctx.progress.update("OAuth URL ready");
ctx.log(`\nOpen this URL in your LOCAL browser:\n\n${authUrl}\n`);
await ctx.openUrl(authUrl);
await ctx.note(`Open this URL in your LOCAL browser:\n\n${authUrl}`, "Gemini CLI OAuth");
ctx.progress.update("Waiting for you to paste the callback URL...");
const callbackInput = await ctx.prompt("Paste the redirect URL here: ");
const parsed = parseCallbackInput(callbackInput);
if ("error" in parsed) {
throw new Error(parsed.error, cause ? { cause } : undefined);
}
if (parsed.state !== state) {
throw new Error("OAuth state mismatch - please try again", cause ? { cause } : undefined);
}
ctx.progress.update("Exchanging authorization code for tokens...");
return exchangeCodeForTokens(parsed.code, verifier, ctx.signal);
}
export async function refreshGeminiCliOAuthToken(
credentials: Pick<GeminiCliOAuthCredentials, "refresh" | "email" | "projectId">,
): Promise<OAuthCredential> {
const refreshed = await refreshTokensForGeminiCli(credentials);
return {
type: "oauth",
provider: "google-gemini-cli",
...refreshed,
};
}