diff --git a/packages/core/src/auth/consoleSessionUtils.ts b/packages/core/src/auth/consoleSessionUtils.ts index e05396de6a8..dd9cb62c9a5 100644 --- a/packages/core/src/auth/consoleSessionUtils.ts +++ b/packages/core/src/auth/consoleSessionUtils.ts @@ -16,8 +16,8 @@ import { CancellationError } from '../shared/utilities/timeoutUtils' import { ToolkitError } from '../shared/errors' import { telemetry } from '../shared/telemetry/telemetry' import { Auth } from './auth' -import { CredentialsId, asString } from './providers/credentials' import { createRegionPrompter } from '../shared/ui/common/region' +import { getConnectionIdFromProfile } from './utils' /** * @description Authenticates with AWS using browser-based login via AWS CLI. @@ -289,14 +289,13 @@ export async function authenticateWithConsoleLogin(profileName?: string, region? ) logger.info(`Activating profile: ${profileName}`) - const credentialsId: CredentialsId = { - credentialSource: 'profile', - credentialTypeId: profileName, - } - const connectionId = asString(credentialsId) + const connectionId = getConnectionIdFromProfile(profileName) // Invalidate cached credentials to force fresh fetch getLogger().info(`Invalidated cached credentials for ${connectionId}`) - globals.loginManager.store.invalidateCredentials(credentialsId) + globals.loginManager.store.invalidateCredentials({ + credentialSource: 'profile', + credentialTypeId: profileName, + }) logger.info(`Looking for connection with ID: ${connectionId}`) // Make sure that connection exists before letting other part use connection diff --git a/packages/core/src/auth/providers/sharedCredentialsProvider.ts b/packages/core/src/auth/providers/sharedCredentialsProvider.ts index c74eca406d8..bb0fb9c106c 100644 --- a/packages/core/src/auth/providers/sharedCredentialsProvider.ts +++ b/packages/core/src/auth/providers/sharedCredentialsProvider.ts @@ -113,7 +113,7 @@ export async function handleInvalidConsoleCredentials( `Reloading window to sync with updated credentials cache using connection for profile: ${profileName}` ) const reloadResponse = await vscode.window.showInformationMessage( - `Credentials for "${profileName}" were updated. A window reload is required to apply them. Save your work before continuing. Reload now?`, + `Credentials for profile "${profileName}" were updated. A window reload is required to apply them. Save your work before continuing. Reload now?`, localizedText.yes, localizedText.no ) diff --git a/packages/core/src/auth/utils.ts b/packages/core/src/auth/utils.ts index 28e2bc1123e..82a79283954 100644 --- a/packages/core/src/auth/utils.ts +++ b/packages/core/src/auth/utils.ts @@ -15,7 +15,7 @@ import { createQuickPick, DataQuickPickItem, showQuickPick } from '../shared/ui/ import { isValidResponse } from '../shared/wizards/wizard' import { CancellationError } from '../shared/utilities/timeoutUtils' import { formatError, ToolkitError } from '../shared/errors' -import { asString } from './providers/credentials' +import { CredentialsId, asString } from './providers/credentials' import { TreeNode } from '../shared/treeview/resourceTreeDataProvider' import { createInputBox } from '../shared/ui/inputPrompter' import { CredentialSourceId, telemetry } from '../shared/telemetry/telemetry' @@ -899,3 +899,32 @@ export function isLocalStackConnection(): boolean { globals.globalState.tryGet('aws.toolkit.externalConnection', String, undefined) === localStackConnectionString ) } + +/** + * Constructs a credentials ID from a profile name. + * + * @param profileName - Profile name + * @returns Credentials ID string + */ +export function getConnectionIdFromProfile(profileName: string): string { + const credentialsId: CredentialsId = { + credentialSource: 'profile', + credentialTypeId: profileName, + } + return asString(credentialsId) +} + +/** + * Sets up and activates a console connection via browser login. + * Prompts user to log in via browser, creates a profile-based connection, and sets it as active. + * + * @param profileName - Profile name (typically Lambda function name) + * @param region - AWS region + * @throws Error if console login fails or user cancels + */ +export async function setupConsoleConnection(profileName: string, region: string): Promise { + getLogger().info('Auth: Sets up a connection via browser login for profile: %s, region: %s', profileName, region) + await vscode.commands.executeCommand('aws.toolkit.auth.consoleLogin', profileName, region) + const connectionId = getConnectionIdFromProfile(profileName) + await Auth.instance.useConnection({ id: connectionId }) +} diff --git a/packages/core/src/lambda/commands/editLambda.ts b/packages/core/src/lambda/commands/editLambda.ts index 5ea8169d280..83552999a5c 100644 --- a/packages/core/src/lambda/commands/editLambda.ts +++ b/packages/core/src/lambda/commands/editLambda.ts @@ -4,6 +4,7 @@ */ import * as vscode from 'vscode' import * as nls from 'vscode-nls' +import { GetFunctionCommandOutput } from '@aws-sdk/client-lambda' import { LambdaFunctionNode } from '../explorer/lambdaFunctionNode' import { downloadLambdaInLocation, openLambdaFile } from './downloadLambda' import { LambdaFunction, runUploadDirectory } from './uploadLambda' @@ -24,6 +25,8 @@ import { telemetry } from '../../shared/telemetry/telemetry' import { ToolkitError } from '../../shared/errors' import { getFunctionWithCredentials } from '../../shared/clients/lambdaClient' import { getLogger } from '../../shared/logger/logger' +import { showViewLogsMessage } from '../../shared/utilities/messages' +import { setupConsoleConnection, getIAMConnection } from '../../auth/utils' const localize = nls.loadMessageBundle() @@ -118,8 +121,9 @@ export async function deployFromTemp(lambda: LambdaFunction, projectUri: vscode. await vscode.workspace.saveAll() try { await runUploadDirectory(lambda, 'zip', projectUri) - } catch { - throw new ToolkitError('Failed to deploy Lambda function', { code: 'deployFailure' }) + } catch (error) { + // Chain error to preserve root cause for troubleshooting deployment failures + throw ToolkitError.chain(error, 'Failed to deploy Lambda function', { code: 'deployFailure' }) } await setFunctionInfo(lambda, { lastDeployed: globals.clock.Date.now(), @@ -225,14 +229,67 @@ export async function editLambda(lambda: LambdaFunction, source?: 'workspace' | }) } +/** + * Retrieves Lambda function configuration with automatic fallback to console credentials. + * Handles credential mismatches (ResourceNotFoundException, AccessDeniedException). + * + * Three scenarios: + * 1. No connection exists → Set up console first, try once, if it fails don't retry (because we already used console) + * 2. Connection exists → Try it first, if it fails with credential error, fall back to console + * 3. Connection exists and fails → Retry with console, if that fails, throw (no second retry) + * + * @param name - Lambda function name + * @param region - AWS region + * @returns Lambda function information with a link to download the deployment package + */ +export async function getFunctionWithFallback(name: string, region: string): Promise { + const activeConnection = await getIAMConnection({ prompt: false }) + // Tracks if we've already attempted console credentials + let calledConsoleLogin = false + + // If no connection, create console connection before first attempt + if (!activeConnection) { + await setupConsoleConnection(name, region) + calledConsoleLogin = true + } + + try { + return await getFunctionWithCredentials(region, name) + } catch (error: any) { + // Detect credential mismatches (ResourceNotFoundException, AccessDeniedException) + let message: string | undefined + if (error.name === 'ResourceNotFoundException') { + message = localize('AWS.lambda.open.functionNotFound', 'Function not found in current account.') + } else if (error.name === 'AccessDeniedException') { + message = localize('AWS.lambda.open.accessDenied', 'Local credentials lack permission to access function.') + } + + if (message) { + void showViewLogsMessage(message, 'warn') + getLogger().warn(message) + } + + if (calledConsoleLogin) { + // Skip retry if we just created console connection - error is not due to credential mismatch + throw ToolkitError.chain(error, 'Failed to get Lambda function with console credentials. Retry skipped.') + } else { + // Retry once with console credentials + await setupConsoleConnection(name, region) + return await getFunctionWithCredentials(region, name) + } + } +} + +/** + * Opens a Lambda function for editing in VS Code. + * Retrieves IAM credentials (with console fallback), downloads function code, and opens it in a new workspace. + * Note: IAM credentials are required to interact with AWS resources, even for SSO users. + */ export async function openLambdaFolderForEdit(name: string, region: string) { const downloadLocation = getTempLocation(name, region) - // Do all authentication work before opening workspace to avoid race condition - const getFunctionOutput = await getFunctionWithCredentials(region, name) + const getFunctionOutput = await getFunctionWithFallback(name, region) const configuration = getFunctionOutput.Configuration - - // Download and set up Lambda code before opening workspace await editLambda( { name, diff --git a/packages/core/src/lambda/uriHandlers.ts b/packages/core/src/lambda/uriHandlers.ts index 8ae1d7b8c35..ee80928b703 100644 --- a/packages/core/src/lambda/uriHandlers.ts +++ b/packages/core/src/lambda/uriHandlers.ts @@ -11,10 +11,24 @@ import { openLambdaFolderForEdit } from './commands/editLambda' import { showConfirmationMessage } from '../shared/utilities/messages' import globals from '../shared/extensionGlobals' import { telemetry } from '../shared/telemetry/telemetry' -import { ToolkitError } from '../shared/errors' +import { ToolkitError, isUserCancelledError } from '../shared/errors' const localize = nls.loadMessageBundle() +export function handleLambdaUriError(e: unknown, functionName: string, region: string): never { + // Classify cancellations for telemetry metrics + const message = e instanceof Error ? e.message.toLowerCase() : '' + const isCancellation = isUserCancelledError(e) || message.includes('canceled') || message.includes('cancelled') + + if (isCancellation) { + throw ToolkitError.chain(e, 'User cancelled operation', { cancelled: true }) + } + + // Handle other errors + void vscode.window.showErrorMessage(`Unable to open function ${functionName} in region ${region}: ${e}`) + throw ToolkitError.chain(e, 'Failed to open Lambda function') +} + export function registerLambdaUriHandler() { async function openFunctionHandler(params: ReturnType) { await telemetry.lambda_uriHandler.run(async () => { @@ -34,7 +48,7 @@ export function registerLambdaUriHandler() { } await openLambdaFolderForEdit(params.functionName, params.region) } catch (e) { - throw new ToolkitError(`Unable to get function ${params.functionName} in region ${params.region}: ${e}`) + handleLambdaUriError(e, params.functionName, params.region) } }) } diff --git a/packages/core/src/login/webview/vue/toolkit/backend_toolkit.ts b/packages/core/src/login/webview/vue/toolkit/backend_toolkit.ts index aea498071cb..01a782ab6a1 100644 --- a/packages/core/src/login/webview/vue/toolkit/backend_toolkit.ts +++ b/packages/core/src/login/webview/vue/toolkit/backend_toolkit.ts @@ -4,7 +4,7 @@ */ import * as vscode from 'vscode' -import { tryAddCredentials } from '../../../../auth/utils' +import { setupConsoleConnection, tryAddCredentials } from '../../../../auth/utils' import { getLogger } from '../../../../shared/logger/logger' import { CommonAuthWebview } from '../backend' import { @@ -21,7 +21,6 @@ import { CodeCatalystAuthenticationProvider } from '../../../../codecatalyst/aut import { AuthError, AuthFlowState } from '../types' import { setContext } from '../../../../shared/vscode/setContext' import { builderIdStartUrl } from '../../../../auth/sso/constants' -import { CredentialsId, asString } from '../../../../auth/providers/credentials' import { RegionProfile } from '../../../../codewhisperer/models/model' import { ProfileSwitchIntent } from '../../../../codewhisperer/region/regionProfileManager' import globals from '../../../../shared/extensionGlobals' @@ -91,16 +90,7 @@ export class ToolkitLoginWebview extends CommonAuthWebview { getLogger().debug(`called startConsoleCredentialSetup()`) const runAuth = async () => { try { - // Execute AWS CLI login command - await vscode.commands.executeCommand('aws.toolkit.auth.consoleLogin', profileName, region) - - // Use profile as active connection - const credentialsId: CredentialsId = { - credentialSource: 'profile', - credentialTypeId: profileName, - } - const connectionId = asString(credentialsId) - await Auth.instance.useConnection({ id: connectionId }) + await setupConsoleConnection(profileName, region) // Hide auth view and show resource explorer await setContext('aws.explorer.showAuthView', false) diff --git a/packages/core/src/shared/clients/lambdaClient.ts b/packages/core/src/shared/clients/lambdaClient.ts index f12bc71c864..7a0f77d858d 100644 --- a/packages/core/src/shared/clients/lambdaClient.ts +++ b/packages/core/src/shared/clients/lambdaClient.ts @@ -349,6 +349,7 @@ export async function getFunctionWithCredentials(region: string, name: string): const credentials = connection.type === 'iam' ? await connection.getCredentials() : fromSSO({ profile: connection.id }) + const client = new LambdaSdkClient({ region, credentials }) const command = new GetFunctionCommand({ FunctionName: name }) diff --git a/packages/core/src/test/auth/utils.test.ts b/packages/core/src/test/auth/utils.test.ts new file mode 100644 index 00000000000..d1ac5c586d0 --- /dev/null +++ b/packages/core/src/test/auth/utils.test.ts @@ -0,0 +1,60 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import assert from 'assert' +import * as sinon from 'sinon' +import * as vscode from 'vscode' +import { Auth } from '../../auth/auth' +import { ToolkitError } from '../../shared/errors' +import * as authUtils from '../../auth/utils' + +describe('getConnectionIdFromProfile', function () { + it('constructs connection ID from profile name', function () { + const result = authUtils.getConnectionIdFromProfile('my-profile') + assert.strictEqual(result, 'profile:my-profile') + }) + + it('handles profile names with special characters', function () { + const result = authUtils.getConnectionIdFromProfile('my-profile-123') + assert.strictEqual(result, 'profile:my-profile-123') + }) +}) + +describe('setupConsoleConnection', function () { + let sandbox: sinon.SinonSandbox + + beforeEach(function () { + sandbox = sinon.createSandbox() + }) + + afterEach(function () { + sandbox.restore() + }) + + it('creates connection after successful console login', async function () { + const executeCommandStub = sandbox.stub(vscode.commands, 'executeCommand').resolves() + const useConnectionStub = sandbox.stub(Auth.instance, 'useConnection').resolves() + + await authUtils.setupConsoleConnection('test-profile', 'us-east-1') + + assert.ok(executeCommandStub.calledOnceWith('aws.toolkit.auth.consoleLogin', 'test-profile', 'us-east-1')) + assert.ok(useConnectionStub.calledOnceWith({ id: 'profile:test-profile' })) + }) + + it('throws error when useConnection fails', async function () { + sandbox.stub(vscode.commands, 'executeCommand').resolves() + const error = new Error('useConnection failed') + sandbox.stub(Auth.instance, 'useConnection').rejects(error) + + await assert.rejects(() => authUtils.setupConsoleConnection('test-profile', 'us-east-1'), error) + }) + + it('throws error when console login command fails', async function () { + const error = new ToolkitError('Console login failed') + sandbox.stub(vscode.commands, 'executeCommand').rejects(error) + + await assert.rejects(() => authUtils.setupConsoleConnection('test-profile', 'us-east-1'), error) + }) +}) diff --git a/packages/core/src/test/lambda/commands/editLambda.test.ts b/packages/core/src/test/lambda/commands/editLambda.test.ts index 44d874c14fe..ec9bbffb97a 100644 --- a/packages/core/src/test/lambda/commands/editLambda.test.ts +++ b/packages/core/src/test/lambda/commands/editLambda.test.ts @@ -14,6 +14,7 @@ import { getReadme, deleteFilesInFolder, overwriteChangesForEdit, + getFunctionWithFallback, } from '../../../lambda/commands/editLambda' import { LambdaFunction } from '../../../lambda/commands/uploadLambda' import * as downloadLambda from '../../../lambda/commands/downloadLambda' @@ -25,6 +26,8 @@ import { LambdaFunctionNodeDecorationProvider } from '../../../lambda/explorer/l import path from 'path' import globals from '../../../shared/extensionGlobals' import { lambdaTempPath } from '../../../lambda/utils' +import * as lambdaClient from '../../../shared/clients/lambdaClient' +import * as authUtils from '../../../auth/utils' describe('editLambda', function () { let mockLambda: LambdaFunction @@ -305,3 +308,127 @@ describe('editLambda', function () { }) }) }) + +describe('getFunctionWithFallback', function () { + let getFunctionWithCredentialsStub: sinon.SinonStub + let setupConsoleConnectionStub: sinon.SinonStub + let showViewLogsMessageStub: sinon.SinonStub + let getIAMConnectionStub: sinon.SinonStub + const mockFunction = { Configuration: { FunctionName: 'test' } } + const mockConnection = { + type: 'iam' as const, + id: 'profile:test', + label: 'profile:test', + getCredentials: sinon.stub().resolves({}), + } + + beforeEach(function () { + getFunctionWithCredentialsStub = sinon.stub(lambdaClient, 'getFunctionWithCredentials') + getIAMConnectionStub = sinon.stub(authUtils, 'getIAMConnection') + setupConsoleConnectionStub = sinon.stub(authUtils, 'setupConsoleConnection') + showViewLogsMessageStub = sinon.stub(messages, 'showViewLogsMessage') + }) + + afterEach(function () { + sinon.restore() + }) + + it('returns function when active connection exists and is valid', async function () { + getFunctionWithCredentialsStub.resolves(mockFunction) + getIAMConnectionStub.resolves(mockConnection) + + const result = await getFunctionWithFallback('test-function', 'us-east-1') + + assert.strictEqual(result, mockFunction) + assert(setupConsoleConnectionStub.notCalled) + }) + + it('creates console connection when no active connection exists', async function () { + getFunctionWithCredentialsStub.resolves(mockFunction) + getIAMConnectionStub.resolves(undefined) + setupConsoleConnectionStub.resolves() + + const result = await getFunctionWithFallback('test-function', 'us-east-1') + + assert.strictEqual(result, mockFunction) + assert(setupConsoleConnectionStub.calledOnce) + assert(setupConsoleConnectionStub.calledWith('test-function', 'us-east-1')) + }) + + it('does not retry when console connection was just created and fails', async function () { + const error = new Error('Console creds not working') + error.name = 'Error' + getFunctionWithCredentialsStub.rejects(error) + getIAMConnectionStub.resolves(undefined) + setupConsoleConnectionStub.resolves() + + await assert.rejects( + () => getFunctionWithFallback('test-function', 'us-east-1'), + (err: Error) => + err.message.includes('Failed to get Lambda function with console credentials. Retry skipped.') + ) + + assert(setupConsoleConnectionStub.calledOnce) + assert(showViewLogsMessageStub.notCalled) + }) + + it('retries with console credentials on ResourceNotFoundException with existing connection', async function () { + const error = new Error('Function not found') + error.name = 'ResourceNotFoundException' + getFunctionWithCredentialsStub.onFirstCall().rejects(error) + getFunctionWithCredentialsStub.onSecondCall().resolves(mockFunction) + const mockConnection = { + type: 'iam' as const, + id: 'profile:test', + label: 'profile:test', + getCredentials: sinon.stub().resolves({ accountId: '123456789' }), + } + getIAMConnectionStub.resolves(mockConnection) + + const result = await getFunctionWithFallback('test-function', 'us-east-1') + + assert.strictEqual(result, mockFunction) + assert(setupConsoleConnectionStub.calledOnce) + assert(setupConsoleConnectionStub.calledWith('test-function', 'us-east-1')) + assert(showViewLogsMessageStub.called) + assert.ok(showViewLogsMessageStub.firstCall.args[0].includes('Function not found')) + }) + + it('retries with console credentials on AccessDeniedException with existing connection', async function () { + const error = new Error('User not authorized to perform: lambda:GetFunction on resource') + error.name = 'AccessDeniedException' + getFunctionWithCredentialsStub.onFirstCall().rejects(error) + getFunctionWithCredentialsStub.onSecondCall().resolves(mockFunction) + const mockConnection = { + type: 'iam' as const, + id: 'profile:test', + label: 'profile:test', + getCredentials: sinon.stub().resolves({ accountId: '987654321' }), + } + getIAMConnectionStub.resolves(mockConnection) + + const result = await getFunctionWithFallback('test-function', 'us-east-2') + + assert.strictEqual(result, mockFunction) + assert(setupConsoleConnectionStub.calledOnce) + assert(setupConsoleConnectionStub.calledWith('test-function', 'us-east-2')) + assert(showViewLogsMessageStub.called) + assert.ok(showViewLogsMessageStub.firstCall.args[0].includes('Local credentials lack permission')) + }) + + it('throws error when setupConsoleConnection fails', async function () { + const setupError = new Error('Console setup failed') + getIAMConnectionStub.resolves(undefined) + setupConsoleConnectionStub.rejects(setupError) + + await assert.rejects( + () => getFunctionWithFallback('test-function', 'us-east-1'), + (err: Error) => err.message.includes('Console setup failed') + ) + + // Verify setupConsoleConnection was called only once (in the initial setup) + assert(setupConsoleConnectionStub.calledOnce) + // Verify getFunctionWithCredentials was never called since setup failed + assert(getFunctionWithCredentialsStub.notCalled) + }) +}) diff --git a/packages/core/src/test/lambda/uriHandlers.test.ts b/packages/core/src/test/lambda/uriHandlers.test.ts index f3e8ae7c368..2131e3d057c 100644 --- a/packages/core/src/test/lambda/uriHandlers.test.ts +++ b/packages/core/src/test/lambda/uriHandlers.test.ts @@ -5,8 +5,10 @@ import assert from 'assert' import { SearchParams } from '../../shared/vscode/uriHandler' -import { parseOpenParams } from '../../lambda/uriHandlers' +import { handleLambdaUriError, parseOpenParams } from '../../lambda/uriHandlers' import { globals } from '../../shared' +import { ToolkitError } from '../../shared/errors' +import { CancellationError } from '../../shared/utilities/timeoutUtils' describe('Lambda URI Handler', function () { describe('load-function', function () { @@ -33,4 +35,38 @@ describe('Lambda URI Handler', function () { assert.deepEqual(parseOpenParams(query), valid) }) }) + + describe('handleLambdaUriError', function () { + it('throws cancelled error for CancellationError', function () { + const error = new CancellationError('user') + assert.throws( + () => handleLambdaUriError(error, 'test-fn', 'us-east-1'), + (e: ToolkitError) => e.cancelled === true + ) + }) + + it('throws cancelled error for "canceled" message', function () { + const error = new Error('Canceled') // vscode reload window + assert.throws( + () => handleLambdaUriError(error, 'test-fn', 'us-east-1'), + (e: ToolkitError) => e.cancelled === true + ) + }) + + it('throws cancelled error for "cancelled" message', function () { + const error = new Error('Timeout token cancelled') + assert.throws( + () => handleLambdaUriError(error, 'test-fn', 'us-east-1'), + (e: ToolkitError) => e.cancelled === true + ) + }) + + it('throws non-cancelled error for other errors', function () { + const error = new Error('Unable to get function') + assert.throws( + () => handleLambdaUriError(error, 'test-fn', 'us-east-1'), + (e: ToolkitError) => e.cancelled !== true + ) + }) + }) }) diff --git a/packages/core/src/test/login/webview/vue/backend_toolkit.test.ts b/packages/core/src/test/login/webview/vue/backend_toolkit.test.ts index 1294a2d0574..a65448412b2 100644 --- a/packages/core/src/test/login/webview/vue/backend_toolkit.test.ts +++ b/packages/core/src/test/login/webview/vue/backend_toolkit.test.ts @@ -120,4 +120,31 @@ describe('Toolkit Login', function () { authEnabledFeatures: 'awsExplorer', }) }) + + it('signs in with console credentials and emits telemetry', async function () { + const stub = sandbox.stub(authUtils, 'setupConsoleConnection').resolves() + await backend.startConsoleCredentialSetup(profileName, region) + + assert.ok(stub.calledOnceWith(profileName, region)) + assertTelemetry('auth_addConnection', { + result: 'Succeeded', + credentialSourceId: 'consoleCredentials', + authEnabledFeatures: 'awsExplorer', + }) + }) + + it('returns error when console credential setup fails', async function () { + const error = new Error('Console login failed') + sandbox.stub(authUtils, 'setupConsoleConnection').rejects(error) + + const result = await backend.startConsoleCredentialSetup(profileName, region) + + assert.strictEqual(result?.id, backend.id) + assert.strictEqual(result?.text, 'Console login failed') + assertTelemetry('auth_addConnection', { + result: 'Failed', + credentialSourceId: 'consoleCredentials', + authEnabledFeatures: 'awsExplorer', + }) + }) }) diff --git a/packages/toolkit/.changes/next-release/Bug Fix-e136b0bf-a7f0-45bb-b6b6-5901c7ad04c0.json b/packages/toolkit/.changes/next-release/Bug Fix-e136b0bf-a7f0-45bb-b6b6-5901c7ad04c0.json new file mode 100644 index 00000000000..e4f4733df2b --- /dev/null +++ b/packages/toolkit/.changes/next-release/Bug Fix-e136b0bf-a7f0-45bb-b6b6-5901c7ad04c0.json @@ -0,0 +1,4 @@ +{ + "type": "Bug Fix", + "description": "Opening Lambda functions from AWS Console now works with missing or mismatched local credentials" +}