diff --git a/docs/lsp.md b/docs/lsp.md index 5ba6d60f5bf..22ada2175fa 100644 --- a/docs/lsp.md +++ b/docs/lsp.md @@ -48,7 +48,6 @@ sequenceDiagram 3. Enable the lsp experiment: ``` "aws.experiments": { - "amazonqLSP": true, "amazonqLSPInline": true, // optional: enables inline completion from flare "amazonqLSPChat": true // optional: enables chat from flare } diff --git a/packages/amazonq/src/api.ts b/packages/amazonq/src/api.ts index 03b2a32ea55..bd7d5c6a361 100644 --- a/packages/amazonq/src/api.ts +++ b/packages/amazonq/src/api.ts @@ -8,6 +8,7 @@ import { GenerateAssistantResponseCommandOutput, GenerateAssistantResponseReques import { AuthUtil } from 'aws-core-vscode/codewhisperer' import { ChatSession } from 'aws-core-vscode/codewhispererChat' import { api } from 'aws-core-vscode/amazonq' +import { getLogger } from 'aws-core-vscode/shared' export default { chatApi: { @@ -26,8 +27,25 @@ export default { await AuthUtil.instance.showReauthenticatePrompt() } }, + /** + * @deprecated use getAuthState() instead + * + * Legacy function for callers who expect auth state to be granular amongst Q features. + * Auth state is consistent between features, so getAuthState() can be consumed safely for all features. + * + */ async getChatAuthState() { - return AuthUtil.instance.getChatAuthState() + getLogger().warn('Warning: getChatAuthState() is deprecated. Use getAuthState() instead.') + const state = AuthUtil.instance.getAuthState() + const convertedState = state === 'notConnected' ? 'disconnected' : state + return { + codewhispererCore: convertedState, + codewhispererChat: convertedState, + amazonQ: convertedState, + } + }, + getAuthState() { + return AuthUtil.instance.getAuthState() }, }, } satisfies api diff --git a/packages/amazonq/src/app/chat/activation.ts b/packages/amazonq/src/app/chat/activation.ts index 10f827814aa..2cd7a494a83 100644 --- a/packages/amazonq/src/app/chat/activation.ts +++ b/packages/amazonq/src/app/chat/activation.ts @@ -27,7 +27,7 @@ export async function activate(context: ExtensionContext) { const setupLsp = funcUtil.debounce(async () => { void amazonq.LspController.instance.trySetupLsp(context, { - startUrl: AuthUtil.instance.startUrl, + startUrl: AuthUtil.instance.connection?.startUrl, maxIndexSize: CodeWhispererSettings.instance.getMaxIndexSize(), isVectorIndexEnabled: CodeWhispererSettings.instance.isLocalIndexEnabled(), }) @@ -44,8 +44,6 @@ export async function activate(context: ExtensionContext) { amazonq.walkthroughSecurityScanExample.register(), amazonq.openAmazonQWalkthrough.register(), amazonq.listCodeWhispererCommandsWalkthrough.register(), - amazonq.focusAmazonQPanel.register(), - amazonq.focusAmazonQPanelKeybinding.register(), amazonq.tryChatCodeLensCommand.register(), vscode.workspace.onDidChangeConfiguration(async (configurationChangeEvent) => { if (configurationChangeEvent.affectsConfiguration('amazonQ.workspaceIndex')) { diff --git a/packages/amazonq/src/extension.ts b/packages/amazonq/src/extension.ts index fe5ce809c9d..4184c9f951f 100644 --- a/packages/amazonq/src/extension.ts +++ b/packages/amazonq/src/extension.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { AuthUtils, CredentialsStore, LoginManager, initializeAuth } from 'aws-core-vscode/auth' +import { CredentialsStore, LoginManager, authUtils, initializeAuth } from 'aws-core-vscode/auth' import { activate as activateCodeWhisperer, shutdown as shutdownCodeWhisperer } from 'aws-core-vscode/codewhisperer' import { makeEndpointsProvider, registerGenericCommands } from 'aws-core-vscode' import { CommonAuthWebview } from 'aws-core-vscode/login' @@ -33,6 +33,7 @@ import { maybeShowMinVscodeWarning, Experiments, isSageMaker, + Commands, } from 'aws-core-vscode/shared' import { ExtStartUpSources } from 'aws-core-vscode/telemetry' import { VSCODE_EXTENSION_ID } from 'aws-core-vscode/utils' @@ -117,11 +118,11 @@ export async function activateAmazonQCommon(context: vscode.ExtensionContext, is const extContext = { extensionContext: context, } + + await activateAmazonqLsp(context) + // This contains every lsp agnostic things (auth, security scan, code scan) await activateCodeWhisperer(extContext as ExtContext) - if (Experiments.instance.get('amazonqLSP', false)) { - await activateAmazonqLsp(context) - } if (!Experiments.instance.get('amazonqLSPInline', false)) { await activateInlineCompletion() @@ -130,6 +131,10 @@ export async function activateAmazonQCommon(context: vscode.ExtensionContext, is // Generic extension commands registerGenericCommands(context, amazonQContextPrefix) + // Create status bar and reference log UI elements + void Commands.tryExecute('aws.amazonq.refreshStatusBar') + void Commands.tryExecute('aws.amazonq.updateReferenceLog') + // Amazon Q specific commands registerCommands(context) @@ -156,7 +161,7 @@ export async function activateAmazonQCommon(context: vscode.ExtensionContext, is // reload webviews await vscode.commands.executeCommand('workbench.action.webview.reloadWebviewAction') - if (AuthUtils.ExtensionUse.instance.isFirstUse()) { + if (authUtils.ExtensionUse.instance.isFirstUse()) { // Give time for the extension to finish initializing. globals.clock.setTimeout(async () => { CommonAuthWebview.authSource = ExtStartUpSources.firstStartUp @@ -166,7 +171,7 @@ export async function activateAmazonQCommon(context: vscode.ExtensionContext, is context.subscriptions.push( Experiments.instance.onDidChange(async (event) => { - if (event.key === 'amazonqLSP' || event.key === 'amazonqChatLSP' || event.key === 'amazonqLSPInline') { + if (event.key === 'amazonqChatLSP' || event.key === 'amazonqLSPInline') { await vscode.window .showInformationMessage( 'Amazon Q LSP setting has changed. Reload VS Code for the changes to take effect.', diff --git a/packages/amazonq/src/extensionNode.ts b/packages/amazonq/src/extensionNode.ts index 945537b38ee..b7d41ef2cdd 100644 --- a/packages/amazonq/src/extensionNode.ts +++ b/packages/amazonq/src/extensionNode.ts @@ -7,29 +7,22 @@ import * as vscode from 'vscode' import { activateAmazonQCommon, amazonQContextPrefix, deactivateCommon } from './extension' import { DefaultAmazonQAppInitContext } from 'aws-core-vscode/amazonq' import { activate as activateQGumby } from 'aws-core-vscode/amazonqGumby' -import { - ExtContext, - globals, - CrashMonitoring, - getLogger, - isNetworkError, - isSageMaker, - Experiments, -} from 'aws-core-vscode/shared' +import { ExtContext, globals, CrashMonitoring /* getLogger, isSageMaker,*/, Experiments } from 'aws-core-vscode/shared' import { filetypes, SchemaService } from 'aws-core-vscode/sharedNode' import { updateDevMode } from 'aws-core-vscode/dev' import { CommonAuthViewProvider } from 'aws-core-vscode/login' import { isExtensionActive, VSCODE_EXTENSION_ID } from 'aws-core-vscode/utils' import { registerSubmitFeedback } from 'aws-core-vscode/feedback' import { DevOptions } from 'aws-core-vscode/dev' -import { Auth, AuthUtils, getTelemetryMetadataForConn, isAnySsoConnection } from 'aws-core-vscode/auth' +import { Auth /* , AuthUtils, getTelemetryMetadataForConn, isAnySsoConnection*/ } from 'aws-core-vscode/auth' import api from './api' import { activate as activateCWChat } from './app/chat/activation' import { activate as activateInlineChat } from './inlineChat/activation' import { beta } from 'aws-core-vscode/dev' -import { activate as activateNotifications, NotificationsController } from 'aws-core-vscode/notifications' -import { AuthState, AuthUtil } from 'aws-core-vscode/codewhisperer' -import { telemetry, AuthUserState } from 'aws-core-vscode/telemetry' +import * as amazonq from 'aws-core-vscode/amazonq' +import { /* activate as activateNotifications,*/ NotificationsController } from 'aws-core-vscode/notifications' +// import { AuthState, AuthUtil } from 'aws-core-vscode/codewhisperer' +// import { telemetry, AuthUserState } from 'aws-core-vscode/telemetry' export async function activate(context: vscode.ExtensionContext) { // IMPORTANT: No other code should be added to this function. Place it in one of the following 2 functions where appropriate. @@ -58,6 +51,8 @@ async function activateAmazonQNode(context: vscode.ExtensionContext) { } activateInlineChat(context) + context.subscriptions.push(amazonq.focusAmazonQPanel.register(), amazonq.focusAmazonQPanelKeybinding.register()) + const authProvider = new CommonAuthViewProvider( context, amazonQContextPrefix, @@ -78,49 +73,34 @@ async function activateAmazonQNode(context: vscode.ExtensionContext) { await setupDevMode(context) await beta.activate(context) - // TODO: Should probably emit for web as well. + // TODO: @opieter Fix telemetry // Will the web metric look the same? - telemetry.auth_userState.emit({ - passive: true, - result: 'Succeeded', - source: AuthUtils.ExtensionUse.instance.sourceForTelemetry(), - ...(await getAuthState()), - }) - - void activateNotifications(context, getAuthState) + // telemetry.auth_userState.emit({ + // passive: true, + // result: 'Succeeded', + // source: AuthUtils.ExtensionUse.instance.sourceForTelemetry(), + // ...(await getAuthState()), + // }) + + // void activateNotifications(context, getAuthState) } -async function getAuthState(): Promise> { - let authState: AuthState = 'disconnected' - try { - // May call connection validate functions that try to refresh the token. - // This could result in network errors. - authState = (await AuthUtil.instance._getChatAuthState(false)).codewhispererChat - } catch (err) { - if ( - isNetworkError(err) && - AuthUtil.instance.conn && - AuthUtil.instance.auth.getConnectionState(AuthUtil.instance.conn) === 'valid' - ) { - authState = 'connectedWithNetworkError' - } else { - throw err - } - } - const currConn = AuthUtil.instance.conn - if (currConn !== undefined && !(isAnySsoConnection(currConn) || isSageMaker())) { - getLogger().error(`Current Amazon Q connection is not SSO, type is: %s`, currConn?.type) - } - - return { - authStatus: - authState === 'connected' || authState === 'expired' || authState === 'connectedWithNetworkError' - ? authState - : 'notConnected', - authEnabledConnections: AuthUtils.getAuthFormIdsFromConnection(currConn).join(','), - ...(await getTelemetryMetadataForConn(currConn)), - } -} +// async function getAuthState(): Promise> { +// let authState: AuthState = 'disconnected' +// authState = AuthUtil.instance.getAuthState() + +// if (AuthUtil.instance.isConnected() && !(AuthUtil.instance.isSsoSession() || isSageMaker())) { +// getLogger().error('Current Amazon Q connection is not SSO') +// } + +// return { +// authStatus: +// authState === 'connected' || authState === 'expired' +// ? authState +// : 'notConnected', +// ...(await getTelemetryMetadataForConn(currConn)), +// } +// } /** * Some parts of this do not work in Web mode so we need to set Dev Mode up here. diff --git a/packages/amazonq/src/lsp/activation.ts b/packages/amazonq/src/lsp/activation.ts index 84bae8a01a6..462974ee042 100644 --- a/packages/amazonq/src/lsp/activation.ts +++ b/packages/amazonq/src/lsp/activation.ts @@ -4,16 +4,20 @@ */ import vscode from 'vscode' -import { startLanguageServer } from './client' +import { clientId, encryptionKey, startLanguageServer } from './client' import { AmazonQLspInstaller } from './lspInstaller' -import { lspSetupStage, ToolkitError, messages } from 'aws-core-vscode/shared' +import { lspSetupStage, ToolkitError } from 'aws-core-vscode/shared' +import { AuthUtil } from 'aws-core-vscode/codewhisperer' +import { auth2 } from 'aws-core-vscode/auth' -export async function activate(ctx: vscode.ExtensionContext): Promise { +export async function activate(ctx: vscode.ExtensionContext) { try { - await lspSetupStage('all', async () => { + const client = await lspSetupStage('all', async () => { const installResult = await new AmazonQLspInstaller().resolve() - await lspSetupStage('launch', async () => await startLanguageServer(ctx, installResult.resourcePaths)) + return await lspSetupStage('launch', () => startLanguageServer(ctx, installResult.resourcePaths)) }) + AuthUtil.create(new auth2.LanguageClientAuth(client, clientId, encryptionKey)) + await AuthUtil.instance.restore() } catch (err) { const e = err as ToolkitError void messages.showViewLogsMessage(`Failed to launch Amazon Q language server: ${e.message}`) diff --git a/packages/amazonq/src/lsp/client.ts b/packages/amazonq/src/lsp/client.ts index 84079ad3b77..659332d8e70 100644 --- a/packages/amazonq/src/lsp/client.ts +++ b/packages/amazonq/src/lsp/client.ts @@ -6,11 +6,22 @@ import vscode, { env, version } from 'vscode' import * as nls from 'vscode-nls' import * as crypto from 'crypto' +import * as jose from 'jose' import { LanguageClient, LanguageClientOptions } from 'vscode-languageclient' import { InlineCompletionManager } from '../app/inline/completion' -import { AmazonQLspAuth, encryptionKey, notificationTypes } from './auth' import { AuthUtil } from 'aws-core-vscode/codewhisperer' -import { ConnectionMetadata } from '@aws/language-server-runtimes/protocol' +import { + ConnectionMetadata, + GetSsoTokenProgress, + GetSsoTokenProgressToken, + GetSsoTokenProgressType, + MessageActionItem, + ShowDocumentParams, + ShowDocumentRequest, + ShowDocumentResult, + ShowMessageRequest, + ShowMessageRequestParams, +} from '@aws/language-server-runtimes/protocol' import { Settings, oidcClientName, @@ -18,19 +29,25 @@ import { globals, Experiments, Commands, + openUrl, validateNodeExe, getLogger, } from 'aws-core-vscode/shared' import { activate } from './chat/activation' import { AmazonQResourcePaths } from './lspInstaller' +import { auth2 } from 'aws-core-vscode/auth' const localize = nls.loadMessageBundle() const logger = getLogger('amazonqLsp.lspClient') +export const clientId = 'amazonq' +export const clientName = oidcClientName() +export const encryptionKey = crypto.randomBytes(32) + export async function startLanguageServer( extensionContext: vscode.ExtensionContext, resourcePaths: AmazonQResourcePaths -) { +): Promise { const toDispose = extensionContext.subscriptions const serverModule = resourcePaths.lsp @@ -50,8 +67,6 @@ export async function startLanguageServer( }) const documentSelector = [{ scheme: 'file', language: '*' }] - - const clientId = 'amazonq' const traceServerEnabled = Settings.instance.isSet(`${clientId}.trace.server`) await validateNodeExe(resourcePaths.node, resourcePaths.lsp, argv, logger) @@ -93,57 +108,89 @@ export async function startLanguageServer( }), } - const client = new LanguageClient( - clientId, - localize('amazonq.server.name', 'Amazon Q Language Server'), - serverOptions, - clientOptions - ) + const lspName = localize('amazonq.server.name', 'Amazon Q Language Server') + const client = new LanguageClient(clientId, lspName, serverOptions, clientOptions) const disposable = client.start() toDispose.push(disposable) - const auth = new AmazonQLspAuth(client) + await client.onReady() - return client.onReady().then(async () => { - // Request handler for when the server wants to know about the clients auth connnection. Must be registered before the initial auth init call - client.onRequest(notificationTypes.getConnectionMetadata.method, () => { - return { - sso: { - startUrl: AuthUtil.instance.auth.startUrl, - }, - } - }) - await auth.refreshConnection() - - if (Experiments.instance.get('amazonqLSPInline', false)) { - const inlineManager = new InlineCompletionManager(client) - inlineManager.registerInlineCompletion() - toDispose.push( - inlineManager, - Commands.register({ id: 'aws.amazonq.invokeInlineCompletion', autoconnect: true }, async () => { - await vscode.commands.executeCommand('editor.action.inlineSuggest.trigger') - }), - vscode.workspace.onDidCloseTextDocument(async () => { - await vscode.commands.executeCommand('aws.amazonq.rejectCodeSuggestion') - }) - ) + // Request handler for when the server wants to know about the clients auth connnection. Must be registered before the initial auth init call + client.onRequest(auth2.notificationTypes.getConnectionMetadata.method, () => { + return { + sso: { + startUrl: AuthUtil.instance.connection?.startUrl, + }, + } + }) + + client.onRequest(ShowDocumentRequest.method, async (params: ShowDocumentParams) => { + try { + return { success: await openUrl(vscode.Uri.parse(params.uri), lspName) } + } catch (err: any) { + getLogger().error(`Failed to open document for LSP: ${lspName}, error: %s`, err) + return { success: false } + } + }) + + client.onRequest( + ShowMessageRequest.method, + async (params: ShowMessageRequestParams) => { + const actions = params.actions?.map((a) => a.title) ?? [] + const response = await vscode.window.showInformationMessage(params.message, { modal: true }, ...actions) + return params.actions?.find((a) => a.title === response) ?? (undefined as unknown as null) } + ) + + let promise: Promise | undefined + let resolver: () => void = () => {} + client.onProgress(GetSsoTokenProgressType, GetSsoTokenProgressToken, async (partialResult: GetSsoTokenProgress) => { + const decryptedKey = await jose.compactDecrypt(partialResult as unknown as string, encryptionKey) + const val: GetSsoTokenProgress = JSON.parse(decryptedKey.plaintext.toString()) - if (Experiments.instance.get('amazonqChatLSP', false)) { - activate(client, encryptionKey, resourcePaths.ui) + if (val.state === 'InProgress') { + if (promise) { + resolver() + } + promise = new Promise((resolve) => { + resolver = resolve + }) + } else { + resolver() + promise = undefined + return } - const refreshInterval = auth.startTokenRefreshInterval() + void vscode.window.withProgress( + { + cancellable: true, + location: vscode.ProgressLocation.Notification, + title: val.message, + }, + async (_) => { + await promise + } + ) + }) + if (Experiments.instance.get('amazonqLSPInline', false)) { + const inlineManager = new InlineCompletionManager(client) + inlineManager.registerInlineCompletion() toDispose.push( - AuthUtil.instance.auth.onDidChangeActiveConnection(async () => { - await auth.refreshConnection() + inlineManager, + Commands.register({ id: 'aws.amazonq.invokeInlineCompletion', autoconnect: true }, async () => { + await vscode.commands.executeCommand('editor.action.inlineSuggest.trigger') }), - AuthUtil.instance.auth.onDidDeleteConnection(async () => { - client.sendNotification(notificationTypes.deleteBearerToken.method) - }), - { dispose: () => clearInterval(refreshInterval) } + vscode.workspace.onDidCloseTextDocument(async () => { + await vscode.commands.executeCommand('aws.amazonq.rejectCodeSuggestion') + }) ) - }) + } + + if (Experiments.instance.get('amazonqChatLSP', false)) { + activate(client, encryptionKey, resourcePaths.ui) + } + + return client } diff --git a/packages/core/src/amazonq/extApi.ts b/packages/core/src/amazonq/extApi.ts index 2eb16e4cde2..af98b73e59a 100644 --- a/packages/core/src/amazonq/extApi.ts +++ b/packages/core/src/amazonq/extApi.ts @@ -7,9 +7,14 @@ import vscode from 'vscode' import { VSCODE_EXTENSION_ID } from '../shared/extensions' import { SendMessageCommandOutput, SendMessageRequest } from '@amzn/amazon-q-developer-streaming-client' import { GenerateAssistantResponseCommandOutput, GenerateAssistantResponseRequest } from '@amzn/codewhisperer-streaming' -import { FeatureAuthState } from '../codewhisperer/util/authUtil' +import { auth2 } from 'aws-core-vscode/auth' import { ToolkitError } from '../shared/errors' +/** + * @deprecated, for backwards comaptibility only. + */ +type OldAuthState = 'disconnected' | 'expired' | 'connected' + /** * This interface is used and exported by the amazon q extension. If you make a change here then * update the corresponding api implementation in packages/amazonq/src/api.ts @@ -21,7 +26,15 @@ export interface api { } authApi: { reauthIfNeeded(): Promise - getChatAuthState(): Promise + /** + * @deprecated, for backwards comaptibility only. + */ + getChatAuthState(): Promise<{ + codewhispererCore: OldAuthState + codewhispererChat: OldAuthState + amazonQ: OldAuthState + }> + getAuthState(): auth2.AuthState } } diff --git a/packages/core/src/auth/auth2.ts b/packages/core/src/auth/auth2.ts index 7cb9127ee12..f93c03baa2e 100644 --- a/packages/core/src/auth/auth2.ts +++ b/packages/core/src/auth/auth2.ts @@ -31,12 +31,27 @@ import { bearerCredentialsDeleteNotificationType, bearerCredentialsUpdateRequestType, SsoTokenChangedKind, + RequestType, + ResponseMessage, + NotificationType, + ConnectionMetadata, + getConnectionMetadataRequestType, } from '@aws/language-server-runtimes/protocol' import { LanguageClient } from 'vscode-languageclient' import { getLogger } from '../shared/logger/logger' import { ToolkitError } from '../shared/errors' import { useDeviceFlow } from './sso/ssoAccessTokenProvider' +export const notificationTypes = { + updateBearerToken: new RequestType( + bearerCredentialsUpdateRequestType.method + ), + deleteBearerToken: new NotificationType(bearerCredentialsDeleteNotificationType.method), + getConnectionMetadata: new RequestType( + getConnectionMetadataRequestType.method + ), +} + export type AuthState = 'notConnected' | 'connected' | 'expired' export type AuthStateEvent = { id: string; state: AuthState | 'refreshed' } @@ -193,6 +208,10 @@ export class SsoLogin implements BaseLogin { // TODO: DeleteProfile api in Identity Service (this doesn't exist yet) } + async getProfile() { + return await this.lspAuth.getProfile(this.profileName) + } + async updateProfile(opts: { startUrl: string; region: string; scopes: string[] }) { await this.lspAuth.updateProfile(this.profileName, opts.startUrl, opts.region, opts.scopes) this._data = { @@ -205,7 +224,7 @@ export class SsoLogin implements BaseLogin { * Restore the connection state and connection details to memory, if they exist. */ async restore() { - const sessionData = await this.lspAuth.getProfile(this.profileName) + const sessionData = await this.getProfile() const ssoSession = sessionData?.ssoSession?.settings if (ssoSession?.sso_region && ssoSession?.sso_start_url) { this._data = { diff --git a/packages/core/src/auth/index.ts b/packages/core/src/auth/index.ts index 54dd17d702b..3fd687e184b 100644 --- a/packages/core/src/auth/index.ts +++ b/packages/core/src/auth/index.ts @@ -22,4 +22,6 @@ export { export { Auth } from './auth' export { CredentialsStore } from './credentials/store' export { LoginManager } from './deprecated/loginManager' -export * as AuthUtils from './utils' +export * as constants from './sso/constants' +export * as authUtils from './utils' +export * as auth2 from './auth2' diff --git a/packages/core/src/auth/sso/constants.ts b/packages/core/src/auth/sso/constants.ts index 0e6bb082d7e..14d2382a692 100644 --- a/packages/core/src/auth/sso/constants.ts +++ b/packages/core/src/auth/sso/constants.ts @@ -10,6 +10,7 @@ export const builderIdStartUrl = 'https://view.awsapps.com/start' export const internalStartUrl = 'https://amzn.awsapps.com/start' +export const builderIdRegion = 'us-east-1' /** * Doc: https://docs.aws.amazon.com/singlesignon/latest/userguide/howtochangeURL.html diff --git a/packages/core/src/codewhisperer/region/regionProfileManager.ts b/packages/core/src/codewhisperer/region/regionProfileManager.ts index 126dcdece09..3ad74f22d51 100644 --- a/packages/core/src/codewhisperer/region/regionProfileManager.ts +++ b/packages/core/src/codewhisperer/region/regionProfileManager.ts @@ -21,7 +21,7 @@ import { parse } from '@aws-sdk/util-arn-parser' import { isAwsError, ToolkitError } from '../../shared/errors' import { telemetry } from '../../shared/telemetry/telemetry' import { localize } from '../../shared/utilities/vsCodeUtils' -import { AuthUtil } from '../util/authUtil' +import { IAuthProvider } from '../util/authUtil' // TODO: is there a better way to manage all endpoint strings in one place? export const defaultServiceConfig: CodeWhispererConfig = { @@ -48,24 +48,25 @@ export class RegionProfileManager { private _activeRegionProfile: RegionProfile | undefined private _onDidChangeRegionProfile = new vscode.EventEmitter() public readonly onDidChangeRegionProfile = this._onDidChangeRegionProfile.event - // Store the last API results (for UI propuse) so we don't need to call service again if doesn't require "latest" result private _profiles: RegionProfile[] = [] + constructor(private readonly authProvider: IAuthProvider) {} + get activeRegionProfile() { - if (AuthUtil.instance.isBuilderIdConnection()) { + if (this.authProvider.isBuilderIdConnection()) { return undefined } return this._activeRegionProfile } get clientConfig(): CodeWhispererConfig { - if (!AuthUtil.instance.isConnected()) { + if (!this.authProvider.isConnected()) { throw new ToolkitError('trying to get client configuration without credential') } // builder id should simply use default IAD - if (AuthUtil.instance.isBuilderIdConnection()) { + if (this.authProvider.isBuilderIdConnection()) { return defaultServiceConfig } @@ -93,12 +94,10 @@ export class RegionProfileManager { return this._profiles } - constructor() {} - async listRegionProfiles(): Promise { this._profiles = [] - if (!AuthUtil.instance.isConnected() || !AuthUtil.instance.isSsoSession()) { + if (!this.authProvider.isConnected() || !this.authProvider.isSsoSession()) { return [] } const availableProfiles: RegionProfile[] = [] @@ -140,7 +139,7 @@ export class RegionProfileManager { } async switchRegionProfile(regionProfile: RegionProfile | undefined, source: ProfileSwitchIntent) { - if (!AuthUtil.instance.isConnected() || !AuthUtil.instance.isIdcConnection()) { + if (!this.authProvider.isConnected() || !this.authProvider.isIdcConnection()) { return } @@ -165,9 +164,9 @@ export class RegionProfileManager { telemetry.amazonq_didSelectProfile.emit({ source: source, amazonQProfileRegion: this.activeRegionProfile?.region ?? 'not-set', - ssoRegion: AuthUtil.instance.connection?.region, + ssoRegion: this.authProvider.connection?.region, result: 'Cancelled', - credentialStartUrl: AuthUtil.instance.connection?.startUrl, + credentialStartUrl: this.authProvider.connection?.startUrl, profileCount: this.profiles.length, }) return @@ -184,9 +183,9 @@ export class RegionProfileManager { telemetry.amazonq_didSelectProfile.emit({ source: source, amazonQProfileRegion: regionProfile?.region ?? 'not-set', - ssoRegion: AuthUtil.instance.connection?.region, + ssoRegion: this.authProvider.connection?.region, result: 'Succeeded', - credentialStartUrl: AuthUtil.instance.connection?.startUrl, + credentialStartUrl: this.authProvider.connection?.startUrl, profileCount: this.profiles.length, }) } @@ -208,14 +207,14 @@ export class RegionProfileManager { } restoreProfileSelection = once(async () => { - if (AuthUtil.instance.isConnected()) { + if (this.authProvider.isConnected()) { await this.restoreRegionProfile() } }) - // Note: should be called after [AuthUtil.instance.conn] returns non null + // Note: should be called after [this.authProvider.isConnected()] returns non null async restoreRegionProfile() { - const previousSelected = this.loadPersistedRegionProfle()[AuthUtil.instance.profileName] || undefined + const previousSelected = this.loadPersistedRegionProfle()[this.authProvider.profileName] || undefined if (!previousSelected) { return } @@ -261,7 +260,7 @@ export class RegionProfileManager { async persistSelectRegionProfile() { // default has empty arn and shouldn't be persisted because it's just a fallback - if (!AuthUtil.instance.isConnected() || this.activeRegionProfile === undefined) { + if (!this.authProvider.isConnected() || this.activeRegionProfile === undefined) { return } @@ -272,7 +271,7 @@ export class RegionProfileManager { {} ) - previousPersistedState[AuthUtil.instance.profileName] = this.activeRegionProfile + previousPersistedState[this.authProvider.profileName] = this.activeRegionProfile await globals.globalState.update('aws.amazonq.regionProfiles', previousPersistedState) } @@ -327,15 +326,15 @@ export class RegionProfileManager { } } - requireProfileSelection() { - if (AuthUtil.instance.isBuilderIdConnection()) { + requireProfileSelection(): boolean { + if (this.authProvider.isBuilderIdConnection()) { return false } - return AuthUtil.instance.isIdcConnection() && this.activeRegionProfile === undefined + return this.authProvider.isIdcConnection() && this.activeRegionProfile === undefined } async createQClient(region: string, endpoint: string): Promise { - const token = await AuthUtil.instance.getToken() + const token = await this.authProvider.getToken() const serviceOption: ServiceOptions = { apiConfig: userApiConfig, region: region, diff --git a/packages/core/src/codewhisperer/util/authUtil.ts b/packages/core/src/codewhisperer/util/authUtil.ts index ff1da27715d..cdd77d66ea7 100644 --- a/packages/core/src/codewhisperer/util/authUtil.ts +++ b/packages/core/src/codewhisperer/util/authUtil.ts @@ -29,11 +29,22 @@ export const codeWhispererCoreScopes = [...scopesCodeWhispererCore] export const codeWhispererChatScopes = [...codeWhispererCoreScopes, ...scopesCodeWhispererChat] export const amazonQScopes = [...codeWhispererChatScopes, ...scopesGumby, ...scopesFeatureDev] +/** AuthProvider interface for the auth functionality needed by RegionProfileManager */ +export interface IAuthProvider { + isConnected(): boolean + isBuilderIdConnection(): boolean + isIdcConnection(): boolean + isSsoSession(): boolean + getToken(): Promise + readonly profileName: string + readonly connection?: { region: string; startUrl: string } +} + /** * Handles authentication within Amazon Q. * Amazon Q only supports a single connection at a time. */ -export class AuthUtil { +export class AuthUtil implements IAuthProvider { public readonly profileName = VSCODE_EXTENSION_ID.amazonq public readonly regionProfileManager: RegionProfileManager @@ -56,7 +67,7 @@ export class AuthUtil { this.session = new SsoLogin(this.profileName, this.lspAuth) this.onDidChangeConnectionState((e: AuthStateEvent) => this.stateChangeHandler(e)) - this.regionProfileManager = new RegionProfileManager() + this.regionProfileManager = new RegionProfileManager(this) this.regionProfileManager.onDidChangeRegionProfile(async () => { await this.setVscodeContextProps() }) diff --git a/packages/core/src/shared/settings-toolkit.gen.ts b/packages/core/src/shared/settings-toolkit.gen.ts index 59a637a4870..95aaa173c2e 100644 --- a/packages/core/src/shared/settings-toolkit.gen.ts +++ b/packages/core/src/shared/settings-toolkit.gen.ts @@ -42,7 +42,6 @@ export const toolkitSettings = { }, "aws.experiments": { "jsonResourceModification": {}, - "amazonqLSP": {}, "amazonqLSPInline": {}, "amazonqChatLSP": {} }, diff --git a/packages/toolkit/package.json b/packages/toolkit/package.json index b84e3d2ee47..2a665754a84 100644 --- a/packages/toolkit/package.json +++ b/packages/toolkit/package.json @@ -247,10 +247,6 @@ "type": "boolean", "default": false }, - "amazonqLSP": { - "type": "boolean", - "default": false - }, "amazonqLSPInline": { "type": "boolean", "default": false