Skip to content

Commit cb62d2f

Browse files
Merge master into feature/v2-to-v3-migration
2 parents 40d49ea + 182103f commit cb62d2f

File tree

14 files changed

+458
-92
lines changed

14 files changed

+458
-92
lines changed

packages/core/src/auth/deprecated/loginManager.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,11 @@ import { isAutomation } from '../../shared/vscode/env'
3030
import { Credentials } from '@aws-sdk/types'
3131
import { ToolkitError } from '../../shared/errors'
3232
import * as localizedText from '../../shared/localizedText'
33-
import { DefaultStsClient, type GetCallerIdentityResponse } from '../../shared/clients/stsClient'
33+
import {
34+
DefaultStsClient,
35+
type GetCallerIdentityResponse,
36+
type GetCallerIdentityResponseWithHeaders,
37+
} from '../../shared/clients/stsClient'
3438
import { findAsync } from '../../shared/utilities/collectionUtils'
3539
import { telemetry } from '../../shared/telemetry/telemetry'
3640
import { withTelemetryContext } from '../../shared/telemetry/util'
@@ -135,9 +139,11 @@ export class LoginManager {
135139
return accountId
136140
}
137141

138-
private async detectExternalConnection(callerIdentity: GetCallerIdentityResponse): Promise<void> {
139-
// @ts-ignore
140-
const headers = callerIdentity.$response?.httpResponse?.headers
142+
private async detectExternalConnection(
143+
callerIdentity: GetCallerIdentityResponse | GetCallerIdentityResponseWithHeaders
144+
): Promise<void> {
145+
// SDK v3: Headers are captured via middleware and attached as $httpHeaders
146+
const headers = (callerIdentity as GetCallerIdentityResponseWithHeaders).$httpHeaders
141147
if (headers !== undefined && localStackConnectionHeader in headers) {
142148
await globals.globalState.update('aws.toolkit.externalConnection', localStackConnectionString)
143149
telemetry.auth_localstackEndpoint.emit({ source: 'validateCredentials', result: 'Succeeded' })

packages/core/src/auth/sso/ssoAccessTokenProvider.ts

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,15 @@ export abstract class SsoAccessTokenProvider {
5959
private static logIfChanged = onceChanged((s: string) => getLogger().info(s))
6060
private readonly className = 'SsoAccessTokenProvider'
6161

62+
/**
63+
* Prevents concurrent token refresh operations.
64+
* Maps tokenCacheKey to an in-flight refresh promise.
65+
*/
66+
private static refreshPromises = new Map<
67+
string,
68+
Promise<{ token: SsoToken; registration: ClientRegistration; region: string; startUrl: string }>
69+
>()
70+
6271
public static set authSource(val: string) {
6372
SsoAccessTokenProvider._authSource = val
6473
}
@@ -108,15 +117,43 @@ export abstract class SsoAccessTokenProvider {
108117
true
109118
)
110119
)
120+
111121
if (!data || !isExpired(data.token)) {
122+
getLogger().debug('Auth: token is valid, returning cached token (key=%s)', this.tokenCacheKey)
112123
return data?.token
113124
}
114125

126+
getLogger().info(
127+
`Auth: bearer token expired (expires at ${data.token.expiresAt}), attempting refresh (key=${this.tokenCacheKey})`
128+
)
129+
115130
if (data.registration && !isExpired(data.registration) && hasProps(data.token, 'refreshToken')) {
116-
const refreshed = await this.refreshToken(data.token, data.registration)
131+
getLogger().debug(`Auth: refresh token available, calling refreshToken() (key=${this.tokenCacheKey})`)
132+
// Check if a refresh is already in progress for this token
133+
const existingRefresh = SsoAccessTokenProvider.refreshPromises.get(this.tokenCacheKey)
134+
if (existingRefresh) {
135+
getLogger().debug(
136+
'SsoAccessTokenProvider: Token refresh already in progress, waiting for existing refresh'
137+
)
138+
const refreshed = await existingRefresh
139+
return refreshed.token
140+
}
141+
142+
// Start a new refresh and store the promise
143+
const refreshPromise = this.refreshToken(data.token, data.registration)
144+
SsoAccessTokenProvider.refreshPromises.set(this.tokenCacheKey, refreshPromise)
117145

118-
return refreshed.token
146+
try {
147+
const refreshed = await refreshPromise
148+
return refreshed.token
149+
} finally {
150+
// Clean up the promise from the map once complete (success or failure)
151+
SsoAccessTokenProvider.refreshPromises.delete(this.tokenCacheKey)
152+
}
119153
} else {
154+
getLogger().warn(
155+
`getToken: cannot refresh - registration expired or no refresh token available (key=${this.tokenCacheKey})`
156+
)
120157
await this.invalidate('allCacheExpired')
121158
}
122159
}
@@ -172,10 +209,18 @@ export abstract class SsoAccessTokenProvider {
172209

173210
try {
174211
const clientInfo = selectFrom(registration, 'clientId', 'clientSecret')
212+
getLogger().debug(`Auth refreshToken: calling OIDC createToken API (key=${this.tokenCacheKey})`)
175213
const response = await this.oidc.createToken({ ...clientInfo, ...token, grantType: refreshGrantType })
214+
215+
getLogger().debug(`Auth refreshToken: got response, now saving to cache...`)
216+
176217
const refreshed = this.formatToken(response, registration)
218+
getLogger().debug(`refreshToken: saving refreshed token to cache (key=${this.tokenCacheKey})`)
177219
await this.cache.token.save(this.tokenCacheKey, refreshed)
178220

221+
getLogger().info(
222+
`Auth refreshToken: token refresh successful (key=${this.tokenCacheKey}, new expiry=${response.expiresAt})`
223+
)
179224
telemetry.aws_refreshCredentials.emit({
180225
result: 'Succeeded',
181226
requestId: response.requestId,
@@ -184,6 +229,10 @@ export abstract class SsoAccessTokenProvider {
184229

185230
return refreshed
186231
} catch (err) {
232+
getLogger().error(
233+
`Auth refreshToken: token refresh failed (key=${this.tokenCacheKey}): ${getErrorMsg(err as unknown as Error)}`
234+
)
235+
187236
if (err instanceof DiskCacheError) {
188237
/**
189238
* Background:
@@ -197,6 +246,9 @@ export abstract class SsoAccessTokenProvider {
197246
* to the logs where the error was logged. Hopefully they can use this information to fix the issue,
198247
* or at least hint for them to provide the logs in a bug report.
199248
*/
249+
getLogger().warn(
250+
`Auth refreshToken: DiskCacheError during refresh, not invalidating session (key=${this.tokenCacheKey})`
251+
)
200252
void DiskCacheErrorMessage.instance.showMessageThrottled(err)
201253
} else if (!isNetworkError(err)) {
202254
const reason = getTelemetryReason(err)

packages/core/src/lambda/remoteDebugging/ldkClient.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import { DefaultLambdaClient } from '../../shared/clients/lambdaClient'
1717
import { LocalProxy } from './localProxy'
1818
import globals from '../../shared/extensionGlobals'
1919
import { getLogger } from '../../shared/logger/logger'
20-
import { getIoTSTClientWithAgent, getLambdaClientWithAgent, getLambdaDebugUserAgent } from './utils'
20+
import { getIoTSTClientWithAgent, getLambdaClientWithAgent, getLambdaDebugUserAgentPairs } from './utils'
2121
import { ToolkitError } from '../../shared/errors'
2222
import * as nls from 'vscode-nls'
2323

@@ -99,7 +99,7 @@ export class LdkClient {
9999
*/
100100
private getLambdaClient(region: string): DefaultLambdaClient {
101101
if (!this.lambdaClientCache.has(region)) {
102-
this.lambdaClientCache.set(region, getLambdaClientWithAgent(region, getLambdaDebugUserAgent()))
102+
this.lambdaClientCache.set(region, getLambdaClientWithAgent(region, getLambdaDebugUserAgentPairs()))
103103
}
104104
return this.lambdaClientCache.get(region)!
105105
}

packages/core/src/lambda/remoteDebugging/utils.ts

Lines changed: 28 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,36 +5,50 @@
55

66
import { IoTSecureTunnelingClient } from '@aws-sdk/client-iotsecuretunneling'
77
import { DefaultLambdaClient } from '../../shared/clients/lambdaClient'
8-
import { getUserAgent } from '../../shared/telemetry/util'
8+
import { getUserAgentPairs, userAgentPairsToString } from '../../shared/telemetry/util'
99
import globals from '../../shared/extensionGlobals'
10+
import type { UserAgent } from '@aws-sdk/types'
1011

11-
const customUserAgentBase = 'LAMBDA-DEBUG/1.0.0'
12+
const customUserAgentName = 'LAMBDA-DEBUG'
13+
const customUserAgentVersion = '1.0.0'
1214

13-
export function getLambdaClientWithAgent(region: string, customUserAgent?: string): DefaultLambdaClient {
15+
export function getLambdaClientWithAgent(region: string, customUserAgent?: UserAgent): DefaultLambdaClient {
1416
if (!customUserAgent) {
15-
customUserAgent = getLambdaUserAgent()
17+
customUserAgent = getLambdaUserAgentPairs()
1618
}
1719
return new DefaultLambdaClient(region, customUserAgent)
1820
}
1921

20-
// Example user agent:
21-
// LAMBDA-DEBUG/1.0.0 AWS-Toolkit-For-VSCode/testPluginVersion Visual-Studio-Code/1.102.2 ClientId/11111111-1111-1111-1111-111111111111
22-
export function getLambdaDebugUserAgent(): string {
23-
return `${customUserAgentBase} ${getLambdaUserAgent()}`
22+
/**
23+
* Returns properly formatted UserAgent pairs for AWS SDK v3
24+
*/
25+
export function getLambdaDebugUserAgentPairs(): UserAgent {
26+
return [
27+
[customUserAgentName, customUserAgentVersion],
28+
...getUserAgentPairs({ includePlatform: true, includeClientId: true }),
29+
]
2430
}
2531

26-
// Example user agent:
27-
// AWS-Toolkit-For-VSCode/testPluginVersion Visual-Studio-Code/1.102.2 ClientId/11111111-1111-1111-1111-111111111111
28-
export function getLambdaUserAgent(): string {
29-
return `${getUserAgent({ includePlatform: true, includeClientId: true })}`
32+
/**
33+
* Returns properly formatted UserAgent pairs for AWS SDK v3
34+
*/
35+
export function getLambdaUserAgentPairs(): UserAgent {
36+
return getUserAgentPairs({ includePlatform: true, includeClientId: true })
37+
}
38+
39+
/**
40+
* Returns user agent string for Lambda debugging in traditional format.
41+
* Example: "LAMBDA-DEBUG/1.0.0 AWS-Toolkit-For-VSCode/testPluginVersion Visual-Studio-Code/1.105.1 ClientId/11111111-1111-1111-1111-111111111111"
42+
*/
43+
export function getLambdaDebugUserAgent(): string {
44+
return userAgentPairsToString(getLambdaDebugUserAgentPairs())
3045
}
3146

3247
export function getIoTSTClientWithAgent(region: string): IoTSecureTunnelingClient {
33-
const customUserAgent = `${customUserAgentBase} ${getUserAgent({ includePlatform: true, includeClientId: true })}`
3448
return globals.sdkClientBuilderV3.createAwsService({
3549
serviceClient: IoTSecureTunnelingClient,
3650
clientOptions: {
37-
userAgent: [[customUserAgent]],
51+
customUserAgent: getLambdaDebugUserAgentPairs(),
3852
region,
3953
},
4054
userAgent: false,

packages/core/src/lambda/vue/remoteInvoke/invokeLambda.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ import { getLambdaHandlerFile } from '../../../awsService/appBuilder/utils'
3636
import { runUploadDirectory } from '../../commands/uploadLambda'
3737
import fs from '../../../shared/fs/fs'
3838
import { showConfirmationMessage, showMessage } from '../../../shared/utilities/messages'
39-
import { getLambdaClientWithAgent, getLambdaDebugUserAgent } from '../../remoteDebugging/utils'
39+
import { getLambdaClientWithAgent, getLambdaDebugUserAgentPairs } from '../../remoteDebugging/utils'
4040
import { isLocalStackConnection } from '../../../auth/utils'
4141
import { getRemoteDebugLayer } from '../../remoteDebugging/remoteLambdaDebugger'
4242

@@ -906,7 +906,7 @@ export async function invokeRemoteLambda(
906906
const resource: LambdaFunctionNode = params.functionNode
907907
const source: string = params.source || 'AwsExplorerRemoteInvoke'
908908
const client = getLambdaClientWithAgent(resource.regionCode)
909-
const clientDebug = getLambdaClientWithAgent(resource.regionCode, getLambdaDebugUserAgent())
909+
const clientDebug = getLambdaClientWithAgent(resource.regionCode, getLambdaDebugUserAgentPairs())
910910

911911
const Panel = VueWebview.compilePanel(RemoteInvokeWebview)
912912

packages/core/src/shared/awsClientBuilderV3.ts

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {
1212
TokenIdentity,
1313
TokenIdentityProvider,
1414
} from '@smithy/types'
15-
import { getUserAgent } from './telemetry/util'
15+
import { getUserAgentPairs } from './telemetry/util'
1616
import { DevSettings } from './settings'
1717
import {
1818
BuildHandler,
@@ -37,7 +37,6 @@ import { HttpResponse, HttpRequest } from '@aws-sdk/protocol-http'
3737
import { ConfiguredRetryStrategy } from '@smithy/util-retry'
3838
import { telemetry } from './telemetry/telemetry'
3939
import { getRequestId, getTelemetryReason, getTelemetryReasonDesc, getTelemetryResult } from './errors'
40-
import { extensionVersion } from './vscode/env'
4140
import { getLogger } from './logger/logger'
4241
import { partialClone } from './utilities/collectionUtils'
4342
import { selectFrom } from './utilities/tsUtils'
@@ -72,7 +71,7 @@ export interface AwsCommand<InputType extends object, OutputType extends object>
7271
export interface AwsClientOptions {
7372
credentials: AwsCredentialIdentityProvider
7473
region: string | Provider<string>
75-
userAgent: UserAgent
74+
customUserAgent: UserAgent
7675
requestHandler: {
7776
metadata?: RequestHandlerMetadata
7877
handle: (req: any, options?: any) => Promise<RequestHandlerOutput<any>>
@@ -155,8 +154,8 @@ export class AWSClientBuilderV3 {
155154
opt.region = serviceOptions.region
156155
}
157156

158-
if (!opt.userAgent && userAgent) {
159-
opt.userAgent = [[getUserAgent({ includePlatform: true, includeClientId: true }), extensionVersion]]
157+
if (!opt.customUserAgent && userAgent) {
158+
opt.customUserAgent = getUserAgentPairs({ includePlatform: true, includeClientId: true })
160159
}
161160

162161
if (!opt.retryStrategy) {
@@ -196,6 +195,7 @@ export class AWSClientBuilderV3 {
196195
}
197196
const service = new serviceOptions.serviceClient(opt)
198197
service.middlewareStack.add(telemetryMiddleware, { step: 'deserialize' })
198+
service.middlewareStack.add(captureHeadersMiddleware, { step: 'deserialize' })
199199
service.middlewareStack.add(loggingMiddleware, { step: 'finalizeRequest' })
200200
service.middlewareStack.add(getEndpointMiddleware(serviceOptions.settings), { step: 'build' })
201201

@@ -254,6 +254,26 @@ function getEndpointMiddleware(settings: DevSettings = DevSettings.instance): Bu
254254
const keepAliveMiddleware: BuildMiddleware<any, any> = (next: BuildHandler<any, any>) => async (args: any) =>
255255
addKeepAliveHeader(next, args)
256256

257+
/**
258+
* Middleware that captures HTTP response headers and attaches them to the output object.
259+
* This makes headers accessible via `response.$httpHeaders` for all AWS SDK v3 operations.
260+
* Useful for detecting custom headers from services like LocalStack.
261+
*/
262+
const captureHeadersMiddleware: DeserializeMiddleware<any, any> =
263+
(next: DeserializeHandler<any, any>) => async (args: any) => {
264+
const result = await next(args)
265+
266+
// Extract headers from HTTP response and attach to output for easy access
267+
if (HttpResponse.isInstance(result.response)) {
268+
const headers = result.response.headers
269+
if (headers && result.output) {
270+
result.output.$httpHeaders = headers
271+
}
272+
}
273+
274+
return result
275+
}
276+
257277
export async function emitOnRequest(next: DeserializeHandler<any, any>, context: HandlerExecutionContext, args: any) {
258278
if (!HttpResponse.isInstance(args.request)) {
259279
return next(args)

packages/core/src/shared/clients/lambdaClient.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import { CancellationError } from '../utilities/timeoutUtils'
4141
import { fromSSO } from '@aws-sdk/credential-provider-sso'
4242
import { getIAMConnection } from '../../auth/utils'
4343
import { NodeHttpHandler } from '@smithy/node-http-handler'
44+
import type { UserAgent } from '@aws-sdk/types'
4445

4546
export type LambdaClient = ClassToInterfaceType<DefaultLambdaClient>
4647

@@ -49,7 +50,7 @@ export class DefaultLambdaClient {
4950

5051
public constructor(
5152
public readonly regionCode: string,
52-
public readonly userAgent: string | undefined = undefined
53+
public readonly userAgent: UserAgent | undefined = undefined
5354
) {
5455
this.defaultTimeoutInMs = 5 * 60 * 1000 // 5 minutes (SDK default is 2 minutes)
5556
}
@@ -322,7 +323,7 @@ export class DefaultLambdaClient {
322323
serviceClient: LambdaSdkClient,
323324
userAgent: !this.userAgent,
324325
clientOptions: {
325-
userAgent: this.userAgent ? [[this.userAgent]] : undefined,
326+
customUserAgent: this.userAgent,
326327
region: this.regionCode,
327328
requestHandler: new NodeHttpHandler({
328329
requestTimeout: this.defaultTimeoutInMs,

packages/core/src/shared/clients/stsClient.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@ import { Credentials } from '@aws-sdk/types'
1010
import globals from '../extensionGlobals'
1111
import { ClassToInterfaceType } from '../utilities/tsUtils'
1212

13+
// Extended response type that includes captured HTTP headers (added by global middleware)
14+
export interface GetCallerIdentityResponseWithHeaders extends GetCallerIdentityResponse {
15+
$httpHeaders?: Record<string, string>
16+
}
17+
1318
export type { GetCallerIdentityResponse }
1419
export type StsClient = ClassToInterfaceType<DefaultStsClient>
1520

@@ -35,8 +40,9 @@ export class DefaultStsClient {
3540
return response
3641
}
3742

38-
public async getCallerIdentity(): Promise<GetCallerIdentityResponse> {
43+
public async getCallerIdentity(): Promise<GetCallerIdentityResponseWithHeaders> {
3944
const sdkClient = this.createSdkClient()
45+
// Note: $httpHeaders are added by global middleware in awsClientBuilderV3
4046
const response = await sdkClient.send(new GetCallerIdentityCommand({}))
4147
return response
4248
}

0 commit comments

Comments
 (0)