Refactor Tunnel

This commit is contained in:
decolua
2026-05-25 16:15:34 +07:00
parent 96a9a2b36d
commit b876e0225a
35 changed files with 642 additions and 574 deletions

View File

@@ -7,35 +7,64 @@ import { proxyAwareFetch } from "../utils/proxyFetch.js";
let _xaiServiceSingleton = null;
async function refreshXaiToken(refreshToken, log) {
if (!refreshToken) return null;
try {
if (!_xaiServiceSingleton) {
const mod = await import("../../src/lib/oauth/services/xai.js");
_xaiServiceSingleton = new mod.XaiService();
return dedupRefresh("xai", refreshToken, async () => {
try {
if (!_xaiServiceSingleton) {
const mod = await import("../../src/lib/oauth/services/xai.js");
_xaiServiceSingleton = new mod.XaiService();
}
const tokens = await _xaiServiceSingleton.refreshAccessToken(refreshToken);
return {
accessToken: tokens.access_token,
refreshToken: tokens.refresh_token || refreshToken,
expiresIn: tokens.expires_in,
idToken: tokens.id_token,
};
} catch (e) {
log?.warn?.("TOKEN_REFRESH", `xai refresh failed: ${e?.message || e}`);
const msg = String(e?.message || "");
if (msg.includes("invalid_grant") || msg.includes("invalid_request")) {
return { error: "invalid_grant" };
}
return null;
}
const tokens = await _xaiServiceSingleton.refreshAccessToken(refreshToken);
return {
accessToken: tokens.access_token,
refreshToken: tokens.refresh_token || refreshToken,
expiresIn: tokens.expires_in,
idToken: tokens.id_token,
};
} catch (e) {
log?.warn?.("TOKEN_REFRESH", `xai refresh failed: ${e?.message || e}`);
const msg = String(e?.message || "");
if (msg.includes("invalid_grant") || msg.includes("invalid_request")) {
return { error: "invalid_grant" };
}
return null;
}
}, log);
}
// Default token expiry buffer (refresh if expires within 5 minutes)
export const TOKEN_EXPIRY_BUFFER_MS = 5 * 60 * 1000;
// In-flight refresh dedup: prevents race condition that triggers refresh_token_reused → Auth0 family revoke
const refreshPromiseCache = new Map();
function getRefreshCacheKey(provider, refreshToken) {
return `${provider}:${refreshToken}`;
// Dedup: cache in-flight promise + recent result to prevent refresh_token_reused (Auth0 family revoke)
const REFRESH_RESULT_TTL_MS = 10_000;
const refreshDedupCache = new Map();
async function dedupRefresh(provider, oldToken, fn, log) {
if (!oldToken) return fn();
const key = `${provider}:${oldToken}`;
const hit = refreshDedupCache.get(key);
if (hit) {
if (hit.promise) {
log?.info?.("TOKEN_REFRESH", `Reusing in-flight refresh for ${provider}`);
return hit.promise;
}
if (hit.expiresAt > Date.now()) {
log?.info?.("TOKEN_REFRESH", `Reusing recent refresh result for ${provider}`);
return hit.result;
}
refreshDedupCache.delete(key);
}
const promise = (async () => {
try {
const result = await fn();
refreshDedupCache.set(key, { result, expiresAt: Date.now() + REFRESH_RESULT_TTL_MS });
return result;
} catch (err) {
refreshDedupCache.delete(key);
throw err;
}
})();
refreshDedupCache.set(key, { promise });
return promise;
}
// Check if refresh result indicates unrecoverable error (caller should stop retry, force re-auth)
@@ -71,6 +100,7 @@ export async function refreshAccessToken(provider, refreshToken, credentials, lo
return null;
}
return dedupRefresh(provider, refreshToken, async () => {
try {
const response = await fetch(config.refreshUrl, {
method: "POST",
@@ -114,12 +144,15 @@ export async function refreshAccessToken(provider, refreshToken, credentials, lo
});
return null;
}
}, log);
}
/**
* Specialized refresh for Claude OAuth tokens
*/
export async function refreshClaudeOAuthToken(refreshToken, log) {
if (!refreshToken) return null;
return dedupRefresh("claude", refreshToken, async () => {
try {
const response = await fetch(OAUTH_ENDPOINTS.anthropic.token, {
method: "POST",
@@ -147,12 +180,15 @@ export async function refreshClaudeOAuthToken(refreshToken, log) {
log?.error?.("TOKEN_REFRESH", `Network error refreshing Claude token: ${error.message}`);
return null;
}
}, log);
}
/**
* Specialized refresh for Google providers (Gemini, Antigravity)
*/
export async function refreshGoogleToken(refreshToken, clientId, clientSecret, log) {
if (!refreshToken) return null;
return dedupRefresh(`google:${clientId}`, refreshToken, async () => {
try {
const response = await fetch(OAUTH_ENDPOINTS.google.token, {
method: "POST",
@@ -181,12 +217,15 @@ export async function refreshGoogleToken(refreshToken, clientId, clientSecret, l
log?.error?.("TOKEN_REFRESH", `Network error refreshing Google token: ${error.message}`);
return null;
}
}, log);
}
/**
* Specialized refresh for Qwen OAuth tokens
*/
export async function refreshQwenToken(refreshToken, log) {
if (!refreshToken) return null;
return dedupRefresh("qwen", refreshToken, async () => {
const endpoint = OAUTH_ENDPOINTS.qwen.token;
try {
@@ -235,6 +274,7 @@ export async function refreshQwenToken(refreshToken, log) {
log?.error?.("TOKEN_REFRESH", "Failed to refresh Qwen token");
return null;
}, log);
}
/**
@@ -244,6 +284,8 @@ export async function refreshQwenToken(refreshToken, log) {
* so callers stop retrying and request re-authentication.
*/
export async function refreshCodexToken(refreshToken, log) {
if (!refreshToken) return null;
return dedupRefresh("codex", refreshToken, async () => {
try {
const response = await fetch(OAUTH_ENDPOINTS.openai.token, {
method: "POST",
@@ -306,6 +348,7 @@ export async function refreshCodexToken(refreshToken, log) {
log?.error?.("TOKEN_REFRESH", `Network error refreshing Codex token: ${error.message}`);
return null;
}
}, log);
}
/**
@@ -313,6 +356,8 @@ export async function refreshCodexToken(refreshToken, log) {
* Supports both AWS SSO OIDC (Builder ID/IDC) and Social Auth (Google/GitHub)
*/
export async function refreshKiroToken(refreshToken, providerSpecificData, log, proxyOptions = null) {
if (!refreshToken) return null;
return dedupRefresh("kiro", refreshToken, async () => {
const authMethod = providerSpecificData?.authMethod;
const clientId = providerSpecificData?.clientId;
const clientSecret = providerSpecificData?.clientSecret;
@@ -397,12 +442,15 @@ export async function refreshKiroToken(refreshToken, providerSpecificData, log,
refreshToken: tokens.refreshToken || refreshToken,
expiresIn: tokens.expiresIn,
};
}, log);
}
/**
* Specialized refresh for iFlow OAuth tokens
*/
export async function refreshIflowToken(refreshToken, log) {
if (!refreshToken) return null;
return dedupRefresh("iflow", refreshToken, async () => {
const basicAuth = btoa(`${PROVIDERS.iflow.clientId}:${PROVIDERS.iflow.clientSecret}`);
const response = await fetch(OAUTH_ENDPOINTS.iflow.token, {
@@ -442,12 +490,15 @@ export async function refreshIflowToken(refreshToken, log) {
refreshToken: tokens.refresh_token || refreshToken,
expiresIn: tokens.expires_in,
};
}, log);
}
/**
* Specialized refresh for GitHub Copilot OAuth tokens
*/
export async function refreshGitHubToken(refreshToken, log) {
if (!refreshToken) return null;
return dedupRefresh("github", refreshToken, async () => {
const params = {
grant_type: "refresh_token",
refresh_token: refreshToken,
@@ -488,12 +539,15 @@ export async function refreshGitHubToken(refreshToken, log) {
refreshToken: tokens.refresh_token || refreshToken,
expiresIn: tokens.expires_in,
};
}, log);
}
/**
* Refresh GitHub Copilot token using GitHub access token
*/
export async function refreshCopilotToken(githubAccessToken, log) {
if (!githubAccessToken) return null;
return dedupRefresh("copilot", githubAccessToken, async () => {
try {
const response = await fetch("https://api.github.com/copilot_internal/v2/token", {
headers: {
@@ -532,6 +586,7 @@ export async function refreshCopilotToken(githubAccessToken, log) {
});
return null;
}
}, log);
}
/**
@@ -544,20 +599,8 @@ export async function getAccessToken(provider, credentials, log) {
log?.warn?.("TOKEN_REFRESH", `No valid refresh token available for provider: ${provider}`);
return null;
}
const cacheKey = getRefreshCacheKey(provider, credentials.refreshToken);
if (refreshPromiseCache.has(cacheKey)) {
log?.info?.("TOKEN_REFRESH", `Reusing in-flight refresh for ${provider}`);
return refreshPromiseCache.get(cacheKey);
}
const refreshPromise = _getAccessTokenInternal(provider, credentials, log).finally(() => {
refreshPromiseCache.delete(cacheKey);
});
refreshPromiseCache.set(cacheKey, refreshPromise);
return refreshPromise;
// Dedup is handled inside each refreshXxxToken function
return _getAccessTokenInternal(provider, credentials, log);
}
async function _getAccessTokenInternal(provider, credentials, log) {