fix: Convert sync file I/O to async in credential manager and usage module
Replace readFileSync/writeFileSync/mkdirSync with async equivalents from fs/promises in default-credential-manager.ts and usage.ts to stop blocking the Node.js event loop during credential read/write operations.
This commit is contained in:
@@ -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<OAuthCredentials | null> {
|
||||
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<void> {
|
||||
const credPath = join(configDir, '.credentials.json');
|
||||
|
||||
// Read existing credentials to preserve other fields
|
||||
let existing: Record<string, unknown> = {};
|
||||
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<boolean> {
|
||||
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<boole
|
||||
}
|
||||
|
||||
const newRefreshToken = refreshed.refreshToken || credentials.refreshToken;
|
||||
writeCredentials(configDir, refreshed.accessToken, newRefreshToken, refreshed.expiresIn);
|
||||
await writeCredentials(configDir, refreshed.accessToken, newRefreshToken, refreshed.expiresIn);
|
||||
log.info({ configDir, expiresIn: refreshed.expiresIn }, 'credentials refreshed before spawn');
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
* and emits events on credential state changes.
|
||||
*/
|
||||
|
||||
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 { EventBus } from '../../events/index.js';
|
||||
import type {
|
||||
@@ -45,12 +46,12 @@ export class DefaultAccountCredentialManager implements AccountCredentialManager
|
||||
/**
|
||||
* Read credentials from a config directory.
|
||||
*/
|
||||
read(configDir: string): OAuthCredentials | null {
|
||||
async read(configDir: string): Promise<OAuthCredentials | null> {
|
||||
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<void> {
|
||||
const credPath = join(configDir, '.credentials.json');
|
||||
|
||||
// Read existing credentials to preserve other fields
|
||||
let existing: Record<string, unknown> = {};
|
||||
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<CredentialValidationResult> {
|
||||
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<CredentialValidationResult> {
|
||||
const credentials = this.read(configDir);
|
||||
const credentials = await this.read(configDir);
|
||||
|
||||
if (!credentials) {
|
||||
this.emitValidated(accountId, false, null, false);
|
||||
|
||||
@@ -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<OAuthCredentials | null>;
|
||||
|
||||
/**
|
||||
* Check if credentials are expired or about to expire.
|
||||
@@ -76,7 +76,7 @@ export interface AccountCredentialManager {
|
||||
accessToken: string,
|
||||
refreshToken: string,
|
||||
expiresIn: number,
|
||||
): void;
|
||||
): Promise<void>;
|
||||
|
||||
/**
|
||||
* Ensure credentials are valid, refreshing if needed.
|
||||
|
||||
Reference in New Issue
Block a user