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 { join, dirname } from 'node:path';
|
||||||
import type { Account } from '../../db/schema.js';
|
import type { Account } from '../../db/schema.js';
|
||||||
import type { AgentInfo } from '../types.js';
|
import type { AgentInfo } from '../types.js';
|
||||||
@@ -57,11 +58,11 @@ export interface AccountHealthResult {
|
|||||||
activeAgentCount: number;
|
activeAgentCount: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
function readCredentials(configDir: string): OAuthCredentials | null {
|
async function readCredentials(configDir: string): Promise<OAuthCredentials | null> {
|
||||||
try {
|
try {
|
||||||
const credPath = join(configDir, '.credentials.json');
|
const credPath = join(configDir, '.credentials.json');
|
||||||
if (!existsSync(credPath)) return null;
|
if (!existsSync(credPath)) return null;
|
||||||
const raw = readFileSync(credPath, 'utf-8');
|
const raw = await readFile(credPath, 'utf-8');
|
||||||
const parsed = JSON.parse(raw);
|
const parsed = JSON.parse(raw);
|
||||||
const oauth = parsed.claudeAiOauth;
|
const oauth = parsed.claudeAiOauth;
|
||||||
if (!oauth || !oauth.accessToken) return null;
|
if (!oauth || !oauth.accessToken) return null;
|
||||||
@@ -86,19 +87,19 @@ function isTokenExpired(credentials: OAuthCredentials): boolean {
|
|||||||
* Write credentials back to the config directory.
|
* Write credentials back to the config directory.
|
||||||
* Matches ccswitch's update_credentials_with_token() behavior.
|
* Matches ccswitch's update_credentials_with_token() behavior.
|
||||||
*/
|
*/
|
||||||
function writeCredentials(
|
async function writeCredentials(
|
||||||
configDir: string,
|
configDir: string,
|
||||||
accessToken: string,
|
accessToken: string,
|
||||||
refreshToken: string,
|
refreshToken: string,
|
||||||
expiresIn: number,
|
expiresIn: number,
|
||||||
): void {
|
): Promise<void> {
|
||||||
const credPath = join(configDir, '.credentials.json');
|
const credPath = join(configDir, '.credentials.json');
|
||||||
|
|
||||||
// Read existing credentials to preserve other fields
|
// Read existing credentials to preserve other fields
|
||||||
let existing: Record<string, unknown> = {};
|
let existing: Record<string, unknown> = {};
|
||||||
try {
|
try {
|
||||||
if (existsSync(credPath)) {
|
if (existsSync(credPath)) {
|
||||||
existing = JSON.parse(readFileSync(credPath, 'utf-8'));
|
existing = JSON.parse(await readFile(credPath, 'utf-8'));
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// Start fresh if can't read
|
// Start fresh if can't read
|
||||||
@@ -116,10 +117,10 @@ function writeCredentials(
|
|||||||
existing.claudeAiOauth = claudeAiOauth;
|
existing.claudeAiOauth = claudeAiOauth;
|
||||||
|
|
||||||
// Ensure directory exists
|
// Ensure directory exists
|
||||||
mkdirSync(dirname(credPath), { recursive: true });
|
await mkdir(dirname(credPath), { recursive: true });
|
||||||
|
|
||||||
// Write back (compact JSON for consistency with ccswitch)
|
// 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');
|
log.debug({ configDir }, 'credentials written after token refresh');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -249,7 +250,7 @@ export async function checkAccountHealth(
|
|||||||
subscriptionType = result.credentials.subscriptionType;
|
subscriptionType = result.credentials.subscriptionType;
|
||||||
} else {
|
} else {
|
||||||
// Legacy path: direct function calls
|
// Legacy path: direct function calls
|
||||||
const credentials = readCredentials(configDir);
|
const credentials = await readCredentials(configDir);
|
||||||
if (!credentials) {
|
if (!credentials) {
|
||||||
return {
|
return {
|
||||||
...base,
|
...base,
|
||||||
@@ -284,7 +285,7 @@ export async function checkAccountHealth(
|
|||||||
|
|
||||||
// Persist the refreshed credentials back to disk
|
// Persist the refreshed credentials back to disk
|
||||||
const newRefreshToken = refreshed.refreshToken || credentials.refreshToken;
|
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);
|
currentExpiresAt = Date.now() + (refreshed.expiresIn * 1000);
|
||||||
log.info({ accountId: account.id, expiresIn: refreshed.expiresIn }, 'token refreshed and persisted');
|
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.
|
* @deprecated Use AccountCredentialManager.ensureValid() instead for event emission support.
|
||||||
*/
|
*/
|
||||||
export async function ensureAccountCredentials(configDir: string): Promise<boolean> {
|
export async function ensureAccountCredentials(configDir: string): Promise<boolean> {
|
||||||
const credentials = readCredentials(configDir);
|
const credentials = await readCredentials(configDir);
|
||||||
if (!credentials) {
|
if (!credentials) {
|
||||||
log.warn({ configDir }, 'no credentials found');
|
log.warn({ configDir }, 'no credentials found');
|
||||||
return false;
|
return false;
|
||||||
@@ -368,7 +369,7 @@ export async function ensureAccountCredentials(configDir: string): Promise<boole
|
|||||||
}
|
}
|
||||||
|
|
||||||
const newRefreshToken = refreshed.refreshToken || credentials.refreshToken;
|
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');
|
log.info({ configDir, expiresIn: refreshed.expiresIn }, 'credentials refreshed before spawn');
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,8 @@
|
|||||||
* and emits events on credential state changes.
|
* 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 { join, dirname } from 'node:path';
|
||||||
import type { EventBus } from '../../events/index.js';
|
import type { EventBus } from '../../events/index.js';
|
||||||
import type {
|
import type {
|
||||||
@@ -45,12 +46,12 @@ export class DefaultAccountCredentialManager implements AccountCredentialManager
|
|||||||
/**
|
/**
|
||||||
* Read credentials from a config directory.
|
* Read credentials from a config directory.
|
||||||
*/
|
*/
|
||||||
read(configDir: string): OAuthCredentials | null {
|
async read(configDir: string): Promise<OAuthCredentials | null> {
|
||||||
try {
|
try {
|
||||||
const credPath = join(configDir, '.credentials.json');
|
const credPath = join(configDir, '.credentials.json');
|
||||||
if (!existsSync(credPath)) return null;
|
if (!existsSync(credPath)) return null;
|
||||||
|
|
||||||
const raw = readFileSync(credPath, 'utf-8');
|
const raw = await readFile(credPath, 'utf-8');
|
||||||
const parsed = JSON.parse(raw);
|
const parsed = JSON.parse(raw);
|
||||||
const oauth = parsed.claudeAiOauth;
|
const oauth = parsed.claudeAiOauth;
|
||||||
|
|
||||||
@@ -112,19 +113,19 @@ export class DefaultAccountCredentialManager implements AccountCredentialManager
|
|||||||
/**
|
/**
|
||||||
* Write updated credentials to the config directory.
|
* Write updated credentials to the config directory.
|
||||||
*/
|
*/
|
||||||
write(
|
async write(
|
||||||
configDir: string,
|
configDir: string,
|
||||||
accessToken: string,
|
accessToken: string,
|
||||||
refreshToken: string,
|
refreshToken: string,
|
||||||
expiresIn: number,
|
expiresIn: number,
|
||||||
): void {
|
): Promise<void> {
|
||||||
const credPath = join(configDir, '.credentials.json');
|
const credPath = join(configDir, '.credentials.json');
|
||||||
|
|
||||||
// Read existing credentials to preserve other fields
|
// Read existing credentials to preserve other fields
|
||||||
let existing: Record<string, unknown> = {};
|
let existing: Record<string, unknown> = {};
|
||||||
try {
|
try {
|
||||||
if (existsSync(credPath)) {
|
if (existsSync(credPath)) {
|
||||||
existing = JSON.parse(readFileSync(credPath, 'utf-8'));
|
existing = JSON.parse(await readFile(credPath, 'utf-8'));
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// Start fresh if can't read
|
// Start fresh if can't read
|
||||||
@@ -142,10 +143,10 @@ export class DefaultAccountCredentialManager implements AccountCredentialManager
|
|||||||
existing.claudeAiOauth = claudeAiOauth;
|
existing.claudeAiOauth = claudeAiOauth;
|
||||||
|
|
||||||
// Ensure directory exists
|
// Ensure directory exists
|
||||||
mkdirSync(dirname(credPath), { recursive: true });
|
await mkdir(dirname(credPath), { recursive: true });
|
||||||
|
|
||||||
// Write back (compact JSON for consistency)
|
// 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');
|
log.debug({ configDir }, 'credentials written after token refresh');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,7 +154,7 @@ export class DefaultAccountCredentialManager implements AccountCredentialManager
|
|||||||
* Ensure credentials are valid, refreshing if needed.
|
* Ensure credentials are valid, refreshing if needed.
|
||||||
*/
|
*/
|
||||||
async ensureValid(configDir: string, accountId?: string): Promise<CredentialValidationResult> {
|
async ensureValid(configDir: string, accountId?: string): Promise<CredentialValidationResult> {
|
||||||
const credentials = this.read(configDir);
|
const credentials = await this.read(configDir);
|
||||||
|
|
||||||
if (!credentials) {
|
if (!credentials) {
|
||||||
log.warn({ configDir, accountId }, 'no credentials found');
|
log.warn({ configDir, accountId }, 'no credentials found');
|
||||||
@@ -206,7 +207,7 @@ export class DefaultAccountCredentialManager implements AccountCredentialManager
|
|||||||
|
|
||||||
// Write refreshed credentials
|
// Write refreshed credentials
|
||||||
const newRefreshToken = refreshed.refreshToken || credentials.refreshToken;
|
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;
|
const newExpiresAt = Date.now() + refreshed.expiresIn * 1000;
|
||||||
log.info({ configDir, accountId, expiresIn: refreshed.expiresIn }, 'credentials refreshed');
|
log.info({ configDir, accountId, expiresIn: refreshed.expiresIn }, 'credentials refreshed');
|
||||||
@@ -215,7 +216,7 @@ export class DefaultAccountCredentialManager implements AccountCredentialManager
|
|||||||
this.emitValidated(accountId, true, newExpiresAt, true);
|
this.emitValidated(accountId, true, newExpiresAt, true);
|
||||||
|
|
||||||
// Read back updated credentials
|
// Read back updated credentials
|
||||||
const updatedCredentials = this.read(configDir);
|
const updatedCredentials = await this.read(configDir);
|
||||||
return {
|
return {
|
||||||
valid: true,
|
valid: true,
|
||||||
credentials: updatedCredentials,
|
credentials: updatedCredentials,
|
||||||
@@ -228,7 +229,7 @@ export class DefaultAccountCredentialManager implements AccountCredentialManager
|
|||||||
* Validate credentials without attempting refresh.
|
* Validate credentials without attempting refresh.
|
||||||
*/
|
*/
|
||||||
async validate(configDir: string, accountId?: string): Promise<CredentialValidationResult> {
|
async validate(configDir: string, accountId?: string): Promise<CredentialValidationResult> {
|
||||||
const credentials = this.read(configDir);
|
const credentials = await this.read(configDir);
|
||||||
|
|
||||||
if (!credentials) {
|
if (!credentials) {
|
||||||
this.emitValidated(accountId, false, null, false);
|
this.emitValidated(accountId, false, null, false);
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ export interface AccountCredentialManager {
|
|||||||
* Read credentials from a config directory.
|
* Read credentials from a config directory.
|
||||||
* Returns null if credentials file is missing or malformed.
|
* 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.
|
* Check if credentials are expired or about to expire.
|
||||||
@@ -76,7 +76,7 @@ export interface AccountCredentialManager {
|
|||||||
accessToken: string,
|
accessToken: string,
|
||||||
refreshToken: string,
|
refreshToken: string,
|
||||||
expiresIn: number,
|
expiresIn: number,
|
||||||
): void;
|
): Promise<void>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ensure credentials are valid, refreshing if needed.
|
* Ensure credentials are valid, refreshing if needed.
|
||||||
|
|||||||
Reference in New Issue
Block a user