Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/fixes/8860-tokenrefresh-projectid.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- **Antigravity**: the shared token-refresh service now recovers a missing `projectId` too — #8842 fixed the executor path, but the Dashboard and the health check refresh through `open-sse/services/tokenRefresh.ts`, which kept returning credentials with an empty project and left those accounts on 422
5 changes: 3 additions & 2 deletions config/quality/file-size-baseline.json
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@
"tests/unit/search-handler-extended.test.ts": 1071,
"tests/unit/sse-auth.test.ts": 1600,
"tests/unit/stream-utils.test.ts": 2445,
"tests/unit/token-refresh-service.test.ts": 1311,
"tests/unit/token-refresh-service.test.ts": 1378,
"tests/unit/translator-openai-responses-req.test.ts": 1194,
"tests/unit/translator-openai-to-gemini.test.ts": 1616,
"tests/unit/translator-openai-to-kiro.test.ts": 1250,
Expand Down Expand Up @@ -409,5 +409,6 @@
"_rebaseline_2026_07_27_v3849_train2": "Merge-train 2 (7 PRs) — owner-approved 2026-07-27. Single entry: chatCore.ts 4955->5006 (#8595, Responses multi-turn image compaction before the context hard-reject). Genuine irreducible growth at the existing compaction chokepoint in handleChatCore — the PR adds a last-resort retry against the concrete budget plus the estimateFinalInputTokens helper, both wired at the pre-existing call site rather than a new branch. Covered by tests/unit/8560-responses-image-compaction.test.ts (4 tests).",
"_rebaseline_2026_07_27_v3849_train3": "Merge-train 3 (13 PRs) — owner-approved 2026-07-27. Both entries are genuine irreducible growth at existing chokepoints, not new branches: src/lib/db/apiKeys.ts 1518->1529 (#8805 cx/* ≡ codex/* API-key model permissions); open-sse/handlers/chatCore.ts 5006->5020 (#8806 real response payload into plugin onResponse hooks). Covered by tests/unit/db-apiKeys-crud.test.ts (4 new cases) and the two plugin-hook test files updated in #8806 respectively.",
"_rebaseline_2026_07_28_8842_antigravity_projectid_refresh": "PR #8842 (fix/antigravity-projectid-refresh) own growth: open-sse/executors/antigravity.ts 1493->1528 (+35 = projectId discovery in refreshCredentials: import ensureAntigravityProjectAssigned + trim projectId + call ensureAntigravityProjectAssigned with 8s timeout + persistDiscoveredAntigravityProjectId + log success/failure). Irreducible wiring at the existing credential-refresh chokepoint. Covered by tests/unit/executor-antigravity.test.ts (4 new test cases).",
"_rebaseline_2026_07_28_8842_antigravity_test": "PR #8842 test growth: tests/unit/executor-antigravity.test.ts new testFrozen 1098 (4 new test cases for projectId discovery during refresh: discovers when empty, skips when set, handles failure, skips when access_token not a string). All above cap 1000 on day one."
"_rebaseline_2026_07_28_8842_antigravity_test": "PR #8842 test growth: tests/unit/executor-antigravity.test.ts new testFrozen 1098 (4 new test cases for projectId discovery during refresh: discovers when empty, skips when set, handles failure, skips when access_token not a string). All above cap 1000 on day one.",
"_rebaseline_2026_07_28_8860_tokenrefresh_projectid": "PR #8860 (fix/antigravity-projectid-centralized) own test growth: tests/unit/token-refresh-service.test.ts 1311->1378 (+67 = 4 cases covering projectId discovery on the tokenRefresh.ts path — the Dashboard/health-check refresh route, which #8842 did not reach since that fixed the executor path). Covered by the same file."
}
45 changes: 43 additions & 2 deletions open-sse/services/tokenRefresh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ import { refreshKimiCodingToken } from "./tokenRefresh/providers/kimiCoding.ts";
import { refreshGitLabDuoToken } from "./tokenRefresh/providers/gitlabDuo.ts";
import { refreshClaudeOAuthToken } from "./tokenRefresh/providers/claudeOAuth.ts";
import { refreshGoogleToken } from "./tokenRefresh/providers/google.ts";
import { ensureAntigravityProjectAssigned } from "./antigravityProjectBootstrap.ts";
import { persistDiscoveredAntigravityProjectId } from "./antigravityProjectPersist.ts";
import { refreshCodexToken } from "./tokenRefresh/providers/codex.ts";
import { refreshKiroToken } from "./tokenRefresh/providers/kiro.ts";
import { refreshQoderToken } from "./tokenRefresh/providers/qoder.ts";
Expand Down Expand Up @@ -275,15 +277,54 @@ async function _getAccessTokenInternal(provider, credentials, log, proxyConfig:

case "gemini":
case "antigravity":
case "agy":
return await refreshGoogleToken(
case "agy": {
const result = await refreshGoogleToken(
credentials.refreshToken,
PROVIDERS[provider].clientId,
PROVIDERS[provider].clientSecret,
log,
proxyConfig
);

// Google One AI accounts get no projectId at OAuth exchange time.
// Recover it via loadCodeAssist so downstream routing works.
if (
result?.accessToken &&
(provider === "antigravity" || provider === "agy") &&
!(credentials.projectId || credentials.providerSpecificData?.projectId)
) {
try {
const discovered = await ensureAntigravityProjectAssigned(
result.accessToken,
fetch
);
if (discovered) {
result.projectId = discovered;
result.providerSpecificData = {
...(credentials.providerSpecificData || {}),
...(result.providerSpecificData || {}),
projectId: discovered,
};
if (credentials.connectionId) {
await persistDiscoveredAntigravityProjectId(
credentials.connectionId,
discovered,
credentials.providerSpecificData
);
}
log?.info?.("TOKEN", "Antigravity projectId discovered during token refresh", {
projectId: discovered,
});
}
} catch (discoveryError) {
const msg = discoveryError instanceof Error ? discoveryError.message : String(discoveryError);
log?.warn?.("TOKEN", `Antigravity projectId discovery failed: ${msg}`);
}
}

return result;
}

case "claude":
return await refreshClaudeOAuthToken(credentials.refreshToken, log, proxyConfig);

Expand Down
67 changes: 67 additions & 0 deletions tests/unit/token-refresh-service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -795,6 +795,73 @@ test("supportsTokenRefresh, isUnrecoverableRefreshError and formatProviderCreden
assert.equal(formatProviderCredentials("missing-provider", {}, log), null);
});

test("getAccessToken discovers projectId for antigravity when stored value is empty", async () => {
const log = createLog();
const { clearAntigravityProjectCache } = await import("../../open-sse/services/antigravityProjectBootstrap.ts");
clearAntigravityProjectCache();

let fetchCalls: string[] = [];
await withMockedFetch(async (url, init) => {
const urlStr = String(url);
fetchCalls.push(urlStr);
if (urlStr.includes("oauth2.googleapis.com/token")) {
return jsonResponse({
access_token: "new-token-1",
refresh_token: "new-refresh",
expires_in: 3600,
});
}
if (urlStr.includes("loadCodeAssist")) {
return jsonResponse({ cloudaicompanionProject: "discovered-project" });
}
return new Response("not found", { status: 404 });
}, async () => {
const result = await getAccessToken("antigravity", {
refreshToken: "refresh",
projectId: "",
connectionId: "conn-1",
providerSpecificData: {},
});
assert.equal(result.projectId, "discovered-project");
assert.ok(fetchCalls.some((u) => u.includes("loadCodeAssist")), "should call loadCodeAssist");
});
clearAntigravityProjectCache();
});


test("getAccessToken handles projectId discovery failure gracefully for antigravity", async () => {
const log = createLog();
const { clearAntigravityProjectCache } = await import("../../open-sse/services/antigravityProjectBootstrap.ts");
clearAntigravityProjectCache();
tokenRefresh._clearTokenRotationMap();

await withMockedFetch(async (url) => {
const urlStr = String(url);
if (urlStr.includes("oauth2.googleapis.com/token")) {
return jsonResponse({
access_token: "new-token-3",
refresh_token: "new-refresh",
expires_in: 3600,
});
}
if (urlStr.includes("loadCodeAssist")) {
return new Response("server error", { status: 500 });
}
return new Response("not found", { status: 404 });
}, async () => {
const result = await getAccessToken("antigravity", {
refreshToken: "refresh",
projectId: "",
connectionId: "conn-1",
providerSpecificData: {},
});
assert.equal(result.accessToken, "new-token-3");
assert.ok(result, "should return a result without throwing");
});
clearAntigravityProjectCache();
});


test("getAccessToken deduplicates concurrent refreshes for the same provider and token", async () => {
const log = createLog();
let fetchCount = 0;
Expand Down
Loading