@microsoft/agents-hosting 1.6.0-beta.8.g226709c215 → 1.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/dist/package.json +4 -9
  2. package/dist/src/agent-client/agentClient.js +2 -2
  3. package/dist/src/agent-client/agentClient.js.map +1 -1
  4. package/dist/src/agent-client/agentResponseHandler.js +2 -2
  5. package/dist/src/agent-client/agentResponseHandler.js.map +1 -1
  6. package/dist/src/app/agentApplication.js +28 -2
  7. package/dist/src/app/agentApplication.js.map +1 -1
  8. package/dist/src/app/agentApplicationOptions.d.ts +10 -0
  9. package/dist/src/app/attachmentDownloader.js +5 -10
  10. package/dist/src/app/attachmentDownloader.js.map +1 -1
  11. package/dist/src/app/auth/authorizationManager.d.ts +4 -0
  12. package/dist/src/app/auth/authorizationManager.js +94 -67
  13. package/dist/src/app/auth/authorizationManager.js.map +1 -1
  14. package/dist/src/app/auth/handlers/azureBotAuthorization.js +2 -2
  15. package/dist/src/app/auth/handlers/azureBotAuthorization.js.map +1 -1
  16. package/dist/src/app/proactive/createConversationOptionsBuilder.js +10 -6
  17. package/dist/src/app/proactive/createConversationOptionsBuilder.js.map +1 -1
  18. package/dist/src/app/proactive/proactive.d.ts +1 -0
  19. package/dist/src/app/proactive/proactive.js +30 -15
  20. package/dist/src/app/proactive/proactive.js.map +1 -1
  21. package/dist/src/app/teamsAttachmentDownloader.js +6 -8
  22. package/dist/src/app/teamsAttachmentDownloader.js.map +1 -1
  23. package/dist/src/auth/authConfiguration.d.ts +2 -143
  24. package/dist/src/auth/authConfiguration.js +301 -257
  25. package/dist/src/auth/authConfiguration.js.map +1 -1
  26. package/dist/src/auth/jwt-middleware.js +2 -1
  27. package/dist/src/auth/jwt-middleware.js.map +1 -1
  28. package/dist/src/auth/msalConnectionManager.js +27 -14
  29. package/dist/src/auth/msalConnectionManager.js.map +1 -1
  30. package/dist/src/auth/msalTokenProvider.d.ts +7 -0
  31. package/dist/src/auth/msalTokenProvider.js +188 -44
  32. package/dist/src/auth/msalTokenProvider.js.map +1 -1
  33. package/dist/src/auth/settings.d.ts +327 -0
  34. package/dist/src/auth/settings.js +158 -0
  35. package/dist/src/auth/settings.js.map +1 -0
  36. package/dist/src/cloudAdapter.d.ts +58 -1
  37. package/dist/src/cloudAdapter.js +232 -52
  38. package/dist/src/cloudAdapter.js.map +1 -1
  39. package/dist/src/connector-client/connectorClient.d.ts +12 -8
  40. package/dist/src/connector-client/connectorClient.js +99 -82
  41. package/dist/src/connector-client/connectorClient.js.map +1 -1
  42. package/dist/src/errorHelper.js +85 -7
  43. package/dist/src/errorHelper.js.map +1 -1
  44. package/dist/src/getProductInfo.d.ts +23 -0
  45. package/dist/src/getProductInfo.js +71 -1
  46. package/dist/src/getProductInfo.js.map +1 -1
  47. package/dist/src/headerPropagation.d.ts +17 -2
  48. package/dist/src/headerPropagation.js +46 -24
  49. package/dist/src/headerPropagation.js.map +1 -1
  50. package/dist/src/httpClient.d.ts +60 -0
  51. package/dist/src/httpClient.js +173 -0
  52. package/dist/src/httpClient.js.map +1 -0
  53. package/dist/src/index.d.ts +2 -1
  54. package/dist/src/index.js +7 -2
  55. package/dist/src/index.js.map +1 -1
  56. package/dist/src/oauth/userTokenClient.d.ts +5 -4
  57. package/dist/src/oauth/userTokenClient.js +97 -78
  58. package/dist/src/oauth/userTokenClient.js.map +1 -1
  59. package/dist/src/observability/traces.d.ts +11 -4
  60. package/dist/src/observability/traces.js +39 -7
  61. package/dist/src/observability/traces.js.map +1 -1
  62. package/dist/src/utils/env.d.ts +50 -0
  63. package/dist/src/utils/env.js +113 -0
  64. package/dist/src/utils/env.js.map +1 -0
  65. package/dist/src/utils.d.ts +10 -0
  66. package/dist/src/utils.js +18 -0
  67. package/dist/src/utils.js.map +1 -0
  68. package/package.json +4 -9
  69. package/src/agent-client/agentClient.ts +2 -2
  70. package/src/agent-client/agentResponseHandler.ts +2 -2
  71. package/src/app/agentApplication.ts +30 -3
  72. package/src/app/agentApplicationOptions.ts +11 -0
  73. package/src/app/attachmentDownloader.ts +6 -8
  74. package/src/app/auth/authorizationManager.ts +84 -41
  75. package/src/app/auth/handlers/azureBotAuthorization.ts +2 -2
  76. package/src/app/proactive/createConversationOptionsBuilder.ts +8 -4
  77. package/src/app/proactive/proactive.ts +39 -23
  78. package/src/app/teamsAttachmentDownloader.ts +7 -6
  79. package/src/auth/authConfiguration.ts +317 -356
  80. package/src/auth/jwt-middleware.ts +1 -1
  81. package/src/auth/msalConnectionManager.ts +27 -10
  82. package/src/auth/msalTokenProvider.ts +180 -42
  83. package/src/auth/settings.ts +323 -0
  84. package/src/cloudAdapter.ts +291 -12
  85. package/src/connector-client/connectorClient.ts +116 -94
  86. package/src/errorHelper.ts +96 -7
  87. package/src/getProductInfo.ts +79 -1
  88. package/src/headerPropagation.ts +55 -23
  89. package/src/httpClient.ts +223 -0
  90. package/src/index.ts +5 -1
  91. package/src/oauth/userTokenClient.ts +103 -84
  92. package/src/observability/traces.ts +39 -8
  93. package/src/utils/env.ts +105 -0
  94. package/src/utils.ts +14 -0
@@ -8,10 +8,11 @@
8
8
  * It filters the incoming request headers based on the definition provided and loads them into the outgoing headers collection.
9
9
  */
10
10
  export class HeaderPropagation implements HeaderPropagationCollection {
11
+ private _keys: string[] = []
11
12
  private _incomingRequests: Record<string, string>
12
13
  private _outgoingHeaders: Record<string, string> = {}
13
14
 
14
- private _headersToPropagate = ['x-ms-correlation-id']
15
+ private _protectedHeaders = ['x-ms-correlation-id']
15
16
 
16
17
  public get incoming (): Record<string, string> {
17
18
  return this._incomingRequests
@@ -27,54 +28,78 @@ export class HeaderPropagation implements HeaderPropagationCollection {
27
28
  }
28
29
 
29
30
  this._incomingRequests = this.normalizeHeaders(headers)
30
- this.propagate(this._headersToPropagate)
31
+
32
+ // Ensure protected headers are propagated from incoming to outgoing by default without allowing modifications.
33
+ const protectedHeaders = [...this._protectedHeaders]
34
+ this._protectedHeaders = [] // Temporarily clear protected headers to allow propagation without restriction
35
+ this.propagate(protectedHeaders)
36
+ this._protectedHeaders = protectedHeaders // Restore protected headers list after propagation
31
37
  }
32
38
 
33
39
  propagate (headers: string[]) {
34
- for (const key of headers ?? []) {
35
- const lowerKey = key.toLowerCase()
36
- if (this._incomingRequests[lowerKey] && !this._outgoingHeaders[lowerKey]) {
37
- this._outgoingHeaders[lowerKey] = this._incomingRequests[lowerKey]
40
+ this.process(headers.map(h => [h, '']), key => {
41
+ if (this._incomingRequests[key] && !this._outgoingHeaders[key]) {
42
+ this._outgoingHeaders[key] = this._incomingRequests[key]
38
43
  }
39
- }
44
+ })
40
45
  }
41
46
 
42
47
  add (headers: Record<string, string>) {
43
- for (const [key, value] of Object.entries(headers ?? {})) {
44
- const lowerKey = key.toLowerCase()
45
- if (!this._incomingRequests[lowerKey] && !this._outgoingHeaders[lowerKey]) {
46
- this._outgoingHeaders[lowerKey] = value
48
+ this.process(Object.entries(headers ?? {}), (key, value) => {
49
+ if (!this._incomingRequests[key] && !this._outgoingHeaders[key]) {
50
+ this._outgoingHeaders[key] = value
47
51
  }
48
- }
52
+ })
49
53
  }
50
54
 
51
55
  concat (headers: Record<string, string>) {
52
- for (const [key, value] of Object.entries(headers ?? {})) {
53
- const lowerKey = key.toLowerCase()
54
- if (this._incomingRequests[lowerKey] && !this._headersToPropagate.includes(lowerKey)) {
55
- this._outgoingHeaders[lowerKey] = `${this._outgoingHeaders[lowerKey] ?? this._incomingRequests[lowerKey]} ${value}`.trim()
56
+ this.process(Object.entries(headers ?? {}), (key, value) => {
57
+ if (this._incomingRequests[key] || this._outgoingHeaders[key]) {
58
+ this._outgoingHeaders[key] = `${this._outgoingHeaders[key] ?? this._incomingRequests[key]} ${value}`.trim()
56
59
  }
57
- }
60
+ })
58
61
  }
59
62
 
60
63
  override (headers: Record<string, string>) {
61
- for (const [key, value] of Object.entries(headers ?? {})) {
62
- const lowerKey = key.toLowerCase()
63
- if (!this._headersToPropagate.includes(lowerKey)) {
64
- this._outgoingHeaders[lowerKey] = value
64
+ this.process(Object.entries(headers ?? {}), (key, value) => {
65
+ this._outgoingHeaders[key] = value
66
+ })
67
+ }
68
+
69
+ key (key: string) {
70
+ return this._keys.find(k => k.toLowerCase() === key.toLowerCase())
71
+ }
72
+
73
+ /**
74
+ * A helper method to process headers based on a provided callback function. It iterates over the headers and applies the callback to each header key and value.
75
+ * @param headers An array of header key-value pairs to process.
76
+ * @param callback A function that takes a header key and value, allowing for custom processing logic to be applied to each header.
77
+ */
78
+ private process (headers: [string, string][], callback: (key: string, value: string) => void) {
79
+ for (const [key, value] of headers) {
80
+ const savedKey = this.key(key)
81
+ const realKey = savedKey ?? key
82
+ if (!savedKey) {
83
+ this._keys.push(realKey)
84
+ }
85
+ if (!this._protectedHeaders.includes(key.toLowerCase())) {
86
+ callback(realKey, value)
65
87
  }
66
88
  }
67
89
  }
68
90
 
69
91
  /**
70
- * Normalizes the headers by lowercasing the keys and ensuring the values are strings.
92
+ * Normalizes the headers by ensuring the values are strings.
71
93
  * @param headers The headers to normalize.
72
94
  * @returns A new object with normalized headers.
73
95
  */
74
96
  private normalizeHeaders (headers: Record<string, string | string[] | undefined>) {
75
97
  return Object.entries(headers).reduce((acc, [key, value]) => {
76
98
  if (value) {
77
- acc[key.toLowerCase()] = Array.isArray(value) ? value.join(' ') : value
99
+ acc[key] = Array.isArray(value) ? value.join(' ') : value
100
+ if (!this._keys.includes(key)) {
101
+ this._keys.push(key) // Store the original casing of the header keys
102
+ }
78
103
  }
79
104
  return acc
80
105
  }, {} as Record<string, string>)
@@ -138,4 +163,11 @@ export interface HeaderPropagationCollection {
138
163
  * If the header does not exist in the incoming headers, it will be added to the outgoing collection.
139
164
  */
140
165
  override(headers: Record<string, string>): void
166
+ /**
167
+ * Resolves the actual header key in a case-insensitive manner.
168
+ * This is useful for ensuring that headers are accessed correctly regardless of their case in the incoming request.
169
+ * @param key The header key to resolve.
170
+ * @returns The resolved header key or undefined if not found.
171
+ */
172
+ key?(key: string): string | undefined
141
173
  }
@@ -0,0 +1,223 @@
1
+ /**
2
+ * Copyright (c) Microsoft Corporation. All rights reserved.
3
+ * Licensed under the MIT License.
4
+ */
5
+
6
+ import { ExceptionHelper } from '@microsoft/agents-activity'
7
+ import { Readable } from 'node:stream'
8
+ import { Errors } from './errorHelper'
9
+
10
+ /**
11
+ * Configuration for an HTTP request.
12
+ */
13
+ export interface HttpRequestConfig {
14
+ method: string
15
+ url: string
16
+ headers?: Record<string, string>
17
+ data?: unknown
18
+ params?: Record<string, string | undefined>
19
+ responseType?: 'json' | 'arraybuffer' | 'stream'
20
+ timeout?: number
21
+ signal?: AbortSignal
22
+ }
23
+
24
+ /**
25
+ * Represents an HTTP response.
26
+ */
27
+ export interface HttpResponse<T = unknown> {
28
+ data: T
29
+ status: number
30
+ statusText: string
31
+ headers: Headers
32
+ config: HttpRequestConfig
33
+ }
34
+
35
+ /**
36
+ * Options for creating an HttpClient instance.
37
+ */
38
+ export interface HttpClientOptions {
39
+ baseURL?: string
40
+ headers?: Record<string, string>
41
+ }
42
+
43
+ /**
44
+ * A lightweight HTTP client built on native fetch.
45
+ */
46
+ export class HttpClient {
47
+ private _baseURL: string
48
+ private _defaultHeaders: Record<string, string>
49
+
50
+ constructor (options: HttpClientOptions = {}) {
51
+ this._baseURL = options.baseURL ?? ''
52
+ this._defaultHeaders = this.normalizeHeaders(options.headers)
53
+ }
54
+
55
+ get baseURL (): string {
56
+ return this._baseURL
57
+ }
58
+
59
+ get defaultHeaders (): Record<string, string> {
60
+ return this._defaultHeaders
61
+ }
62
+
63
+ set defaultHeaders (headers: Record<string, string>) {
64
+ this._defaultHeaders = this.normalizeHeaders(headers)
65
+ }
66
+
67
+ setHeader (name: string, value: string): void {
68
+ this._defaultHeaders[name.toLowerCase()] = value
69
+ }
70
+
71
+ async request<T = unknown> (config: HttpRequestConfig): Promise<HttpResponse<T>> {
72
+ const url = this.buildUrl(config.url, config.params)
73
+ const headers = this.normalizeHeaders({ ...this._defaultHeaders, ...config.headers })
74
+ const requestHeaders = new Headers(headers)
75
+ const fetchOptions: RequestInit = {
76
+ method: config.method.toUpperCase(),
77
+ headers: requestHeaders,
78
+ signal: config.signal ??
79
+ (config.timeout ? AbortSignal.timeout(config.timeout) : undefined)
80
+ }
81
+
82
+ if (config.data !== undefined && config.data !== null) {
83
+ const contentType = requestHeaders.get('content-type') ?? ''
84
+ if (contentType.includes('application/x-www-form-urlencoded')) {
85
+ fetchOptions.body = new URLSearchParams(config.data as Record<string, string>).toString()
86
+ } else if (config.data instanceof URLSearchParams) {
87
+ if (!contentType) {
88
+ requestHeaders.set('content-type', 'application/x-www-form-urlencoded;charset=utf-8')
89
+ }
90
+ fetchOptions.body = config.data.toString()
91
+ } else if (typeof config.data === 'string') {
92
+ fetchOptions.body = config.data
93
+ } else if (config.data instanceof Uint8Array || config.data instanceof ArrayBuffer ||
94
+ config.data instanceof Blob || config.data instanceof FormData ||
95
+ config.data instanceof ReadableStream) {
96
+ fetchOptions.body = config.data
97
+ } else {
98
+ if (!contentType) {
99
+ requestHeaders.set('content-type', 'application/json')
100
+ }
101
+ fetchOptions.body = JSON.stringify(config.data)
102
+ }
103
+ }
104
+
105
+ const response = await fetch(url, fetchOptions)
106
+
107
+ let data: T
108
+ if (config.responseType === 'stream') {
109
+ if (!response.ok) {
110
+ data = await response.text() as T
111
+ } else {
112
+ data = Readable.fromWeb(response.body ?? new ReadableStream()) as T
113
+ }
114
+ } else if (config.responseType === 'arraybuffer') {
115
+ data = await response.arrayBuffer() as T
116
+ } else {
117
+ const text = await response.text()
118
+ try {
119
+ data = JSON.parse(text) as T
120
+ } catch {
121
+ data = text as T
122
+ }
123
+ }
124
+
125
+ const httpResponse: HttpResponse<T> = {
126
+ data,
127
+ status: response.status,
128
+ statusText: response.statusText,
129
+ headers: response.headers,
130
+ config,
131
+ }
132
+
133
+ if (!response.ok) {
134
+ const error: HttpError = new HttpError(
135
+ `Request failed with status ${response.status}`,
136
+ httpResponse,
137
+ config
138
+ )
139
+ throw error
140
+ }
141
+
142
+ return httpResponse
143
+ }
144
+
145
+ async get<T = unknown> (url: string, options?: Partial<HttpRequestConfig>): Promise<HttpResponse<T>> {
146
+ return this.request<T>({ method: 'get', url, ...options })
147
+ }
148
+
149
+ private buildUrl (path: string, params?: Record<string, string | undefined>): string {
150
+ let url: string
151
+ if (path.startsWith('http://') || path.startsWith('https://')) {
152
+ url = path
153
+ } else {
154
+ if (!this._baseURL) {
155
+ throw ExceptionHelper.generateException(Error, Errors.HttpClientRelativeUrlRequiresBaseUrl, undefined, { url: path })
156
+ }
157
+
158
+ const base = this._baseURL.endsWith('/') ? this._baseURL.slice(0, -1) : this._baseURL
159
+ const cleanPath = path.startsWith('/') ? path : `/${path}`
160
+ url = `${base}${cleanPath}`
161
+ }
162
+
163
+ if (params) {
164
+ const searchParams = new URLSearchParams()
165
+ for (const [key, value] of Object.entries(params)) {
166
+ if (value !== undefined && value !== null) {
167
+ searchParams.set(key, value)
168
+ }
169
+ }
170
+ const queryString = searchParams.toString()
171
+ if (queryString) {
172
+ url += `${url.includes('?') ? '&' : '?'}${queryString}`
173
+ }
174
+ }
175
+
176
+ return url
177
+ }
178
+
179
+ private normalizeHeaders (headers?: Record<string, string>): Record<string, string> {
180
+ if (!headers) {
181
+ return {}
182
+ }
183
+
184
+ return Object.entries(headers).reduce((normalized, [name, value]) => {
185
+ normalized[name.toLowerCase()] = value
186
+ return normalized
187
+ }, {} as Record<string, string>)
188
+ }
189
+ }
190
+
191
+ /**
192
+ * Error thrown when an HTTP request fails.
193
+ */
194
+ export class HttpError extends Error {
195
+ public readonly response: HttpResponse
196
+ public readonly config: HttpRequestConfig
197
+ public readonly status: number
198
+
199
+ constructor (message: string, response: HttpResponse, config: HttpRequestConfig) {
200
+ super(message)
201
+ this.name = 'HttpError'
202
+ this.response = response
203
+ this.config = config
204
+ this.status = response.status
205
+ }
206
+
207
+ toJSON (): Record<string, unknown> {
208
+ return {
209
+ message: this.message,
210
+ status: this.status,
211
+ config: {
212
+ url: this.config.url,
213
+ method: this.config.method,
214
+ data: this.config.data,
215
+ },
216
+ response: {
217
+ status: this.response.status,
218
+ statusText: this.response.statusText,
219
+ data: this.response.data,
220
+ },
221
+ }
222
+ }
223
+ }
package/src/index.ts CHANGED
@@ -18,7 +18,10 @@ export * from './transcript'
18
18
 
19
19
  export * from './activityHandler'
20
20
  export * from './baseAdapter'
21
- export * from './cloudAdapter'
21
+ // Note: re-export the public surface of `cloudAdapter` explicitly so the
22
+ // test-only internals (e.g., the per-process warning dedup set) do not leak
23
+ // into the package's public API.
24
+ export { CloudAdapter, type CloudAdapterOptions } from './cloudAdapter'
22
25
  export * from './middlewareSet'
23
26
  export * from './messageFactory'
24
27
  export * from './statusCodes'
@@ -26,5 +29,6 @@ export * from './turnContext'
26
29
  export * from './turnContextStateCollection'
27
30
  export * from './storage/storage'
28
31
  export * from './headerPropagation'
32
+ export * from './httpClient'
29
33
 
30
34
  export * from './agent-client'
@@ -1,12 +1,12 @@
1
1
  // Copyright (c) Microsoft Corporation. All rights reserved.
2
2
  // Licensed under the MIT License.
3
3
 
4
- import axios, { AxiosInstance } from 'axios'
4
+ import { HttpClient, HttpError } from '../httpClient'
5
5
  import { Activity, ConversationReference } from '@microsoft/agents-activity'
6
6
  import { debug } from '@microsoft/agents-telemetry'
7
7
  import { normalizeTokenExchangeState } from '../activityWireCompat'
8
8
  import { AadResourceUrls, SignInResource, TokenExchangeRequest, TokenOrSinginResourceResponse, TokenResponse, TokenStatus } from './userTokenClient.types'
9
- import { getProductInfo } from '../getProductInfo'
9
+ import { applyUserAgentHeader, getProductInfo } from '../getProductInfo'
10
10
  import { AuthProvider } from '../auth'
11
11
  import { HeaderPropagation, HeaderPropagationCollection } from '../headerPropagation'
12
12
  import { getTokenServiceEndpoint } from './customUserTokenAPI'
@@ -15,11 +15,25 @@ import { UserTokenClientTraceDefinitions } from '../observability'
15
15
 
16
16
  const logger = debug('agents:user-token-client')
17
17
 
18
+ function formatHttpErrorMessage (error: HttpError): string {
19
+ const responseData = error.response?.data
20
+ if (responseData === undefined) {
21
+ return error.message
22
+ }
23
+
24
+ try {
25
+ const serializedResponseData = JSON.stringify(responseData)
26
+ return serializedResponseData === undefined ? error.message : `${error.message}: ${serializedResponseData}`
27
+ } catch {
28
+ return error.message
29
+ }
30
+ }
31
+
18
32
  /**
19
33
  * Client for managing user tokens.
20
34
  */
21
35
  export class UserTokenClient {
22
- client: AxiosInstance
36
+ client: HttpClient
23
37
  private msAppId: string = ''
24
38
  /**
25
39
  * Creates a new instance of UserTokenClient.
@@ -28,14 +42,14 @@ export class UserTokenClient {
28
42
  constructor (msAppId: string)
29
43
  /**
30
44
  * Creates a new instance of UserTokenClient.
31
- * @param axiosInstance The axios instance.
45
+ * @param httpClient The HttpClient instance.
32
46
  */
33
- constructor (axiosInstance: AxiosInstance)
47
+ constructor (httpClient: HttpClient)
34
48
 
35
- constructor (param: string | AxiosInstance) {
49
+ constructor (param: string | HttpClient) {
36
50
  if (typeof param === 'string') {
37
51
  const baseURL = getTokenServiceEndpoint()
38
- this.client = axios.create({
52
+ this.client = new HttpClient({
39
53
  baseURL,
40
54
  headers: {
41
55
  Accept: 'application/json',
@@ -45,55 +59,6 @@ export class UserTokenClient {
45
59
  } else {
46
60
  this.client = param
47
61
  }
48
-
49
- this.client.interceptors.request.use((config) => {
50
- const { method, url, data, headers, params } = config
51
- const { Authorization, authorization, ...headersToLog } = headers || {}
52
- logger.debug('Request: ', {
53
- host: this.client.getUri(),
54
- url,
55
- data,
56
- method,
57
- params,
58
- headers: headersToLog
59
- })
60
- return config
61
- })
62
-
63
- this.client.interceptors.response.use(
64
- (config) => {
65
- const { status, statusText, config: requestConfig, headers } = config
66
- const { Authorization, authorization, ...headersToLog } = headers || {}
67
- const { token, ...redactedData } = requestConfig?.data || {}
68
- logger.debug('Response: ', {
69
- status,
70
- statusText,
71
- host: this.client.getUri(),
72
- url: requestConfig?.url,
73
- data: redactedData,
74
- method: requestConfig?.method,
75
- headers: headersToLog
76
- })
77
- return config
78
- },
79
- (error) => {
80
- const { code, status, message, stack, response } = error
81
- const { headers } = response || {}
82
- const errorDetails = {
83
- code,
84
- host: this.client.getUri(),
85
- url: error.config.url,
86
- method: error.config.method,
87
- data: error.config.data,
88
- message: message + JSON.stringify(response?.data),
89
- headers,
90
- stack,
91
- }
92
- logger.debug('Response error: ', errorDetails)
93
- if (errorDetails.url === '/api/usertoken/GetToken' && status !== 404) {
94
- return Promise.reject(errorDetails)
95
- }
96
- })
97
62
  }
98
63
 
99
64
  /**
@@ -112,29 +77,24 @@ export class UserTokenClient {
112
77
  headers?: HeaderPropagationCollection
113
78
  ): Promise<UserTokenClient> {
114
79
  const headerPropagation = headers ?? new HeaderPropagation({})
115
- const userAgent = headerPropagation.outgoing['user-agent']
116
- const productInfo = getProductInfo()
117
- if (!userAgent) {
118
- headerPropagation.add({ 'User-Agent': productInfo })
119
- } else if (!userAgent.includes(productInfo)) {
120
- headerPropagation.concat({ 'User-Agent': productInfo })
121
- }
80
+ applyUserAgentHeader(headerPropagation)
122
81
  headerPropagation.override({
123
82
  Accept: 'application/json',
124
- 'Content-Type': 'application/json', // Required by transformRequest
125
- })
126
-
127
- const axiosInstance = axios.create({
128
- baseURL,
129
- headers: headerPropagation.outgoing,
83
+ 'Content-Type': 'application/json',
130
84
  })
131
85
 
86
+ const clientHeaders: Record<string, string> = { ...headerPropagation.outgoing }
132
87
  const token = await authProvider?.getAccessToken(scope)
133
88
  if (token && token.length > 1) {
134
- axiosInstance.defaults.headers.common.Authorization = `Bearer ${token}`
89
+ clientHeaders.Authorization = `Bearer ${token}`
135
90
  }
136
91
 
137
- return new UserTokenClient(axiosInstance)
92
+ const httpClient = new HttpClient({
93
+ baseURL,
94
+ headers: clientHeaders,
95
+ })
96
+
97
+ return new UserTokenClient(httpClient)
138
98
  }
139
99
 
140
100
  /**
@@ -149,13 +109,21 @@ export class UserTokenClient {
149
109
  return trace(UserTokenClientTraceDefinitions.getUserToken, async ({ record }) => {
150
110
  const [channelId] = Activity.parseChannelId(channelIdComposite)
151
111
  const params = { connectionName, channelId, userId, code }
152
- const response = await this.client.get('/api/usertoken/GetToken', { params })
153
- record({ connectionName, channelId, userId, httpStatusCode: response.status?.toString() })
112
+ try {
113
+ const response = await this.executeRequest('get', '/api/usertoken/GetToken', undefined, { params })
114
+ record({ connectionName, channelId, userId, httpStatusCode: response.status?.toString() })
154
115
 
155
- if (response?.data) {
156
- return response.data as TokenResponse
116
+ if (response?.data) {
117
+ return response.data as TokenResponse
118
+ }
119
+ return { token: undefined }
120
+ } catch (error: any) {
121
+ if (error instanceof HttpError && error.status === 404) {
122
+ record({ connectionName, channelId, userId, httpStatusCode: '404' })
123
+ return { token: undefined }
124
+ }
125
+ throw error
157
126
  }
158
- return { token: undefined }
159
127
  })
160
128
  }
161
129
 
@@ -170,7 +138,7 @@ export class UserTokenClient {
170
138
  return trace(UserTokenClientTraceDefinitions.signOut, async ({ record }) => {
171
139
  const [channelId] = Activity.parseChannelId(channelIdComposite)
172
140
  const params = { userId, connectionName, channelId }
173
- const response = await this.client.delete('/api/usertoken/SignOut', { params })
141
+ const response = await this.executeRequest('delete', '/api/usertoken/SignOut', undefined, { params })
174
142
  record({ userId, connectionName, channelId, httpStatusCode: response.status?.toString() })
175
143
  if (response.status !== 200) {
176
144
  throw new Error('Failed to sign out')
@@ -197,7 +165,7 @@ export class UserTokenClient {
197
165
  const tokenExchangeStateNormalized = normalizeTokenExchangeState(tokenExchangeState)
198
166
  const state = Buffer.from(JSON.stringify(tokenExchangeStateNormalized)).toString('base64')
199
167
  const params = { state }
200
- const response = await this.client.get('/api/botsignin/GetSignInResource', { params })
168
+ const response = await this.executeRequest('get', '/api/botsignin/GetSignInResource', undefined, { params })
201
169
  record({ connectionName, httpStatusCode: response.status?.toString() })
202
170
  return response.data as SignInResource
203
171
  })
@@ -215,7 +183,7 @@ export class UserTokenClient {
215
183
  return trace(UserTokenClientTraceDefinitions.exchangeToken, async ({ record }) => {
216
184
  const [channelId] = Activity.parseChannelId(channelIdComposite)
217
185
  const params = { userId, connectionName, channelId }
218
- const response = await this.client.post('/api/usertoken/exchange', tokenExchangeRequest, { params })
186
+ const response = await this.executeRequest('post', '/api/usertoken/exchange', tokenExchangeRequest, { params })
219
187
  record({ userId, connectionName, channelId, httpStatusCode: response.status?.toString() })
220
188
  if (response?.data) {
221
189
  return response.data as TokenResponse
@@ -242,8 +210,8 @@ export class UserTokenClient {
242
210
  const [channelId] = Activity.parseChannelId(channelIdComposite)
243
211
  const state = Buffer.from(JSON.stringify({ conversation, relatesTo, connectionName, msAppId: this.msAppId })).toString('base64')
244
212
  const params = { userId, connectionName, channelId, state, code, finalRedirect, fwdUrl }
245
- const response = await this.client.get('/api/usertoken/GetTokenOrSignInResource', { params })
246
- record({ userId, connectionName, channelId, httpStatusCode: response.status?.toString() })
213
+ const response = await this.executeRequest('get', '/api/usertoken/GetTokenOrSignInResource', undefined, { params })
214
+ record({ userId, connectionName, channelId, httpStatusCode: response?.status?.toString() })
247
215
  return response.data as TokenOrSinginResourceResponse
248
216
  })
249
217
  }
@@ -259,7 +227,7 @@ export class UserTokenClient {
259
227
  return trace(UserTokenClientTraceDefinitions.getTokenStatus, async ({ record }) => {
260
228
  const [channelId] = Activity.parseChannelId(channelIdComposite)
261
229
  const params = { userId, channelId, include }
262
- const response = await this.client.get('/api/usertoken/GetTokenStatus', { params })
230
+ const response = await this.executeRequest('get', '/api/usertoken/GetTokenStatus', undefined, { params })
263
231
  record({ userId, channelId, httpStatusCode: response.status?.toString() })
264
232
  return response.data as TokenStatus[]
265
233
  })
@@ -277,13 +245,64 @@ export class UserTokenClient {
277
245
  return trace(UserTokenClientTraceDefinitions.getAadTokens, async ({ record }) => {
278
246
  const [channelId] = Activity.parseChannelId(channelIdComposite)
279
247
  const params = { userId, connectionName, channelId }
280
- const response = await this.client.post('/api/usertoken/GetAadTokens', resourceUrls, { params })
248
+ const response = await this.executeRequest('post', '/api/usertoken/GetAadTokens', resourceUrls, { params })
281
249
  record({ userId, connectionName, channelId, httpStatusCode: response.status?.toString() })
282
250
  return response.data as Record<string, TokenResponse>
283
251
  })
284
252
  }
285
253
 
286
254
  public updateAuthToken (token: string): void {
287
- this.client.defaults.headers.common.Authorization = `Bearer ${token}`
255
+ this.client.setHeader('Authorization', `Bearer ${token}`)
256
+ }
257
+
258
+ private async executeRequest<T = unknown> (method: string, url: string, data?: unknown, options?: { params?: Record<string, string | undefined> }): Promise<{ data: T, status: number, statusText: string }> {
259
+ const { params } = options ?? {}
260
+ const { Authorization, authorization, ...headersToLog } = this.client.defaultHeaders
261
+ logger.debug('Request: ', {
262
+ host: this.client.baseURL,
263
+ url,
264
+ data,
265
+ method,
266
+ params,
267
+ headers: headersToLog
268
+ })
269
+
270
+ try {
271
+ const response = await this.client.request<T>({ method, url, data, params })
272
+ const { token: _token, ...redactedData } = (response.config?.data ?? {}) as Record<string, unknown>
273
+ logger.debug('Response: ', {
274
+ status: response.status,
275
+ statusText: response.statusText,
276
+ host: this.client.baseURL,
277
+ url: response.config?.url,
278
+ data: redactedData,
279
+ method: response.config?.method,
280
+ })
281
+ return response
282
+ } catch (error: any) {
283
+ if (error instanceof HttpError) {
284
+ const message = formatHttpErrorMessage(error)
285
+ const errorDetails = {
286
+ host: this.client.baseURL,
287
+ url: error.config.url,
288
+ method: error.config.method,
289
+ data: error.config.data,
290
+ message,
291
+ headers: error.response?.headers,
292
+ stack: error.stack,
293
+ }
294
+ logger.debug('Response error: ', errorDetails)
295
+
296
+ Object.assign(error, {
297
+ host: this.client.baseURL,
298
+ url: error.config.url,
299
+ method: error.config.method,
300
+ data: error.config.data,
301
+ headers: error.response?.headers,
302
+ message,
303
+ })
304
+ }
305
+ throw error
306
+ }
288
307
  }
289
308
  }