mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-19 09:33:53 +00:00
refactor(auth): save authType after successfully authenticated (#1036)
This commit is contained in:
@@ -30,6 +30,7 @@ export {
|
||||
logExtensionEnable,
|
||||
logIdeConnection,
|
||||
logExtensionDisable,
|
||||
logAuth,
|
||||
} from './src/telemetry/loggers.js';
|
||||
|
||||
export {
|
||||
@@ -40,6 +41,7 @@ export {
|
||||
ExtensionEnableEvent,
|
||||
ExtensionUninstallEvent,
|
||||
ModelSlashCommandEvent,
|
||||
AuthEvent,
|
||||
} from './src/telemetry/types.js';
|
||||
export { makeFakeConfig } from './src/test-utils/config.js';
|
||||
export * from './src/utils/pathReader.js';
|
||||
|
||||
@@ -562,7 +562,7 @@ export class Config {
|
||||
}
|
||||
}
|
||||
|
||||
async refreshAuth(authMethod: AuthType) {
|
||||
async refreshAuth(authMethod: AuthType, isInitialAuth?: boolean) {
|
||||
// Vertex and Genai have incompatible encryption and sending history with
|
||||
// throughtSignature from Genai to Vertex will fail, we need to strip them
|
||||
if (
|
||||
@@ -582,6 +582,7 @@ export class Config {
|
||||
newContentGeneratorConfig,
|
||||
this,
|
||||
this.getSessionId(),
|
||||
isInitialAuth,
|
||||
);
|
||||
// Only assign to instance properties after successful initialization
|
||||
this.contentGeneratorConfig = newContentGeneratorConfig;
|
||||
|
||||
@@ -120,6 +120,7 @@ export async function createContentGenerator(
|
||||
config: ContentGeneratorConfig,
|
||||
gcConfig: Config,
|
||||
sessionId?: string,
|
||||
isInitialAuth?: boolean,
|
||||
): Promise<ContentGenerator> {
|
||||
const version = process.env['CLI_VERSION'] || process.version;
|
||||
const userAgent = `QwenCode/${version} (${process.platform}; ${process.arch})`;
|
||||
@@ -191,13 +192,17 @@ export async function createContentGenerator(
|
||||
|
||||
try {
|
||||
// Get the Qwen OAuth client (now includes integrated token management)
|
||||
const qwenClient = await getQwenOauthClient(gcConfig);
|
||||
// If this is initial auth, require cached credentials to detect missing credentials
|
||||
const qwenClient = await getQwenOauthClient(
|
||||
gcConfig,
|
||||
isInitialAuth ? { requireCachedCredentials: true } : undefined,
|
||||
);
|
||||
|
||||
// Create the content generator with dynamic token management
|
||||
return new QwenContentGenerator(qwenClient, config, gcConfig);
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Failed to initialize Qwen: ${error instanceof Error ? error.message : String(error)}`,
|
||||
`${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -825,7 +825,7 @@ describe('getQwenOAuthClient', () => {
|
||||
import('./qwenOAuth2.js').then((module) =>
|
||||
module.getQwenOAuthClient(mockConfig),
|
||||
),
|
||||
).rejects.toThrow('Qwen OAuth authentication failed');
|
||||
).rejects.toThrow('Device authorization flow failed');
|
||||
|
||||
SharedTokenManager.getInstance = originalGetInstance;
|
||||
});
|
||||
@@ -983,7 +983,7 @@ describe('getQwenOAuthClient - Enhanced Error Scenarios', () => {
|
||||
import('./qwenOAuth2.js').then((module) =>
|
||||
module.getQwenOAuthClient(mockConfig),
|
||||
),
|
||||
).rejects.toThrow('Qwen OAuth authentication failed');
|
||||
).rejects.toThrow('Device authorization flow failed');
|
||||
|
||||
SharedTokenManager.getInstance = originalGetInstance;
|
||||
});
|
||||
@@ -1032,7 +1032,7 @@ describe('getQwenOAuthClient - Enhanced Error Scenarios', () => {
|
||||
import('./qwenOAuth2.js').then((module) =>
|
||||
module.getQwenOAuthClient(mockConfig),
|
||||
),
|
||||
).rejects.toThrow('Qwen OAuth authentication timed out');
|
||||
).rejects.toThrow('Authorization timeout, please restart the process.');
|
||||
|
||||
SharedTokenManager.getInstance = originalGetInstance;
|
||||
});
|
||||
@@ -1082,7 +1082,7 @@ describe('getQwenOAuthClient - Enhanced Error Scenarios', () => {
|
||||
module.getQwenOAuthClient(mockConfig),
|
||||
),
|
||||
).rejects.toThrow(
|
||||
'Too many request for Qwen OAuth authentication, please try again later.',
|
||||
'Too many requests. The server is rate limiting our requests. Please select a different authentication method or try again later.',
|
||||
);
|
||||
|
||||
SharedTokenManager.getInstance = originalGetInstance;
|
||||
@@ -1119,7 +1119,7 @@ describe('getQwenOAuthClient - Enhanced Error Scenarios', () => {
|
||||
import('./qwenOAuth2.js').then((module) =>
|
||||
module.getQwenOAuthClient(mockConfig),
|
||||
),
|
||||
).rejects.toThrow('Qwen OAuth authentication failed');
|
||||
).rejects.toThrow('Device authorization flow failed');
|
||||
|
||||
SharedTokenManager.getInstance = originalGetInstance;
|
||||
});
|
||||
@@ -1177,7 +1177,7 @@ describe('authWithQwenDeviceFlow - Comprehensive Testing', () => {
|
||||
import('./qwenOAuth2.js').then((module) =>
|
||||
module.getQwenOAuthClient(mockConfig),
|
||||
),
|
||||
).rejects.toThrow('Qwen OAuth authentication failed');
|
||||
).rejects.toThrow('Device authorization flow failed');
|
||||
|
||||
SharedTokenManager.getInstance = originalGetInstance;
|
||||
});
|
||||
@@ -1264,7 +1264,9 @@ describe('authWithQwenDeviceFlow - Comprehensive Testing', () => {
|
||||
import('./qwenOAuth2.js').then((module) =>
|
||||
module.getQwenOAuthClient(mockConfig),
|
||||
),
|
||||
).rejects.toThrow('Qwen OAuth authentication failed');
|
||||
).rejects.toThrow(
|
||||
'Device code expired or invalid, please restart the authorization process.',
|
||||
);
|
||||
|
||||
SharedTokenManager.getInstance = originalGetInstance;
|
||||
});
|
||||
|
||||
@@ -467,6 +467,7 @@ export type AuthResult =
|
||||
| {
|
||||
success: false;
|
||||
reason: 'timeout' | 'cancelled' | 'error' | 'rate_limit';
|
||||
message?: string; // Detailed error message for better error reporting
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -476,6 +477,7 @@ export const qwenOAuth2Events = new EventEmitter();
|
||||
|
||||
export async function getQwenOAuthClient(
|
||||
config: Config,
|
||||
options?: { requireCachedCredentials?: boolean },
|
||||
): Promise<QwenOAuth2Client> {
|
||||
const client = new QwenOAuth2Client();
|
||||
|
||||
@@ -488,11 +490,6 @@ export async function getQwenOAuthClient(
|
||||
client.setCredentials(credentials);
|
||||
return client;
|
||||
} catch (error: unknown) {
|
||||
console.debug(
|
||||
'Shared token manager failed, attempting device flow:',
|
||||
error,
|
||||
);
|
||||
|
||||
// Handle specific token manager errors
|
||||
if (error instanceof TokenManagerError) {
|
||||
switch (error.type) {
|
||||
@@ -520,12 +517,20 @@ export async function getQwenOAuthClient(
|
||||
// Try device flow instead of forcing refresh
|
||||
const result = await authWithQwenDeviceFlow(client, config);
|
||||
if (!result.success) {
|
||||
throw new Error('Qwen OAuth authentication failed');
|
||||
// Use detailed error message if available, otherwise use default
|
||||
const errorMessage =
|
||||
result.message || 'Qwen OAuth authentication failed';
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
return client;
|
||||
}
|
||||
|
||||
// No cached credentials, use device authorization flow for authentication
|
||||
if (options?.requireCachedCredentials) {
|
||||
throw new Error(
|
||||
'No cached Qwen-OAuth credentials found. Please re-authenticate.',
|
||||
);
|
||||
}
|
||||
|
||||
const result = await authWithQwenDeviceFlow(client, config);
|
||||
if (!result.success) {
|
||||
// Only emit timeout event if the failure reason is actually timeout
|
||||
@@ -538,20 +543,24 @@ export async function getQwenOAuthClient(
|
||||
);
|
||||
}
|
||||
|
||||
// Throw error with appropriate message based on failure reason
|
||||
switch (result.reason) {
|
||||
case 'timeout':
|
||||
throw new Error('Qwen OAuth authentication timed out');
|
||||
case 'cancelled':
|
||||
throw new Error('Qwen OAuth authentication was cancelled by user');
|
||||
case 'rate_limit':
|
||||
throw new Error(
|
||||
'Too many request for Qwen OAuth authentication, please try again later.',
|
||||
);
|
||||
case 'error':
|
||||
default:
|
||||
throw new Error('Qwen OAuth authentication failed');
|
||||
}
|
||||
// Use detailed error message if available, otherwise use default based on reason
|
||||
const errorMessage =
|
||||
result.message ||
|
||||
(() => {
|
||||
switch (result.reason) {
|
||||
case 'timeout':
|
||||
return 'Qwen OAuth authentication timed out';
|
||||
case 'cancelled':
|
||||
return 'Qwen OAuth authentication was cancelled by user';
|
||||
case 'rate_limit':
|
||||
return 'Too many request for Qwen OAuth authentication, please try again later.';
|
||||
case 'error':
|
||||
default:
|
||||
return 'Qwen OAuth authentication failed';
|
||||
}
|
||||
})();
|
||||
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
return client;
|
||||
@@ -644,13 +653,10 @@ async function authWithQwenDeviceFlow(
|
||||
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
||||
// Check if authentication was cancelled
|
||||
if (isCancelled) {
|
||||
console.debug('\nAuthentication cancelled by user.');
|
||||
qwenOAuth2Events.emit(
|
||||
QwenOAuth2Event.AuthProgress,
|
||||
'error',
|
||||
'Authentication cancelled by user.',
|
||||
);
|
||||
return { success: false, reason: 'cancelled' };
|
||||
const message = 'Authentication cancelled by user.';
|
||||
console.debug('\n' + message);
|
||||
qwenOAuth2Events.emit(QwenOAuth2Event.AuthProgress, 'error', message);
|
||||
return { success: false, reason: 'cancelled', message };
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -738,13 +744,14 @@ async function authWithQwenDeviceFlow(
|
||||
|
||||
// Check for cancellation after waiting
|
||||
if (isCancelled) {
|
||||
console.debug('\nAuthentication cancelled by user.');
|
||||
const message = 'Authentication cancelled by user.';
|
||||
console.debug('\n' + message);
|
||||
qwenOAuth2Events.emit(
|
||||
QwenOAuth2Event.AuthProgress,
|
||||
'error',
|
||||
'Authentication cancelled by user.',
|
||||
message,
|
||||
);
|
||||
return { success: false, reason: 'cancelled' };
|
||||
return { success: false, reason: 'cancelled', message };
|
||||
}
|
||||
|
||||
continue;
|
||||
@@ -758,7 +765,7 @@ async function authWithQwenDeviceFlow(
|
||||
);
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
// Handle specific error cases
|
||||
// Extract error information
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : String(error);
|
||||
const statusCode =
|
||||
@@ -766,42 +773,49 @@ async function authWithQwenDeviceFlow(
|
||||
? (error as Error & { status?: number }).status
|
||||
: null;
|
||||
|
||||
if (errorMessage.includes('401') || statusCode === 401) {
|
||||
const message =
|
||||
'Device code expired or invalid, please restart the authorization process.';
|
||||
|
||||
// Emit error event
|
||||
qwenOAuth2Events.emit(QwenOAuth2Event.AuthProgress, 'error', message);
|
||||
|
||||
return { success: false, reason: 'error' };
|
||||
}
|
||||
|
||||
// Handle 429 Too Many Requests error
|
||||
if (errorMessage.includes('429') || statusCode === 429) {
|
||||
const message =
|
||||
'Too many requests. The server is rate limiting our requests. Please select a different authentication method or try again later.';
|
||||
|
||||
// Emit rate limit event to notify user
|
||||
// Helper function to handle error and stop polling
|
||||
const handleError = (
|
||||
reason: 'error' | 'rate_limit',
|
||||
message: string,
|
||||
eventType: 'error' | 'rate_limit' = 'error',
|
||||
): AuthResult => {
|
||||
qwenOAuth2Events.emit(
|
||||
QwenOAuth2Event.AuthProgress,
|
||||
'rate_limit',
|
||||
eventType,
|
||||
message,
|
||||
);
|
||||
console.error('\n' + message);
|
||||
return { success: false, reason, message };
|
||||
};
|
||||
|
||||
console.log('\n' + message);
|
||||
// Handle credential caching failures - stop polling immediately
|
||||
if (errorMessage.includes('Failed to cache credentials')) {
|
||||
return handleError('error', errorMessage);
|
||||
}
|
||||
|
||||
// Return false to stop polling and go back to auth selection
|
||||
return { success: false, reason: 'rate_limit' };
|
||||
// Handle 401 Unauthorized - device code expired or invalid
|
||||
if (errorMessage.includes('401') || statusCode === 401) {
|
||||
return handleError(
|
||||
'error',
|
||||
'Device code expired or invalid, please restart the authorization process.',
|
||||
);
|
||||
}
|
||||
|
||||
// Handle 429 Too Many Requests - rate limiting
|
||||
if (errorMessage.includes('429') || statusCode === 429) {
|
||||
return handleError(
|
||||
'rate_limit',
|
||||
'Too many requests. The server is rate limiting our requests. Please select a different authentication method or try again later.',
|
||||
'rate_limit',
|
||||
);
|
||||
}
|
||||
|
||||
const message = `Error polling for token: ${errorMessage}`;
|
||||
|
||||
// Emit error event
|
||||
qwenOAuth2Events.emit(QwenOAuth2Event.AuthProgress, 'error', message);
|
||||
|
||||
// Check for cancellation before waiting
|
||||
if (isCancelled) {
|
||||
return { success: false, reason: 'cancelled' };
|
||||
const message = 'Authentication cancelled by user.';
|
||||
return { success: false, reason: 'cancelled', message };
|
||||
}
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, pollInterval));
|
||||
@@ -818,11 +832,12 @@ async function authWithQwenDeviceFlow(
|
||||
);
|
||||
|
||||
console.error('\n' + timeoutMessage);
|
||||
return { success: false, reason: 'timeout' };
|
||||
return { success: false, reason: 'timeout', message: timeoutMessage };
|
||||
} catch (error: unknown) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
console.error('Device authorization flow failed:', errorMessage);
|
||||
return { success: false, reason: 'error' };
|
||||
const message = `Device authorization flow failed: ${errorMessage}`;
|
||||
console.error(message);
|
||||
return { success: false, reason: 'error', message };
|
||||
} finally {
|
||||
// Clean up event listener
|
||||
qwenOAuth2Events.off(QwenOAuth2Event.AuthCancel, cancelHandler);
|
||||
@@ -852,10 +867,30 @@ async function loadCachedQwenCredentials(
|
||||
|
||||
async function cacheQwenCredentials(credentials: QwenCredentials) {
|
||||
const filePath = getQwenCachedCredentialPath();
|
||||
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
||||
try {
|
||||
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
||||
|
||||
const credString = JSON.stringify(credentials, null, 2);
|
||||
await fs.writeFile(filePath, credString);
|
||||
const credString = JSON.stringify(credentials, null, 2);
|
||||
await fs.writeFile(filePath, credString);
|
||||
} catch (error: unknown) {
|
||||
// Handle file system errors (e.g., EACCES permission denied)
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
const errorCode =
|
||||
error instanceof Error && 'code' in error
|
||||
? (error as Error & { code?: string }).code
|
||||
: undefined;
|
||||
|
||||
if (errorCode === 'EACCES') {
|
||||
throw new Error(
|
||||
`Failed to cache credentials: Permission denied (EACCES). Current user has no permission to access \`${filePath}\`. Please check permissions.`,
|
||||
);
|
||||
}
|
||||
|
||||
// Throw error for other file system failures
|
||||
throw new Error(
|
||||
`Failed to cache credentials: error when creating folder \`${path.dirname(filePath)}\` and writing to \`${filePath}\`. ${errorMessage}. Please check permissions.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -33,6 +33,7 @@ export const EVENT_MALFORMED_JSON_RESPONSE =
|
||||
export const EVENT_FILE_OPERATION = 'qwen-code.file_operation';
|
||||
export const EVENT_MODEL_SLASH_COMMAND = 'qwen-code.slash_command.model';
|
||||
export const EVENT_SUBAGENT_EXECUTION = 'qwen-code.subagent_execution';
|
||||
export const EVENT_AUTH = 'qwen-code.auth';
|
||||
|
||||
// Performance Events
|
||||
export const EVENT_STARTUP_PERFORMANCE = 'qwen-code.startup.performance';
|
||||
|
||||
@@ -43,6 +43,7 @@ export {
|
||||
logExtensionUninstall,
|
||||
logRipgrepFallback,
|
||||
logNextSpeakerCheck,
|
||||
logAuth,
|
||||
} from './loggers.js';
|
||||
export type { SlashCommandEvent, ChatCompressionEvent } from './types.js';
|
||||
export {
|
||||
@@ -61,6 +62,7 @@ export {
|
||||
ToolOutputTruncatedEvent,
|
||||
RipgrepFallbackEvent,
|
||||
NextSpeakerCheckEvent,
|
||||
AuthEvent,
|
||||
} from './types.js';
|
||||
export { makeSlashCommandEvent, makeChatCompressionEvent } from './types.js';
|
||||
export type { TelemetryEvent } from './types.js';
|
||||
|
||||
@@ -37,6 +37,7 @@ import {
|
||||
EVENT_SUBAGENT_EXECUTION,
|
||||
EVENT_MALFORMED_JSON_RESPONSE,
|
||||
EVENT_INVALID_CHUNK,
|
||||
EVENT_AUTH,
|
||||
} from './constants.js';
|
||||
import {
|
||||
recordApiErrorMetrics,
|
||||
@@ -83,6 +84,7 @@ import type {
|
||||
SubagentExecutionEvent,
|
||||
MalformedJsonResponseEvent,
|
||||
InvalidChunkEvent,
|
||||
AuthEvent,
|
||||
} from './types.js';
|
||||
import type { UiEvent } from './uiTelemetry.js';
|
||||
import { uiTelemetryService } from './uiTelemetry.js';
|
||||
@@ -838,3 +840,29 @@ export function logExtensionDisable(
|
||||
};
|
||||
logger.emit(logRecord);
|
||||
}
|
||||
|
||||
export function logAuth(config: Config, event: AuthEvent): void {
|
||||
QwenLogger.getInstance(config)?.logAuthEvent(event);
|
||||
if (!isTelemetrySdkInitialized()) return;
|
||||
|
||||
const attributes: LogAttributes = {
|
||||
...getCommonAttributes(config),
|
||||
...event,
|
||||
'event.name': EVENT_AUTH,
|
||||
'event.timestamp': new Date().toISOString(),
|
||||
auth_type: event.auth_type,
|
||||
action_type: event.action_type,
|
||||
status: event.status,
|
||||
};
|
||||
|
||||
if (event.error_message) {
|
||||
attributes['error.message'] = event.error_message;
|
||||
}
|
||||
|
||||
const logger = logs.getLogger(SERVICE_NAME);
|
||||
const logRecord: LogRecord = {
|
||||
body: `Auth event: ${event.action_type} ${event.status} for ${event.auth_type}`,
|
||||
attributes,
|
||||
};
|
||||
logger.emit(logRecord);
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@ import type {
|
||||
ExtensionEnableEvent,
|
||||
ModelSlashCommandEvent,
|
||||
ExtensionDisableEvent,
|
||||
AuthEvent,
|
||||
} from '../types.js';
|
||||
import { EndSessionEvent } from '../types.js';
|
||||
import type {
|
||||
@@ -746,6 +747,25 @@ export class QwenLogger {
|
||||
this.flushIfNeeded();
|
||||
}
|
||||
|
||||
logAuthEvent(event: AuthEvent): void {
|
||||
const snapshots: Record<string, unknown> = {
|
||||
auth_type: event.auth_type,
|
||||
action_type: event.action_type,
|
||||
status: event.status,
|
||||
};
|
||||
|
||||
if (event.error_message) {
|
||||
snapshots['error_message'] = event.error_message;
|
||||
}
|
||||
|
||||
const rumEvent = this.createActionEvent('auth', 'auth', {
|
||||
snapshots: JSON.stringify(snapshots),
|
||||
});
|
||||
|
||||
this.enqueueLogEvent(rumEvent);
|
||||
this.flushIfNeeded();
|
||||
}
|
||||
|
||||
// misc events
|
||||
logFlashFallbackEvent(event: FlashFallbackEvent): void {
|
||||
const rumEvent = this.createActionEvent('misc', 'flash_fallback', {
|
||||
|
||||
@@ -686,6 +686,29 @@ export class SubagentExecutionEvent implements BaseTelemetryEvent {
|
||||
}
|
||||
}
|
||||
|
||||
export class AuthEvent implements BaseTelemetryEvent {
|
||||
'event.name': 'auth';
|
||||
'event.timestamp': string;
|
||||
auth_type: AuthType;
|
||||
action_type: 'auto' | 'manual';
|
||||
status: 'success' | 'error' | 'cancelled';
|
||||
error_message?: string;
|
||||
|
||||
constructor(
|
||||
auth_type: AuthType,
|
||||
action_type: 'auto' | 'manual',
|
||||
status: 'success' | 'error' | 'cancelled',
|
||||
error_message?: string,
|
||||
) {
|
||||
this['event.name'] = 'auth';
|
||||
this['event.timestamp'] = new Date().toISOString();
|
||||
this.auth_type = auth_type;
|
||||
this.action_type = action_type;
|
||||
this.status = status;
|
||||
this.error_message = error_message;
|
||||
}
|
||||
}
|
||||
|
||||
export type TelemetryEvent =
|
||||
| StartSessionEvent
|
||||
| EndSessionEvent
|
||||
@@ -713,7 +736,8 @@ export type TelemetryEvent =
|
||||
| ExtensionInstallEvent
|
||||
| ExtensionUninstallEvent
|
||||
| ToolOutputTruncatedEvent
|
||||
| ModelSlashCommandEvent;
|
||||
| ModelSlashCommandEvent
|
||||
| AuthEvent;
|
||||
|
||||
export class ExtensionDisableEvent implements BaseTelemetryEvent {
|
||||
'event.name': 'extension_disable';
|
||||
|
||||
Reference in New Issue
Block a user