@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.
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 -14
  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 -11
  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
@@ -3,102 +3,291 @@
3
3
  * Licensed under the MIT License.
4
4
  */
5
5
 
6
- import { debug } from '@microsoft/agents-telemetry'
7
- import { ConnectionMapItem } from './msalConnectionManager'
8
- import objectPath from 'object-path'
6
+ import type { ConnectionMapItem } from './msalConnectionManager'
7
+ import { debug, redactString, redactScopes, redactUrl } from '@microsoft/agents-telemetry'
8
+ import { loadEnvSettings, AuthConfiguration, envParser, envParserUtils, LoadEnv, applyDefaultSettings, DEFAULT_CONNECTION_MAP, ConnectionKeys, ConnectionMapKeys } from './settings'
9
+
10
+ export { type AuthConfiguration, AuthType, resolveAuthority } from './settings'
11
+ import { prune } from '../utils'
9
12
 
10
13
  const logger = debug('agents:authConfiguration')
11
- const DEFAULT_CONNECTION = 'serviceConnection'
14
+
15
+ type NonOptional<T> = { [K in keyof Required<T>]: T[K] }
12
16
 
13
17
  /**
14
- * Represents the authentication configuration.
18
+ * Summarizes the authentication configuration for logging by redacting sensitive information and pruning undefined values. This is used to log the loaded authentication settings without exposing secrets or personally identifiable information.
19
+ * @remarks AuthConfiguration properties can change its shape, since this function is intended for logging, e.g. `scopes` will be a string instead of an array.
15
20
  */
16
- export interface AuthConfiguration {
17
- /**
18
- * The tenant ID for the authentication configuration.
19
- */
20
- tenantId?: string
21
-
22
- /**
23
- * The client ID for the authentication configuration. Required in production.
24
- */
25
- clientId?: string
26
-
27
- /**
28
- * The client secret for the authentication configuration.
29
- */
30
- clientSecret?: string
31
-
32
- /**
33
- * The path to the certificate PEM file.
34
- */
35
- certPemFile?: string
36
-
37
- /**
38
- * The path to the certificate key file.
39
- */
40
- certKeyFile?: string
41
-
42
- /**
43
- * Indicates whether to send the X5C param or not (for SNI authentication).
44
- */
45
- sendX5C?: boolean
46
-
47
- /**
48
- * A list of valid issuers for the authentication configuration.
49
- */
50
- issuers?: string[]
51
-
52
- /**
53
- * The connection name for the authentication configuration.
54
- */
55
- connectionName?: string
56
-
57
- /**
58
- * The FIC (First-Party Integration Channel) client ID.
59
- */
60
- FICClientId?: string,
61
-
62
- /**
63
- * Entra Authentication Endpoint to use.
64
- *
65
- * @remarks
66
- * If not populated the Entra Public Cloud endpoint is assumed.
67
- * This example of Public Cloud Endpoint is https://login.microsoftonline.com
68
- * see also https://learn.microsoft.com/entra/identity-platform/authentication-national-cloud
69
- */
70
- authority?: string
71
-
72
- scope?: string
73
-
74
- /**
75
- * A map of connection names to their respective authentication configurations.
76
- */
77
- connections?: Map<string, AuthConfiguration>
78
-
79
- /**
80
- * A list of connection map items to map service URLs to connection names.
81
- */
82
- connectionsMap?: ConnectionMapItem[],
83
-
84
- /**
85
- * An optional alternative blueprint Connection name used when constructing a connector client.
86
- */
87
- altBlueprintConnectionName?: string
88
-
89
- /**
90
- * The path to K8s provided token.
91
- */
92
- WIDAssertionFile?: string
93
-
94
- /**
95
- * The Azure region for ESTS-R regional token acquisition (e.g. 'westus', 'eastus').
96
- * When set, MSAL routes token requests to the specified regional endpoint.
97
- * See https://learn.microsoft.com/en-us/entra/msal/javascript/node/regional-authorities for details.
98
- */
99
- azureRegion?: string
21
+ function summarizeAuthConfiguration (authConfig: AuthConfiguration) {
22
+ return [...authConfig.connections?.entries() ?? []].reduce((summary, [name, config]) => {
23
+ summary[name] = prune({
24
+ clientId: redactString(config.clientId, true),
25
+ tenantId: redactString(config.tenantId, true),
26
+ clientSecret: redactString(config.clientSecret),
27
+ authorityEndpoint: config.authorityEndpoint ? redactUrl(config.authorityEndpoint) : undefined,
28
+ scopes: (config.scopes ? redactScopes(config.scopes) : undefined) as any,
29
+ issuers: config.issuers?.map(redactUrl).filter(e => e !== undefined),
30
+ federatedClientId: redactString(config.federatedClientId, true),
31
+ certPemFile: redactString(config.certPemFile),
32
+ certKeyFile: redactString(config.certKeyFile),
33
+ WIDAssertionFile: redactString(config.WIDAssertionFile),
34
+ federatedTokenFile: config.federatedTokenFile ? redactString(config.federatedTokenFile) : undefined,
35
+ authType: config.authType ?? undefined,
36
+ idpmResource: config.idpmResource ? redactUrl(config.idpmResource) : undefined,
37
+ connectionName: config.connectionName,
38
+ altBlueprintConnectionName: config.altBlueprintConnectionName,
39
+ azureRegion: config.azureRegion,
40
+ sendX5C: config.sendX5C,
41
+ // Don't log the following properties
42
+ authority: undefined, // Deprecated, same as authorityEndpoint, avoid logging duplicate info
43
+ FICClientId: undefined, // Deprecated, same as federatedClientId, avoid logging duplicate info
44
+ scope: undefined, // Deprecated, same as scopes, avoid logging duplicate info
45
+ connections: undefined, // Avoid logging nested connections
46
+ connectionsMap: undefined, // Avoid logging nested connections map
47
+ } satisfies NonOptional<AuthConfiguration>)
48
+ return summary
49
+ }, {} as Record<string, AuthConfiguration>)
100
50
  }
101
51
 
52
+ /**
53
+ * Latest authentication configuration loaded from environment variables, with support for hot-reloading in test mode.
54
+ * Environment variables for connections should be in the format Connections__<id>__Settings__<property>, e.g. Connections__MyConnection__Settings__ClientId, Connections__MyConnection__Settings__TenantId, etc.
55
+ * Environment variables for connections map should be in the format ConnectionsMap__<index>__<property>, e.g. ConnectionsMap__0__ServiceUrl, ConnectionsMap__0__Connection, etc.
56
+ */
57
+ const connectionsEnv = {
58
+ connections: new Map<string, AuthConfiguration>(),
59
+ parser: envParser<ConnectionKeys>({
60
+ authType: envParserUtils.bypass,
61
+ tenantId: envParserUtils.bypass,
62
+ clientId: envParserUtils.bypass,
63
+ clientSecret: envParserUtils.bypass,
64
+ certPemFile: envParserUtils.bypass,
65
+ certKeyFile: envParserUtils.bypass,
66
+ connectionName: envParserUtils.bypass,
67
+ federatedClientId: envParserUtils.bypass,
68
+ FICClientId (value) {
69
+ logger.warn('Connections__<id>__Settings__FICClientId is deprecated, please use Connections__<id>__Settings__FederatedClientId instead.')
70
+ return { key: 'federatedClientId', value } // redirect
71
+ },
72
+ authorityEndpoint: envParserUtils.bypass,
73
+ authority (value) {
74
+ logger.warn('Connections__<id>__Settings__Authority is deprecated, please use Connections__<id>__Settings__AuthorityEndpoint instead.')
75
+ return { key: 'authorityEndpoint', value } // redirect
76
+ },
77
+ scopes (value) {
78
+ return this.issuers(value) // scopes can be comma or space separated list, same as issuers.
79
+ },
80
+ scope (value) {
81
+ logger.warn('Connections__<id>__Settings__Scope is deprecated, please use Connections__<id>__Settings__Scopes instead.')
82
+ return { key: 'scopes', value: this.scopes(value)?.value } // redirect with single scope
83
+ },
84
+ altBlueprintConnectionName: envParserUtils.bypass,
85
+ WIDAssertionFile: envParserUtils.bypass,
86
+ federatedTokenFile: envParserUtils.bypass,
87
+ idpmResource: envParserUtils.bypass,
88
+ azureRegion: envParserUtils.bypass,
89
+ sendX5C: (value) => ({ value: value === 'true' }),
90
+ issuers (value) {
91
+ if (value.includes(',')) {
92
+ return { value: value.split(',').map(s => s.trim()).filter(Boolean) }
93
+ }
94
+ return { value: value.split(/\s+/).filter(Boolean) }
95
+ },
96
+ }),
97
+ default (connections?: AuthConfiguration['connections'], connectionsMap?: AuthConfiguration['connectionsMap']) {
98
+ const conn = connections ?? this.connections
99
+ const map = connectionsMap ?? connectionsMapEnv.connectionsMap
100
+ const name = map?.find((item) => item.serviceUrl === '*')?.connection
101
+ if (!name) {
102
+ throw new Error('No default connection found in environment connections.')
103
+ }
104
+
105
+ const connection = conn?.get(name ?? '')
106
+ if (!connection) {
107
+ throw new Error(`Connection "${name}" not found in environment connections.`)
108
+ }
109
+
110
+ return applyDefaultSettings({ ...connection, connections: conn, connectionsMap: map })
111
+ },
112
+ process (key:string, value: string) {
113
+ const format = 'Connections__<id>__Settings__<property>'
114
+ const parts = key.split('__')
115
+ const [connections, id, settings, prop] = parts
116
+
117
+ if (`${connections}/${settings}`.toUpperCase() !== 'CONNECTIONS/SETTINGS') {
118
+ return false
119
+ }
120
+
121
+ if (parts.length !== 4) {
122
+ logger.warn(`Invalid connection environment variable: ${key}. Expected format: ${format}.`)
123
+ return false
124
+ }
125
+
126
+ if (!id?.trim()) {
127
+ logger.warn(`Invalid connection <id> in environment variable: ${key}. Expected format: ${format}.`)
128
+ return false
129
+ }
130
+
131
+ if (!prop?.trim()) {
132
+ logger.warn(`Invalid connection <property> in environment variable: ${key}. Expected format: ${format}.`)
133
+ return false
134
+ }
135
+
136
+ const result = this.parser.parse(prop as ConnectionKeys, value)
137
+ if (!result.key) {
138
+ return false
139
+ }
140
+
141
+ const config = this.connections.get(id) ?? {}
142
+ config[result.key as keyof AuthConfiguration] = result.value
143
+ this.connections.set(id, config)
144
+ return true
145
+ },
146
+ }
147
+
148
+ const connectionsMapEnv = {
149
+ connectionsMap: [] as ConnectionMapItem[],
150
+ parser: envParser<ConnectionMapKeys>({
151
+ serviceUrl: envParserUtils.bypass,
152
+ connection: envParserUtils.bypass,
153
+ audience: envParserUtils.bypass,
154
+ }),
155
+ process (key: string, value: string) {
156
+ const format = 'ConnectionsMap__<index>__<property>'
157
+ const parts = key.split('__')
158
+ const [connectionsMap, index, prop] = parts
159
+
160
+ if (connectionsMap.toUpperCase() !== 'CONNECTIONSMAP') {
161
+ return false
162
+ }
163
+
164
+ if (parts.length !== 3) {
165
+ logger.warn(`Invalid connection map environment variable: ${key}. Expected format: ${format}.`)
166
+ return false
167
+ }
168
+
169
+ const indexNumber = parseInt(index, 10)
170
+ if (!index?.trim() || isNaN(indexNumber) || indexNumber < 0) {
171
+ logger.warn(`Invalid connection map <index> in environment variable: ${key}. Expected format: ${format}, where <index> is a number.`)
172
+ return false
173
+ }
174
+
175
+ if (!prop?.trim()) {
176
+ logger.warn(`Invalid connection map <property> in environment variable: ${key}. Expected format: ${format}.`)
177
+ return false
178
+ }
179
+
180
+ const result = this.parser.parse(prop as ConnectionMapKeys, value)
181
+ if (!result.key) {
182
+ return false
183
+ }
184
+
185
+ const mapItem = this.connectionsMap[indexNumber] ?? { ...DEFAULT_CONNECTION_MAP }
186
+ mapItem[result.key as keyof ConnectionMapItem] = result.value
187
+ this.connectionsMap[indexNumber] = mapItem
188
+ return true
189
+ },
190
+ }
191
+
192
+ /**
193
+ * Legacy BotFramework style-like authentication configuration loaded from environment variables, with support for hot-reloading in test mode.
194
+ * Environment variables should be named MicrosoftAppTenantId, MicrosoftAppId, MicrosoftAppPassword, etc.
195
+ */
196
+ const legacyBotFrameworkEnv = {
197
+ parser: envParser({
198
+ MicrosoftAppTenantId: envParserUtils.redirect(connectionsEnv.parser, 'tenantId'),
199
+ MicrosoftAppId: envParserUtils.redirect(connectionsEnv.parser, 'clientId'),
200
+ MicrosoftAppPassword: envParserUtils.redirect(connectionsEnv.parser, 'clientSecret'),
201
+ certPemFile: envParserUtils.redirect(connectionsEnv.parser, 'certPemFile'),
202
+ certKeyFile: envParserUtils.redirect(connectionsEnv.parser, 'certKeyFile'),
203
+ connectionName: envParserUtils.redirect(connectionsEnv.parser, 'connectionName'),
204
+ MicrosoftAppClientId: envParserUtils.redirect(connectionsEnv.parser, 'federatedClientId'),
205
+ authorityEndpoint: envParserUtils.redirect(connectionsEnv.parser, 'authorityEndpoint'),
206
+ scope: envParserUtils.redirect(connectionsEnv.parser, 'scopes'),
207
+ altBlueprintConnectionName: envParserUtils.redirect(connectionsEnv.parser, 'altBlueprintConnectionName'),
208
+ WIDAssertionFile: envParserUtils.redirect(connectionsEnv.parser, 'WIDAssertionFile'),
209
+ azureRegion: envParserUtils.redirect(connectionsEnv.parser, 'azureRegion'),
210
+ sendX5C: envParserUtils.redirect(connectionsEnv.parser, 'sendX5C'),
211
+ authType: envParserUtils.redirect(connectionsEnv.parser, 'authType'),
212
+ federatedTokenFile: envParserUtils.redirect(connectionsEnv.parser, 'federatedTokenFile'),
213
+ idpmResource: envParserUtils.redirect(connectionsEnv.parser, 'idpmResource'),
214
+ }),
215
+ process (env: LoadEnv) {
216
+ return legacyPrefixEnv.process.call(this, env)
217
+ },
218
+ }
219
+
220
+ /**
221
+ * Legacy prefix-based authentication configuration loaded from environment variables, with support for hot-reloading in test mode.
222
+ * Environment variables should be prefixed with the connection name, e.g. <CONNECTION_NAME>_ClientId, <CONNECTION_NAME>_TenantId, etc.
223
+ */
224
+ const legacyPrefixEnv = {
225
+ parser: envParser({
226
+ tenantId: envParserUtils.redirect(connectionsEnv.parser, 'tenantId'),
227
+ clientId: envParserUtils.redirect(connectionsEnv.parser, 'clientId'),
228
+ clientSecret: envParserUtils.redirect(connectionsEnv.parser, 'clientSecret'),
229
+ certPemFile: envParserUtils.redirect(connectionsEnv.parser, 'certPemFile'),
230
+ certKeyFile: envParserUtils.redirect(connectionsEnv.parser, 'certKeyFile'),
231
+ connectionName: envParserUtils.redirect(connectionsEnv.parser, 'connectionName'),
232
+ FICClientId: envParserUtils.redirect(connectionsEnv.parser, 'federatedClientId'),
233
+ authorityEndpoint: envParserUtils.redirect(connectionsEnv.parser, 'authorityEndpoint'),
234
+ scope: envParserUtils.redirect(connectionsEnv.parser, 'scopes'),
235
+ altBlueprintConnectionName: envParserUtils.redirect(connectionsEnv.parser, 'altBlueprintConnectionName'),
236
+ WIDAssertionFile: envParserUtils.redirect(connectionsEnv.parser, 'WIDAssertionFile'),
237
+ azureRegion: envParserUtils.redirect(connectionsEnv.parser, 'azureRegion'),
238
+ sendX5C: envParserUtils.redirect(connectionsEnv.parser, 'sendX5C'),
239
+ authType: envParserUtils.redirect(connectionsEnv.parser, 'authType'),
240
+ federatedTokenFile: envParserUtils.redirect(connectionsEnv.parser, 'federatedTokenFile'),
241
+ idpmResource: envParserUtils.redirect(connectionsEnv.parser, 'idpmResource'),
242
+ }),
243
+ process (env: LoadEnv, prefix?: string) {
244
+ const settings: Partial<AuthConfiguration> = {}
245
+ for (const key of this.parser.keys) {
246
+ const k = prefix ? `${prefix}_${key}` : key
247
+ const envValue = env[k.toUpperCase()]
248
+ if (!envValue) {
249
+ continue
250
+ }
251
+ const result = this.parser.parse(key, envValue.value)
252
+ if (result.key) {
253
+ settings[result.key as keyof AuthConfiguration] = result.value
254
+ }
255
+ }
256
+ return settings
257
+ },
258
+ }
259
+
260
+ const loadEnv = () => {
261
+ connectionsEnv.connections = new Map<string, AuthConfiguration>()
262
+ connectionsMapEnv.connectionsMap = []
263
+
264
+ const env = loadEnvSettings((key, value) => {
265
+ // Process the first parser that matches the environment variable key and value,
266
+ // otherwise it continues to the next parser.
267
+ return connectionsEnv.process(key, value) ||
268
+ connectionsMapEnv.process(key, value)
269
+ })
270
+
271
+ if (connectionsEnv.connections.size === 0) {
272
+ logger.warn('No connections found in configuration.')
273
+ }
274
+
275
+ if (connectionsMapEnv.connectionsMap.length === 0 && connectionsEnv.connections.size > 0) {
276
+ logger.warn('No connections map found in configuration, assuming default connection map with serviceUrl "*" for the first connection.')
277
+ const [key] = connectionsEnv.connections.keys()
278
+ connectionsMapEnv.connectionsMap.push({ ...DEFAULT_CONNECTION_MAP, connection: key })
279
+ }
280
+
281
+ return {
282
+ env,
283
+ legacyBotFrameworkSettings: legacyBotFrameworkEnv.process(env),
284
+ legacyPrefixSettings: legacyPrefixEnv.process(env)
285
+ }
286
+ }
287
+
288
+ // Initial load of environment variables
289
+ let globalEnv = loadEnv()
290
+
102
291
  /**
103
292
  * Loads the authentication configuration from environment variables.
104
293
  *
@@ -126,43 +315,26 @@ export interface AuthConfiguration {
126
315
  *
127
316
  */
128
317
  export const loadAuthConfigFromEnv = (cnxName?: string): AuthConfiguration => {
129
- const envConnections = loadConnectionsMapFromEnv()
130
- let authConfig: AuthConfiguration
131
-
132
- if (envConnections.connectionsMap.length === 0) {
133
- // No connections provided, we need to populate the connections map with the old config settings
134
- authConfig = buildLegacyAuthConfig(cnxName)
135
- envConnections.connections.set(DEFAULT_CONNECTION, authConfig)
136
- envConnections.connectionsMap.push({
137
- serviceUrl: '*',
138
- connection: DEFAULT_CONNECTION,
139
- })
140
- } else {
141
- // There are connections provided, use the default or specified connection
142
- if (cnxName) {
143
- const entry = envConnections.connections.get(cnxName)
144
- if (entry) {
145
- authConfig = entry
146
- } else {
147
- throw new Error(`Connection "${cnxName}" not found in environment.`)
148
- }
149
- } else {
150
- const defaultItem = envConnections.connectionsMap.find((item) => item.serviceUrl === '*')
151
- const defaultConn = defaultItem ? envConnections.connections.get(defaultItem.connection) : undefined
152
- if (!defaultConn) {
153
- throw new Error('No default connection found in environment connections.')
154
- }
155
- authConfig = defaultConn
156
- }
318
+ if (process.env.TEST_MODE === 'true') {
319
+ globalEnv = loadEnv()
320
+ }
157
321
 
158
- authConfig.authority ??= 'https://login.microsoftonline.com'
159
- authConfig.issuers ??= getDefaultIssuers(authConfig.tenantId ?? '', authConfig.authority)
322
+ if (connectionsEnv.connections.size > 0) {
323
+ return cnxName?.trim() ? connectionsEnv.default(undefined, [{ ...DEFAULT_CONNECTION_MAP, connection: cnxName }]) : connectionsEnv.default()
160
324
  }
161
325
 
162
- return {
163
- ...authConfig,
164
- ...envConnections,
326
+ // No connections provided, we need to populate the connections map with the old config settings
327
+ const result = applyDefaultSettings(cnxName?.trim() ? legacyPrefixEnv.process(globalEnv.env, cnxName) : globalEnv.legacyPrefixSettings)
328
+ if (cnxName && !result.clientId) {
329
+ throw new Error(`ClientId not found for connection: ${cnxName}`)
165
330
  }
331
+
332
+ logger.info('Auth settings loaded from environment', {
333
+ connections: summarizeAuthConfiguration(result),
334
+ connectionsMap: result.connectionsMap?.map(e => ({ ...e, serviceUrl: e.serviceUrl !== '*' ? redactUrl(e.serviceUrl) : e.serviceUrl })),
335
+ })
336
+
337
+ return result
166
338
  }
167
339
 
168
340
  /**
@@ -180,100 +352,20 @@ export const loadAuthConfigFromEnv = (cnxName?: string): AuthConfiguration => {
180
352
  *
181
353
  */
182
354
  export const loadPrevAuthConfigFromEnv: () => AuthConfiguration = () => {
183
- const envConnections = loadConnectionsMapFromEnv()
184
- let authConfig: AuthConfiguration = {}
355
+ if (process.env.TEST_MODE === 'true') {
356
+ globalEnv = loadEnv()
357
+ }
185
358
 
186
- if (envConnections.connectionsMap.length === 0) {
187
- // No connections provided, we need to populate the connection map with the old config settings
188
- if (process.env.MicrosoftAppId === undefined && process.env.NODE_ENV === 'production') {
189
- throw new Error('ClientId required in production')
190
- }
191
- const authority = process.env.authorityEndpoint ?? 'https://login.microsoftonline.com'
192
- authConfig = {
193
- tenantId: process.env.MicrosoftAppTenantId,
194
- clientId: process.env.MicrosoftAppId,
195
- clientSecret: process.env.MicrosoftAppPassword,
196
- certPemFile: process.env.certPemFile,
197
- certKeyFile: process.env.certKeyFile,
198
- sendX5C: process.env.sendX5C === 'true',
199
- connectionName: process.env.connectionName,
200
- FICClientId: process.env.MicrosoftAppClientId,
201
- authority,
202
- scope: process.env.scope,
203
- issuers: getDefaultIssuers(process.env.MicrosoftAppTenantId ?? '', authority),
204
- altBlueprintConnectionName: process.env.altBlueprintConnectionName,
205
- WIDAssertionFile: process.env.WIDAssertionFile,
206
- azureRegion: process.env.azureRegion,
207
- }
208
- envConnections.connections.set(DEFAULT_CONNECTION, authConfig)
209
- envConnections.connectionsMap.push({
210
- serviceUrl: '*',
211
- connection: DEFAULT_CONNECTION,
212
- })
359
+ let result: AuthConfiguration
360
+ if (connectionsEnv.connections.size > 0) {
361
+ result = connectionsEnv.default()
213
362
  } else {
214
- // There are connections provided, use the default one.
215
- const defaultItem = envConnections.connectionsMap.find((item) => item.serviceUrl === '*')
216
- const defaultConn = defaultItem ? envConnections.connections.get(defaultItem.connection) : undefined
217
- if (!defaultConn) {
218
- throw new Error('No default connection found in environment connections.')
219
- }
220
- authConfig = defaultConn
363
+ // No connections provided, we need to populate the connection map with the old config settings
364
+ result = applyDefaultSettings(globalEnv.legacyBotFrameworkSettings)
221
365
  }
222
366
 
223
- authConfig.authority ??= 'https://login.microsoftonline.com'
224
- authConfig.issuers ??= getDefaultIssuers(authConfig.tenantId ?? '', authConfig.authority)
225
-
226
- return { ...authConfig, ...envConnections }
227
- }
228
-
229
- function loadConnectionsMapFromEnv () {
230
- const envVars = process.env
231
- const connectionsObj: Record<string, any> = {}
232
- const connectionsMap: ConnectionMapItem[] = []
233
- const CONNECTIONS_PREFIX = 'connections__'
234
- const CONNECTIONS_MAP_PREFIX = 'connectionsMap__'
235
-
236
- for (const [key, rawValue] of Object.entries(envVars)) {
237
- if (key.startsWith(CONNECTIONS_PREFIX)) {
238
- // Convert to dot notation
239
- let path = key.substring(CONNECTIONS_PREFIX.length).replace(/__/g, '.')
240
- // Remove ".settings." from the path
241
- path = path.replace('.settings.', '.')
242
- // Convert "true"/"false" strings into boolean values
243
- const value = rawValue === 'true' ? true : rawValue === 'false' ? false : rawValue
244
- objectPath.set(connectionsObj, path, value)
245
- } else if (key.startsWith(CONNECTIONS_MAP_PREFIX)) {
246
- const path = key.substring(CONNECTIONS_MAP_PREFIX.length).replace(/__/g, '.')
247
- objectPath.set(connectionsMap, path, rawValue)
248
- }
249
- }
250
-
251
- // Convert connectionsObj to Map<string, AuthConfiguration>
252
- const connections: Map<string, AuthConfiguration> = new Map(Object.entries(connectionsObj))
253
-
254
- if (connections.size === 0) {
255
- logger.warn('No connections found in configuration.')
256
- }
257
-
258
- if (connectionsMap.length === 0) {
259
- logger.warn('No connections map found in configuration.')
260
- if (connections.size > 0) {
261
- const firstEntry = connections.entries().next().value
262
-
263
- if (firstEntry) {
264
- const [firstKey] = firstEntry
265
- // Provide a default connection map if none is specified
266
- connectionsMap.push({
267
- serviceUrl: '*',
268
- connection: firstKey,
269
- })
270
- }
271
- }
272
- }
273
- return {
274
- connections,
275
- connectionsMap,
276
- }
367
+ logger.info('Legacy auth settings loaded from environment', summarizeAuthConfiguration(result), result.connectionsMap)
368
+ return result
277
369
  }
278
370
 
279
371
  /**
@@ -301,153 +393,22 @@ function loadConnectionsMapFromEnv () {
301
393
  *
302
394
  */
303
395
  export function getAuthConfigWithDefaults (config?: AuthConfiguration): AuthConfiguration {
304
- if (!config) return loadAuthConfigFromEnv()
305
-
306
- const providedConnections = config.connections && config.connectionsMap
307
- ? { connections: config.connections, connectionsMap: config.connectionsMap }
308
- : undefined
309
-
310
- const connections = providedConnections ?? loadConnectionsMapFromEnv()
311
-
312
- let mergedConfig: AuthConfiguration
313
-
314
- if (connections && connections.connectionsMap?.length === 0) {
315
- // No connections provided, we need to populate the connections map with the old config settings
316
- mergedConfig = buildLegacyAuthConfig(undefined, config)
317
- connections.connections?.set(DEFAULT_CONNECTION, mergedConfig)
318
- connections.connectionsMap.push({ serviceUrl: '*', connection: DEFAULT_CONNECTION })
319
- } else {
320
- // There are connections provided, use the default connection
321
- const defaultItem = connections.connectionsMap?.find((item) => item.serviceUrl === '*')
322
- const defaultConn = defaultItem ? connections.connections?.get(defaultItem.connection) : undefined
323
- if (!defaultConn) {
324
- throw new Error('No default connection found in environment connections.')
325
- }
326
- mergedConfig = buildLegacyAuthConfig(undefined, defaultConn)
327
- }
328
-
329
- return {
330
- ...mergedConfig,
331
- ...connections,
332
- }
333
- }
334
-
335
- function buildLegacyAuthConfig (envPrefix: string = '', customConfig?: AuthConfiguration): AuthConfiguration {
336
- const prefix = envPrefix ? `${envPrefix}_` : ''
337
- const authority = customConfig?.authority ?? process.env[`${prefix}authorityEndpoint`] ?? 'https://login.microsoftonline.com'
338
-
339
- const clientId = customConfig?.clientId ?? process.env[`${prefix}clientId`]
340
-
341
- if (!clientId && !envPrefix && process.env.NODE_ENV === 'production') {
342
- throw new Error('ClientId required in production')
343
- }
344
- if (!clientId && envPrefix) {
345
- throw new Error(`ClientId not found for connection: ${envPrefix}`)
396
+ if (process.env.TEST_MODE === 'true') {
397
+ globalEnv = loadEnv()
346
398
  }
347
399
 
348
- const tenantId = customConfig?.tenantId ?? process.env[`${prefix}tenantId`]
349
-
350
- return {
351
- tenantId,
352
- clientId: clientId!,
353
- clientSecret: customConfig?.clientSecret ?? process.env[`${prefix}clientSecret`],
354
- certPemFile: customConfig?.certPemFile ?? process.env[`${prefix}certPemFile`],
355
- certKeyFile: customConfig?.certKeyFile ?? process.env[`${prefix}certKeyFile`],
356
- sendX5C: customConfig?.sendX5C ?? (process.env[`${prefix}sendX5C`] === 'true'),
357
- connectionName: customConfig?.connectionName ?? process.env[`${prefix}connectionName`],
358
- FICClientId: customConfig?.FICClientId ?? process.env[`${prefix}FICClientId`],
359
- authority,
360
- scope: customConfig?.scope ?? process.env[`${prefix}scope`],
361
- issuers: customConfig?.issuers ?? getDefaultIssuers(tenantId as string, authority),
362
- altBlueprintConnectionName: customConfig?.altBlueprintConnectionName ?? process.env[`${prefix}altBlueprintConnectionName`],
363
- WIDAssertionFile: customConfig?.WIDAssertionFile ?? process.env[`${prefix}WIDAssertionFile`],
364
- azureRegion: customConfig?.azureRegion ?? process.env[`${prefix}azureRegion`]
365
- }
366
- }
367
-
368
- /**
369
- * Resolves the full authority URL including the tenant ID.
370
- * Supports both patterns:
371
- * - Tenant embedded in authority: https://login.microsoftonline.com/my-tenant
372
- * - Authority + separate tenantId: https://login.microsoftonline.com + tenantId
373
- * Also handles trailing slashes on authority.
374
- */
375
- export function resolveAuthority (authority?: string, tenantId?: string): string {
376
- const base = (authority ?? 'https://login.microsoftonline.com').replace(/\/+$/, '')
377
- const url = new URL(base)
378
- const hasPathSegment = url.pathname !== '/'
379
- if (hasPathSegment) {
380
- return base
381
- }
382
- return `${base}/${tenantId ?? 'botframework.com'}`
383
- }
384
-
385
- function getDefaultIssuers (tenantId: string, authority: string) : string[] {
386
- // Convert empty string to undefined so resolveAuthority applies its 'botframework.com' default
387
- const t = tenantId || undefined
388
- if (!t) {
389
- logger.warn('tenantId is not configured, defaulting to botframework.com')
390
- }
391
- return [
392
- 'https://api.botframework.com',
393
- `${resolveAuthority('https://sts.windows.net', t)}/`,
394
- `${resolveAuthority(authority, t)}/v2.0`
395
- ]
396
- }
397
-
398
- /**
399
- * A type representing a parser settings object.
400
- */
401
- type ParserSettings<K extends string> = {
402
- [key in K]: (value: string) => { key?: string, value?: any } | undefined
403
- }
404
-
405
- /**
406
- * Creates an environment variable parser that maps the variable keys to parsing functions.
407
- * @param settings An object where each key is an environment variable name and the value is a function
408
- * that takes the variable value as input and returns an object with optional `key` and `value` properties.
409
- * @remarks
410
- * The `key` property in the returned object can be used to rename the environment variable key,
411
- * while the `value` property contains the parsed value.
412
- * @returns An object with a `parse` method that takes an environment variable key and value,
413
- * and returns the parsed result.
414
- */
415
- export function envParser<K extends string> (settings: ParserSettings<K> & ThisType<ParserSettings<K>>) {
416
- const keys = Object.keys(settings) as K[]
417
- return {
418
- /**
419
- * Parses the given environment variable key and value using the provided settings.
420
- * @param key The environment variable key.
421
- * @param value The environment variable value.
422
- * @returns The parsed result with optional renamed key and parsed value.
423
- */
424
- parse (key: K, value: string) {
425
- const match = keys.find(k => k.toUpperCase() === key.toUpperCase())
426
- if (!match) {
427
- return {}
428
- }
429
-
430
- const result = settings[match](value)
431
- return { key: result?.key ?? match, value: result?.value }
400
+ let result: AuthConfiguration
401
+ if (!config) {
402
+ result = loadAuthConfigFromEnv()
403
+ } else {
404
+ const { connections, connectionsMap } = config.connections?.size ? config : { connections: connectionsEnv.connections, connectionsMap: connectionsMapEnv.connectionsMap }
405
+ if (connections?.size) {
406
+ result = { ...globalEnv.legacyPrefixSettings, ...connectionsEnv.default(connections, connectionsMap) }
407
+ } else {
408
+ result = applyDefaultSettings({ ...globalEnv.legacyPrefixSettings, ...config })
432
409
  }
433
410
  }
434
- }
435
411
 
436
- /**
437
- * Utility functions for environment variable parsers.
438
- */
439
- export const envParserUtils = {
440
- /**
441
- * Bypass parser that returns the value as is.
442
- * @param value The environment variable value.
443
- * @returns An object with the original value.
444
- */
445
- bypass: (value: string) => ({ value }),
446
- /**
447
- * Redirects the parsing to another parser for a specific key.
448
- * @param parser The target parser to redirect to.
449
- * @param key The key to use in the target parser.
450
- * @returns A function that takes the environment variable value and returns the parsed result from the target parser.
451
- */
452
- redirect: <Parser extends ReturnType<typeof envParser>>(parser: Parser, key: Parameters<Parser['parse']>[0]) => (value: string) => parser.parse(key, value)
412
+ logger.info('Auth settings loaded from runtime configuration', summarizeAuthConfiguration(result), result.connectionsMap)
413
+ return result
453
414
  }