From 60b9d2419225d7d0caefbf501217b242d7ad7bd0 Mon Sep 17 00:00:00 2001 From: mkdir700 Date: Tue, 31 Mar 2026 19:54:34 -0700 Subject: [PATCH] fix: refresh token invalid after first use --- package-lock.json | 1 + src/config.ts | 22 ++++++++++++++++++++-- src/index.ts | 3 ++- src/oauth.ts | 12 +++++++++++- 4 files changed, 34 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index a6600b8..cdb4fa8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,6 +7,7 @@ "": { "name": "cc-gateway", "version": "0.1.0", + "license": "MIT", "dependencies": { "yaml": "^2.7.0" }, diff --git a/src/config.ts b/src/config.ts index 34e022b..c17720b 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,5 +1,5 @@ -import { readFileSync } from 'fs' -import { parse } from 'yaml' +import { readFileSync, writeFileSync } from 'fs' +import { parse, stringify } from 'yaml' import { resolve } from 'path' export type TokenEntry = { @@ -65,3 +65,21 @@ export function loadConfig(configPath?: string): Config { return config } + +let configFilePath: string | undefined + +export function setConfigPath(path: string) { + configFilePath = path +} + +/** + * Persist a rotated refresh token back to config.yaml so it survives restarts. + */ +export function persistRefreshToken(newToken: string) { + const filePath = configFilePath || resolve(process.cwd(), 'config.yaml') + const raw = readFileSync(filePath, 'utf-8') + const config = parse(raw) as Config + if (config.oauth.refresh_token === newToken) return // no change + config.oauth.refresh_token = newToken + writeFileSync(filePath, stringify(config), 'utf-8') +} diff --git a/src/index.ts b/src/index.ts index 990a221..3e56876 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,4 @@ -import { loadConfig } from './config.js' +import { loadConfig, setConfigPath } from './config.js' import { setLogLevel, log } from './logger.js' import { initOAuth } from './oauth.js' import { startProxy } from './proxy.js' @@ -7,6 +7,7 @@ const configPath = process.argv[2] try { const config = loadConfig(configPath) + if (configPath) setConfigPath(configPath) setLogLevel(config.logging.level) log('info', 'CC Gateway starting...') diff --git a/src/oauth.ts b/src/oauth.ts index 4578856..b35fe6a 100644 --- a/src/oauth.ts +++ b/src/oauth.ts @@ -1,5 +1,6 @@ import { request as httpsRequest } from 'https' import { log } from './logger.js' +import { persistRefreshToken } from './config.js' const TOKEN_URL = 'https://platform.claude.com/v1/oauth/token' const CLIENT_ID = '9d1c250a-e61b-44d9-88ed-5944d1962f5e' @@ -97,9 +98,18 @@ function refreshOAuthToken(refreshToken: string): Promise { reject(new Error(`OAuth refresh failed (${res.statusCode}): ${JSON.stringify(data)}`)) return } + const newRefreshToken = data.refresh_token || refreshToken + if (data.refresh_token && data.refresh_token !== refreshToken) { + log('info', 'Refresh token rotated, persisting to config...') + try { + persistRefreshToken(data.refresh_token) + } catch (err) { + log('error', `Failed to persist rotated refresh token: ${err}`) + } + } resolve({ accessToken: data.access_token, - refreshToken: data.refresh_token || refreshToken, + refreshToken: newRefreshToken, expiresAt: Date.now() + (data.expires_in || 3600) * 1000, }) })