mirror of
https://wget.la/https://github.com/leookun/cursor-byok
synced 2026-10-04 19:31:28 +08:00
158 lines
4.9 KiB
TypeScript
158 lines
4.9 KiB
TypeScript
import type { JsonValue, PluginContext } from "cursor-byok:plugin";
|
|
import type {
|
|
OAuth2AuthorizationCodeAddMethod,
|
|
OAuth2AuthorizationCodeBegin,
|
|
ResourceDraft,
|
|
} from "cursor-byok:resource";
|
|
import { credentialDraft, queryAccountQuota } from "./resources.ts";
|
|
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";
|
|
|
|
const AUTHORIZATION_LIFETIME_MS = 5 * 60 * 1000;
|
|
|
|
type Session = { createdAtMs: number };
|
|
|
|
function object(value: unknown): Record<string, unknown> | null {
|
|
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
? value as Record<string, unknown>
|
|
: null;
|
|
}
|
|
|
|
function text(value: unknown): string | null {
|
|
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
}
|
|
|
|
function parseBody(body: string): Record<string, unknown> {
|
|
try {
|
|
return object(JSON.parse(body)) ?? {};
|
|
} catch {
|
|
return {};
|
|
}
|
|
}
|
|
|
|
function parseSession(value: JsonValue): Session {
|
|
const session = object(value);
|
|
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 };
|
|
}
|
|
|
|
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: input.redirectUri,
|
|
scope: SCOPES.join(" "),
|
|
state: input.state,
|
|
code_challenge: input.codeChallenge,
|
|
code_challenge_method: "S256",
|
|
access_type: "offline",
|
|
prompt: "consent",
|
|
});
|
|
return {
|
|
session: { createdAtMs: Date.now() },
|
|
authorizationUrl: `${GOOGLE_AUTHORIZATION_URL}?${authParams.toString()}`,
|
|
expiresAtMs: Date.now() + AUTHORIZATION_LIFETIME_MS,
|
|
};
|
|
}
|
|
|
|
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}`);
|
|
}
|
|
|
|
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 userInfoResponse = await context.network.fetch(
|
|
"https://www.googleapis.com/oauth2/v1/userinfo?alt=json",
|
|
{
|
|
method: "GET",
|
|
headers: { authorization: `Bearer ${accessToken}`, accept: "application/json" },
|
|
},
|
|
);
|
|
if (userInfoResponse.status >= 200 && userInfoResponse.status < 300) {
|
|
displayName = text(parseBody(userInfoResponse.body).email) ?? displayName;
|
|
}
|
|
} catch {
|
|
// Account identity has a token fingerprint fallback.
|
|
}
|
|
|
|
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 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 and Claude models.",
|
|
"zh-CN": "使用 Google 账号完成 Antigravity 授权,以使用 Gemini 与 Claude 模型。",
|
|
},
|
|
callback: { port: CALLBACK_PORT, path: CALLBACK_PATH },
|
|
begin,
|
|
complete,
|
|
};
|