@microsoft/agents-hosting 1.6.0-beta.9.gce9d8facd2 → 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.
- package/dist/package.json +4 -9
- package/dist/src/agent-client/agentClient.js +2 -2
- package/dist/src/agent-client/agentClient.js.map +1 -1
- package/dist/src/agent-client/agentResponseHandler.js +2 -2
- package/dist/src/agent-client/agentResponseHandler.js.map +1 -1
- package/dist/src/app/agentApplication.js +28 -2
- package/dist/src/app/agentApplication.js.map +1 -1
- package/dist/src/app/agentApplicationOptions.d.ts +10 -0
- package/dist/src/app/attachmentDownloader.js +5 -10
- package/dist/src/app/attachmentDownloader.js.map +1 -1
- package/dist/src/app/auth/authorizationManager.d.ts +4 -0
- package/dist/src/app/auth/authorizationManager.js +94 -67
- package/dist/src/app/auth/authorizationManager.js.map +1 -1
- package/dist/src/app/auth/handlers/azureBotAuthorization.js +2 -2
- package/dist/src/app/auth/handlers/azureBotAuthorization.js.map +1 -1
- package/dist/src/app/proactive/createConversationOptionsBuilder.js +10 -6
- package/dist/src/app/proactive/createConversationOptionsBuilder.js.map +1 -1
- package/dist/src/app/proactive/proactive.d.ts +1 -0
- package/dist/src/app/proactive/proactive.js +30 -15
- package/dist/src/app/proactive/proactive.js.map +1 -1
- package/dist/src/app/teamsAttachmentDownloader.js +6 -14
- package/dist/src/app/teamsAttachmentDownloader.js.map +1 -1
- package/dist/src/auth/authConfiguration.d.ts +2 -143
- package/dist/src/auth/authConfiguration.js +301 -257
- package/dist/src/auth/authConfiguration.js.map +1 -1
- package/dist/src/auth/jwt-middleware.js +2 -1
- package/dist/src/auth/jwt-middleware.js.map +1 -1
- package/dist/src/auth/msalConnectionManager.js +27 -14
- package/dist/src/auth/msalConnectionManager.js.map +1 -1
- package/dist/src/auth/msalTokenProvider.d.ts +7 -0
- package/dist/src/auth/msalTokenProvider.js +188 -44
- package/dist/src/auth/msalTokenProvider.js.map +1 -1
- package/dist/src/auth/settings.d.ts +327 -0
- package/dist/src/auth/settings.js +158 -0
- package/dist/src/auth/settings.js.map +1 -0
- package/dist/src/cloudAdapter.d.ts +58 -1
- package/dist/src/cloudAdapter.js +232 -52
- package/dist/src/cloudAdapter.js.map +1 -1
- package/dist/src/connector-client/connectorClient.d.ts +12 -8
- package/dist/src/connector-client/connectorClient.js +99 -82
- package/dist/src/connector-client/connectorClient.js.map +1 -1
- package/dist/src/errorHelper.js +85 -7
- package/dist/src/errorHelper.js.map +1 -1
- package/dist/src/getProductInfo.d.ts +23 -0
- package/dist/src/getProductInfo.js +71 -1
- package/dist/src/getProductInfo.js.map +1 -1
- package/dist/src/headerPropagation.d.ts +17 -2
- package/dist/src/headerPropagation.js +46 -24
- package/dist/src/headerPropagation.js.map +1 -1
- package/dist/src/httpClient.d.ts +60 -0
- package/dist/src/httpClient.js +173 -0
- package/dist/src/httpClient.js.map +1 -0
- package/dist/src/index.d.ts +2 -1
- package/dist/src/index.js +7 -2
- package/dist/src/index.js.map +1 -1
- package/dist/src/oauth/userTokenClient.d.ts +5 -4
- package/dist/src/oauth/userTokenClient.js +97 -78
- package/dist/src/oauth/userTokenClient.js.map +1 -1
- package/dist/src/observability/traces.d.ts +11 -4
- package/dist/src/observability/traces.js +39 -7
- package/dist/src/observability/traces.js.map +1 -1
- package/dist/src/utils/env.d.ts +50 -0
- package/dist/src/utils/env.js +113 -0
- package/dist/src/utils/env.js.map +1 -0
- package/dist/src/utils.d.ts +10 -0
- package/dist/src/utils.js +18 -0
- package/dist/src/utils.js.map +1 -0
- package/package.json +4 -9
- package/src/agent-client/agentClient.ts +2 -2
- package/src/agent-client/agentResponseHandler.ts +2 -2
- package/src/app/agentApplication.ts +30 -3
- package/src/app/agentApplicationOptions.ts +11 -0
- package/src/app/attachmentDownloader.ts +6 -8
- package/src/app/auth/authorizationManager.ts +84 -41
- package/src/app/auth/handlers/azureBotAuthorization.ts +2 -2
- package/src/app/proactive/createConversationOptionsBuilder.ts +8 -4
- package/src/app/proactive/proactive.ts +39 -23
- package/src/app/teamsAttachmentDownloader.ts +7 -11
- package/src/auth/authConfiguration.ts +317 -356
- package/src/auth/jwt-middleware.ts +1 -1
- package/src/auth/msalConnectionManager.ts +27 -10
- package/src/auth/msalTokenProvider.ts +180 -42
- package/src/auth/settings.ts +323 -0
- package/src/cloudAdapter.ts +291 -12
- package/src/connector-client/connectorClient.ts +116 -94
- package/src/errorHelper.ts +96 -7
- package/src/getProductInfo.ts +79 -1
- package/src/headerPropagation.ts +55 -23
- package/src/httpClient.ts +223 -0
- package/src/index.ts +5 -1
- package/src/oauth/userTokenClient.ts +103 -84
- package/src/observability/traces.ts +39 -8
- package/src/utils/env.ts +105 -0
- package/src/utils.ts +14 -0
package/src/headerPropagation.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
45
|
+
* @param httpClient The HttpClient instance.
|
|
32
46
|
*/
|
|
33
|
-
constructor (
|
|
47
|
+
constructor (httpClient: HttpClient)
|
|
34
48
|
|
|
35
|
-
constructor (param: string |
|
|
49
|
+
constructor (param: string | HttpClient) {
|
|
36
50
|
if (typeof param === 'string') {
|
|
37
51
|
const baseURL = getTokenServiceEndpoint()
|
|
38
|
-
this.client =
|
|
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
|
-
|
|
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',
|
|
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
|
-
|
|
89
|
+
clientHeaders.Authorization = `Bearer ${token}`
|
|
135
90
|
}
|
|
136
91
|
|
|
137
|
-
|
|
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
|
-
|
|
153
|
-
|
|
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
|
-
|
|
156
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
246
|
-
record({ userId, connectionName, channelId, httpStatusCode: response
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
}
|