@microsoft/agents-hosting 1.8.1 → 1.9.0-beta.18.g6b8111bba5

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 (101) hide show
  1. package/README.md +179 -9
  2. package/dist/package.json +4 -4
  3. package/dist/src/app/agentApplication.d.ts +3 -3
  4. package/dist/src/app/agentApplication.js +6 -16
  5. package/dist/src/app/agentApplication.js.map +1 -1
  6. package/dist/src/app/agentApplicationOptions.d.ts +6 -0
  7. package/dist/src/app/auth/authorization.d.ts +54 -0
  8. package/dist/src/app/auth/authorization.js +63 -2
  9. package/dist/src/app/auth/authorization.js.map +1 -1
  10. package/dist/src/app/auth/authorizationManager.d.ts +0 -8
  11. package/dist/src/app/auth/authorizationManager.js +104 -178
  12. package/dist/src/app/auth/authorizationManager.js.map +1 -1
  13. package/dist/src/auth/authConfiguration.d.ts +10 -3
  14. package/dist/src/auth/authConfiguration.js +292 -259
  15. package/dist/src/auth/authConfiguration.js.map +1 -1
  16. package/dist/src/auth/delegatedTokenCredential.d.ts +29 -0
  17. package/dist/src/auth/delegatedTokenCredential.js +49 -0
  18. package/dist/src/auth/delegatedTokenCredential.js.map +1 -0
  19. package/dist/src/auth/msal/msalTokenCredential.js +1 -1
  20. package/dist/src/auth/msal/msalTokenCredential.js.map +1 -1
  21. package/dist/src/auth/settings.d.ts +8 -70
  22. package/dist/src/auth/settings.js +5 -67
  23. package/dist/src/auth/settings.js.map +1 -1
  24. package/dist/src/cloudAdapter.d.ts +5 -0
  25. package/dist/src/cloudAdapter.js +13 -103
  26. package/dist/src/cloudAdapter.js.map +1 -1
  27. package/dist/src/configuration/botFrameworkEnvironmentCompatibility.d.ts +23 -0
  28. package/dist/src/configuration/botFrameworkEnvironmentCompatibility.js +183 -0
  29. package/dist/src/configuration/botFrameworkEnvironmentCompatibility.js.map +1 -0
  30. package/dist/src/configuration/configuration.d.ts +83 -0
  31. package/dist/src/configuration/configuration.js +836 -0
  32. package/dist/src/configuration/configuration.js.map +1 -0
  33. package/dist/src/configuration/configurationSource.d.ts +57 -0
  34. package/dist/src/configuration/configurationSource.js +7 -0
  35. package/dist/src/configuration/configurationSource.js.map +1 -0
  36. package/dist/src/configuration/environmentConfiguration.d.ts +14 -0
  37. package/dist/src/configuration/environmentConfiguration.js +271 -0
  38. package/dist/src/configuration/environmentConfiguration.js.map +1 -0
  39. package/dist/src/configuration/index.d.ts +7 -0
  40. package/dist/src/configuration/index.js +14 -0
  41. package/dist/src/configuration/index.js.map +1 -0
  42. package/dist/src/configuration/jsonFileConfigurationSource.d.ts +15 -0
  43. package/dist/src/configuration/jsonFileConfigurationSource.js +58 -0
  44. package/dist/src/configuration/jsonFileConfigurationSource.js.map +1 -0
  45. package/dist/src/createCloudAdapter.d.ts +30 -5
  46. package/dist/src/createCloudAdapter.js +17 -10
  47. package/dist/src/createCloudAdapter.js.map +1 -1
  48. package/dist/src/errorHelper.js +90 -2
  49. package/dist/src/errorHelper.js.map +1 -1
  50. package/dist/src/index.d.ts +1 -0
  51. package/dist/src/index.js +1 -0
  52. package/dist/src/index.js.map +1 -1
  53. package/dist/src/outboundHostValidator.d.ts +4 -1
  54. package/dist/src/outboundHostValidator.js +25 -48
  55. package/dist/src/outboundHostValidator.js.map +1 -1
  56. package/dist/src/storage/fileStorage.d.ts +5 -2
  57. package/dist/src/storage/fileStorage.js +41 -3
  58. package/dist/src/storage/fileStorage.js.map +1 -1
  59. package/dist/src/storage/index.d.ts +1 -0
  60. package/dist/src/storage/index.js +1 -0
  61. package/dist/src/storage/index.js.map +1 -1
  62. package/dist/src/storage/memoryStorage.d.ts +4 -2
  63. package/dist/src/storage/memoryStorage.js +32 -6
  64. package/dist/src/storage/memoryStorage.js.map +1 -1
  65. package/dist/src/storage/storage.d.ts +14 -1
  66. package/dist/src/storage/storageExpiry.d.ts +6 -0
  67. package/dist/src/storage/storageExpiry.js +20 -0
  68. package/dist/src/storage/storageExpiry.js.map +1 -0
  69. package/dist/src/utils/env.d.ts +43 -0
  70. package/dist/src/utils/env.js +42 -6
  71. package/dist/src/utils/env.js.map +1 -1
  72. package/dist/src/utils.d.ts +1 -0
  73. package/dist/src/utils.js +10 -1
  74. package/dist/src/utils.js.map +1 -1
  75. package/package.json +4 -4
  76. package/src/app/agentApplication.ts +11 -14
  77. package/src/app/agentApplicationOptions.ts +7 -0
  78. package/src/app/auth/authorization.ts +67 -2
  79. package/src/app/auth/authorizationManager.ts +126 -204
  80. package/src/auth/authConfiguration.ts +415 -260
  81. package/src/auth/delegatedTokenCredential.ts +49 -0
  82. package/src/auth/msal/msalTokenCredential.ts +1 -1
  83. package/src/auth/settings.ts +5 -82
  84. package/src/cloudAdapter.ts +38 -113
  85. package/src/configuration/botFrameworkEnvironmentCompatibility.ts +245 -0
  86. package/src/configuration/configuration.ts +1076 -0
  87. package/src/configuration/configurationSource.ts +76 -0
  88. package/src/configuration/environmentConfiguration.ts +377 -0
  89. package/src/configuration/index.ts +19 -0
  90. package/src/configuration/jsonFileConfigurationSource.ts +74 -0
  91. package/src/createCloudAdapter.ts +52 -9
  92. package/src/errorHelper.ts +110 -2
  93. package/src/index.ts +1 -0
  94. package/src/outboundHostValidator.ts +37 -44
  95. package/src/storage/fileStorage.ts +43 -5
  96. package/src/storage/index.ts +1 -0
  97. package/src/storage/memoryStorage.ts +34 -8
  98. package/src/storage/storage.ts +15 -1
  99. package/src/storage/storageExpiry.ts +21 -0
  100. package/src/utils/env.ts +63 -0
  101. package/src/utils.ts +10 -0
@@ -4,17 +4,27 @@
4
4
  */
5
5
 
6
6
  import { debug, redactString, redactScopes, redactUrl } from '@microsoft/agents-telemetry'
7
- import { loadEnvSettings, AuthConfiguration, envParser, envParserUtils, LoadEnv, applyDefaultSettings, DEFAULT_CONNECTION_MAP, ConnectionKeys, ConnectionMapKeys, ConnectionMapItem } from './settings'
7
+ import { AuthConfiguration, applyDefaultSettings, DEFAULT_CONNECTION_MAP, ConnectionMapItem } from './settings'
8
8
 
9
9
  export { type AuthConfiguration, type ConnectionSettings, type ConnectionSettingsBase, type MsalConnectionSettings, type SidecarConnectionSettings, AuthType, resolveAuthority, type ConnectionMapItem, resolveAuthType } from './settings'
10
10
  import { prune } from '../utils'
11
- import { parseBooleanEnv } from '../utils/env'
12
11
  import { ExceptionHelper } from '@microsoft/agents-activity'
13
12
  import { Errors } from '../errorHelper'
13
+ import {
14
+ ConfigurationContext,
15
+ ConfigurationLayer,
16
+ getConfigurationSnapshot
17
+ } from '../configuration/configuration'
18
+ import { loadModernEnvironmentConfiguration } from '../configuration/environmentConfiguration'
19
+ import {
20
+ loadBotFrameworkEnvironmentConfiguration,
21
+ loadBotFrameworkPrefixedEnvironmentConfiguration
22
+ } from '../configuration/botFrameworkEnvironmentCompatibility'
14
23
 
15
24
  const logger = debug('agents:authConfiguration')
16
25
 
17
26
  type NonOptional<T> = { [K in keyof Required<T>]: T[K] }
27
+ type ConnectionMapPatch = Partial<ConnectionMapItem>
18
28
 
19
29
  /**
20
30
  * 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.
@@ -66,275 +76,374 @@ function summarizeAuthConfiguration (authConfig: AuthConfiguration) {
66
76
  * Environment variables for connections map should be in the format ConnectionsMap__<index>__<property>, e.g. ConnectionsMap__0__ServiceUrl, ConnectionsMap__0__Connection, etc.
67
77
  */
68
78
  const connectionsEnv = {
69
- connections: new Map<string, AuthConfiguration>(),
70
- parser: envParser<ConnectionKeys>({
71
- authType: envParserUtils.bypass,
72
- tenantId: envParserUtils.bypass,
73
- clientId: envParserUtils.bypass,
74
- clientSecret: envParserUtils.bypass,
75
- certPemFile: envParserUtils.bypass,
76
- certKeyFile: envParserUtils.bypass,
77
- connectionName: envParserUtils.bypass,
78
- federatedClientId: envParserUtils.bypass,
79
- FICClientId (value) {
80
- logger.warn('Connections__<id>__Settings__FICClientId is deprecated, please use Connections__<id>__Settings__FederatedClientId instead.')
81
- return { key: 'federatedClientId', value } // redirect
82
- },
83
- authorityEndpoint: envParserUtils.bypass,
84
- authority (value) {
85
- logger.warn('Connections__<id>__Settings__Authority is deprecated, please use Connections__<id>__Settings__AuthorityEndpoint instead.')
86
- return { key: 'authorityEndpoint', value } // redirect
87
- },
88
- scopes (value) {
89
- return this.issuers(value) // scopes can be comma or space separated list, same as issuers.
90
- },
91
- scope (value) {
92
- logger.warn('Connections__<id>__Settings__Scope is deprecated, please use Connections__<id>__Settings__Scopes instead.')
93
- return { key: 'scopes', value: this.scopes(value)?.value } // redirect with single scope
94
- },
95
- altBlueprintConnectionName: envParserUtils.bypass,
96
- alternateBlueprintConnectionName: (value) => ({ key: 'altBlueprintConnectionName', value }), // .NET parity alias
97
- WIDAssertionFile: envParserUtils.bypass,
98
- federatedTokenFile: envParserUtils.bypass,
99
- idpmResource: envParserUtils.bypass,
100
- azureRegion: envParserUtils.bypass,
101
- sendX5C: (value) => ({ value: value === 'true' }),
102
- msalRetryCount: (value) => {
103
- const n = parseInt(value, 10)
104
- return { value: Number.isFinite(n) && n >= 0 ? n : undefined }
105
- },
106
- sidecarBaseUrl: envParserUtils.bypass,
107
- serviceName: envParserUtils.bypass,
108
- blueprintServiceName: envParserUtils.bypass,
109
- bypassLocalNetworkRestriction: (value) => ({ value: value === 'true' }),
110
- requestTimeout: (value) => {
111
- const n = parseInt(value, 10)
112
- return { value: Number.isFinite(n) && n > 0 ? n : undefined }
113
- },
114
- retryCount: (value) => {
115
- const n = parseInt(value, 10)
116
- return { value: Number.isFinite(n) && n >= 0 ? n : undefined }
117
- },
118
- issuers (value) {
119
- if (value.includes(',')) {
120
- return { value: value.split(',').map(s => s.trim()).filter(Boolean) }
121
- }
122
- return { value: value.split(/\s+/).filter(Boolean) }
123
- },
124
- validateIssuer: (value) => ({ value: parseBooleanEnv(value) }),
125
- }),
126
- default (connections?: AuthConfiguration['connections'], connectionsMap?: AuthConfiguration['connectionsMap']) {
127
- const conn = connections ?? this.connections
128
- const map = connectionsMap ?? connectionsMapEnv.connectionsMap
129
- const name = map?.find((item) => item.serviceUrl === '*')?.connection
130
- if (!name) {
131
- throw ExceptionHelper.generateException(Error, Errors.NoDefaultConnectionFound)
132
- }
79
+ connections: new Map<string, AuthConfiguration>()
80
+ }
133
81
 
134
- const connection = conn?.get(name ?? '')
135
- if (!connection) {
136
- throw ExceptionHelper.generateException(Error, Errors.ConnectionNotFoundInEnvironment, undefined, { connectionName: name })
137
- }
82
+ const connectionsMapEnv = {
83
+ connectionsMap: new Map<number, ConnectionMapPatch>(),
84
+ finalized: [] as ConnectionMapItem[]
85
+ }
138
86
 
139
- return applyDefaultSettings({ ...connection, connections: conn, connectionsMap: map })
140
- },
141
- process (key:string, value: string) {
142
- const format = 'Connections__<id>__Settings__<property>'
143
- const parts = key.split('__')
144
- const [connections, id, settings, prop] = parts
87
+ const loadEnv = () => {
88
+ const modern = loadModernEnvironmentConfiguration()
89
+ connectionsEnv.connections.clear()
90
+ for (const connection of modern.connections.values()) {
91
+ connectionsEnv.connections.set(
92
+ connection.id,
93
+ { ...connection.settings } as AuthConfiguration
94
+ )
95
+ }
96
+ connectionsMapEnv.connectionsMap.clear()
97
+ for (const [index, item] of modern.connectionsMap) {
98
+ connectionsMapEnv.connectionsMap.set(index, { ...item })
99
+ }
145
100
 
146
- if (`${connections}/${settings}`.toUpperCase() !== 'CONNECTIONS/SETTINGS') {
147
- return false
148
- }
101
+ if (connectionsEnv.connections.size === 0) {
102
+ logger.warn('No connections found in configuration.')
103
+ }
149
104
 
150
- if (parts.length !== 4) {
151
- logger.warn(`Invalid connection environment variable: ${key}. Expected format: ${format}.`)
152
- return false
153
- }
105
+ if (connectionsMapEnv.connectionsMap.size === 0 && connectionsEnv.connections.size > 0) {
106
+ logger.warn('No connections map found in configuration, assuming default connection map with serviceUrl "*" for the first connection.')
107
+ const [key] = connectionsEnv.connections.keys()
108
+ connectionsMapEnv.connectionsMap.set(0, { ...DEFAULT_CONNECTION_MAP, connection: key })
109
+ }
110
+ return {
111
+ legacyBotFrameworkSettings: configurationLayerSettings(
112
+ loadBotFrameworkEnvironmentConfiguration()
113
+ ),
114
+ legacyPrefixSettings: configurationLayerSettings(
115
+ loadBotFrameworkPrefixedEnvironmentConfiguration('')
116
+ )
117
+ }
118
+ }
154
119
 
155
- if (!id?.trim()) {
156
- logger.warn(`Invalid connection <id> in environment variable: ${key}. Expected format: ${format}.`)
157
- return false
158
- }
120
+ interface AuthOperation {
121
+ readonly connections?: ReadonlyMap<string, Readonly<AuthConfiguration>>
122
+ readonly connectionsMap?: ReadonlyMap<number, Readonly<ConnectionMapPatch>>
123
+ readonly flatSettings?: Readonly<AuthConfiguration>
124
+ readonly replaceRegistry?: boolean
125
+ readonly synthesizeFlatConnection?: boolean
126
+ readonly materializeRequestedConnection?: boolean
127
+ }
159
128
 
160
- if (!prop?.trim()) {
161
- logger.warn(`Invalid connection <property> in environment variable: ${key}. Expected format: ${format}.`)
162
- return false
163
- }
129
+ function externalAuthOperation (layer: ConfigurationLayer): AuthOperation {
130
+ return {
131
+ connections: new Map(
132
+ [...layer.connections.values()].map(connection => [connection.id, connection.settings])
133
+ ),
134
+ connectionsMap: layer.connectionsMap
135
+ }
136
+ }
164
137
 
165
- const result = this.parser.parse(prop as ConnectionKeys, value)
166
- if (!result.key) {
167
- return false
168
- }
138
+ function configurationLayerSettings (
139
+ layer: ConfigurationLayer,
140
+ connectionName?: string
141
+ ): AuthConfiguration {
142
+ const connection = connectionName
143
+ ? layer.connections.get(connectionName.toLowerCase())
144
+ : layer.connections.values().next().value
145
+ return connection ? { ...connection.settings } as AuthConfiguration : {}
146
+ }
169
147
 
170
- const config = this.connections.get(id) ?? {}
171
- config[result.key as keyof AuthConfiguration] = result.value
172
- this.connections.set(id, config)
173
- return true
174
- },
148
+ function settingsOperation (
149
+ settings: AuthConfiguration,
150
+ synthesizeFlatConnection = false,
151
+ materializeRequestedConnection = false
152
+ ): AuthOperation {
153
+ const { connections, connectionsMap, ...flatSettings } = settings
154
+ return {
155
+ flatSettings,
156
+ connections,
157
+ connectionsMap: connectionsMap
158
+ ? new Map(connectionsMap.map((item, index) => [index, item]))
159
+ : undefined,
160
+ synthesizeFlatConnection,
161
+ materializeRequestedConnection
162
+ }
175
163
  }
176
164
 
177
- const connectionsMapEnv = {
178
- connectionsMap: [] as ConnectionMapItem[],
179
- parser: envParser<ConnectionMapKeys>({
180
- serviceUrl: envParserUtils.bypass,
181
- connection: envParserUtils.bypass,
182
- audience: envParserUtils.bypass,
183
- }),
184
- process (key: string, value: string) {
185
- const format = 'ConnectionsMap__<index>__<property>'
186
- const parts = key.split('__')
187
- const [connectionsMap, index, prop] = parts
188
-
189
- if (connectionsMap.toUpperCase() !== 'CONNECTIONSMAP') {
190
- return false
191
- }
165
+ function registryOperation (settings: AuthConfiguration): AuthOperation {
166
+ return {
167
+ connections: settings.connections,
168
+ connectionsMap: settings.connectionsMap
169
+ ? new Map(settings.connectionsMap.map((item, index) => [index, item]))
170
+ : undefined,
171
+ replaceRegistry: true
172
+ }
173
+ }
192
174
 
193
- if (parts.length !== 3) {
194
- logger.warn(`Invalid connection map environment variable: ${key}. Expected format: ${format}.`)
195
- return false
196
- }
175
+ function validateDirectIssuerConfiguration (config: AuthConfiguration): void {
176
+ validateDirectIssuerValue(config, 'validateIssuer')
177
+ for (const [id, settings] of config.connections ?? []) {
178
+ validateDirectIssuerValue(settings, `connections.${id}.settings.validateIssuer`)
179
+ }
180
+ }
197
181
 
198
- const indexNumber = parseInt(index, 10)
199
- if (!index?.trim() || isNaN(indexNumber) || indexNumber < 0) {
200
- logger.warn(`Invalid connection map <index> in environment variable: ${key}. Expected format: ${format}, where <index> is a number.`)
201
- return false
202
- }
182
+ function validateDirectIssuerValue (settings: AuthConfiguration, path: string): void {
183
+ if (settings.validateIssuer !== undefined && typeof settings.validateIssuer !== 'boolean') {
184
+ throw ExceptionHelper.generateException(
185
+ TypeError,
186
+ Errors.InvalidBooleanConfigurationValue,
187
+ undefined,
188
+ { sourceName: 'runtime configuration', path }
189
+ )
190
+ }
191
+ }
203
192
 
204
- if (!prop?.trim()) {
205
- logger.warn(`Invalid connection map <property> in environment variable: ${key}. Expected format: ${format}.`)
206
- return false
207
- }
193
+ function environmentRegistryOperation (): AuthOperation {
194
+ return {
195
+ connections: connectionsEnv.connections,
196
+ connectionsMap: connectionsMapEnv.connectionsMap
197
+ }
198
+ }
208
199
 
209
- const result = this.parser.parse(prop as ConnectionMapKeys, value)
210
- if (!result.key) {
211
- return false
200
+ function environmentRouteOperation (): AuthOperation {
201
+ return {
202
+ connectionsMap: connectionsMapEnv.connectionsMap
203
+ }
204
+ }
205
+
206
+ function applyConnectionOperation (
207
+ connections: Map<string, AuthConfiguration>,
208
+ operation: AuthOperation
209
+ ): void {
210
+ if (operation.replaceRegistry) {
211
+ const retained = retainedConnectionIds(operation)
212
+ for (const id of connections.keys()) {
213
+ if (!retained.has(id.toLowerCase())) {
214
+ connections.delete(id)
215
+ }
212
216
  }
217
+ }
213
218
 
214
- const mapItem = this.connectionsMap[indexNumber] ?? { ...DEFAULT_CONNECTION_MAP }
215
- mapItem[result.key as keyof ConnectionMapItem] = result.value
216
- this.connectionsMap[indexNumber] = mapItem
217
- return true
218
- },
219
+ for (const [id, settings] of operation.connections ?? []) {
220
+ const existingId = findConnectionKey(connections, id)
221
+ const targetId = existingId ?? id
222
+ connections.set(targetId, { ...connections.get(targetId), ...settings })
223
+ }
219
224
  }
220
225
 
221
- /**
222
- * Legacy BotFramework style-like authentication configuration loaded from environment variables, with support for hot-reloading in test mode.
223
- * Environment variables should be named MicrosoftAppTenantId, MicrosoftAppId, MicrosoftAppPassword, etc.
224
- */
225
- const legacyBotFrameworkEnv = {
226
- parser: envParser({
227
- MicrosoftAppTenantId: envParserUtils.redirect(connectionsEnv.parser, 'tenantId'),
228
- MicrosoftAppId: envParserUtils.redirect(connectionsEnv.parser, 'clientId'),
229
- MicrosoftAppPassword: envParserUtils.redirect(connectionsEnv.parser, 'clientSecret'),
230
- certPemFile: envParserUtils.redirect(connectionsEnv.parser, 'certPemFile'),
231
- certKeyFile: envParserUtils.redirect(connectionsEnv.parser, 'certKeyFile'),
232
- connectionName: envParserUtils.redirect(connectionsEnv.parser, 'connectionName'),
233
- MicrosoftAppClientId: envParserUtils.redirect(connectionsEnv.parser, 'federatedClientId'),
234
- authorityEndpoint: envParserUtils.redirect(connectionsEnv.parser, 'authorityEndpoint'),
235
- scope: envParserUtils.redirect(connectionsEnv.parser, 'scopes'),
236
- altBlueprintConnectionName: envParserUtils.redirect(connectionsEnv.parser, 'altBlueprintConnectionName'),
237
- alternateBlueprintConnectionName: envParserUtils.redirect(connectionsEnv.parser, 'altBlueprintConnectionName'),
238
- WIDAssertionFile: envParserUtils.redirect(connectionsEnv.parser, 'WIDAssertionFile'),
239
- azureRegion: envParserUtils.redirect(connectionsEnv.parser, 'azureRegion'),
240
- sendX5C: envParserUtils.redirect(connectionsEnv.parser, 'sendX5C'),
241
- msalRetryCount: envParserUtils.redirect(connectionsEnv.parser, 'msalRetryCount'),
242
- authType: envParserUtils.redirect(connectionsEnv.parser, 'authType'),
243
- federatedTokenFile: envParserUtils.redirect(connectionsEnv.parser, 'federatedTokenFile'),
244
- idpmResource: envParserUtils.redirect(connectionsEnv.parser, 'idpmResource'),
245
- sidecarBaseUrl: envParserUtils.redirect(connectionsEnv.parser, 'sidecarBaseUrl'),
246
- serviceName: envParserUtils.redirect(connectionsEnv.parser, 'serviceName'),
247
- blueprintServiceName: envParserUtils.redirect(connectionsEnv.parser, 'blueprintServiceName'),
248
- bypassLocalNetworkRestriction: envParserUtils.redirect(connectionsEnv.parser, 'bypassLocalNetworkRestriction'),
249
- requestTimeout: envParserUtils.redirect(connectionsEnv.parser, 'requestTimeout'),
250
- retryCount: envParserUtils.redirect(connectionsEnv.parser, 'retryCount'),
251
- validateIssuer: envParserUtils.redirect(connectionsEnv.parser, 'validateIssuer'),
252
- }),
253
- process (env: LoadEnv) {
254
- return legacyPrefixEnv.process.call(this, env)
255
- },
226
+ function retainedConnectionIds (operation: AuthOperation): ReadonlySet<string> {
227
+ return new Set(
228
+ [...operation.connections?.keys() ?? []].map(id => id.toLowerCase())
229
+ )
256
230
  }
257
231
 
258
- /**
259
- * Legacy prefix-based authentication configuration loaded from environment variables, with support for hot-reloading in test mode.
260
- * Environment variables should be prefixed with the connection name, e.g. <CONNECTION_NAME>_ClientId, <CONNECTION_NAME>_TenantId, etc.
261
- */
262
- const legacyPrefixEnv = {
263
- parser: envParser({
264
- tenantId: envParserUtils.redirect(connectionsEnv.parser, 'tenantId'),
265
- clientId: envParserUtils.redirect(connectionsEnv.parser, 'clientId'),
266
- clientSecret: envParserUtils.redirect(connectionsEnv.parser, 'clientSecret'),
267
- certPemFile: envParserUtils.redirect(connectionsEnv.parser, 'certPemFile'),
268
- certKeyFile: envParserUtils.redirect(connectionsEnv.parser, 'certKeyFile'),
269
- connectionName: envParserUtils.redirect(connectionsEnv.parser, 'connectionName'),
270
- FICClientId: envParserUtils.redirect(connectionsEnv.parser, 'federatedClientId'),
271
- authorityEndpoint: envParserUtils.redirect(connectionsEnv.parser, 'authorityEndpoint'),
272
- scope: envParserUtils.redirect(connectionsEnv.parser, 'scopes'),
273
- altBlueprintConnectionName: envParserUtils.redirect(connectionsEnv.parser, 'altBlueprintConnectionName'),
274
- alternateBlueprintConnectionName: envParserUtils.redirect(connectionsEnv.parser, 'altBlueprintConnectionName'),
275
- WIDAssertionFile: envParserUtils.redirect(connectionsEnv.parser, 'WIDAssertionFile'),
276
- azureRegion: envParserUtils.redirect(connectionsEnv.parser, 'azureRegion'),
277
- sendX5C: envParserUtils.redirect(connectionsEnv.parser, 'sendX5C'),
278
- msalRetryCount: envParserUtils.redirect(connectionsEnv.parser, 'msalRetryCount'),
279
- authType: envParserUtils.redirect(connectionsEnv.parser, 'authType'),
280
- federatedTokenFile: envParserUtils.redirect(connectionsEnv.parser, 'federatedTokenFile'),
281
- idpmResource: envParserUtils.redirect(connectionsEnv.parser, 'idpmResource'),
282
- sidecarBaseUrl: envParserUtils.redirect(connectionsEnv.parser, 'sidecarBaseUrl'),
283
- serviceName: envParserUtils.redirect(connectionsEnv.parser, 'serviceName'),
284
- blueprintServiceName: envParserUtils.redirect(connectionsEnv.parser, 'blueprintServiceName'),
285
- bypassLocalNetworkRestriction: envParserUtils.redirect(connectionsEnv.parser, 'bypassLocalNetworkRestriction'),
286
- requestTimeout: envParserUtils.redirect(connectionsEnv.parser, 'requestTimeout'),
287
- retryCount: envParserUtils.redirect(connectionsEnv.parser, 'retryCount'),
288
- validateIssuer: envParserUtils.redirect(connectionsEnv.parser, 'validateIssuer'),
289
- }),
290
- process (env: LoadEnv, prefix?: string) {
291
- const settings: Partial<AuthConfiguration> = {}
292
- for (const key of this.parser.keys) {
293
- const k = prefix ? `${prefix}_${key}` : key
294
- const envValue = env[k.toUpperCase()]
295
- if (!envValue) {
296
- continue
297
- }
298
- const result = this.parser.parse(key, envValue.value)
299
- if (result.key) {
300
- settings[result.key as keyof AuthConfiguration] = result.value
232
+ function findConnectionKey (
233
+ connections: ReadonlyMap<string, AuthConfiguration> | undefined,
234
+ requested: string | undefined
235
+ ): string | undefined {
236
+ if (!connections || !requested) {
237
+ return undefined
238
+ }
239
+ const normalized = requested.toLowerCase()
240
+ return [...connections.keys()].find(id => id.toLowerCase() === normalized)
241
+ }
242
+
243
+ function resolveConnectionsMap (operations: readonly AuthOperation[]): Map<number, ConnectionMapPatch> {
244
+ const connectionsMap = new Map<number, ConnectionMapPatch>()
245
+ for (const operation of operations) {
246
+ if (operation.replaceRegistry) {
247
+ const retained = retainedConnectionIds(operation)
248
+ for (const [index, item] of connectionsMap) {
249
+ if (item.connection && !retained.has(item.connection.toLowerCase())) {
250
+ connectionsMap.delete(index)
251
+ }
301
252
  }
302
253
  }
303
- return settings
304
- },
254
+ for (const [index, item] of operation.connectionsMap ?? []) {
255
+ connectionsMap.set(index, { ...connectionsMap.get(index), ...item })
256
+ }
257
+ }
258
+ return connectionsMap
305
259
  }
306
260
 
307
- const loadEnv = () => {
308
- connectionsEnv.connections = new Map<string, AuthConfiguration>()
309
- connectionsMapEnv.connectionsMap = []
310
-
311
- const env = loadEnvSettings((key, value) => {
312
- // Process the first parser that matches the environment variable key and value,
313
- // otherwise it continues to the next parser.
314
- return connectionsEnv.process(key, value) ||
315
- connectionsMapEnv.process(key, value)
316
- })
261
+ function finalizeConnectionsMap (
262
+ patches: ReadonlyMap<number, Readonly<ConnectionMapPatch>>,
263
+ connections: ReadonlyMap<string, AuthConfiguration>,
264
+ synthesizeDefault = true
265
+ ): ConnectionMapItem[] {
266
+ if (synthesizeDefault && patches.size === 0 && connections.size === 1) {
267
+ const [connection] = connections.keys()
268
+ return [{ ...DEFAULT_CONNECTION_MAP, connection }]
269
+ }
317
270
 
318
- if (connectionsEnv.connections.size === 0) {
319
- logger.warn('No connections found in configuration.')
271
+ return [...patches.entries()]
272
+ .sort(([left], [right]) => left - right)
273
+ .map(([index, item]) => {
274
+ if (!item.serviceUrl || !item.connection) {
275
+ throw ExceptionHelper.generateException(
276
+ Error,
277
+ Errors.InvalidConnectionMapEntry,
278
+ undefined,
279
+ { index: index.toString() }
280
+ )
281
+ }
282
+ const connection = findConnectionKey(connections, item.connection)
283
+ if (!connection) {
284
+ throw ExceptionHelper.generateException(
285
+ Error,
286
+ Errors.ConnectionNotFoundInEnvironment,
287
+ undefined,
288
+ { connectionName: item.connection }
289
+ )
290
+ }
291
+ return {
292
+ serviceUrl: item.serviceUrl,
293
+ connection,
294
+ ...(item.audience === undefined ? {} : { audience: item.audience })
295
+ }
296
+ })
297
+ }
298
+
299
+ function resolveAuthOperations (
300
+ operations: readonly AuthOperation[],
301
+ connectionName?: string
302
+ ): AuthConfiguration {
303
+ const connections = new Map<string, AuthConfiguration>()
304
+ for (const operation of operations) {
305
+ applyConnectionOperation(connections, operation)
320
306
  }
321
307
 
322
- if (connectionsMapEnv.connectionsMap.length === 0 && connectionsEnv.connections.size > 0) {
323
- logger.warn('No connections map found in configuration, assuming default connection map with serviceUrl "*" for the first connection.')
324
- const [key] = connectionsEnv.connections.keys()
325
- connectionsMapEnv.connectionsMap.push({ ...DEFAULT_CONNECTION_MAP, connection: key })
308
+ if (
309
+ connections.size === 0 &&
310
+ operations.some(operation => operation.synthesizeFlatConnection)
311
+ ) {
312
+ connections.set(DEFAULT_CONNECTION_MAP.connection, {})
313
+ }
314
+
315
+ const requestedName = connectionName?.trim()
316
+ if (
317
+ requestedName &&
318
+ !findConnectionKey(connections, requestedName) &&
319
+ operations.some(operation => operation.materializeRequestedConnection)
320
+ ) {
321
+ connections.set(requestedName, {})
322
+ }
323
+
324
+ const requestedConnection = connections.size > 0 ? requestedName : undefined
325
+ const requiresDefaultRoute = operations.some(operation => operation.replaceRegistry)
326
+ let connectionsMap = finalizeConnectionsMap(
327
+ resolveConnectionsMap(operations),
328
+ connections,
329
+ !requiresDefaultRoute
330
+ )
331
+ if (connectionsMap.length === 0 && (requiresDefaultRoute || connections.size > 1)) {
332
+ throw ExceptionHelper.generateException(Error, Errors.NoDefaultConnectionFound)
333
+ }
334
+ const defaultConnection = connectionsMap.find(item => item.serviceUrl === '*')?.connection
335
+ if (connectionsMap.length > 0 && !requestedConnection && !defaultConnection) {
336
+ throw ExceptionHelper.generateException(Error, Errors.NoDefaultConnectionFound)
337
+ }
338
+ const selectedConnection = requestedConnection || defaultConnection || connections.keys().next().value
339
+
340
+ if (connections.size > 0 && !selectedConnection) {
341
+ throw ExceptionHelper.generateException(Error, Errors.NoDefaultConnectionFound)
342
+ }
343
+ const selectedConnectionKey = findConnectionKey(connections, selectedConnection)
344
+ if (selectedConnection && !selectedConnectionKey) {
345
+ throw ExceptionHelper.generateException(
346
+ Error,
347
+ Errors.ConnectionNotFoundInEnvironment,
348
+ undefined,
349
+ { connectionName: selectedConnection }
350
+ )
351
+ }
352
+ if (requestedConnection) {
353
+ connectionsMap = [{ ...DEFAULT_CONNECTION_MAP, connection: selectedConnectionKey ?? requestedConnection }]
354
+ }
355
+
356
+ if (selectedConnection) {
357
+ const selectedSettings: AuthConfiguration = {}
358
+ for (const operation of operations) {
359
+ const operationConnectionKey = findConnectionKey(operation.connections, selectedConnection)
360
+ const connectionSettings = operationConnectionKey
361
+ ? operation.connections?.get(operationConnectionKey)
362
+ : undefined
363
+ if (connectionSettings) {
364
+ Object.assign(selectedSettings, connectionSettings)
365
+ }
366
+ if (operation.flatSettings) {
367
+ Object.assign(selectedSettings, operation.flatSettings)
368
+ }
369
+ }
370
+
371
+ const resolved = applyDefaultSettings(selectedSettings)
372
+ const providerSettings = { ...resolved }
373
+ delete providerSettings.connections
374
+ delete providerSettings.connectionsMap
375
+ connections.set(selectedConnectionKey ?? selectedConnection, providerSettings)
376
+ return { ...providerSettings, connections, connectionsMap }
377
+ }
378
+
379
+ const flatSettings: AuthConfiguration = {}
380
+ for (const operation of operations) {
381
+ Object.assign(flatSettings, operation.flatSettings)
326
382
  }
383
+ return applyDefaultSettings(flatSettings)
384
+ }
327
385
 
386
+ function environmentOperation (
387
+ legacySettings: AuthConfiguration,
388
+ connectionName?: string
389
+ ): AuthOperation {
390
+ return connectionsEnv.connections.size > 0
391
+ ? environmentRegistryOperation()
392
+ : settingsOperation(
393
+ legacySettings,
394
+ false,
395
+ Boolean(connectionName?.trim() && Object.keys(legacySettings).length > 0)
396
+ )
397
+ }
398
+
399
+ function externalOperations (context?: ConfigurationContext) {
400
+ const snapshot = getConfigurationSnapshot(context)
328
401
  return {
329
- env,
330
- legacyBotFrameworkSettings: legacyBotFrameworkEnv.process(env),
331
- legacyPrefixSettings: legacyPrefixEnv.process(env)
402
+ hasAuth: (['fallback', 'overrideEnvironment', 'enforce'] as const).some(mode =>
403
+ snapshot[mode].connections.size > 0 ||
404
+ snapshot[mode].connectionsMap.size > 0
405
+ ),
406
+ fallback: externalAuthOperation(snapshot.fallback),
407
+ overrideEnvironment: externalAuthOperation(snapshot.overrideEnvironment),
408
+ enforce: externalAuthOperation(snapshot.enforce)
332
409
  }
333
410
  }
334
411
 
412
+ function preserveEnvironmentRegistryIdentity (
413
+ result: AuthConfiguration,
414
+ external: ReturnType<typeof externalOperations>,
415
+ environmentRegistrySelected: boolean
416
+ ): AuthConfiguration {
417
+ if (external.hasAuth || !environmentRegistrySelected) {
418
+ return result
419
+ }
420
+
421
+ for (const [id, settings] of result.connections ?? []) {
422
+ const environmentId = findConnectionKey(connectionsEnv.connections, id)
423
+ if (environmentId) {
424
+ connectionsEnv.connections.set(environmentId, settings)
425
+ }
426
+ }
427
+ connectionsMapEnv.finalized.splice(
428
+ 0,
429
+ connectionsMapEnv.finalized.length,
430
+ ...(result.connectionsMap ?? [])
431
+ )
432
+ result.connections = connectionsEnv.connections
433
+ result.connectionsMap = connectionsMapEnv.finalized
434
+ return result
435
+ }
436
+
335
437
  // Initial load of environment variables
336
438
  let globalEnv = loadEnv()
337
439
 
440
+ /**
441
+ * Optional host-scoped inputs used while resolving authentication settings.
442
+ */
443
+ export interface AuthConfigurationResolutionOptions {
444
+ configurationContext?: ConfigurationContext
445
+ }
446
+
338
447
  /**
339
448
  * Loads the authentication configuration from environment variables.
340
449
  *
@@ -361,17 +470,27 @@ let globalEnv = loadEnv()
361
470
  * ```
362
471
  *
363
472
  */
364
- export const loadAuthConfigFromEnv = (cnxName?: string): AuthConfiguration => {
473
+ export const loadAuthConfigFromEnv = (
474
+ cnxName?: string,
475
+ options?: AuthConfigurationResolutionOptions
476
+ ): AuthConfiguration => {
365
477
  if (process.env.TEST_MODE === 'true') {
366
478
  globalEnv = loadEnv()
367
479
  }
368
480
 
369
- if (connectionsEnv.connections.size > 0) {
370
- return cnxName?.trim() ? connectionsEnv.default(undefined, [{ ...DEFAULT_CONNECTION_MAP, connection: cnxName }]) : connectionsEnv.default()
371
- }
372
-
373
- // No connections provided, we need to populate the connections map with the old config settings
374
- const result = applyDefaultSettings(cnxName?.trim() ? legacyPrefixEnv.process(globalEnv.env, cnxName) : globalEnv.legacyPrefixSettings)
481
+ const legacySettings = cnxName?.trim()
482
+ ? configurationLayerSettings(
483
+ loadBotFrameworkPrefixedEnvironmentConfiguration(cnxName),
484
+ cnxName
485
+ )
486
+ : globalEnv.legacyPrefixSettings
487
+ const external = externalOperations(options?.configurationContext)
488
+ const result = preserveEnvironmentRegistryIdentity(resolveAuthOperations([
489
+ external.fallback,
490
+ environmentOperation(legacySettings, cnxName),
491
+ external.overrideEnvironment,
492
+ external.enforce
493
+ ], cnxName), external, connectionsEnv.connections.size > 0 && !cnxName?.trim())
375
494
  if (cnxName && !result.clientId) {
376
495
  throw ExceptionHelper.generateException(Error, Errors.ClientIdNotFoundForConnection, undefined, { connectionName: cnxName })
377
496
  }
@@ -398,18 +517,20 @@ export const loadAuthConfigFromEnv = (cnxName?: string): AuthConfiguration => {
398
517
  * ```
399
518
  *
400
519
  */
401
- export const loadPrevAuthConfigFromEnv: () => AuthConfiguration = () => {
520
+ export const loadPrevAuthConfigFromEnv = (
521
+ options?: AuthConfigurationResolutionOptions
522
+ ): AuthConfiguration => {
402
523
  if (process.env.TEST_MODE === 'true') {
403
524
  globalEnv = loadEnv()
404
525
  }
405
526
 
406
- let result: AuthConfiguration
407
- if (connectionsEnv.connections.size > 0) {
408
- result = connectionsEnv.default()
409
- } else {
410
- // No connections provided, we need to populate the connection map with the old config settings
411
- result = applyDefaultSettings(globalEnv.legacyBotFrameworkSettings)
412
- }
527
+ const external = externalOperations(options?.configurationContext)
528
+ const result = preserveEnvironmentRegistryIdentity(resolveAuthOperations([
529
+ external.fallback,
530
+ environmentOperation(globalEnv.legacyBotFrameworkSettings),
531
+ external.overrideEnvironment,
532
+ external.enforce
533
+ ]), external, connectionsEnv.connections.size > 0)
413
534
 
414
535
  logger.info('Legacy auth settings loaded from environment', summarizeAuthConfiguration(result), result.connectionsMap)
415
536
  return result
@@ -439,23 +560,57 @@ export const loadPrevAuthConfigFromEnv: () => AuthConfiguration = () => {
439
560
  * ```
440
561
  *
441
562
  */
442
- export function getAuthConfigWithDefaults (config?: AuthConfiguration): AuthConfiguration {
563
+ export function getAuthConfigWithDefaults (
564
+ config?: AuthConfiguration,
565
+ options?: AuthConfigurationResolutionOptions
566
+ ): AuthConfiguration {
567
+ if (config) {
568
+ validateDirectIssuerConfiguration(config)
569
+ }
443
570
  if (process.env.TEST_MODE === 'true') {
444
571
  globalEnv = loadEnv()
445
572
  }
446
573
 
447
- let result: AuthConfiguration
448
- if (!config) {
449
- result = loadAuthConfigFromEnv()
574
+ const external = externalOperations(options?.configurationContext)
575
+ const operations: AuthOperation[] = [external.fallback]
576
+
577
+ if (config?.connections?.size) {
578
+ operations.push(settingsOperation(globalEnv.legacyPrefixSettings))
579
+ if (!config.connectionsMap?.length) {
580
+ operations.push(environmentRouteOperation())
581
+ }
582
+ operations.push(external.overrideEnvironment)
583
+ operations.push(registryOperation(config))
584
+ } else if (connectionsEnv.connections.size > 0) {
585
+ if (config) {
586
+ operations.push(settingsOperation(globalEnv.legacyPrefixSettings))
587
+ }
588
+ operations.push(environmentRegistryOperation())
589
+ operations.push(external.overrideEnvironment)
450
590
  } else {
451
- const { connections, connectionsMap } = config.connections?.size ? config : { connections: connectionsEnv.connections, connectionsMap: connectionsMapEnv.connectionsMap }
452
- if (connections?.size) {
453
- result = { ...globalEnv.legacyPrefixSettings, ...connectionsEnv.default(connections, connectionsMap) }
454
- } else {
455
- result = applyDefaultSettings({ ...globalEnv.legacyPrefixSettings, ...config })
591
+ operations.push(settingsOperation(globalEnv.legacyPrefixSettings))
592
+ operations.push(external.overrideEnvironment)
593
+ if (config) {
594
+ operations.push(settingsOperation(config, true))
595
+ }
596
+ }
597
+ operations.push(external.enforce)
598
+
599
+ const result = preserveEnvironmentRegistryIdentity(
600
+ resolveAuthOperations(operations),
601
+ external,
602
+ !config?.connections?.size && connectionsEnv.connections.size > 0
603
+ )
604
+ const directParticipates = Boolean(config?.connections?.size) ||
605
+ connectionsEnv.connections.size === 0
606
+ if (!external.hasAuth && config && directParticipates) {
607
+ if (config.connections?.size) {
608
+ result.connections = config.connections
609
+ }
610
+ if (config.connectionsMap?.length) {
611
+ result.connectionsMap = config.connectionsMap
456
612
  }
457
613
  }
458
-
459
614
  logger.info('Auth settings loaded from runtime configuration', summarizeAuthConfiguration(result), result.connectionsMap)
460
615
  return result
461
616
  }