feat: implement real project ID fetching for Antigravity (#170)

* feat: implement Project ID service to fetch and cache real Project IDs from Google Cloud Code API

* fix: implement caching and cleanup for Project ID retrieval

* feat: add project ID invalidation and refresh logic after token updates

* refactor: remove unnecessary format changes

* feat: add on-demand project ID retrieval for antigravity requests
This commit is contained in:
zx07
2026-02-21 23:15:18 +07:00
committed by GitHub
parent 94c4320632
commit ea67742f2a
5 changed files with 546 additions and 115 deletions
+12 -1
View File
@@ -13,6 +13,7 @@ import { handleComboChat } from "open-sse/services/combo.js";
import { HTTP_STATUS } from "open-sse/config/constants.js";
import * as log from "../utils/logger.js";
import { updateProviderCredentials, checkAndRefreshToken } from "../services/tokenRefresh.js";
import { getProjectIdForConnection } from "open-sse/services/projectId.js";
/**
* Handle chat completion request
@@ -144,7 +145,17 @@ async function handleSingleModelChat(body, modelStr, clientRawRequest = null, re
log.info("AUTH", `Using ${provider} account: ${accountId}...`);
const refreshedCredentials = await checkAndRefreshToken(provider, credentials);
// Ensure real project ID is available for providers that need it (P0 fix: cold miss)
if ((provider === "antigravity" || provider === "gemini-cli") && !refreshedCredentials.projectId) {
const pid = await getProjectIdForConnection(credentials.connectionId, refreshedCredentials.accessToken);
if (pid) {
refreshedCredentials.projectId = pid;
// Persist to DB in background so subsequent requests have it immediately
updateProviderCredentials(credentials.connectionId, { projectId: pid }).catch(() => { });
}
}
// Use shared chatCore
const result = await handleChatCore({
body: { ...body, model: `${provider}/${model}` },
+181 -83
View File
@@ -1,6 +1,11 @@
// Re-export from open-sse with local logger
import * as log from "../utils/logger.js";
import { updateProviderConnection } from "../../lib/localDb.js";
import {
getProjectIdForConnection,
invalidateProjectId,
removeConnection,
} from "open-sse/services/projectId.js";
import {
TOKEN_EXPIRY_BUFFER_MS as BUFFER_MS,
refreshAccessToken as _refreshAccessToken,
@@ -19,66 +24,139 @@ import {
export const TOKEN_EXPIRY_BUFFER_MS = BUFFER_MS;
// Wrap functions with local logger
export const refreshAccessToken = (provider, refreshToken, credentials) =>
// ─── Re-exports wrapped with local logger ─────────────────────────────────────
export const refreshAccessToken = (provider, refreshToken, credentials) =>
_refreshAccessToken(provider, refreshToken, credentials, log);
export const refreshClaudeOAuthToken = (refreshToken) =>
export const refreshClaudeOAuthToken = (refreshToken) =>
_refreshClaudeOAuthToken(refreshToken, log);
export const refreshGoogleToken = (refreshToken, clientId, clientSecret) =>
export const refreshGoogleToken = (refreshToken, clientId, clientSecret) =>
_refreshGoogleToken(refreshToken, clientId, clientSecret, log);
export const refreshQwenToken = (refreshToken) =>
export const refreshQwenToken = (refreshToken) =>
_refreshQwenToken(refreshToken, log);
export const refreshCodexToken = (refreshToken) =>
export const refreshCodexToken = (refreshToken) =>
_refreshCodexToken(refreshToken, log);
export const refreshIflowToken = (refreshToken) =>
export const refreshIflowToken = (refreshToken) =>
_refreshIflowToken(refreshToken, log);
export const refreshGitHubToken = (refreshToken) =>
export const refreshGitHubToken = (refreshToken) =>
_refreshGitHubToken(refreshToken, log);
export const refreshCopilotToken = (githubAccessToken) =>
export const refreshCopilotToken = (githubAccessToken) =>
_refreshCopilotToken(githubAccessToken, log);
export const getAccessToken = (provider, credentials) =>
export const getAccessToken = (provider, credentials) =>
_getAccessToken(provider, credentials, log);
export const refreshTokenByProvider = (provider, credentials) =>
export const refreshTokenByProvider = (provider, credentials) =>
_refreshTokenByProvider(provider, credentials, log);
export const formatProviderCredentials = (provider, credentials) =>
export const formatProviderCredentials = (provider, credentials) =>
_formatProviderCredentials(provider, credentials, log);
export const getAllAccessTokens = (userInfo) =>
export const getAllAccessTokens = (userInfo) =>
_getAllAccessTokens(userInfo, log);
// Local-specific: Update credentials in localDb
// ─── Lifecycle hook ───────────────────────────────────────────────────────────
/**
* Call this when a connection is fully closed / removed.
* Aborts any in-flight projectId fetch and evicts its cache entry,
* preventing the module-level Maps from accumulating stale entries.
*
* @param {string} connectionId
*/
export function releaseConnection(connectionId) {
if (!connectionId) return;
removeConnection(connectionId);
log.debug("TOKEN_REFRESH", "Released connection resources", { connectionId });
}
// ─── Internal helpers ─────────────────────────────────────────────────────────
/**
* Compute an ISO expiry timestamp from a relative expiresIn (seconds).
* @param {number} expiresIn
* @returns {string}
*/
function toExpiresAt(expiresIn) {
return new Date(Date.now() + expiresIn * 1000).toISOString();
}
/**
* Providers that carry a real Google project ID.
* @param {string} provider
* @returns {boolean}
*/
function needsProjectId(provider) {
return provider === "antigravity" || provider === "gemini-cli";
}
/**
* Non-blocking: fetch the project ID for a connection after a token refresh and
* persist it to localDb. Invalidates the stale cached value first so the fetch
* always retrieves a fresh one.
*
* @param {string} provider
* @param {string} connectionId
* @param {string} accessToken
*/
function _refreshProjectId(provider, connectionId, accessToken) {
if (!needsProjectId(provider) || !connectionId || !accessToken) return;
// Evict the stale cached entry so getProjectIdForConnection does a real fetch
invalidateProjectId(connectionId);
getProjectIdForConnection(connectionId, accessToken)
.then((projectId) => {
if (!projectId) return;
updateProviderCredentials(connectionId, { projectId }).catch((err) => {
log.debug("TOKEN_REFRESH", "Failed to persist refreshed projectId", {
connectionId,
error: err?.message ?? err,
});
});
})
.catch((err) => {
log.debug("TOKEN_REFRESH", "Failed to fetch projectId after token refresh", {
connectionId,
error: err?.message ?? err,
});
});
}
// ─── Local-specific: persist credentials to localDb ──────────────────────────
/**
* Persist updated credentials for a connection to localDb.
* Only fields that are present in `newCredentials` are written.
*
* @param {string} connectionId
* @param {object} newCredentials
* @returns {Promise<boolean>}
*/
export async function updateProviderCredentials(connectionId, newCredentials) {
try {
const updates = {};
if (newCredentials.accessToken) {
updates.accessToken = newCredentials.accessToken;
}
if (newCredentials.refreshToken) {
updates.refreshToken = newCredentials.refreshToken;
}
if (newCredentials.accessToken) updates.accessToken = newCredentials.accessToken;
if (newCredentials.refreshToken) updates.refreshToken = newCredentials.refreshToken;
if (newCredentials.expiresIn) {
updates.expiresAt = new Date(Date.now() + newCredentials.expiresIn * 1000).toISOString();
updates.expiresAt = toExpiresAt(newCredentials.expiresIn);
updates.expiresIn = newCredentials.expiresIn;
}
if (newCredentials.providerSpecificData) {
updates.providerSpecificData = newCredentials.providerSpecificData;
}
if (newCredentials.providerSpecificData) updates.providerSpecificData = newCredentials.providerSpecificData;
if (newCredentials.projectId) updates.projectId = newCredentials.projectId;
const result = await updateProviderConnection(connectionId, updates);
log.info("TOKEN_REFRESH", "Credentials updated in localDb", {
connectionId,
success: !!result
log.info("TOKEN_REFRESH", "Credentials updated in localDb", {
connectionId,
success: !!result
});
return !!result;
} catch (error) {
@@ -90,84 +168,104 @@ export async function updateProviderCredentials(connectionId, newCredentials) {
}
}
// Local-specific: Check and refresh token proactively
// ─── Local-specific: proactive token refresh ─────────────────────────────────
/**
* Check whether the provider token (and, for GitHub, the Copilot token) is
* about to expire and refresh it proactively.
*
* @param {string} provider
* @param {object} credentials
* @returns {Promise<object>} updated credentials object
*/
export async function checkAndRefreshToken(provider, credentials) {
let updatedCredentials = { ...credentials };
let creds = { ...credentials };
// Check regular token expiry
if (updatedCredentials.expiresAt) {
const expiresAt = new Date(updatedCredentials.expiresAt).getTime();
const now = Date.now();
// ── 1. Regular access-token expiry ────────────────────────────────────────
if (creds.expiresAt) {
const expiresAt = new Date(creds.expiresAt).getTime();
const now = Date.now();
const remaining = expiresAt - now;
if (expiresAt - now < TOKEN_EXPIRY_BUFFER_MS) {
if (remaining < TOKEN_EXPIRY_BUFFER_MS) {
log.info("TOKEN_REFRESH", "Token expiring soon, refreshing proactively", {
provider,
expiresIn: Math.round((expiresAt - now) / 1000)
expiresIn: Math.round(remaining / 1000),
});
const newCredentials = await getAccessToken(provider, updatedCredentials);
if (newCredentials && newCredentials.accessToken) {
await updateProviderCredentials(updatedCredentials.connectionId, newCredentials);
updatedCredentials = {
...updatedCredentials,
accessToken: newCredentials.accessToken,
refreshToken: newCredentials.refreshToken || updatedCredentials.refreshToken,
expiresAt: newCredentials.expiresIn
? new Date(Date.now() + newCredentials.expiresIn * 1000).toISOString()
: updatedCredentials.expiresAt
const newCreds = await getAccessToken(provider, creds);
if (newCreds?.accessToken) {
// Persist to DB (non-blocking path continues below)
await updateProviderCredentials(creds.connectionId, newCreds);
creds = {
...creds,
accessToken: newCreds.accessToken,
refreshToken: newCreds.refreshToken ?? creds.refreshToken,
expiresAt: newCreds.expiresIn
? toExpiresAt(newCreds.expiresIn)
: creds.expiresAt,
};
// Non-blocking: refresh projectId with the new access token
_refreshProjectId(provider, creds.connectionId, creds.accessToken);
}
}
}
// Check GitHub copilot token expiry
if (provider === "github" && updatedCredentials.providerSpecificData?.copilotTokenExpiresAt) {
const copilotExpiresAt = updatedCredentials.providerSpecificData.copilotTokenExpiresAt * 1000;
const now = Date.now();
// ── 2. GitHub Copilot token expiry ────────────────────────────────────────
if (provider === "github" && creds.providerSpecificData?.copilotTokenExpiresAt) {
const copilotExpiresAt = creds.providerSpecificData.copilotTokenExpiresAt * 1000;
const now = Date.now();
const remaining = copilotExpiresAt - now;
if (copilotExpiresAt - now < TOKEN_EXPIRY_BUFFER_MS) {
if (remaining < TOKEN_EXPIRY_BUFFER_MS) {
log.info("TOKEN_REFRESH", "Copilot token expiring soon, refreshing proactively", {
provider,
expiresIn: Math.round((copilotExpiresAt - now) / 1000)
expiresIn: Math.round(remaining / 1000),
});
const copilotToken = await refreshCopilotToken(updatedCredentials.accessToken);
const copilotToken = await refreshCopilotToken(creds.accessToken);
if (copilotToken) {
await updateProviderCredentials(updatedCredentials.connectionId, {
providerSpecificData: {
...updatedCredentials.providerSpecificData,
copilotToken: copilotToken.token,
copilotTokenExpiresAt: copilotToken.expiresAt
}
});
updatedCredentials.providerSpecificData = {
...updatedCredentials.providerSpecificData,
copilotToken: copilotToken.token,
copilotTokenExpiresAt: copilotToken.expiresAt
const updatedSpecific = {
...creds.providerSpecificData,
copilotToken: copilotToken.token,
copilotTokenExpiresAt: copilotToken.expiresAt,
};
await updateProviderCredentials(creds.connectionId, {
providerSpecificData: updatedSpecific,
});
creds.providerSpecificData = updatedSpecific;
}
}
}
return updatedCredentials;
return creds;
}
// Local-specific: Refresh GitHub and Copilot tokens together
// ─── Local-specific: combined GitHub + Copilot refresh ───────────────────────
/**
* Refresh the GitHub OAuth token and immediately exchange it for a fresh
* Copilot token.
*
* @param {object} credentials must contain `refreshToken`
* @returns {Promise<object|null>} merged credentials or the raw GitHub credentials on Copilot failure
*/
export async function refreshGitHubAndCopilotTokens(credentials) {
const newGitHubCredentials = await refreshGitHubToken(credentials.refreshToken);
if (newGitHubCredentials?.accessToken) {
const copilotToken = await refreshCopilotToken(newGitHubCredentials.accessToken);
if (copilotToken) {
return {
...newGitHubCredentials,
providerSpecificData: {
copilotToken: copilotToken.token,
copilotTokenExpiresAt: copilotToken.expiresAt
}
};
}
}
return newGitHubCredentials;
const newGitHubCreds = await refreshGitHubToken(credentials.refreshToken);
if (!newGitHubCreds?.accessToken) return newGitHubCreds;
const copilotToken = await refreshCopilotToken(newGitHubCreds.accessToken);
if (!copilotToken) return newGitHubCreds;
return {
...newGitHubCreds,
providerSpecificData: {
copilotToken: copilotToken.token,
copilotTokenExpiresAt: copilotToken.expiresAt,
},
};
}