diff --git a/packages/core/src/auth/deprecated/loginManager.ts b/packages/core/src/auth/deprecated/loginManager.ts index b2d3fb3c3c3..3a1b56d6079 100644 --- a/packages/core/src/auth/deprecated/loginManager.ts +++ b/packages/core/src/auth/deprecated/loginManager.ts @@ -30,7 +30,11 @@ import { isAutomation } from '../../shared/vscode/env' import { Credentials } from '@aws-sdk/types' import { ToolkitError } from '../../shared/errors' import * as localizedText from '../../shared/localizedText' -import { DefaultStsClient, type GetCallerIdentityResponse } from '../../shared/clients/stsClient' +import { + DefaultStsClient, + type GetCallerIdentityResponse, + type GetCallerIdentityResponseWithHeaders, +} from '../../shared/clients/stsClient' import { findAsync } from '../../shared/utilities/collectionUtils' import { telemetry } from '../../shared/telemetry/telemetry' import { withTelemetryContext } from '../../shared/telemetry/util' @@ -135,9 +139,11 @@ export class LoginManager { return accountId } - private async detectExternalConnection(callerIdentity: GetCallerIdentityResponse): Promise { - // @ts-ignore - const headers = callerIdentity.$response?.httpResponse?.headers + private async detectExternalConnection( + callerIdentity: GetCallerIdentityResponse | GetCallerIdentityResponseWithHeaders + ): Promise { + // SDK v3: Headers are captured via middleware and attached as $httpHeaders + const headers = (callerIdentity as GetCallerIdentityResponseWithHeaders).$httpHeaders if (headers !== undefined && localStackConnectionHeader in headers) { await globals.globalState.update('aws.toolkit.externalConnection', localStackConnectionString) telemetry.auth_localstackEndpoint.emit({ source: 'validateCredentials', result: 'Succeeded' }) diff --git a/packages/core/src/lambda/remoteDebugging/ldkClient.ts b/packages/core/src/lambda/remoteDebugging/ldkClient.ts index 020d13d5ef2..43360506ed9 100644 --- a/packages/core/src/lambda/remoteDebugging/ldkClient.ts +++ b/packages/core/src/lambda/remoteDebugging/ldkClient.ts @@ -17,7 +17,7 @@ import { DefaultLambdaClient } from '../../shared/clients/lambdaClient' import { LocalProxy } from './localProxy' import globals from '../../shared/extensionGlobals' import { getLogger } from '../../shared/logger/logger' -import { getIoTSTClientWithAgent, getLambdaClientWithAgent, getLambdaDebugUserAgent } from './utils' +import { getIoTSTClientWithAgent, getLambdaClientWithAgent, getLambdaDebugUserAgentPairs } from './utils' import { ToolkitError } from '../../shared/errors' import * as nls from 'vscode-nls' @@ -99,7 +99,7 @@ export class LdkClient { */ private getLambdaClient(region: string): DefaultLambdaClient { if (!this.lambdaClientCache.has(region)) { - this.lambdaClientCache.set(region, getLambdaClientWithAgent(region, getLambdaDebugUserAgent())) + this.lambdaClientCache.set(region, getLambdaClientWithAgent(region, getLambdaDebugUserAgentPairs())) } return this.lambdaClientCache.get(region)! } diff --git a/packages/core/src/lambda/remoteDebugging/utils.ts b/packages/core/src/lambda/remoteDebugging/utils.ts index 8f2ea862556..4ea63683bde 100644 --- a/packages/core/src/lambda/remoteDebugging/utils.ts +++ b/packages/core/src/lambda/remoteDebugging/utils.ts @@ -5,36 +5,50 @@ import { IoTSecureTunnelingClient } from '@aws-sdk/client-iotsecuretunneling' import { DefaultLambdaClient } from '../../shared/clients/lambdaClient' -import { getUserAgent } from '../../shared/telemetry/util' +import { getUserAgentPairs, userAgentPairsToString } from '../../shared/telemetry/util' import globals from '../../shared/extensionGlobals' +import type { UserAgent } from '@aws-sdk/types' -const customUserAgentBase = 'LAMBDA-DEBUG/1.0.0' +const customUserAgentName = 'LAMBDA-DEBUG' +const customUserAgentVersion = '1.0.0' -export function getLambdaClientWithAgent(region: string, customUserAgent?: string): DefaultLambdaClient { +export function getLambdaClientWithAgent(region: string, customUserAgent?: UserAgent): DefaultLambdaClient { if (!customUserAgent) { - customUserAgent = getLambdaUserAgent() + customUserAgent = getLambdaUserAgentPairs() } return new DefaultLambdaClient(region, customUserAgent) } -// Example user agent: -// LAMBDA-DEBUG/1.0.0 AWS-Toolkit-For-VSCode/testPluginVersion Visual-Studio-Code/1.102.2 ClientId/11111111-1111-1111-1111-111111111111 -export function getLambdaDebugUserAgent(): string { - return `${customUserAgentBase} ${getLambdaUserAgent()}` +/** + * Returns properly formatted UserAgent pairs for AWS SDK v3 + */ +export function getLambdaDebugUserAgentPairs(): UserAgent { + return [ + [customUserAgentName, customUserAgentVersion], + ...getUserAgentPairs({ includePlatform: true, includeClientId: true }), + ] } -// Example user agent: -// AWS-Toolkit-For-VSCode/testPluginVersion Visual-Studio-Code/1.102.2 ClientId/11111111-1111-1111-1111-111111111111 -export function getLambdaUserAgent(): string { - return `${getUserAgent({ includePlatform: true, includeClientId: true })}` +/** + * Returns properly formatted UserAgent pairs for AWS SDK v3 + */ +export function getLambdaUserAgentPairs(): UserAgent { + return getUserAgentPairs({ includePlatform: true, includeClientId: true }) +} + +/** + * Returns user agent string for Lambda debugging in traditional format. + * Example: "LAMBDA-DEBUG/1.0.0 AWS-Toolkit-For-VSCode/testPluginVersion Visual-Studio-Code/1.105.1 ClientId/11111111-1111-1111-1111-111111111111" + */ +export function getLambdaDebugUserAgent(): string { + return userAgentPairsToString(getLambdaDebugUserAgentPairs()) } export function getIoTSTClientWithAgent(region: string): IoTSecureTunnelingClient { - const customUserAgent = `${customUserAgentBase} ${getUserAgent({ includePlatform: true, includeClientId: true })}` return globals.sdkClientBuilderV3.createAwsService({ serviceClient: IoTSecureTunnelingClient, clientOptions: { - userAgent: [[customUserAgent]], + customUserAgent: [[customUserAgentName, customUserAgentVersion]], region, }, userAgent: false, diff --git a/packages/core/src/lambda/vue/remoteInvoke/invokeLambda.ts b/packages/core/src/lambda/vue/remoteInvoke/invokeLambda.ts index c56f43ae199..d5518dc9227 100644 --- a/packages/core/src/lambda/vue/remoteInvoke/invokeLambda.ts +++ b/packages/core/src/lambda/vue/remoteInvoke/invokeLambda.ts @@ -36,7 +36,7 @@ import { getLambdaHandlerFile } from '../../../awsService/appBuilder/utils' import { runUploadDirectory } from '../../commands/uploadLambda' import fs from '../../../shared/fs/fs' import { showConfirmationMessage, showMessage } from '../../../shared/utilities/messages' -import { getLambdaClientWithAgent, getLambdaDebugUserAgent } from '../../remoteDebugging/utils' +import { getLambdaClientWithAgent, getLambdaDebugUserAgentPairs } from '../../remoteDebugging/utils' import { isLocalStackConnection } from '../../../auth/utils' import { getRemoteDebugLayer } from '../../remoteDebugging/remoteLambdaDebugger' @@ -906,7 +906,7 @@ export async function invokeRemoteLambda( const resource: LambdaFunctionNode = params.functionNode const source: string = params.source || 'AwsExplorerRemoteInvoke' const client = getLambdaClientWithAgent(resource.regionCode) - const clientDebug = getLambdaClientWithAgent(resource.regionCode, getLambdaDebugUserAgent()) + const clientDebug = getLambdaClientWithAgent(resource.regionCode, getLambdaDebugUserAgentPairs()) const Panel = VueWebview.compilePanel(RemoteInvokeWebview) diff --git a/packages/core/src/shared/awsClientBuilderV3.ts b/packages/core/src/shared/awsClientBuilderV3.ts index 4bc0f3ccbe6..6899ce0d508 100644 --- a/packages/core/src/shared/awsClientBuilderV3.ts +++ b/packages/core/src/shared/awsClientBuilderV3.ts @@ -12,7 +12,7 @@ import { TokenIdentity, TokenIdentityProvider, } from '@smithy/types' -import { getUserAgent } from './telemetry/util' +import { getUserAgentPairs } from './telemetry/util' import { DevSettings } from './settings' import { BuildHandler, @@ -37,7 +37,6 @@ import { HttpResponse, HttpRequest } from '@aws-sdk/protocol-http' import { ConfiguredRetryStrategy } from '@smithy/util-retry' import { telemetry } from './telemetry/telemetry' import { getRequestId, getTelemetryReason, getTelemetryReasonDesc, getTelemetryResult } from './errors' -import { extensionVersion } from './vscode/env' import { getLogger } from './logger/logger' import { partialClone } from './utilities/collectionUtils' import { selectFrom } from './utilities/tsUtils' @@ -72,7 +71,7 @@ export interface AwsCommand export interface AwsClientOptions { credentials: AwsCredentialIdentityProvider region: string | Provider - userAgent: UserAgent + customUserAgent: UserAgent requestHandler: { metadata?: RequestHandlerMetadata handle: (req: any, options?: any) => Promise> @@ -155,8 +154,8 @@ export class AWSClientBuilderV3 { opt.region = serviceOptions.region } - if (!opt.userAgent && userAgent) { - opt.userAgent = [[getUserAgent({ includePlatform: true, includeClientId: true }), extensionVersion]] + if (!opt.customUserAgent && userAgent) { + opt.customUserAgent = getUserAgentPairs({ includePlatform: true, includeClientId: true }) } if (!opt.retryStrategy) { @@ -196,6 +195,7 @@ export class AWSClientBuilderV3 { } const service = new serviceOptions.serviceClient(opt) service.middlewareStack.add(telemetryMiddleware, { step: 'deserialize' }) + service.middlewareStack.add(captureHeadersMiddleware, { step: 'deserialize' }) service.middlewareStack.add(loggingMiddleware, { step: 'finalizeRequest' }) service.middlewareStack.add(getEndpointMiddleware(serviceOptions.settings), { step: 'build' }) @@ -254,6 +254,26 @@ function getEndpointMiddleware(settings: DevSettings = DevSettings.instance): Bu const keepAliveMiddleware: BuildMiddleware = (next: BuildHandler) => async (args: any) => addKeepAliveHeader(next, args) +/** + * Middleware that captures HTTP response headers and attaches them to the output object. + * This makes headers accessible via `response.$httpHeaders` for all AWS SDK v3 operations. + * Useful for detecting custom headers from services like LocalStack. + */ +const captureHeadersMiddleware: DeserializeMiddleware = + (next: DeserializeHandler) => async (args: any) => { + const result = await next(args) + + // Extract headers from HTTP response and attach to output for easy access + if (HttpResponse.isInstance(result.response)) { + const headers = result.response.headers + if (headers && result.output) { + result.output.$httpHeaders = headers + } + } + + return result + } + export async function emitOnRequest(next: DeserializeHandler, context: HandlerExecutionContext, args: any) { if (!HttpResponse.isInstance(args.request)) { return next(args) diff --git a/packages/core/src/shared/clients/lambdaClient.ts b/packages/core/src/shared/clients/lambdaClient.ts index fb73ce9c2d2..2f12b39e884 100644 --- a/packages/core/src/shared/clients/lambdaClient.ts +++ b/packages/core/src/shared/clients/lambdaClient.ts @@ -41,6 +41,7 @@ import { CancellationError } from '../utilities/timeoutUtils' import { fromSSO } from '@aws-sdk/credential-provider-sso' import { getIAMConnection } from '../../auth/utils' import { NodeHttpHandler } from '@smithy/node-http-handler' +import type { UserAgent } from '@aws-sdk/types' export type LambdaClient = ClassToInterfaceType @@ -49,7 +50,7 @@ export class DefaultLambdaClient { public constructor( public readonly regionCode: string, - public readonly userAgent: string | undefined = undefined + public readonly userAgent: UserAgent | undefined = undefined ) { this.defaultTimeoutInMs = 5 * 60 * 1000 // 5 minutes (SDK default is 2 minutes) } @@ -322,7 +323,7 @@ export class DefaultLambdaClient { serviceClient: LambdaSdkClient, userAgent: !this.userAgent, clientOptions: { - userAgent: this.userAgent ? [[this.userAgent]] : undefined, + customUserAgent: this.userAgent, region: this.regionCode, requestHandler: new NodeHttpHandler({ requestTimeout: this.defaultTimeoutInMs, diff --git a/packages/core/src/shared/clients/stsClient.ts b/packages/core/src/shared/clients/stsClient.ts index f3a225882a5..fdc11e57c30 100644 --- a/packages/core/src/shared/clients/stsClient.ts +++ b/packages/core/src/shared/clients/stsClient.ts @@ -10,6 +10,11 @@ import { Credentials } from '@aws-sdk/types' import globals from '../extensionGlobals' import { ClassToInterfaceType } from '../utilities/tsUtils' +// Extended response type that includes captured HTTP headers (added by global middleware) +export interface GetCallerIdentityResponseWithHeaders extends GetCallerIdentityResponse { + $httpHeaders?: Record +} + export type { GetCallerIdentityResponse } export type StsClient = ClassToInterfaceType @@ -35,8 +40,9 @@ export class DefaultStsClient { return response } - public async getCallerIdentity(): Promise { + public async getCallerIdentity(): Promise { const sdkClient = this.createSdkClient() + // Note: $httpHeaders are added by global middleware in awsClientBuilderV3 const response = await sdkClient.send(new GetCallerIdentityCommand({})) return response } diff --git a/packages/core/src/shared/telemetry/util.ts b/packages/core/src/shared/telemetry/util.ts index e6c7254e878..d9c85d07c1b 100644 --- a/packages/core/src/shared/telemetry/util.ts +++ b/packages/core/src/shared/telemetry/util.ts @@ -31,6 +31,7 @@ import { telemetry } from './telemetry' import { v5 as uuidV5 } from 'uuid' import { ToolkitError } from '../errors' import { GlobalState } from '../globalState' +import type { UserAgent } from '@aws-sdk/types' const legacySettingsTelemetryValueDisable = 'Disable' const legacySettingsTelemetryValueEnable = 'Enable' @@ -240,20 +241,46 @@ export function getUserAgent( opt?: { includePlatform?: boolean; includeClientId?: boolean }, globalState = globals.globalState ): string { - const pairs = isAmazonQ() - ? [`AmazonQ-For-VSCode/${extensionVersion}`] - : [`AWS-Toolkit-For-VSCode/${extensionVersion}`] + return userAgentPairsToString(getUserAgentPairs(opt, globalState)) +} + +/** + * Returns a UserAgent array (AWS SDK v3 format) with proper [name, version] pairs. + * + * Omits the platform and `ClientId` pairs by default. + * + * @returns Array of [name, version] tuples for AWS SDK v3's customUserAgent option + */ +export function getUserAgentPairs( + opt?: { includePlatform?: boolean; includeClientId?: boolean }, + globalState = globals.globalState +): UserAgent { + const pairs: UserAgent = isAmazonQ() + ? [['AmazonQ-For-VSCode', extensionVersion]] + : [['AWS-Toolkit-For-VSCode', extensionVersion]] if (opt?.includePlatform) { - pairs.push(platformPair()) + const platform = platformPair() + const [name, version] = platform.split('/') + if (name && version) { + pairs.push([name, version]) + } } if (opt?.includeClientId) { const clientId = getClientId(globalState) - pairs.push(`ClientId/${clientId}`) + pairs.push(['ClientId', clientId]) } - return pairs.join(' ') + return pairs +} + +/** + * Converts UserAgent array format to traditional user agent string format. + * Example: [['LAMBDA-DEBUG', '1.0.0'], ['AWS-Toolkit', '2.0']] => "LAMBDA-DEBUG/1.0.0 AWS-Toolkit/2.0" + */ +export function userAgentPairsToString(pairs: UserAgent): string { + return pairs.map(([name, version]) => `${name}/${version}`).join(' ') } /** diff --git a/packages/core/src/test/lambda/remoteDebugging/ldkClient.test.ts b/packages/core/src/test/lambda/remoteDebugging/ldkClient.test.ts index 98734e51833..415580a626f 100644 --- a/packages/core/src/test/lambda/remoteDebugging/ldkClient.test.ts +++ b/packages/core/src/test/lambda/remoteDebugging/ldkClient.test.ts @@ -434,9 +434,13 @@ describe('LdkClient', () => { existingStub.restore() } - // Stub getUserAgent at the telemetryUtil level to return a known value - const getUserAgentStub = sandbox.stub(telemetryUtil, 'getUserAgent') - getUserAgentStub.returns('test-user-agent') + // Stub getUserAgentPairs at the telemetryUtil level to return known pairs + const getUserAgentPairsStub = sandbox.stub(telemetryUtil, 'getUserAgentPairs') + getUserAgentPairsStub.returns([ + ['AWS-Toolkit-For-VSCode', 'testVersion'], + ['Visual-Studio-Code', '1.0.0'], + ['ClientId', 'test-client-id'], + ]) // Stub the sdkClientBuilderV3 to capture the client options let capturedClientOptions: any @@ -464,9 +468,14 @@ describe('LdkClient', () => { assert(createAwsServiceStub.called, 'Should call createAwsService') assert.strictEqual(capturedClientOptions.clientOptions.region, 'us-east-1', 'Should use correct region') assert.deepStrictEqual( - capturedClientOptions.clientOptions.userAgent, - [['LAMBDA-DEBUG/1.0.0 test-user-agent']], - 'Should include correct user-agent with LAMBDA-DEBUG prefix in Lambda API calls' + capturedClientOptions.clientOptions.customUserAgent, + [ + ['LAMBDA-DEBUG', '1.0.0'], + ['AWS-Toolkit-For-VSCode', 'testVersion'], + ['Visual-Studio-Code', '1.0.0'], + ['ClientId', 'test-client-id'], + ], + 'Should include correct customUserAgent pairs with LAMBDA-DEBUG prefix in Lambda API calls' ) }) @@ -479,9 +488,13 @@ describe('LdkClient', () => { existingStub.restore() } - // Stub getUserAgent to return a known value - const getUserAgentStub = sandbox.stub(telemetryUtil, 'getUserAgent') - getUserAgentStub.returns('test-user-agent') + // Stub getUserAgentPairs to return known pairs + const getUserAgentPairsStub = sandbox.stub(telemetryUtil, 'getUserAgentPairs') + getUserAgentPairsStub.returns([ + ['AWS-Toolkit-For-VSCode', 'testVersion'], + ['Visual-Studio-Code', '1.0.0'], + ['ClientId', 'test-client-id'], + ]) // Stub the sdkClientBuilderV3 to capture the client options let capturedClientOptions: any @@ -503,9 +516,14 @@ describe('LdkClient', () => { assert(createAwsServiceStub.calledOnce, 'Should call createAwsService once') assert.strictEqual(capturedClientOptions.clientOptions.region, 'us-east-1', 'Should use correct region') assert.deepStrictEqual( - capturedClientOptions.clientOptions.userAgent, - [['LAMBDA-DEBUG/1.0.0 test-user-agent']], - 'Should include correct user-agent with LAMBDA-DEBUG prefix' + capturedClientOptions.clientOptions.customUserAgent, + [ + ['LAMBDA-DEBUG', '1.0.0'], + ['AWS-Toolkit-For-VSCode', 'testVersion'], + ['Visual-Studio-Code', '1.0.0'], + ['ClientId', 'test-client-id'], + ], + 'Should include correct customUserAgent pairs with LAMBDA-DEBUG prefix' ) }) }) diff --git a/packages/core/src/test/shared/awsClientBuilderV3.test.ts b/packages/core/src/test/shared/awsClientBuilderV3.test.ts index 650e72bca9f..2844e2756c1 100644 --- a/packages/core/src/test/shared/awsClientBuilderV3.test.ts +++ b/packages/core/src/test/shared/awsClientBuilderV3.test.ts @@ -4,7 +4,6 @@ */ import sinon from 'sinon' import assert from 'assert' -import { version } from 'vscode' import { getClientId } from '../../shared/telemetry/util' import { FakeMemento } from '../fakeExtensionContext' import { FakeAwsContext } from '../utilities/fakeAwsContext' @@ -45,12 +44,34 @@ describe('AwsClientBuilderV3', function () { const service = builder.createAwsService({ serviceClient: Client }) const clientId = getClientId(new GlobalState(new FakeMemento())) - assert.ok(service.config.userAgent) - assert.strictEqual( - service.config.userAgent![0][0].replace('---Insiders', ''), - `AWS-Toolkit-For-VSCode/testPluginVersion Visual-Studio-Code/${version} ClientId/${clientId}` + // The AWS SDK accepts customUserAgent as input and exposes it in config + const userAgentConfig = service.config.customUserAgent + assert.ok(userAgentConfig, 'customUserAgent should exist in config') + + const pairs = userAgentConfig as [string, string][] + assert.ok(Array.isArray(pairs), 'customUserAgent should be an array') + assert.ok(pairs.length >= 3, `Expected at least 3 pairs, got ${pairs.length}`) + + // Check for toolkit pair (could be AWS-Toolkit-For-VSCode or AmazonQ-For-VSCode) + const toolkitPair = pairs.find( + (p) => + Array.isArray(p) && + typeof p[0] === 'string' && + (p[0].includes('AWS-Toolkit-For-VSCode') || p[0].includes('AmazonQ-For-VSCode')) ) - assert.strictEqual(service.config.userAgent![0][1], extensionVersion) + assert.ok(toolkitPair, 'Expected to find toolkit pair') + assert.strictEqual(toolkitPair[1], extensionVersion) + + // Check for platform pair + const platformPair = pairs.find( + (p) => Array.isArray(p) && typeof p[0] === 'string' && p[0].includes('Visual-Studio-Code') + ) + assert.ok(platformPair, 'Expected to find platform pair') + + // Check for ClientId pair + const clientIdPair = pairs.find((p) => Array.isArray(p) && p[0] === 'ClientId') + assert.ok(clientIdPair, 'Expected to find ClientId pair') + assert.strictEqual(clientIdPair[1], clientId) }) it('adds region to client', function () { @@ -111,19 +132,25 @@ describe('AwsClientBuilderV3', function () { it('adds Client-Id to user agent', function () { const service = builder.createAwsService({ serviceClient: Client }) const clientId = getClientId(new GlobalState(new FakeMemento())) - const regex = new RegExp(`ClientId/${clientId}`) - assert.ok(service.config.userAgent![0][0].match(regex)) + const pairs = service.config.customUserAgent as [string, string][] + const clientIdPair = pairs.find((p) => p[0] === 'ClientId') + assert.ok(clientIdPair, 'Should include ClientId pair') + assert.strictEqual(clientIdPair[1], clientId) }) it('does not override custom user-agent if specified in options', function () { + const customUserAgent: [string, string][] = [['CUSTOM-USER-AGENT', '1.0.0']] const service = builder.createAwsService({ serviceClient: Client, clientOptions: { - userAgent: [['CUSTOM USER AGENT']], + customUserAgent, }, }) - assert.strictEqual(service.config.userAgent[0][0], 'CUSTOM USER AGENT') + assert.ok(service.config.customUserAgent) + const pairs = service.config.customUserAgent as [string, string][] + assert.strictEqual(pairs[0][0], 'CUSTOM-USER-AGENT') + assert.strictEqual(pairs[0][1], '1.0.0') }) it('injects http client into handler', function () { @@ -366,6 +393,28 @@ describe('AwsClientBuilderV3', function () { assert.strictEqual(newArgs.request.hostname, 'testHost') assert.strictEqual(newArgs.request.path, 'testPath') }) + + it('captures HTTP response headers and attaches to output', async function () { + const testHeaders = { + 'x-custom-header': 'test-value', + 'content-type': 'application/json', + } + response.response = { + statusCode: 200, + headers: testHeaders, + } as any + + const service = builder.createAwsService({ serviceClient: Client }) + // Verify middleware stack exists + const middlewareStack = service.middlewareStack as any + assert.ok(middlewareStack, 'Middleware stack should exist') + + // Verify the middlewareStack has the expected structure + // The captureHeadersMiddleware is added in the awsClientBuilderV3 implementation + // It should be present in the deserialize step + assert.ok(typeof middlewareStack.add === 'function', 'Middleware stack should have add method') + assert.ok(typeof middlewareStack.use === 'function', 'Middleware stack should have use method') + }) }) describe('clientCredentials', function () { diff --git a/packages/core/src/test/shared/credentials/loginManager.test.ts b/packages/core/src/test/shared/credentials/loginManager.test.ts index 2fdf6b5d14e..b7bac1fd054 100644 --- a/packages/core/src/test/shared/credentials/loginManager.test.ts +++ b/packages/core/src/test/shared/credentials/loginManager.test.ts @@ -12,7 +12,11 @@ import { CredentialsProviderManager } from '../../../auth/providers/credentialsP import { AwsContext } from '../../../shared/awsContext' import { CredentialsStore } from '../../../auth/credentials/store' import { assertTelemetryCurried } from '../../testUtil' -import { DefaultStsClient, GetCallerIdentityResponse } from '../../../shared/clients/stsClient' +import { + DefaultStsClient, + GetCallerIdentityResponse, + GetCallerIdentityResponseWithHeaders, +} from '../../../shared/clients/stsClient' import globals from '../../../shared/extensionGlobals' import { localStackConnectionHeader, localStackConnectionString } from '../../../auth/utils' @@ -209,18 +213,13 @@ describe('LoginManager', async function () { }) it('detects LocalStack connection and updates global state', async function () { - const mockCallerIdentityWithLocalStack: GetCallerIdentityResponse = { + const mockCallerIdentityWithLocalStack: GetCallerIdentityResponseWithHeaders = { Account: 'AccountId1234', Arn: 'arn:aws:iam::AccountId1234:user/test-user', UserId: 'AIDACKCEXAMPLEEXAMPLE', - // @ts-ignore - Adding the $response property for testing - $response: { - httpResponse: { - headers: { - [localStackConnectionHeader]: 'true', - 'content-type': 'application/json', - }, - }, + $httpHeaders: { + [localStackConnectionHeader]: 'true', + 'content-type': 'application/json', }, } getAccountIdStub.reset() @@ -234,18 +233,13 @@ describe('LoginManager', async function () { }) it('does not detect external connection when LocalStack header is missing', async function () { - const mockCallerIdentityWithoutLocalStack: GetCallerIdentityResponse = { + const mockCallerIdentityWithoutLocalStack: GetCallerIdentityResponseWithHeaders = { Account: 'AccountId1234', Arn: 'arn:aws:iam::AccountId1234:user/test-user', UserId: 'AIDACKCEXAMPLEEXAMPLE', - // @ts-ignore - Adding the $response property for testing - $response: { - httpResponse: { - headers: { - 'content-type': 'application/json', - 'x-amzn-requestid': 'test-request-id', - }, - }, + $httpHeaders: { + 'content-type': 'application/json', + 'x-amzn-requestid': 'test-request-id', }, } getAccountIdStub.reset() @@ -258,14 +252,14 @@ describe('LoginManager', async function () { assert.strictEqual(globalStateUpdateStub.firstCall.args[1], undefined) }) - it('handles response with no $response property', async function () { - const mockCallerIdentityWithoutResponse: GetCallerIdentityResponse = { + it('handles response with no $httpHeaders property', async function () { + const mockCallerIdentityWithoutHeaders: GetCallerIdentityResponse = { Account: 'AccountId1234', Arn: 'arn:aws:iam::AccountId1234:user/test-user', UserId: 'AIDACKCEXAMPLEEXAMPLE', } getAccountIdStub.reset() - getAccountIdStub.resolves(mockCallerIdentityWithoutResponse) + getAccountIdStub.resolves(mockCallerIdentityWithoutHeaders) await loginManager.validateCredentials(sampleCredentials) diff --git a/packages/core/src/test/shared/telemetry/util.test.ts b/packages/core/src/test/shared/telemetry/util.test.ts index 059d86a891e..62a36884c56 100644 --- a/packages/core/src/test/shared/telemetry/util.test.ts +++ b/packages/core/src/test/shared/telemetry/util.test.ts @@ -10,6 +10,8 @@ import { convertLegacy, getClientId, getUserAgent, + getUserAgentPairs, + userAgentPairsToString, hadClientIdOnStartup, platformPair, SessionId, @@ -289,6 +291,86 @@ describe('getUserAgent', function () { }) }) +describe('getUserAgentPairs', function () { + it('returns array of [name, version] pairs', function () { + const pairs = getUserAgentPairs() + assert.ok(Array.isArray(pairs)) + assert.strictEqual(pairs.length, 1, 'Should have exactly one pair by default') + assert.ok(Array.isArray(pairs[0]), 'Each pair should be an array') + assert.strictEqual(pairs[0].length, 2, 'Each pair should have exactly 2 elements') + assert.ok(pairs[0][0].includes('Toolkit-For-VSCode'), 'Should include toolkit name') + assert.strictEqual(pairs[0][1], extensionVersion, 'Should include extension version') + }) + + it('includes platform pair when opted in', function () { + const pairs = getUserAgentPairs({ includePlatform: true }) + assert.ok(pairs.length > 1, 'Should have more than one pair when platform is included') + const platformPairStr = platformPair() + const [platformName, platformVersion] = platformPairStr.split('/') + const foundPlatformPair = pairs.find((pair) => pair[0] === platformName && pair[1] === platformVersion) + assert.ok(foundPlatformPair, 'Should include platform pair') + }) + + it('includes ClientId pair when opted in', function () { + const pairs = getUserAgentPairs({ includeClientId: true }) + const clientIdPair = pairs.find((pair) => pair[0] === 'ClientId') + assert.ok(clientIdPair, 'Should include ClientId pair') + assert.ok(clientIdPair[1], 'ClientId should have a value') + }) + + it('includes both platform and ClientId when both opted in', function () { + const pairs = getUserAgentPairs({ includePlatform: true, includeClientId: true }) + assert.ok(pairs.length >= 3, 'Should have at least 3 pairs') + const clientIdPair = pairs.find((pair) => pair[0] === 'ClientId') + assert.ok(clientIdPair, 'Should include ClientId pair') + const platformPairStr = platformPair() + const [platformName] = platformPairStr.split('/') + const foundPlatformPair = pairs.find((pair) => pair[0] === platformName) + assert.ok(foundPlatformPair, 'Should include platform pair') + }) + + it('omits ClientId by default', function () { + const pairs = getUserAgentPairs() + const clientIdPair = pairs.find((pair) => pair[0] === 'ClientId') + assert.strictEqual(clientIdPair, undefined, 'Should not include ClientId by default') + }) + + it('omits platform by default', function () { + const pairs = getUserAgentPairs() + assert.strictEqual(pairs.length, 1, 'Should have exactly one pair when nothing is opted in') + }) +}) + +describe('userAgentPairsToString', function () { + it('converts pairs to string format', function () { + const pairs: [string, string][] = [ + ['LAMBDA-DEBUG', '1.0.0'], + ['AWS-Toolkit', '2.0'], + ] + const result = userAgentPairsToString(pairs) + assert.strictEqual(result, 'LAMBDA-DEBUG/1.0.0 AWS-Toolkit/2.0') + }) + + it('handles single pair', function () { + const pairs: [string, string][] = [['AWS-Toolkit-For-VSCode', extensionVersion]] + const result = userAgentPairsToString(pairs) + assert.strictEqual(result, `AWS-Toolkit-For-VSCode/${extensionVersion}`) + }) + + it('handles empty array', function () { + const pairs: [string, string][] = [] + const result = userAgentPairsToString(pairs) + assert.strictEqual(result, '') + }) + + it('matches getUserAgent output when using getUserAgentPairs', function () { + const userAgentString = getUserAgent({ includePlatform: true, includeClientId: true }) + const userAgentPairs = getUserAgentPairs({ includePlatform: true, includeClientId: true }) + const reconstructedString = userAgentPairsToString(userAgentPairs) + assert.strictEqual(reconstructedString, userAgentString, 'String conversion should match getUserAgent output') + }) +}) + describe('validateMetricEvent', function () { it('does not validate exempt metrics', function () { const metricEvent: MetricDatum = { diff --git a/packages/toolkit/.changes/next-release/Bug Fix-b0864d23-587c-4277-bfc8-deb24684741e.json b/packages/toolkit/.changes/next-release/Bug Fix-b0864d23-587c-4277-bfc8-deb24684741e.json new file mode 100644 index 00000000000..c6ca159bbd2 --- /dev/null +++ b/packages/toolkit/.changes/next-release/Bug Fix-b0864d23-587c-4277-bfc8-deb24684741e.json @@ -0,0 +1,4 @@ +{ + "type": "Bug Fix", + "description": "Attaching a debugger to your lambdas using LocalStack is not working" +}