diff --git a/apps/server/agent/accounts/usage.ts b/apps/server/agent/accounts/usage.ts index d75c761..9e70400 100644 --- a/apps/server/agent/accounts/usage.ts +++ b/apps/server/agent/accounts/usage.ts @@ -1,4 +1,5 @@ -import { readFileSync, existsSync, writeFileSync, mkdirSync } from 'node:fs'; +import { existsSync } from 'node:fs'; +import { readFile, writeFile, mkdir } from 'node:fs/promises'; import { join, dirname } from 'node:path'; import type { Account } from '../../db/schema.js'; import type { AgentInfo } from '../types.js'; @@ -57,11 +58,11 @@ export interface AccountHealthResult { activeAgentCount: number; } -function readCredentials(configDir: string): OAuthCredentials | null { +async function readCredentials(configDir: string): Promise { try { const credPath = join(configDir, '.credentials.json'); if (!existsSync(credPath)) return null; - const raw = readFileSync(credPath, 'utf-8'); + const raw = await readFile(credPath, 'utf-8'); const parsed = JSON.parse(raw); const oauth = parsed.claudeAiOauth; if (!oauth || !oauth.accessToken) return null; @@ -86,19 +87,19 @@ function isTokenExpired(credentials: OAuthCredentials): boolean { * Write credentials back to the config directory. * Matches ccswitch's update_credentials_with_token() behavior. */ -function writeCredentials( +async function writeCredentials( configDir: string, accessToken: string, refreshToken: string, expiresIn: number, -): void { +): Promise { const credPath = join(configDir, '.credentials.json'); // Read existing credentials to preserve other fields let existing: Record = {}; try { if (existsSync(credPath)) { - existing = JSON.parse(readFileSync(credPath, 'utf-8')); + existing = JSON.parse(await readFile(credPath, 'utf-8')); } } catch { // Start fresh if can't read @@ -116,10 +117,10 @@ function writeCredentials( existing.claudeAiOauth = claudeAiOauth; // Ensure directory exists - mkdirSync(dirname(credPath), { recursive: true }); + await mkdir(dirname(credPath), { recursive: true }); // Write back (compact JSON for consistency with ccswitch) - writeFileSync(credPath, JSON.stringify(existing)); + await writeFile(credPath, JSON.stringify(existing)); log.debug({ configDir }, 'credentials written after token refresh'); } @@ -249,7 +250,7 @@ export async function checkAccountHealth( subscriptionType = result.credentials.subscriptionType; } else { // Legacy path: direct function calls - const credentials = readCredentials(configDir); + const credentials = await readCredentials(configDir); if (!credentials) { return { ...base, @@ -284,7 +285,7 @@ export async function checkAccountHealth( // Persist the refreshed credentials back to disk const newRefreshToken = refreshed.refreshToken || credentials.refreshToken; - writeCredentials(configDir, accessToken, newRefreshToken, refreshed.expiresIn); + await writeCredentials(configDir, accessToken, newRefreshToken, refreshed.expiresIn); currentExpiresAt = Date.now() + (refreshed.expiresIn * 1000); log.info({ accountId: account.id, expiresIn: refreshed.expiresIn }, 'token refreshed and persisted'); } @@ -344,7 +345,7 @@ export async function checkAccountHealth( * @deprecated Use AccountCredentialManager.ensureValid() instead for event emission support. */ export async function ensureAccountCredentials(configDir: string): Promise { - const credentials = readCredentials(configDir); + const credentials = await readCredentials(configDir); if (!credentials) { log.warn({ configDir }, 'no credentials found'); return false; @@ -368,7 +369,7 @@ export async function ensureAccountCredentials(configDir: string): Promise { try { const credPath = join(configDir, '.credentials.json'); if (!existsSync(credPath)) return null; - const raw = readFileSync(credPath, 'utf-8'); + const raw = await readFile(credPath, 'utf-8'); const parsed = JSON.parse(raw); const oauth = parsed.claudeAiOauth; @@ -112,19 +113,19 @@ export class DefaultAccountCredentialManager implements AccountCredentialManager /** * Write updated credentials to the config directory. */ - write( + async write( configDir: string, accessToken: string, refreshToken: string, expiresIn: number, - ): void { + ): Promise { const credPath = join(configDir, '.credentials.json'); // Read existing credentials to preserve other fields let existing: Record = {}; try { if (existsSync(credPath)) { - existing = JSON.parse(readFileSync(credPath, 'utf-8')); + existing = JSON.parse(await readFile(credPath, 'utf-8')); } } catch { // Start fresh if can't read @@ -142,10 +143,10 @@ export class DefaultAccountCredentialManager implements AccountCredentialManager existing.claudeAiOauth = claudeAiOauth; // Ensure directory exists - mkdirSync(dirname(credPath), { recursive: true }); + await mkdir(dirname(credPath), { recursive: true }); // Write back (compact JSON for consistency) - writeFileSync(credPath, JSON.stringify(existing)); + await writeFile(credPath, JSON.stringify(existing)); log.debug({ configDir }, 'credentials written after token refresh'); } @@ -153,7 +154,7 @@ export class DefaultAccountCredentialManager implements AccountCredentialManager * Ensure credentials are valid, refreshing if needed. */ async ensureValid(configDir: string, accountId?: string): Promise { - const credentials = this.read(configDir); + const credentials = await this.read(configDir); if (!credentials) { log.warn({ configDir, accountId }, 'no credentials found'); @@ -206,7 +207,7 @@ export class DefaultAccountCredentialManager implements AccountCredentialManager // Write refreshed credentials const newRefreshToken = refreshed.refreshToken || credentials.refreshToken; - this.write(configDir, refreshed.accessToken, newRefreshToken, refreshed.expiresIn); + await this.write(configDir, refreshed.accessToken, newRefreshToken, refreshed.expiresIn); const newExpiresAt = Date.now() + refreshed.expiresIn * 1000; log.info({ configDir, accountId, expiresIn: refreshed.expiresIn }, 'credentials refreshed'); @@ -215,7 +216,7 @@ export class DefaultAccountCredentialManager implements AccountCredentialManager this.emitValidated(accountId, true, newExpiresAt, true); // Read back updated credentials - const updatedCredentials = this.read(configDir); + const updatedCredentials = await this.read(configDir); return { valid: true, credentials: updatedCredentials, @@ -228,7 +229,7 @@ export class DefaultAccountCredentialManager implements AccountCredentialManager * Validate credentials without attempting refresh. */ async validate(configDir: string, accountId?: string): Promise { - const credentials = this.read(configDir); + const credentials = await this.read(configDir); if (!credentials) { this.emitValidated(accountId, false, null, false); diff --git a/apps/server/agent/credentials/types.ts b/apps/server/agent/credentials/types.ts index 4d34535..edfdf07 100644 --- a/apps/server/agent/credentials/types.ts +++ b/apps/server/agent/credentials/types.ts @@ -53,7 +53,7 @@ export interface AccountCredentialManager { * Read credentials from a config directory. * Returns null if credentials file is missing or malformed. */ - read(configDir: string): OAuthCredentials | null; + read(configDir: string): Promise; /** * Check if credentials are expired or about to expire. @@ -76,7 +76,7 @@ export interface AccountCredentialManager { accessToken: string, refreshToken: string, expiresIn: number, - ): void; + ): Promise; /** * Ensure credentials are valid, refreshing if needed.