mirror of
https://wget.la/https://github.com/leookun/cursor-byok
synced 2026-10-10 10:18:30 +08:00
Merge main and host authorization-code OAuth in core
This commit is contained in:
@@ -1,38 +1,24 @@
|
||||
import type { JsonValue, PluginContext } from "cursor-byok:plugin";
|
||||
import type { OAuth2AddMethod, OAuth2Begin, OAuth2Poll } from "cursor-byok:resource";
|
||||
import type {
|
||||
OAuth2AuthorizationCodeAddMethod,
|
||||
OAuth2AuthorizationCodeBegin,
|
||||
ResourceDraft,
|
||||
} from "cursor-byok:resource";
|
||||
import { credentialDraft, queryAccountQuota } from "./resources.ts";
|
||||
|
||||
/**
|
||||
* Official Google Antigravity OAuth Client credentials.
|
||||
*/
|
||||
const _P1 = "1071006060591";
|
||||
const _P2 = "tmhssin2h21lcre235vtolojh4g403ep";
|
||||
const _P3 = "apps.googleusercontent.com";
|
||||
export const CLIENT_ID = [_P1, _P2, _P3].join("-").replace("-apps", ".apps");
|
||||
|
||||
const _S1 = "GOCSPX";
|
||||
const _S2 = "K58FWR486LdLJ1mLB8sXC4z6qDAf";
|
||||
export const CLIENT_SECRET = [_S1, _S2].join("-");
|
||||
import {
|
||||
CLIENT_ID,
|
||||
CLIENT_SECRET,
|
||||
GOOGLE_AUTHORIZATION_URL,
|
||||
GOOGLE_TOKEN_URL,
|
||||
SCOPES,
|
||||
} from "./google_oauth.ts";
|
||||
|
||||
export const CALLBACK_PORT = 51121;
|
||||
export const CALLBACK_PATH = "/oauth-callback";
|
||||
export const REDIRECT_URI = `http://127.0.0.1:${CALLBACK_PORT}${CALLBACK_PATH}`;
|
||||
|
||||
export 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 AUTHORIZATION_LIFETIME_MS = 5 * 60 * 1000;
|
||||
|
||||
const AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
|
||||
const TOKEN_URL = "https://oauth2.googleapis.com/token";
|
||||
|
||||
type Session = {
|
||||
state: string;
|
||||
createdAt: number;
|
||||
};
|
||||
type Session = { createdAtMs: number };
|
||||
|
||||
function object(value: unknown): Record<string, unknown> | null {
|
||||
return value !== null && typeof value === "object" && !Array.isArray(value)
|
||||
@@ -54,146 +40,118 @@ function parseBody(body: string): Record<string, unknown> {
|
||||
|
||||
function parseSession(value: JsonValue): Session {
|
||||
const session = object(value);
|
||||
const state = text(session?.state);
|
||||
const createdAt = typeof session?.createdAt === "number" ? session.createdAt : Date.now();
|
||||
if (!state) throw new Error("Antigravity OAuth session is invalid");
|
||||
return { state, createdAt };
|
||||
const createdAtMs = session?.createdAtMs;
|
||||
if (typeof createdAtMs !== "number") throw new Error("Antigravity OAuth session is invalid");
|
||||
if (Date.now() - createdAtMs > AUTHORIZATION_LIFETIME_MS) {
|
||||
throw new Error("Google authorization expired. Please try again.");
|
||||
}
|
||||
return { createdAtMs };
|
||||
}
|
||||
|
||||
function randomState(): string {
|
||||
const array = new Uint8Array(24);
|
||||
crypto.getRandomValues(array);
|
||||
return Array.from(array, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
||||
}
|
||||
|
||||
async function begin(_context: PluginContext): Promise<OAuth2Begin> {
|
||||
const state = randomState();
|
||||
async function begin(
|
||||
input: { redirectUri: string; state: string; codeChallenge: string },
|
||||
_context: PluginContext,
|
||||
): Promise<OAuth2AuthorizationCodeBegin> {
|
||||
const authParams = new URLSearchParams({
|
||||
client_id: CLIENT_ID,
|
||||
response_type: "code",
|
||||
redirect_uri: REDIRECT_URI,
|
||||
redirect_uri: input.redirectUri,
|
||||
scope: SCOPES.join(" "),
|
||||
state,
|
||||
state: input.state,
|
||||
code_challenge: input.codeChallenge,
|
||||
code_challenge_method: "S256",
|
||||
access_type: "offline",
|
||||
prompt: "consent",
|
||||
});
|
||||
const verificationUrl = `${AUTH_URL}?${authParams.toString()}`;
|
||||
|
||||
const session: Session = { state, createdAt: Date.now() };
|
||||
return {
|
||||
session: session as unknown as JsonValue,
|
||||
userCode: "Google Sign-in",
|
||||
verificationUrl,
|
||||
verificationUrlComplete: verificationUrl,
|
||||
expiresAtMs: Date.now() + 300 * 1000,
|
||||
pollIntervalMs: 1500,
|
||||
session: { createdAtMs: Date.now() },
|
||||
authorizationUrl: `${GOOGLE_AUTHORIZATION_URL}?${authParams.toString()}`,
|
||||
expiresAtMs: Date.now() + AUTHORIZATION_LIFETIME_MS,
|
||||
};
|
||||
}
|
||||
|
||||
async function poll(sessionValue: JsonValue, context: PluginContext): Promise<OAuth2Poll> {
|
||||
const session = parseSession(sessionValue);
|
||||
|
||||
// Check if authorization timed out
|
||||
if (Date.now() - session.createdAt > 300 * 1000) {
|
||||
return { status: "failed", message: "Sign-in timed out. Please try again." };
|
||||
async function complete(
|
||||
sessionValue: JsonValue,
|
||||
input: { code: string; redirectUri: string; codeVerifier: string },
|
||||
context: PluginContext,
|
||||
): Promise<ResourceDraft[]> {
|
||||
parseSession(sessionValue);
|
||||
const tokenResponse = await context.network.fetch(GOOGLE_TOKEN_URL, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
accept: "application/json",
|
||||
"content-type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
client_id: CLIENT_ID,
|
||||
client_secret: CLIENT_SECRET,
|
||||
code: input.code,
|
||||
code_verifier: input.codeVerifier,
|
||||
grant_type: "authorization_code",
|
||||
redirect_uri: input.redirectUri,
|
||||
}).toString(),
|
||||
});
|
||||
const tokenBody = parseBody(tokenResponse.body);
|
||||
if (tokenResponse.status < 200 || tokenResponse.status >= 300) {
|
||||
const detail = text(tokenBody.error_description) ?? text(tokenBody.error) ??
|
||||
`HTTP ${tokenResponse.status}`;
|
||||
throw new Error(`Google token exchange failed: ${detail}`);
|
||||
}
|
||||
|
||||
// Attempt to check if local callback server on 51121 received the auth code
|
||||
const accessToken = text(tokenBody.access_token);
|
||||
if (!accessToken) throw new Error("Google token response did not include an access token");
|
||||
|
||||
let displayName = text(tokenBody.email);
|
||||
try {
|
||||
const callbackCheck = await context.network.fetch(
|
||||
`http://127.0.0.1:${CALLBACK_PORT}/auth-status?state=${session.state}`,
|
||||
{ method: "GET" },
|
||||
const userInfoResponse = await context.network.fetch(
|
||||
"https://www.googleapis.com/oauth2/v1/userinfo?alt=json",
|
||||
{
|
||||
method: "GET",
|
||||
headers: { authorization: `Bearer ${accessToken}`, accept: "application/json" },
|
||||
},
|
||||
);
|
||||
if (callbackCheck.status === 200) {
|
||||
const body = parseBody(callbackCheck.body);
|
||||
const code = text(body.code);
|
||||
if (code) {
|
||||
// Exchange code for tokens
|
||||
const tokenResponse = await context.network.fetch(TOKEN_URL, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
accept: "application/json",
|
||||
"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: REDIRECT_URI,
|
||||
}).toString(),
|
||||
});
|
||||
const tokenBody = parseBody(tokenResponse.body);
|
||||
if (tokenResponse.status >= 200 && tokenResponse.status < 300) {
|
||||
const accessToken = text(tokenBody.access_token);
|
||||
if (accessToken) {
|
||||
let email: string | null = text(tokenBody.email);
|
||||
try {
|
||||
const userInfoRes = await context.network.fetch(
|
||||
"https://www.googleapis.com/oauth2/v1/userinfo?alt=json",
|
||||
{
|
||||
method: "GET",
|
||||
headers: {
|
||||
authorization: `Bearer ${accessToken}`,
|
||||
accept: "application/json",
|
||||
},
|
||||
},
|
||||
);
|
||||
if (userInfoRes.status === 200) {
|
||||
const userInfo = parseBody(userInfoRes.body);
|
||||
email = text(userInfo.email) ?? email;
|
||||
}
|
||||
} catch {
|
||||
// Ignore error, fallback to default display name
|
||||
}
|
||||
|
||||
let projectId = "bamboo-precept-lgxtn";
|
||||
let quota = null;
|
||||
try {
|
||||
const res = await queryAccountQuota(accessToken, context.network);
|
||||
projectId = res.projectId;
|
||||
quota = res.quota;
|
||||
} catch {
|
||||
// Ignore error
|
||||
}
|
||||
|
||||
return {
|
||||
status: "completed",
|
||||
resources: [
|
||||
await credentialDraft({
|
||||
accessToken,
|
||||
refreshToken: text(tokenBody.refresh_token),
|
||||
displayName: email ?? "Google Antigravity",
|
||||
projectId,
|
||||
quota,
|
||||
}),
|
||||
],
|
||||
};
|
||||
}
|
||||
} else {
|
||||
const errMsg = text(tokenBody.error_description ?? tokenBody.error) ?? `HTTP ${tokenResponse.status}`;
|
||||
return { status: "failed", message: `Token exchange failed: ${errMsg}` };
|
||||
}
|
||||
}
|
||||
if (userInfoResponse.status >= 200 && userInfoResponse.status < 300) {
|
||||
displayName = text(parseBody(userInfoResponse.body).email) ?? displayName;
|
||||
}
|
||||
} catch {
|
||||
// Network retry on pending callback
|
||||
// Account identity has a token fingerprint fallback.
|
||||
}
|
||||
|
||||
return { status: "pending" };
|
||||
let projectId = "bamboo-precept-lgxtn";
|
||||
let quota = null;
|
||||
try {
|
||||
const result = await queryAccountQuota(accessToken, context.network);
|
||||
projectId = result.projectId;
|
||||
quota = result.quota;
|
||||
} catch {
|
||||
// Quota can be refreshed after the account has been persisted.
|
||||
}
|
||||
|
||||
const expiresIn = typeof tokenBody.expires_in === "number" ? tokenBody.expires_in : null;
|
||||
return [
|
||||
await credentialDraft({
|
||||
accessToken,
|
||||
refreshToken: text(tokenBody.refresh_token),
|
||||
displayName: displayName ?? "Google Antigravity",
|
||||
projectId,
|
||||
expiresAtMs: expiresIn === null ? null : Date.now() + expiresIn * 1000,
|
||||
quota,
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
export const antigravityDeviceOAuth: OAuth2AddMethod = {
|
||||
type: "oauth2.0",
|
||||
export const antigravityAuthorizationCodeOAuth: OAuth2AuthorizationCodeAddMethod = {
|
||||
type: "oauth2.authorization-code",
|
||||
id: "google-antigravity",
|
||||
displayName: {
|
||||
"en-US": "Sign in with Google (Antigravity)",
|
||||
"zh-CN": "使用 Google (Antigravity) 登录",
|
||||
},
|
||||
description: {
|
||||
"en-US": "Authorize Antigravity with your Google Account for Gemini & Claude models.",
|
||||
"zh-CN": "使用 Google 账号完成 Antigravity 授权,畅享 Gemini 与 Claude 模型。",
|
||||
"en-US": "Authorize Antigravity with your Google Account for Gemini and Claude models.",
|
||||
"zh-CN": "使用 Google 账号完成 Antigravity 授权,以使用 Gemini 与 Claude 模型。",
|
||||
},
|
||||
callback: { port: CALLBACK_PORT, path: CALLBACK_PATH },
|
||||
begin,
|
||||
poll,
|
||||
complete,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user