feat: auto-refresh OAuth token (5min skew, serialized, atomic write)
This commit is contained in:
75
server.js
75
server.js
@@ -21,15 +21,70 @@ const MODELS = {
|
|||||||
haiku: 'claude-haiku-4-5-20251001',
|
haiku: 'claude-haiku-4-5-20251001',
|
||||||
};
|
};
|
||||||
|
|
||||||
function getAccessToken() {
|
const CONFIG_DIR = process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude');
|
||||||
const configDir = process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude');
|
const CREDS_PATH = path.join(CONFIG_DIR, '.credentials.json');
|
||||||
const credsPath = path.join(configDir, '.credentials.json');
|
const OAUTH_CLIENT_ID = '9d1c250a-e61b-44d9-88ed-5944d1962f5e';
|
||||||
try {
|
const REFRESH_SKEW_MS = 5 * 60 * 1000;
|
||||||
const creds = JSON.parse(fs.readFileSync(credsPath, 'utf8'));
|
|
||||||
return creds?.claudeAiOauth?.accessToken || null;
|
function readCreds() {
|
||||||
} catch (e) {
|
try { return JSON.parse(fs.readFileSync(CREDS_PATH, 'utf8')); } catch { return null; }
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function writeCreds(creds) {
|
||||||
|
const tmp = CREDS_PATH + '.tmp';
|
||||||
|
fs.writeFileSync(tmp, JSON.stringify(creds, null, 2));
|
||||||
|
fs.renameSync(tmp, CREDS_PATH);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshTokens(refreshToken) {
|
||||||
|
const resp = await fetch('https://console.anthropic.com/v1/oauth/token', {
|
||||||
|
method: 'POST',
|
||||||
|
dispatcher: anthropicAgent,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'User-Agent': 'claude-cli/2.1.114 (external, cli)',
|
||||||
|
'Accept': '*/*',
|
||||||
|
'anthropic-beta': 'oauth-2025-04-20',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
grant_type: 'refresh_token',
|
||||||
|
refresh_token: refreshToken,
|
||||||
|
client_id: OAUTH_CLIENT_ID,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
if (!resp.ok) throw new Error(`refresh ${resp.status}: ${(await resp.text()).slice(0, 200)}`);
|
||||||
|
return resp.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
let refreshInFlight = null;
|
||||||
|
async function ensureFreshToken() {
|
||||||
|
const creds = readCreds();
|
||||||
|
if (!creds?.claudeAiOauth) return null;
|
||||||
|
const t = creds.claudeAiOauth;
|
||||||
|
if (t.expiresAt && t.expiresAt > Date.now() + REFRESH_SKEW_MS) return t.accessToken;
|
||||||
|
|
||||||
|
// Serialize concurrent refreshes
|
||||||
|
if (!refreshInFlight) {
|
||||||
|
refreshInFlight = (async () => {
|
||||||
|
// Re-read: another fetch may have refreshed while we waited
|
||||||
|
const latest = readCreds();
|
||||||
|
if (latest.claudeAiOauth.expiresAt > Date.now() + REFRESH_SKEW_MS) return;
|
||||||
|
console.log('[oauth] refreshing token…');
|
||||||
|
const r = await refreshTokens(latest.claudeAiOauth.refreshToken);
|
||||||
|
latest.claudeAiOauth.accessToken = r.access_token;
|
||||||
|
latest.claudeAiOauth.refreshToken = r.refresh_token;
|
||||||
|
latest.claudeAiOauth.expiresAt = Date.now() + r.expires_in * 1000;
|
||||||
|
writeCreds(latest);
|
||||||
|
console.log(`[oauth] ✓ refreshed, expires ${new Date(latest.claudeAiOauth.expiresAt).toISOString()}`);
|
||||||
|
})().finally(() => { refreshInFlight = null; });
|
||||||
|
}
|
||||||
|
try { await refreshInFlight; } catch (e) { console.error('[oauth] refresh failed:', e.message); }
|
||||||
|
return readCreds()?.claudeAiOauth?.accessToken || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// keep old sync name for compatibility
|
||||||
|
function getAccessToken() {
|
||||||
|
return readCreds()?.claudeAiOauth?.accessToken || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SYSTEM_PROMPT = [
|
const SYSTEM_PROMPT = [
|
||||||
@@ -90,9 +145,9 @@ app.post('/api/chat', express.json({ limit: '50mb' }), async (req, res) => {
|
|||||||
content = text;
|
content = text;
|
||||||
}
|
}
|
||||||
|
|
||||||
const accessToken = getAccessToken();
|
const accessToken = await ensureFreshToken();
|
||||||
if (!accessToken) {
|
if (!accessToken) {
|
||||||
return res.status(500).json({ error: 'No credentials in ' + (process.env.CLAUDE_CONFIG_DIR || '~/.claude') });
|
return res.status(500).json({ error: 'No credentials in ' + CONFIG_DIR });
|
||||||
}
|
}
|
||||||
|
|
||||||
res.setHeader('Content-Type', 'text/event-stream');
|
res.setHeader('Content-Type', 'text/event-stream');
|
||||||
|
|||||||
Reference in New Issue
Block a user