Merge main and host authorization-code OAuth in core

This commit is contained in:
leokun
2026-09-04 17:16:29 +08:00
224 changed files with 10351 additions and 21466 deletions
@@ -0,0 +1,20 @@
/** Official Google Antigravity OAuth client configuration. */
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("-");
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",
];
export const GOOGLE_AUTHORIZATION_URL = "https://accounts.google.com/o/oauth2/v2/auth";
export const GOOGLE_TOKEN_URL = "https://oauth2.googleapis.com/token";
@@ -1,12 +1,7 @@
import { defineProviderPlugin } from "cursor-byok:plugin";
import { antigravityDeviceOAuth } from "./oauth.ts";
import { antigravityAuthorizationCodeOAuth } from "./oauth.ts";
import { antigravityProvider } from "./provider.ts";
import {
credentialImport,
presentAccount,
refreshAccount,
RESOURCE_TYPE,
} from "./resources.ts";
import { credentialImport, presentAccount, refreshAccount, RESOURCE_TYPE } from "./resources.ts";
export default defineProviderPlugin({
providers: [antigravityProvider],
@@ -16,7 +11,7 @@ export default defineProviderPlugin({
"en-US": "Google accounts & API keys",
"zh-CN": "Google 账号与 API 密钥",
},
add: [antigravityDeviceOAuth],
add: [antigravityAuthorizationCodeOAuth],
import: credentialImport,
present: presentAccount,
refresh: refreshAccount,
@@ -277,7 +277,9 @@ export function parseAntigravityModels(payload: unknown): ModelDefinition[] {
models.push({
id,
displayName,
capabilities: { images: model.supportsImages === true || id.includes("gemini") || id.includes("claude") },
capabilities: {
images: model.supportsImages === true || id.includes("gemini") || id.includes("claude"),
},
maxOutputTokens,
privateData: { reasoningEfforts },
});
@@ -318,16 +320,19 @@ export const antigravityModels: ModelSupport = {
for (const endpoint of ANTIGRAVITY_ENDPOINTS) {
for (const bodyPayload of payloads) {
try {
const response = await context.network.fetch(`${endpoint}${FETCH_AVAILABLE_MODELS_PATH}`, {
method: "POST",
headers: {
authorization: `Bearer ${data.accessToken}`,
"content-type": "application/json",
"user-agent": ANTIGRAVITY_USER_AGENT,
...ANTIGRAVITY_CLIENT_HEADERS,
const response = await context.network.fetch(
`${endpoint}${FETCH_AVAILABLE_MODELS_PATH}`,
{
method: "POST",
headers: {
authorization: `Bearer ${data.accessToken}`,
"content-type": "application/json",
"user-agent": ANTIGRAVITY_USER_AGENT,
...ANTIGRAVITY_CLIENT_HEADERS,
},
body: bodyPayload,
},
body: bodyPayload,
});
);
if (response.status >= 200 && response.status < 300) {
const body = JSON.parse(response.body);
const models = parseAntigravityModels(body);
+97 -139
View File
@@ -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,
};
@@ -0,0 +1,79 @@
import type { PluginContext } from "cursor-byok:plugin";
import { antigravityAuthorizationCodeOAuth } from "./oauth.ts";
function assert(condition: unknown, message: string): asserts condition {
if (!condition) throw new Error(message);
}
function context(requests: Array<{ url: string; body?: string }>): PluginContext {
return {
network: {
fetch: async (url, init = {}) => {
requests.push({ url, body: init.body });
if (url === "https://oauth2.googleapis.com/token") {
return {
status: 200,
headers: {},
body: JSON.stringify({
access_token: "access-token",
refresh_token: "refresh-token",
expires_in: 3600,
}),
};
}
if (url.startsWith("https://www.googleapis.com/oauth2/v1/userinfo")) {
return { status: 200, headers: {}, body: JSON.stringify({ email: "user@example.com" }) };
}
return { status: 500, headers: {}, body: "{}" };
},
stream: () => Promise.reject(new Error("stream is not expected")),
},
signal: new AbortController().signal,
};
}
Deno.test("authorization URL uses Core-owned state, callback, and PKCE challenge", async () => {
const result = await antigravityAuthorizationCodeOAuth.begin(
{
redirectUri: "http://127.0.0.1:51121/oauth-callback",
state: "core-state",
codeChallenge: "core-challenge",
},
context([]),
);
const url = new URL(result.authorizationUrl);
assert(url.searchParams.get("state") === "core-state", "state must come from Core");
assert(
url.searchParams.get("redirect_uri")?.endsWith("/oauth-callback"),
"callback must be forwarded",
);
assert(
url.searchParams.get("code_challenge") === "core-challenge",
"PKCE challenge must be forwarded",
);
assert(url.searchParams.get("code_challenge_method") === "S256", "PKCE must use S256");
});
Deno.test("authorization completion exchanges the code with the Core PKCE verifier", async () => {
const requests: Array<{ url: string; body?: string }> = [];
const resources = await antigravityAuthorizationCodeOAuth.complete(
{ createdAtMs: Date.now() },
{
code: "authorization-code",
redirectUri: "http://127.0.0.1:51121/oauth-callback",
codeVerifier: "core-verifier",
},
context(requests),
);
const tokenRequest = requests.find((request) =>
request.url === "https://oauth2.googleapis.com/token"
);
const body = new URLSearchParams(tokenRequest?.body);
assert(body.get("code") === "authorization-code", "authorization code must be exchanged");
assert(body.get("code_verifier") === "core-verifier", "PKCE verifier must come from Core");
assert(resources.length === 1, "one Google account resource must be returned");
assert(
resources[0].key === "antigravity:user@example.com",
"the account email must be the resource key",
);
});
@@ -2,23 +2,18 @@
"apiVersion": 1,
"id": "dev.cursorbyok.plugins.antigravity-auth",
"name": "Antigravity",
"version": "0.2.0",
"version": "0.3.0",
"author": "Antigravity",
"minAppVersion": "0.1.0",
"icon": "assets/antigravity.svg",
"entry": "main.ts",
"permissions": {
"network": [
"127.0.0.1",
"localhost",
"daily-cloudcode-pa.googleapis.com",
"daily-cloudcode-pa.sandbox.googleapis.com",
"cloudcode-pa.googleapis.com",
"generativelanguage.googleapis.com",
"oauth2.googleapis.com",
"accounts.google.com",
"www.googleapis.com",
"antigravity.google"
"www.googleapis.com"
]
}
}
@@ -88,16 +88,23 @@ function resolveAntigravityModel(modelId: string): string {
}
// 2. Canonical Antigravity-Manager mapping for aliases
if (lower === "claude-3-7-sonnet" || lower === "claude-3-5-sonnet" || lower === "claude-sonnet-4-5") {
if (
lower === "claude-3-7-sonnet" || lower === "claude-3-5-sonnet" || lower === "claude-sonnet-4-5"
) {
return "claude-sonnet-4-6";
}
if (lower === "claude-3-5-haiku" || lower === "claude-haiku-4") {
return "claude-sonnet-4-6";
}
if (lower === "claude-3-7-opus" || lower === "claude-opus-4" || lower === "claude-opus-4.6" || lower === "claude-opus-4-5-thinking") {
if (
lower === "claude-3-7-opus" || lower === "claude-opus-4" || lower === "claude-opus-4.6" ||
lower === "claude-opus-4-5-thinking"
) {
return "claude-opus-4-6-thinking";
}
if (lower === "gpt-4" || lower === "gpt-4o" || lower === "gpt-4o-mini" || lower === "gpt-3.5-turbo") {
if (
lower === "gpt-4" || lower === "gpt-4o" || lower === "gpt-4o-mini" || lower === "gpt-3.5-turbo"
) {
return "gemini-2.5-flash";
}
if (lower === "gemini-2.5-flash-lite") {
@@ -195,7 +202,7 @@ function convertToCloudCodeContents(
messages: LlmMessage[],
): {
contents: Array<{ role: string; parts: Array<Record<string, unknown>> }>;
systemInstruction?: { parts: Array<{ text: string }> };
systemInstruction?: { role: string; parts: Array<{ text: string }> };
} {
const rawContents: Array<{ role: string; parts: Array<Record<string, unknown>> }> = [];
let systemText = instructions || "";
@@ -221,13 +228,17 @@ function convertToCloudCodeContents(
const replayVal = msg.replayState?.providerKind === "antigravity"
? (msg.replayState.value as Record<string, unknown> | null)
: null;
const sig = typeof replayVal?.thoughtSignature === "string" ? replayVal.thoughtSignature : null;
const sig = typeof replayVal?.thoughtSignature === "string"
? replayVal.thoughtSignature
: null;
for (const call of msg.toolCalls) {
parts.push({
functionCall: {
name: call.name,
args: typeof call.arguments === "object" && call.arguments !== null ? call.arguments : {},
args: typeof call.arguments === "object" && call.arguments !== null
? call.arguments
: {},
},
thoughtSignature: sig || "skip_thought_signature_validator",
});
@@ -287,7 +298,9 @@ function convertToCloudCodeContents(
return {
contents,
...(systemText.trim() ? { systemInstruction: { role: "system", parts: [{ text: systemText.trim() }] } } : {}),
...(systemText.trim()
? { systemInstruction: { role: "system", parts: [{ text: systemText.trim() }] } }
: {}),
};
}
@@ -307,20 +320,20 @@ async function streamCloudCode(
const tools = input.request.tools && input.request.tools.length > 0
? [
{
functionDeclarations: input.request.tools.map((t) => ({
name: t.name,
description: t.description || "",
parameters: sanitizeSchema(t.parameters),
})),
},
]
{
functionDeclarations: input.request.tools.map((t) => ({
name: t.name,
description: t.description || "",
parameters: sanitizeSchema(t.parameters),
})),
},
]
: undefined;
const toolConfig = tools
? {
functionCallingConfig: { mode: "AUTO" },
}
functionCallingConfig: { mode: "AUTO" },
}
: undefined;
const payload = {
@@ -348,7 +361,8 @@ async function streamCloudCode(
...ANTIGRAVITY_CLIENT_HEADERS,
};
if (actualModel.toLowerCase().includes("claude")) {
headers["anthropic-beta"] = "claude-code-20250219,interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14";
headers["anthropic-beta"] =
"claude-code-20250219,interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14";
}
let lastError: Error | null = null;
@@ -370,7 +384,10 @@ async function streamCloudCode(
if (response.status < 200 || response.status >= 300) {
const errorBody = await readBody(response.lines);
lastError = new HttpError(response.status, errorBody);
if (response.status === 503 || response.status === 502 || response.status === 504 || response.status === 404) {
if (
response.status === 503 || response.status === 502 || response.status === 504 ||
response.status === 404
) {
continue;
}
throw lastError;
@@ -382,7 +399,11 @@ async function streamCloudCode(
let hasTools = false;
let toolIndex = 0;
let lastThoughtSignature: string | null = null;
let finalUsage: { inputTokens: number | null; outputTokens: number | null; totalTokens: number | null } | null = null;
let finalUsage: {
inputTokens: number | null;
outputTokens: number | null;
totalTokens: number | null;
} | null = null;
for await (const line of response.lines) {
if (!line.startsWith("data:")) continue;
@@ -403,7 +424,9 @@ async function streamCloudCode(
if (usage) {
finalUsage = {
inputTokens: typeof usage.promptTokenCount === "number" ? usage.promptTokenCount : null,
outputTokens: typeof usage.candidatesTokenCount === "number" ? usage.candidatesTokenCount : null,
outputTokens: typeof usage.candidatesTokenCount === "number"
? usage.candidatesTokenCount
: null,
totalTokens: typeof usage.totalTokenCount === "number" ? usage.totalTokenCount : null,
};
}
@@ -485,7 +508,9 @@ async function streamCloudCode(
}
}
const finishReason = typeof candidate?.finishReason === "string" ? candidate.finishReason : null;
const finishReason = typeof candidate?.finishReason === "string"
? candidate.finishReason
: null;
if (finishReason) {
if (thinkingStarted) {
thinkingStarted = false;
@@ -517,7 +542,8 @@ async function streamCloudCode(
});
finalUsage = null;
}
const isTool = finishReason === "STOP" && (hasTools || parts?.some((p) => p.functionCall));
const isTool = finishReason === "STOP" &&
(hasTools || parts?.some((p) => p.functionCall));
output.emit({
type: "done",
reason: isTool ? "tool-use" : "stop",
@@ -610,17 +636,30 @@ async function invoke(
try {
await streamCloudCode(data.accessToken, projectId, input.model.id, input, output, context);
return patchData
? { status: "completed", patch: { privateData: patchData as unknown as JsonValue, state: { status: "ready" } } }
? {
status: "completed",
patch: { privateData: patchData as unknown as JsonValue, state: { status: "ready" } },
}
: { status: "completed" };
} catch (error) {
if (error instanceof HttpError) {
if ((error.status === 401 || error.status === 403) && data.refreshToken && !isQuotaHttpError(error)) {
if (
(error.status === 401 || error.status === 403) && data.refreshToken &&
!isQuotaHttpError(error)
) {
try {
const refreshed = await refreshAccount(input.resource, context);
if (refreshed.privateData) {
const freshData = refreshed.privateData as unknown as AccountData;
const freshProj = freshData.projectId ?? projectId;
await streamCloudCode(freshData.accessToken, freshProj, input.model.id, input, output, context);
await streamCloudCode(
freshData.accessToken,
freshProj,
input.model.id,
input,
output,
context,
);
return {
status: "completed",
patch: { privateData: freshData as unknown as JsonValue, state: { status: "ready" } },
@@ -1,4 +1,4 @@
import type { JsonValue, PluginContext } from "cursor-byok:plugin";
import type { JsonValue, NetworkRequestInit, PluginContext } from "cursor-byok:plugin";
import type {
ResourceDraft,
ResourceImportFile,
@@ -15,11 +15,25 @@ import {
ANTIGRAVITY_ENDPOINTS,
ANTIGRAVITY_USER_AGENT,
} from "./models.ts";
import { CLIENT_ID, CLIENT_SECRET } from "./google_oauth.ts";
export const RESOURCE_TYPE = "antigravity-account";
const REFRESH_TOKEN_URL = "https://oauth2.googleapis.com/token";
async function fetchText(
network: PluginContext["network"] | undefined,
url: string,
init: NetworkRequestInit,
): Promise<{ status: number; body: string }> {
if (network) {
const response = await network.fetch(url, init);
return { status: response.status, body: response.body };
}
const response = await fetch(url, init);
return { status: response.status, body: await response.text() };
}
export type QuotaMetric = {
remainingPercent: number;
resetAtMs: number | null;
@@ -73,12 +87,15 @@ export async function fetchAccountProjectAndTier(
const project = text(body?.cloudaicompanionProject);
const paid = object(body?.paidTier);
const current = object(body?.currentTier);
const tierName = text(paid?.name) ?? text(paid?.id) ?? text(current?.name) ?? text(current?.id);
const tierName = text(paid?.name) ?? text(paid?.id) ?? text(current?.name) ??
text(current?.id);
let planLabel = "FREE";
if (tierName) {
const lower = tierName.toLowerCase();
if (lower.includes("ultra")) planLabel = "ULTRA";
else if (lower.includes("pro") || lower.includes("premium") || lower.includes("advanced")) planLabel = "PRO";
else if (
lower.includes("pro") || lower.includes("premium") || lower.includes("advanced")
) planLabel = "PRO";
}
return { projectId: project ?? "bamboo-precept-lgxtn", planLabel };
}
@@ -121,7 +138,9 @@ export async function queryAccountQuota(
for (const [key, value] of Object.entries(models)) {
const info = object(value);
const quota = object(info?.quotaInfo);
const fraction = typeof quota?.remainingFraction === "number" ? quota.remainingFraction : null;
const fraction = typeof quota?.remainingFraction === "number"
? quota.remainingFraction
: null;
const resetTime = text(quota?.resetTime);
const resetAtMs = resetTime ? Date.parse(resetTime) : null;
if (fraction === null) continue;
@@ -147,8 +166,12 @@ export async function queryAccountQuota(
limitReached: false,
coolingUntilMs: null,
updatedAtMs: Date.now(),
claude: claudeFraction !== null ? { remainingPercent: Math.round(claudeFraction * 100), resetAtMs: claudeResetAtMs } : null,
gemini: geminiFraction !== null ? { remainingPercent: Math.round(geminiFraction * 100), resetAtMs: geminiResetAtMs } : null,
claude: claudeFraction !== null
? { remainingPercent: Math.round(claudeFraction * 100), resetAtMs: claudeResetAtMs }
: null,
gemini: geminiFraction !== null
? { remainingPercent: Math.round(geminiFraction * 100), resetAtMs: geminiResetAtMs }
: null,
},
};
} catch {
@@ -235,7 +258,8 @@ export async function accountIdentity(
const identity = (providedDisplayName && !providedDisplayName.includes("Antigravity"))
? providedDisplayName
: (email ?? sub ?? fingerprint);
const displayName = providedDisplayName ?? email ?? name ?? (token.startsWith("AIza") ? `API Key (${fingerprint.slice(0, 6)})` : identity);
const displayName = providedDisplayName ?? email ?? name ??
(token.startsWith("AIza") ? `API Key (${fingerprint.slice(0, 6)})` : identity);
return { key: `antigravity:${identity}`, displayName };
}
@@ -246,7 +270,8 @@ export async function credentialDraft(credential: CredentialCandidate): Promise<
refreshToken: credential.refreshToken,
displayName: credential.displayName ?? identity.displayName,
projectId: credential.projectId ?? "bamboo-precept-lgxtn",
expiresAtMs: credential.expiresAtMs ?? (credential.refreshToken ? Date.now() + 3500 * 1000 : null),
expiresAtMs: credential.expiresAtMs ??
(credential.refreshToken ? Date.now() + 3500 * 1000 : null),
quota: credential.quota ?? null,
};
return { key: identity.key, privateData: data as unknown as JsonValue };
@@ -346,8 +371,6 @@ export function presentAccount(resource: ResourceSnapshot): ResourceView {
};
}
import { CLIENT_ID, CLIENT_SECRET } from "./oauth.ts";
export async function refreshAccount(
resource: ResourceSnapshot,
context: PluginContext,
@@ -378,7 +401,10 @@ export async function refreshAccount(
// Only mark invalid if token is revoked or client is invalid
if (bodyText.includes("invalid_grant") || bodyText.includes("unauthorized_client")) {
return {
state: { status: "invalid", message: "Google authorization expired or revoked; please sign in again" },
state: {
status: "invalid",
message: "Google authorization expired or revoked; please sign in again",
},
};
}
// On network glitches or temporary Google server errors, keep ready
@@ -461,18 +487,18 @@ function collectCredentials(value: unknown, output: CredentialCandidate[]): void
firstText(item, ["refresh", "refresh_token", "refreshToken"]);
const displayName = firstText(item, ["email", "display_name", "displayName", "name"]) ??
firstText(tokens, ["email", "display_name", "displayName", "name"]);
const projectId = firstText(item, ["project", "projectId", "project_id", "cloudaicompanionProject"]) ??
firstText(tokens, ["project", "projectId", "project_id", "cloudaicompanionProject"]);
const projectId =
firstText(item, ["project", "projectId", "project_id", "cloudaicompanionProject"]) ??
firstText(tokens, ["project", "projectId", "project_id", "cloudaicompanionProject"]);
if (!accessToken && !refreshToken) return;
if (!accessToken && refreshToken) {
accessToken = refreshToken;
}
if (!accessToken) return;
output.push({ accessToken, refreshToken, displayName, projectId });
}
import { REDIRECT_URI } from "./oauth.ts";
export async function parseCredentialFiles(
files: ResourceImportFile[],
network?: PluginContext["network"],
@@ -486,41 +512,6 @@ export async function parseCredentialFiles(
const raw = file.content.trim();
if (!raw) continue;
// Check if user pasted the Google OAuth callback URL or raw auth code (4/0ATs...)
const authCodeMatch = raw.match(/code=([40][a-zA-Z0-9_\-%]+)/) || (raw.startsWith("4/") ? [null, raw] : null);
if (authCodeMatch?.[1]) {
const code = decodeURIComponent(authCodeMatch[1]);
try {
const fetcher = network ? (url: string, init: RequestInit) => network.fetch(url, init) : (url: string, init: RequestInit) => fetch(url, init).then(async (r) => ({ status: r.status, body: await r.text() }));
const response = await fetcher(REFRESH_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 = object(JSON.parse(response.body));
if (response.status >= 200 && response.status < 300 && text(tokenBody?.access_token)) {
credentials.push({
accessToken: text(tokenBody?.access_token)!,
refreshToken: text(tokenBody?.refresh_token),
displayName: text(tokenBody?.email) ?? "Google Antigravity Account",
expiresAtMs: Date.now() + ((typeof tokenBody?.expires_in === "number" ? tokenBody.expires_in : 3600) * 1000),
});
continue;
}
} catch {
// Fallback to normal parsing
}
}
// Check if file is raw API key or JWT token string
if (raw.startsWith("AIza") || (raw.split(".").length === 3 && !raw.includes(" "))) {
credentials.push({ accessToken: raw, refreshToken: null, displayName: file.name });
@@ -532,9 +523,15 @@ export async function parseCredentialFiles(
try {
content = JSON.parse(raw);
} catch {
const envMatch = raw.match(/(?:API_KEY|TOKEN|GEMINI_API_KEY|GOOGLE_API_KEY|ANTIGRAVITY_API_KEY)\s*=\s*["']?([^"'\r\n]+)/i);
const envMatch = raw.match(
/(?:API_KEY|TOKEN|GEMINI_API_KEY|GOOGLE_API_KEY|ANTIGRAVITY_API_KEY)\s*=\s*["']?([^"'\r\n]+)/i,
);
if (envMatch?.[1]) {
credentials.push({ accessToken: envMatch[1].trim(), refreshToken: null, displayName: file.name });
credentials.push({
accessToken: envMatch[1].trim(),
refreshToken: null,
displayName: file.name,
});
continue;
}
const keyMatch = raw.match(/AIza[0-9A-Za-z-_]{35}/);
@@ -560,11 +557,7 @@ export async function parseCredentialFiles(
for (const candidate of found) {
if (candidate.refreshToken && candidate.accessToken === candidate.refreshToken) {
try {
const fetcher = network
? (url: string, init: RequestInit) => network.fetch(url, init)
: (url: string, init: RequestInit) =>
fetch(url, init).then(async (r) => ({ status: r.status, body: await r.text() }));
const response = await fetcher(REFRESH_TOKEN_URL, {
const response = await fetchText(network, REFRESH_TOKEN_URL, {
method: "POST",
headers: {
accept: "application/json",
@@ -581,7 +574,8 @@ export async function parseCredentialFiles(
if (response.status >= 200 && response.status < 300 && text(body?.access_token)) {
candidate.accessToken = text(body?.access_token)!;
candidate.refreshToken = text(body?.refresh_token) ?? candidate.refreshToken;
candidate.expiresAtMs = Date.now() + ((typeof body?.expires_in === "number" ? body.expires_in : 3600) * 1000);
candidate.expiresAtMs = Date.now() +
((typeof body?.expires_in === "number" ? body.expires_in : 3600) * 1000);
}
} catch {
// Keep placeholder
@@ -599,15 +593,22 @@ export const credentialImport: ResourceImportSupport = {
"zh-CN": "导入 Google / Antigravity 凭证",
},
description: {
"en-US": "Import a JSON, TXT, or Callback URL containing Antigravity tokens or Google API keys.",
"zh-CN": "导入包含 Antigravity Token、Google API Key 或授权回调 URL 的 JSON/TXT 文件。",
"en-US":
"Import a JSON, TXT, or environment file containing Antigravity tokens or Google API keys.",
"zh-CN": "导入包含 Antigravity Token 或 Google API Key 的 JSON、TXT 或环境变量文件。",
},
accept: [".json", ".txt", ".key", ".env"],
multiple: true,
parse: async (files: ResourceImportFile[], context: PluginContext): Promise<ResourceImportResult> => {
parse: async (
files: ResourceImportFile[],
context: PluginContext,
): Promise<ResourceImportResult> => {
const { credentials, warnings } = await parseCredentialFiles(files, context.network);
if (credentials.length === 0) {
throw new Error(warnings.join("; ") || "credential file does not contain a valid token, authorization code, or API key");
throw new Error(
warnings.join("; ") ||
"credential file does not contain a valid token or API key",
);
}
const drafts = await Promise.all(
credentials.map(async (c) => {
@@ -8,6 +8,7 @@ import type { LlmRequest, ModelEvent } from "cursor-byok:provider";
import type { ResourceSnapshot } from "cursor-byok:resource";
import { codexDeviceOAuth } from "./oauth.ts";
import { parseOfficialModels } from "./models.ts";
import { buildResponsesBody } from "cursor-byok:protocol/openai-responses";
import { codexProvider, isQuotaError } from "./provider.ts";
import {
accountIdentity,
@@ -305,6 +306,43 @@ Deno.test("invoke streams normalized events from the Codex Responses API", async
]);
});
Deno.test("reasoning replay projects response items to valid input items", () => {
const replayRequest = request();
replayRequest.messages = [{
role: "assistant",
text: "",
thinking: "",
replayState: {
providerKind: "openai_responses",
value: {
items: [{
type: "reasoning",
id: "item-1",
status: "completed",
summary: [{ type: "summary_text", text: "why" }],
content: [],
encrypted_content: "opaque",
output_only: true,
}],
},
},
toolCalls: [],
}];
const body = buildResponsesBody({
url: "https://example.com/responses",
model: "gpt-test",
request: replayRequest,
});
assertEquals(body.input, [{
type: "reasoning",
id: "item-1",
summary: [{ type: "summary_text", text: "why" }],
content: [],
encrypted_content: "opaque",
}]);
});
Deno.test("invoke streams incremental tool calls and replays reasoning items", async () => {
const token = jwt({ "https://api.openai.com/auth": { chatgpt_account_id: "acct-1" } });
const draft = await credentialDraft({
@@ -277,7 +277,9 @@ Deno.test("invoke streams normalized events from the xAI Chat Completions API",
);
assertEquals(result, { status: "completed" });
const body = JSON.parse(requestBody) as Record<string, unknown>;
assert(!("prompt_cache_key" in body), "standard OpenAI chat completion does not include prompt_cache_key");
assertEquals(body.model, "grok-4");
assertEquals(body.stream, true);
assertEquals(body.prompt_cache_key, "conversation-1");
assert(!("reasoning_effort" in body), "xAI endpoint rejects reasoning_effort");
assert(!("service_tier" in body), "xAI endpoint rejects service_tier");
assertEquals(requestHeaders["authorization"], `Bearer ${token}`);