feat: 增强 Token 过期验证机制

- 添加 tokenExpiresAt 状态变量追踪实际过期时间
- saveTokens() 保存 expires_at 字段到文件
- loadAuthConfig() 启动时验证 token 是否过期
- shouldRefresh() 优先使用实际过期时间判断
- 提前 30 分钟刷新避免临界问题
- 修复 refreshApiKey() 中的代码缩进问题

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
empty
2025-12-27 15:22:01 +08:00
parent 42fc3f2cf3
commit ed888edfc9

72
auth.js
View File

@@ -10,6 +10,7 @@ import { getRefreshConfig, requestRefreshToken } from './refresh-client.js';
let currentApiKey = null;
let currentRefreshToken = null;
let lastRefreshTime = null;
let tokenExpiresAt = null; // Token 过期时间戳 (ms)
let clientId = null;
let authSource = null; // 'env' or 'file' or 'factory_key' or 'client' or 'multi_account'
let authFilePath = null;
@@ -21,6 +22,7 @@ let refreshInFlight = null; // 刷新锁,避免并发刷新
const REFRESH_URL = 'https://api.workos.com/user_management/authenticate';
const REFRESH_INTERVAL_HOURS = 6; // Refresh every 6 hours
const TOKEN_VALID_HOURS = 8; // Token valid for 8 hours
const REFRESH_BUFFER_MS = 30 * 60 * 1000; // 提前 30 分钟刷新
/**
* Generate a ULID (Universally Unique Lexicographically Sortable Identifier)
@@ -123,9 +125,25 @@ function loadAuthConfig() {
authSource = 'file';
authFilePath = factoryAuthPath;
// Also load access_token if available
// Also load access_token if available and not expired
if (authData.access_token) {
currentApiKey = authData.access_token.trim();
const expiresAt = authData.expires_at ? new Date(authData.expires_at).getTime() : null;
const now = Date.now();
if (expiresAt && expiresAt > now + REFRESH_BUFFER_MS) {
// Token 还有效且距离过期超过30分钟
currentApiKey = authData.access_token.trim();
tokenExpiresAt = expiresAt;
lastRefreshTime = authData.last_updated ? new Date(authData.last_updated).getTime() : now;
logInfo(`Loaded valid token from file, expires at: ${new Date(expiresAt).toISOString()}`);
} else if (expiresAt) {
// Token 已过期或即将过期
logInfo(`Stored token expired or expiring soon (expires_at: ${authData.expires_at}), will refresh`);
} else {
// 没有过期时间记录,按旧逻辑处理
currentApiKey = authData.access_token.trim();
logInfo('Loaded token from file (no expiry info, will check on first use)');
}
}
return { type: 'refresh', value: authData.refresh_token.trim() };
@@ -162,20 +180,22 @@ async function refreshApiKey() {
logInfo('Refreshing API key...');
try {
const proxyAgentInfo = getNextProxyAgent(REFRESH_URL);
const refreshConfig = getRefreshConfig();
const data = await requestRefreshToken({
refreshUrl: REFRESH_URL,
refreshToken: currentRefreshToken,
clientId,
proxyAgentInfo,
...refreshConfig
});
const proxyAgentInfo = getNextProxyAgent(REFRESH_URL);
const refreshConfig = getRefreshConfig();
const data = await requestRefreshToken({
refreshUrl: REFRESH_URL,
refreshToken: currentRefreshToken,
clientId,
proxyAgentInfo,
...refreshConfig
});
// Update tokens
currentApiKey = data.access_token;
currentRefreshToken = data.refresh_token;
lastRefreshTime = Date.now();
// 设置过期时间默认8小时
tokenExpiresAt = lastRefreshTime + TOKEN_VALID_HOURS * 60 * 60 * 1000;
// Log user info
if (data.user) {
@@ -188,6 +208,7 @@ async function refreshApiKey() {
saveTokens(data.access_token, data.refresh_token);
logInfo(`New Refresh-Key: ${currentRefreshToken}`);
logInfo(`Token expires at: ${new Date(tokenExpiresAt).toISOString()}`);
logInfo('API key refreshed successfully');
return data.access_token;
@@ -206,13 +227,20 @@ async function refreshApiKey() {
/**
* Save tokens to appropriate file
* @param {string} accessToken - Access token to save
* @param {string} refreshToken - Refresh token to save
* @param {number} expiresInMs - Token validity duration in milliseconds (default: TOKEN_VALID_HOURS)
*/
function saveTokens(accessToken, refreshToken) {
function saveTokens(accessToken, refreshToken, expiresInMs = TOKEN_VALID_HOURS * 60 * 60 * 1000) {
try {
const now = Date.now();
const expiresAt = new Date(now + expiresInMs).toISOString();
const authData = {
access_token: accessToken,
refresh_token: refreshToken,
last_updated: new Date().toISOString()
expires_at: expiresAt,
last_updated: new Date(now).toISOString()
};
// Ensure directory exists
@@ -228,6 +256,7 @@ function saveTokens(accessToken, refreshToken) {
Object.assign(authData, existingData, {
access_token: accessToken,
refresh_token: refreshToken,
expires_at: expiresAt,
last_updated: authData.last_updated
});
} catch (error) {
@@ -244,14 +273,27 @@ function saveTokens(accessToken, refreshToken) {
}
/**
* Check if API key needs refresh (older than 6 hours)
* Check if API key needs refresh
* Uses actual expiration time if available, falls back to time-based check
*/
function shouldRefresh() {
const now = Date.now();
// 如果有过期时间使用过期时间判断提前30分钟刷新
if (tokenExpiresAt) {
const shouldRefreshByExpiry = now + REFRESH_BUFFER_MS >= tokenExpiresAt;
if (shouldRefreshByExpiry) {
logDebug(`Token expiring soon (expires_at: ${new Date(tokenExpiresAt).toISOString()})`);
}
return shouldRefreshByExpiry;
}
// 回退到基于刷新时间的判断
if (!lastRefreshTime) {
return true;
}
const hoursSinceRefresh = (Date.now() - lastRefreshTime) / (1000 * 60 * 60);
const hoursSinceRefresh = (now - lastRefreshTime) / (1000 * 60 * 60);
return hoursSinceRefresh >= REFRESH_INTERVAL_HOURS;
}