@microsoft/agents-hosting 1.9.0-beta.8.gf58e66b2ee → 1.9.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 (139) hide show
  1. package/README.md +182 -4
  2. package/dist/package.json +3 -3
  3. package/dist/src/app/agentApplication.d.ts +4 -3
  4. package/dist/src/app/agentApplication.js +24 -21
  5. package/dist/src/app/agentApplication.js.map +1 -1
  6. package/dist/src/app/agentApplicationBuilder.d.ts +9 -2
  7. package/dist/src/app/agentApplicationBuilder.js +9 -0
  8. package/dist/src/app/agentApplicationBuilder.js.map +1 -1
  9. package/dist/src/app/agentApplicationOptions.d.ts +16 -4
  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/app/auth/handlerStorage.d.ts +5 -3
  14. package/dist/src/app/auth/handlerStorage.js +36 -10
  15. package/dist/src/app/auth/handlerStorage.js.map +1 -1
  16. package/dist/src/app/auth/types.d.ts +2 -2
  17. package/dist/src/app/index.d.ts +1 -0
  18. package/dist/src/app/proactive/proactive.js +10 -6
  19. package/dist/src/app/proactive/proactive.js.map +1 -1
  20. package/dist/src/app/proactive/proactiveOptions.d.ts +2 -2
  21. package/dist/src/app/rateLimit.d.ts +315 -0
  22. package/dist/src/app/rateLimit.js +333 -0
  23. package/dist/src/app/rateLimit.js.map +1 -0
  24. package/dist/src/app/turnState.d.ts +6 -3
  25. package/dist/src/app/turnState.js +43 -4
  26. package/dist/src/app/turnState.js.map +1 -1
  27. package/dist/src/auth/authConfiguration.d.ts +10 -3
  28. package/dist/src/auth/authConfiguration.js +292 -259
  29. package/dist/src/auth/authConfiguration.js.map +1 -1
  30. package/dist/src/auth/settings.d.ts +8 -70
  31. package/dist/src/auth/settings.js +5 -67
  32. package/dist/src/auth/settings.js.map +1 -1
  33. package/dist/src/cloudAdapter.d.ts +5 -0
  34. package/dist/src/cloudAdapter.js +13 -103
  35. package/dist/src/cloudAdapter.js.map +1 -1
  36. package/dist/src/configuration/botFrameworkEnvironmentCompatibility.d.ts +23 -0
  37. package/dist/src/configuration/botFrameworkEnvironmentCompatibility.js +183 -0
  38. package/dist/src/configuration/botFrameworkEnvironmentCompatibility.js.map +1 -0
  39. package/dist/src/configuration/configuration.d.ts +83 -0
  40. package/dist/src/configuration/configuration.js +836 -0
  41. package/dist/src/configuration/configuration.js.map +1 -0
  42. package/dist/src/configuration/configurationSource.d.ts +57 -0
  43. package/dist/src/configuration/configurationSource.js +7 -0
  44. package/dist/src/configuration/configurationSource.js.map +1 -0
  45. package/dist/src/configuration/environmentConfiguration.d.ts +14 -0
  46. package/dist/src/configuration/environmentConfiguration.js +271 -0
  47. package/dist/src/configuration/environmentConfiguration.js.map +1 -0
  48. package/dist/src/configuration/index.d.ts +7 -0
  49. package/dist/src/configuration/index.js +14 -0
  50. package/dist/src/configuration/index.js.map +1 -0
  51. package/dist/src/configuration/jsonFileConfigurationSource.d.ts +15 -0
  52. package/dist/src/configuration/jsonFileConfigurationSource.js +58 -0
  53. package/dist/src/configuration/jsonFileConfigurationSource.js.map +1 -0
  54. package/dist/src/createCloudAdapter.d.ts +30 -5
  55. package/dist/src/createCloudAdapter.js +17 -10
  56. package/dist/src/createCloudAdapter.js.map +1 -1
  57. package/dist/src/errorHelper.js +117 -5
  58. package/dist/src/errorHelper.js.map +1 -1
  59. package/dist/src/index.d.ts +1 -0
  60. package/dist/src/index.js +1 -0
  61. package/dist/src/index.js.map +1 -1
  62. package/dist/src/observability/traces.d.ts +6 -4
  63. package/dist/src/observability/traces.js +10 -9
  64. package/dist/src/observability/traces.js.map +1 -1
  65. package/dist/src/outboundHostValidator.d.ts +4 -1
  66. package/dist/src/outboundHostValidator.js +25 -48
  67. package/dist/src/outboundHostValidator.js.map +1 -1
  68. package/dist/src/state/agentState.d.ts +8 -3
  69. package/dist/src/state/agentState.js +26 -8
  70. package/dist/src/state/agentState.js.map +1 -1
  71. package/dist/src/state/conversationState.d.ts +2 -2
  72. package/dist/src/state/conversationState.js.map +1 -1
  73. package/dist/src/state/userState.d.ts +2 -2
  74. package/dist/src/state/userState.js.map +1 -1
  75. package/dist/src/statusCodes.d.ts +5 -0
  76. package/dist/src/statusCodes.js +5 -0
  77. package/dist/src/statusCodes.js.map +1 -1
  78. package/dist/src/storage/fileStorage.d.ts +83 -68
  79. package/dist/src/storage/fileStorage.js +279 -105
  80. package/dist/src/storage/fileStorage.js.map +1 -1
  81. package/dist/src/storage/index.d.ts +1 -0
  82. package/dist/src/storage/index.js +1 -0
  83. package/dist/src/storage/index.js.map +1 -1
  84. package/dist/src/storage/memoryStorage.d.ts +85 -60
  85. package/dist/src/storage/memoryStorage.js +244 -87
  86. package/dist/src/storage/memoryStorage.js.map +1 -1
  87. package/dist/src/storage/storage.d.ts +100 -2
  88. package/dist/src/storage/storage.js +25 -0
  89. package/dist/src/storage/storage.js.map +1 -1
  90. package/dist/src/storage/storageCompatibility.d.ts +17 -0
  91. package/dist/src/storage/storageCompatibility.js +157 -0
  92. package/dist/src/storage/storageCompatibility.js.map +1 -0
  93. package/dist/src/storage/storageExpiry.d.ts +6 -0
  94. package/dist/src/storage/storageExpiry.js +20 -0
  95. package/dist/src/storage/storageExpiry.js.map +1 -0
  96. package/dist/src/utils/env.d.ts +43 -0
  97. package/dist/src/utils/env.js +42 -6
  98. package/dist/src/utils/env.js.map +1 -1
  99. package/dist/src/utils.d.ts +1 -0
  100. package/dist/src/utils.js +10 -1
  101. package/dist/src/utils.js.map +1 -1
  102. package/package.json +3 -3
  103. package/src/app/agentApplication.ts +27 -14
  104. package/src/app/agentApplicationBuilder.ts +13 -2
  105. package/src/app/agentApplicationOptions.ts +18 -4
  106. package/src/app/auth/authorizationManager.ts +126 -204
  107. package/src/app/auth/handlerStorage.ts +51 -7
  108. package/src/app/auth/types.ts +2 -2
  109. package/src/app/index.ts +7 -0
  110. package/src/app/proactive/proactive.ts +19 -9
  111. package/src/app/proactive/proactiveOptions.ts +2 -2
  112. package/src/app/rateLimit.ts +659 -0
  113. package/src/app/turnState.ts +50 -7
  114. package/src/auth/authConfiguration.ts +415 -260
  115. package/src/auth/settings.ts +5 -82
  116. package/src/cloudAdapter.ts +38 -113
  117. package/src/configuration/botFrameworkEnvironmentCompatibility.ts +245 -0
  118. package/src/configuration/configuration.ts +1076 -0
  119. package/src/configuration/configurationSource.ts +76 -0
  120. package/src/configuration/environmentConfiguration.ts +377 -0
  121. package/src/configuration/index.ts +19 -0
  122. package/src/configuration/jsonFileConfigurationSource.ts +74 -0
  123. package/src/createCloudAdapter.ts +52 -9
  124. package/src/errorHelper.ts +143 -5
  125. package/src/index.ts +1 -0
  126. package/src/observability/traces.ts +8 -7
  127. package/src/outboundHostValidator.ts +37 -44
  128. package/src/state/agentState.ts +39 -13
  129. package/src/state/conversationState.ts +2 -2
  130. package/src/state/userState.ts +2 -2
  131. package/src/statusCodes.ts +6 -0
  132. package/src/storage/fileStorage.ts +299 -106
  133. package/src/storage/index.ts +1 -0
  134. package/src/storage/memoryStorage.ts +268 -85
  135. package/src/storage/storage.ts +114 -2
  136. package/src/storage/storageCompatibility.ts +185 -0
  137. package/src/storage/storageExpiry.ts +21 -0
  138. package/src/utils/env.ts +63 -0
  139. package/src/utils.ts +10 -0
@@ -0,0 +1,1076 @@
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 { Errors } from '../errorHelper'
8
+ import { parseBooleanEnv, suggestClosest } from '../utils/env'
9
+ import {
10
+ ConfigurationDocument,
11
+ ConfigurationSourceMode,
12
+ ConfigurationSourceRegistration,
13
+ ConfigurationSourceResult
14
+ } from './configurationSource'
15
+ import { isSafeJsonFileConfigurationError } from './jsonFileConfigurationSource'
16
+
17
+ export type AuthConnectionPatch = Record<string, unknown>
18
+ export type AuthConnectionMapPatch = Partial<{
19
+ serviceUrl: string
20
+ audience: string
21
+ connection: string
22
+ }>
23
+ export type CloudAdapterConfigurationPatch = Partial<{
24
+ emitStackTrace: boolean
25
+ validateServiceUrl: boolean
26
+ }>
27
+ export type OutboundHostValidatorConfigurationPatch = Partial<{
28
+ enabled: boolean
29
+ includeDefaultMicrosoftHosts: boolean
30
+ hosts: readonly string[]
31
+ }>
32
+ export type AuthorizationHandlerPatch = Record<string, unknown>
33
+
34
+ export interface ConfigurationLayer {
35
+ readonly connections: ReadonlyMap<string, Readonly<{
36
+ id: string
37
+ settings: Readonly<AuthConnectionPatch>
38
+ }>>
39
+ readonly connectionsMap: ReadonlyMap<number, Readonly<AuthConnectionMapPatch>>
40
+ readonly cloudAdapterOptions: Readonly<CloudAdapterConfigurationPatch>
41
+ readonly outboundHostValidator: Readonly<OutboundHostValidatorConfigurationPatch>
42
+ readonly agentApplication: Readonly<{
43
+ userAuthorization: Readonly<{
44
+ handlers: ReadonlyMap<string, Readonly<{
45
+ id: string
46
+ settings: Readonly<AuthorizationHandlerPatch>
47
+ }>>
48
+ }>
49
+ }>
50
+ }
51
+
52
+ type ConfigurationSnapshot = Readonly<Record<ConfigurationSourceMode, ConfigurationLayer>>
53
+ type ConfigurationInputFormat = ConfigurationSourceResult['format'] | 'environment'
54
+
55
+ type ConfigurationState =
56
+ | { kind: 'uninitialized' }
57
+ | { kind: 'loading' }
58
+ | { kind: 'ready', snapshot: ConfigurationSnapshot }
59
+ | { kind: 'consumed', snapshot: ConfigurationSnapshot }
60
+
61
+ const modes: readonly ConfigurationSourceMode[] = ['fallback', 'overrideEnvironment', 'enforce']
62
+
63
+ const authParsers = {
64
+ authType: asString,
65
+ tenantId: asString,
66
+ clientId: asString,
67
+ clientSecret: asString,
68
+ certPemFile: asString,
69
+ certKeyFile: asString,
70
+ connectionName: asString,
71
+ federatedClientId: asString,
72
+ authorityEndpoint: asString,
73
+ scopes: asList,
74
+ altBlueprintConnectionName: asString,
75
+ WIDAssertionFile: asString,
76
+ federatedTokenFile: asString,
77
+ idpmResource: asString,
78
+ azureRegion: asString,
79
+ sendX5C: asBoolean,
80
+ msalRetryCount: asNonNegativeInteger,
81
+ sidecarBaseUrl: asString,
82
+ serviceName: asString,
83
+ blueprintServiceName: asString,
84
+ bypassLocalNetworkRestriction: asBoolean,
85
+ // Superset field: the JavaScript millisecond form parses here. A document
86
+ // string is the .NET-only TimeSpan form and is rejected by
87
+ // dotNetOnlyTimeSpanSettings below, not by this parser.
88
+ requestTimeout: asPositiveInteger,
89
+ retryCount: asNonNegativeInteger,
90
+ issuers: asList,
91
+ validateIssuer: asBoolean
92
+ } satisfies Record<string, (value: string) => unknown>
93
+
94
+ const cloudAdapterParsers = {
95
+ emitStackTrace: asBoolean,
96
+ validateServiceUrl: asBoolean
97
+ } satisfies Record<string, (value: string) => unknown>
98
+
99
+ const connectionMapParsers = {
100
+ serviceUrl: asString,
101
+ audience: asString,
102
+ connection: asString
103
+ } satisfies Record<string, (value: string) => unknown>
104
+
105
+ const outboundHostValidatorParsers = {
106
+ enabled: asBoolean,
107
+ includeDefaultMicrosoftHosts: asBoolean,
108
+ hosts: asList
109
+ } satisfies Record<string, (value: string) => unknown>
110
+
111
+ const authorizationParsers = {
112
+ type: asString,
113
+ azureBotOAuthConnectionName: asString,
114
+ title: asString,
115
+ text: asString,
116
+ invalidSignInRetryMessage: asString,
117
+ invalidSignInRetryMessageFormat: asString,
118
+ invalidSignInRetryMaxExceededMessage: asString,
119
+ oboConnectionName: asString,
120
+ enableSso: asBoolean,
121
+ invalidSignInRetryMax: asNonNegativeInteger,
122
+ oboScopes: asList,
123
+ altBlueprintConnectionName: asString,
124
+ scopes: asList
125
+ } satisfies Record<string, (value: string) => unknown>
126
+
127
+ const authPropertyAliases: Readonly<Record<string, string>> = {
128
+ alternateblueprintconnectionname: 'altBlueprintConnectionName',
129
+ ficclientid: 'federatedClientId',
130
+ authority: 'authorityEndpoint',
131
+ scope: 'scopes'
132
+ } as const
133
+
134
+ const authorizationPropertyAliases: Readonly<Record<string, string>> = {
135
+ alternateblueprintconnectionname: 'altBlueprintConnectionName'
136
+ } as const
137
+
138
+ const dotNetOnlyConnectionSettings = new Set([
139
+ 'certificatethumbprint',
140
+ 'certificatesubjectname',
141
+ 'certificatestorelocation',
142
+ 'certificatestorename',
143
+ 'validcertificateonly'
144
+ ])
145
+
146
+ const dotNetOnlyAuthorizationSettings = new Set([
147
+ 'cancelsignincommands',
148
+ 'endoninvalidmessage',
149
+ 'showsigninlink',
150
+ 'signincancelledmessage',
151
+ 'teamssignininprogressmessage',
152
+ 'timeout'
153
+ ])
154
+
155
+ // Connection settings that are a JS/.NET superset: JavaScript accepts the
156
+ // positive-integer millisecond form, while a document string is the
157
+ // .NET-only TimeSpan representation and is rejected as unsupported rather
158
+ // than as a generic invalid value. Canonical (environment) string values are
159
+ // unaffected, since every canonical value is a string regardless of runtime.
160
+ const dotNetOnlyTimeSpanSettings = new Set([
161
+ 'requesttimeout'
162
+ ])
163
+
164
+ let state: ConfigurationState = { kind: 'uninitialized' }
165
+ const contextSnapshots = new WeakMap<ConfigurationContext, ConfigurationSnapshot>()
166
+ let instantiateConfigurationContext: () => ConfigurationContext
167
+
168
+ /**
169
+ * An immutable, host-scoped set of resolved external configuration sources.
170
+ *
171
+ * @remarks
172
+ * Create instances with {@link createConfigurationContext}. The context is
173
+ * intentionally opaque; consumers pass it through their named options.
174
+ */
175
+ export class ConfigurationContext {
176
+ private readonly configurationContextBrand: undefined
177
+
178
+ private constructor () {
179
+ this.configurationContextBrand = undefined
180
+ }
181
+
182
+ static {
183
+ instantiateConfigurationContext = () => new ConfigurationContext()
184
+ }
185
+ }
186
+
187
+ function asString (value: string): string {
188
+ return value
189
+ }
190
+
191
+ function asBoolean (value: string): boolean | undefined {
192
+ return parseBooleanEnv(value)
193
+ }
194
+
195
+ function asInteger (value: string): number | undefined {
196
+ const normalized = value.trim()
197
+ if (!/^-?\d+$/.test(normalized)) {
198
+ return undefined
199
+ }
200
+ const parsed = Number.parseInt(normalized, 10)
201
+ return Number.isSafeInteger(parsed) ? parsed : undefined
202
+ }
203
+
204
+ function asNonNegativeInteger (value: string): number | undefined {
205
+ const parsed = asInteger(value)
206
+ return parsed !== undefined && parsed >= 0 ? parsed : undefined
207
+ }
208
+
209
+ function asPositiveInteger (value: string): number | undefined {
210
+ const parsed = asInteger(value)
211
+ return parsed !== undefined && parsed > 0 ? parsed : undefined
212
+ }
213
+
214
+ function asList (value: string): string[] {
215
+ return value.includes(',')
216
+ ? value.split(',').map(item => item.trim()).filter(Boolean)
217
+ : value.split(/\s+/).filter(Boolean)
218
+ }
219
+
220
+ export function createConfigurationLayer (): ConfigurationLayer {
221
+ return {
222
+ connections: new Map(),
223
+ connectionsMap: new Map(),
224
+ cloudAdapterOptions: {},
225
+ outboundHostValidator: {},
226
+ agentApplication: {
227
+ userAuthorization: {
228
+ handlers: new Map()
229
+ }
230
+ }
231
+ }
232
+ }
233
+
234
+ function emptySnapshot (): ConfigurationSnapshot {
235
+ return Object.freeze({
236
+ fallback: freezeConfigurationLayer(createConfigurationLayer()),
237
+ overrideEnvironment: freezeConfigurationLayer(createConfigurationLayer()),
238
+ enforce: freezeConfigurationLayer(createConfigurationLayer())
239
+ })
240
+ }
241
+
242
+ export function freezeConfigurationLayer (layer: ConfigurationLayer): ConfigurationLayer {
243
+ return Object.freeze({
244
+ connections: new Map([...layer.connections].map(([lookup, connection]) => [
245
+ lookup,
246
+ Object.freeze({
247
+ id: connection.id,
248
+ settings: freezeRecord(connection.settings)
249
+ })
250
+ ])),
251
+ connectionsMap: new Map([...layer.connectionsMap].map(([index, item]) => [
252
+ index,
253
+ Object.freeze({ ...item })
254
+ ])),
255
+ cloudAdapterOptions: Object.freeze({ ...layer.cloudAdapterOptions }),
256
+ outboundHostValidator: Object.freeze({
257
+ ...layer.outboundHostValidator,
258
+ ...(layer.outboundHostValidator.hosts === undefined
259
+ ? {}
260
+ : { hosts: Object.freeze([...layer.outboundHostValidator.hosts]) })
261
+ }),
262
+ agentApplication: Object.freeze({
263
+ userAuthorization: Object.freeze({
264
+ handlers: new Map([...layer.agentApplication.userAuthorization.handlers].map(([lookup, handler]) => [
265
+ lookup,
266
+ Object.freeze({ id: handler.id, settings: freezeRecord(handler.settings) })
267
+ ]))
268
+ })
269
+ })
270
+ })
271
+ }
272
+
273
+ export function mergeConfigurationLayers (
274
+ ...layers: readonly ConfigurationLayer[]
275
+ ): ConfigurationLayer {
276
+ const merged = createConfigurationLayer()
277
+
278
+ for (const layer of layers) {
279
+ for (const connection of layer.connections.values()) {
280
+ const lookup = connection.id.toLowerCase()
281
+ const current = merged.connections.get(lookup)
282
+ ;(merged.connections as Map<string, { id: string, settings: AuthConnectionPatch }>).set(
283
+ lookup,
284
+ {
285
+ id: current?.id ?? connection.id,
286
+ settings: {
287
+ ...current?.settings,
288
+ ...connection.settings
289
+ }
290
+ }
291
+ )
292
+ }
293
+ for (const [index, item] of layer.connectionsMap) {
294
+ ;(merged.connectionsMap as Map<number, AuthConnectionMapPatch>).set(index, {
295
+ ...merged.connectionsMap.get(index),
296
+ ...item
297
+ })
298
+ }
299
+ Object.assign(merged.cloudAdapterOptions, layer.cloudAdapterOptions)
300
+ Object.assign(merged.outboundHostValidator, layer.outboundHostValidator)
301
+
302
+ const mergedHandlers = merged.agentApplication.userAuthorization.handlers
303
+ for (const handler of layer.agentApplication.userAuthorization.handlers.values()) {
304
+ const lookup = handler.id.toLowerCase()
305
+ const current = mergedHandlers.get(lookup)
306
+ ;(mergedHandlers as Map<string, { id: string, settings: AuthorizationHandlerPatch }>).set(
307
+ lookup,
308
+ {
309
+ id: current?.id ?? handler.id,
310
+ settings: {
311
+ ...current?.settings,
312
+ ...handler.settings
313
+ }
314
+ }
315
+ )
316
+ }
317
+ }
318
+
319
+ return freezeConfigurationLayer(merged)
320
+ }
321
+
322
+ function freezeRecord<T extends Record<string, unknown>> (record: T): Readonly<T> {
323
+ return Object.freeze(Object.fromEntries(
324
+ Object.entries(record).map(([key, value]) => [
325
+ key,
326
+ Array.isArray(value) ? Object.freeze([...value]) : value
327
+ ])
328
+ )) as Readonly<T>
329
+ }
330
+
331
+ const canonicalRoots = [
332
+ 'connections',
333
+ 'connectionsMap',
334
+ 'cloudAdapterOptions',
335
+ 'outboundHostValidator',
336
+ 'agentApplication'
337
+ ] as const
338
+
339
+ /**
340
+ * Suggests a canonical path by correcting only fixed schema segments.
341
+ * Dynamic connection/handler IDs and route indexes are preserved verbatim.
342
+ */
343
+ export function suggestConfigurationPath (path: string): string | undefined {
344
+ const parts = path.split('.')
345
+ const root = findCanonicalSegment(parts[0], canonicalRoots)
346
+ if (!root) {
347
+ return undefined
348
+ }
349
+
350
+ let suggested: string[] | undefined
351
+ if (parts.length === 1) {
352
+ suggested = [root]
353
+ } else if (root === 'connections' && parts.length === 3) {
354
+ const settings = findCanonicalSegment(parts[2], ['settings'])
355
+ if (settings) {
356
+ suggested = [root, parts[1], settings]
357
+ }
358
+ } else if (root === 'connections' && parts.length >= 4) {
359
+ const settingsIndex = parts.length - 2
360
+ const settings = findCanonicalSegment(parts[settingsIndex], ['settings'])
361
+ const property = findCanonicalProperty(parts.at(-1) ?? '', authParsers, authPropertyAliases)
362
+ if (settings && property && settingsIndex > 1) {
363
+ suggested = [root, ...parts.slice(1, settingsIndex), settings, property]
364
+ }
365
+ } else if (root === 'connectionsMap' && parts.length === 3 && /^\d+$/.test(parts[1])) {
366
+ const property = findCanonicalProperty(parts[2], connectionMapParsers)
367
+ if (property) {
368
+ suggested = [root, parts[1], property]
369
+ }
370
+ } else if (root === 'cloudAdapterOptions' && parts.length === 2) {
371
+ const property = findCanonicalProperty(parts[1], cloudAdapterParsers)
372
+ if (property) {
373
+ suggested = [root, property]
374
+ }
375
+ } else if (root === 'outboundHostValidator' && parts.length === 2) {
376
+ const property = findCanonicalProperty(parts[1], outboundHostValidatorParsers)
377
+ if (property) {
378
+ suggested = [root, property]
379
+ }
380
+ } else if (root === 'agentApplication' && parts.length === 2) {
381
+ const userAuthorization = findCanonicalSegment(parts[1], ['userAuthorization'])
382
+ if (userAuthorization) {
383
+ suggested = [root, userAuthorization]
384
+ }
385
+ } else if (root === 'agentApplication' && parts.length === 3) {
386
+ const userAuthorization = findCanonicalSegment(parts[1], ['userAuthorization'])
387
+ const handlers = findCanonicalSegment(parts[2], ['handlers'])
388
+ if (userAuthorization && handlers) {
389
+ suggested = [root, userAuthorization, handlers]
390
+ }
391
+ } else if (root === 'agentApplication' && parts.length === 5) {
392
+ const userAuthorization = findCanonicalSegment(parts[1], ['userAuthorization'])
393
+ const handlers = findCanonicalSegment(parts[2], ['handlers'])
394
+ const settings = findCanonicalSegment(parts[4], ['settings'])
395
+ if (userAuthorization && handlers && settings) {
396
+ suggested = [root, userAuthorization, handlers, parts[3], settings]
397
+ }
398
+ } else if (root === 'agentApplication' && parts.length >= 6) {
399
+ const settingsIndex = parts.length - 2
400
+ const userAuthorization = findCanonicalSegment(parts[1], ['userAuthorization'])
401
+ const handlers = findCanonicalSegment(parts[2], ['handlers'])
402
+ const settings = findCanonicalSegment(parts[settingsIndex], ['settings'])
403
+ const property = findCanonicalProperty(
404
+ parts.at(-1) ?? '',
405
+ authorizationParsers,
406
+ authorizationPropertyAliases
407
+ )
408
+ if (userAuthorization && handlers && settings && property && settingsIndex > 3) {
409
+ suggested = [
410
+ root,
411
+ userAuthorization,
412
+ handlers,
413
+ ...parts.slice(3, settingsIndex),
414
+ settings,
415
+ property
416
+ ]
417
+ }
418
+ }
419
+
420
+ const result = suggested?.join('.')
421
+ return result && result.toLowerCase() !== path.toLowerCase() ? result : undefined
422
+ }
423
+
424
+ function findCanonicalSegment (
425
+ value: string | undefined,
426
+ candidates: readonly string[]
427
+ ): string | undefined {
428
+ if (!value) {
429
+ return undefined
430
+ }
431
+ return candidates.find(candidate => candidate.toLowerCase() === value.toLowerCase()) ??
432
+ suggestClosest(value, candidates, 4)
433
+ }
434
+
435
+ function findCanonicalProperty (
436
+ value: string,
437
+ parsers: Readonly<Record<string, (value: string) => unknown>>,
438
+ aliases: Readonly<Record<string, string>> = {}
439
+ ): string | undefined {
440
+ const alias = aliases[value.toLowerCase()]
441
+ return alias ??
442
+ findCanonicalSegment(value, Object.keys(parsers))
443
+ }
444
+
445
+ function invalidConfigurationPath (path: string, sourceName: string): Error {
446
+ const suggestion = suggestConfigurationPath(path)
447
+ const definition = suggestion
448
+ ? {
449
+ ...Errors.InvalidConfigurationPath,
450
+ description: `${Errors.InvalidConfigurationPath.description} Did you mean \`${suggestion}\`?`
451
+ }
452
+ : Errors.InvalidConfigurationPath
453
+ return ExceptionHelper.generateException(Error, definition, undefined, { path, sourceName })
454
+ }
455
+
456
+ function parseValue (
457
+ parsers: Record<string, (value: string) => unknown>,
458
+ property: string,
459
+ value: unknown,
460
+ path: string,
461
+ sourceName: string,
462
+ format: ConfigurationInputFormat,
463
+ aliases: Readonly<Record<string, string>> = {},
464
+ allowExtension = false
465
+ ): { key: string, value: unknown } {
466
+ const lookup = property.toLowerCase()
467
+ const aliasedKey = format === 'canonical' ? undefined : aliases[lookup]
468
+ const key = aliasedKey ??
469
+ Object.keys(parsers).find(candidate => candidate.toLowerCase() === lookup)
470
+ if (!key) {
471
+ if (format === 'canonical' && aliases[lookup]) {
472
+ throw invalidConfigurationPath(path, sourceName)
473
+ }
474
+ if (allowExtension) {
475
+ validateDynamicSegment(property, path, sourceName)
476
+ const extensionValue = format === 'document'
477
+ ? parseExtensionValue(value)
478
+ : typeof value === 'string' ? value : undefined
479
+ if (extensionValue !== undefined) {
480
+ return { key: property, value: extensionValue }
481
+ }
482
+ throw ExceptionHelper.generateException(TypeError, Errors.InvalidConfigurationValue, undefined, { path, sourceName })
483
+ }
484
+ throw invalidConfigurationPath(path, sourceName)
485
+ }
486
+
487
+ const parser = parsers[key]
488
+ const parsed = format === 'document'
489
+ ? parseDocumentValue(parser, value)
490
+ : typeof value === 'string' ? parser(value) : undefined
491
+ if (parsed === undefined) {
492
+ if (key === 'validateIssuer') {
493
+ throw ExceptionHelper.generateException(
494
+ TypeError,
495
+ Errors.InvalidBooleanConfigurationValue,
496
+ undefined,
497
+ { path, sourceName }
498
+ )
499
+ }
500
+ if (format === 'document' && typeof value === 'string' && dotNetOnlyTimeSpanSettings.has(lookup)) {
501
+ unsupportedRuntimeField(path, sourceName)
502
+ }
503
+ throw ExceptionHelper.generateException(TypeError, Errors.InvalidConfigurationValue, undefined, { path, sourceName })
504
+ }
505
+
506
+ function parseDocumentValue (
507
+ parser: (value: string) => unknown,
508
+ value: unknown
509
+ ): unknown {
510
+ if (parser === asString) {
511
+ return typeof value === 'string' ? value : undefined
512
+ }
513
+ if (parser === asBoolean) {
514
+ return typeof value === 'boolean' ? value : undefined
515
+ }
516
+ if (parser === asInteger) {
517
+ return typeof value === 'number' && Number.isSafeInteger(value) ? value : undefined
518
+ }
519
+ if (parser === asNonNegativeInteger) {
520
+ return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0 ? value : undefined
521
+ }
522
+ if (parser === asPositiveInteger) {
523
+ return typeof value === 'number' && Number.isSafeInteger(value) && value > 0 ? value : undefined
524
+ }
525
+ if (parser === asList) {
526
+ return Array.isArray(value) &&
527
+ value.every(item => typeof item === 'string' && item.trim().length > 0)
528
+ ? [...value]
529
+ : undefined
530
+ }
531
+ return undefined
532
+ }
533
+ return { key, value: parsed }
534
+ }
535
+
536
+ function parseExtensionValue (value: unknown): unknown {
537
+ if (typeof value === 'string' || typeof value === 'boolean' ||
538
+ (typeof value === 'number' && Number.isSafeInteger(value))) {
539
+ return value
540
+ }
541
+ if (Array.isArray(value) &&
542
+ value.every(item => typeof item === 'string' && item.trim().length > 0)) {
543
+ return [...value]
544
+ }
545
+ return undefined
546
+ }
547
+
548
+ function validateDynamicSegment (segment: string, path: string, sourceName: string): void {
549
+ if (['__proto__', 'constructor', 'prototype'].includes(segment.toLowerCase())) {
550
+ throw ExceptionHelper.generateException(Error, Errors.InvalidConfigurationPath, undefined, { path, sourceName })
551
+ }
552
+ }
553
+
554
+ export function setConfigurationValue (
555
+ target: ConfigurationLayer,
556
+ path: string,
557
+ rawValue: unknown,
558
+ sourceName: string,
559
+ format: ConfigurationInputFormat
560
+ ): void {
561
+ const parts = path.split('.')
562
+ const authConnectionMatch = /^connections\.(.+)\.settings\.([^.]+)$/.exec(path)
563
+ if (authConnectionMatch) {
564
+ const [, id, property] = authConnectionMatch
565
+ validateDynamicSegment(id, path, sourceName)
566
+ const parsed = parseValue(
567
+ authParsers,
568
+ property,
569
+ rawValue,
570
+ path,
571
+ sourceName,
572
+ format,
573
+ authPropertyAliases,
574
+ true
575
+ )
576
+ const current = ensureConnection(target, id)
577
+ ;(current.settings as AuthConnectionPatch)[parsed.key] = parsed.value
578
+ return
579
+ }
580
+
581
+ if (parts.length === 3 && parts[0] === 'connectionsMap' && /^\d+$/.test(parts[1])) {
582
+ const index = Number.parseInt(parts[1], 10)
583
+ const parsed = parseValue(connectionMapParsers, parts[2], rawValue, path, sourceName, format)
584
+ const current = { ...(target.connectionsMap.get(index) ?? {}) }
585
+ current[parsed.key as keyof AuthConnectionMapPatch] = parsed.value as string
586
+ ;(target.connectionsMap as Map<number, AuthConnectionMapPatch>).set(index, current)
587
+ return
588
+ }
589
+
590
+ if (parts.length === 2 && parts[0] === 'cloudAdapterOptions') {
591
+ const parsed = parseValue(cloudAdapterParsers, parts[1], rawValue, path, sourceName, format)
592
+ ;(target.cloudAdapterOptions as CloudAdapterConfigurationPatch)[parsed.key as keyof CloudAdapterConfigurationPatch] = parsed.value as boolean
593
+ return
594
+ }
595
+
596
+ if (parts.length === 2 && parts[0] === 'outboundHostValidator') {
597
+ const parsed = parseValue(outboundHostValidatorParsers, parts[1], rawValue, path, sourceName, format)
598
+ if (parsed.key === 'hosts') {
599
+ ;(target.outboundHostValidator as OutboundHostValidatorConfigurationPatch).hosts = parsed.value as string[]
600
+ } else {
601
+ ;(target.outboundHostValidator as OutboundHostValidatorConfigurationPatch)[parsed.key as 'enabled' | 'includeDefaultMicrosoftHosts'] = parsed.value as boolean
602
+ }
603
+ return
604
+ }
605
+
606
+ const authorizationHandlerMatch = /^agentApplication\.userAuthorization\.handlers\.(.+)\.settings\.([^.]+)$/.exec(path)
607
+ if (authorizationHandlerMatch) {
608
+ const [, id, property] = authorizationHandlerMatch
609
+ validateDynamicSegment(id, path, sourceName)
610
+ const parsed = parseValue(
611
+ authorizationParsers,
612
+ property,
613
+ rawValue,
614
+ path,
615
+ sourceName,
616
+ format,
617
+ authorizationPropertyAliases,
618
+ true
619
+ )
620
+ const current = ensureAuthorizationHandler(target, id)
621
+ ;(current.settings as AuthorizationHandlerPatch)[parsed.key] = parsed.value
622
+ return
623
+ }
624
+
625
+ throw invalidConfigurationPath(path, sourceName)
626
+ }
627
+
628
+ function ensureConnection (
629
+ target: ConfigurationLayer,
630
+ id: string
631
+ ): { id: string, settings: AuthConnectionPatch } {
632
+ const connections = target.connections as Map<string, { id: string, settings: AuthConnectionPatch }>
633
+ const lookup = id.toLowerCase()
634
+ const connection = connections.get(lookup) ?? { id, settings: {} }
635
+ connections.set(lookup, connection)
636
+ return connection
637
+ }
638
+
639
+ function ensureAuthorizationHandler (
640
+ target: ConfigurationLayer,
641
+ id: string
642
+ ): { id: string, settings: AuthorizationHandlerPatch } {
643
+ const handlers = target.agentApplication.userAuthorization.handlers as Map<
644
+ string,
645
+ { id: string, settings: AuthorizationHandlerPatch }
646
+ >
647
+ const lookup = id.toLowerCase()
648
+ const handler = handlers.get(lookup) ?? { id, settings: {} }
649
+ handlers.set(lookup, handler)
650
+ return handler
651
+ }
652
+
653
+ export function isConfigurationInputError (error: unknown): boolean {
654
+ const code = (error as { code?: unknown } | null)?.code
655
+ return code === Errors.InvalidConfigurationPath.code ||
656
+ isInvalidConfigurationValueError(error) ||
657
+ code === Errors.UnsupportedRuntimeConfigurationField.code
658
+ }
659
+
660
+ export function isInvalidConfigurationValueError (error: unknown): boolean {
661
+ const code = (error as { code?: unknown } | null)?.code
662
+ return code === Errors.InvalidConfigurationValue.code ||
663
+ code === Errors.InvalidBooleanConfigurationValue.code
664
+ }
665
+
666
+ function asDocumentObject (
667
+ value: unknown,
668
+ path: string,
669
+ sourceName: string
670
+ ): Readonly<ConfigurationDocument> {
671
+ if (value === null || Array.isArray(value) || typeof value !== 'object') {
672
+ throw ExceptionHelper.generateException(TypeError, Errors.InvalidConfigurationValue, undefined, { path, sourceName })
673
+ }
674
+ return value as Readonly<ConfigurationDocument>
675
+ }
676
+
677
+ function uniqueEntries (
678
+ value: Readonly<ConfigurationDocument>,
679
+ path: string,
680
+ sourceName: string
681
+ ): Array<[string, ConfigurationDocument[keyof ConfigurationDocument]]> {
682
+ const normalized = new Set<string>()
683
+ return Object.entries(value).map(([key, entryValue]) => {
684
+ const lookup = key.toLowerCase()
685
+ if (normalized.has(lookup)) {
686
+ throw ExceptionHelper.generateException(Error, Errors.InvalidConfigurationPath, undefined, {
687
+ path: path ? `${path}.${key}` : key,
688
+ sourceName
689
+ })
690
+ }
691
+ normalized.add(lookup)
692
+ return [key, entryValue]
693
+ })
694
+ }
695
+
696
+ function unsupportedRuntimeField (path: string, sourceName: string): never {
697
+ throw ExceptionHelper.generateException(
698
+ Error,
699
+ Errors.UnsupportedRuntimeConfigurationField,
700
+ undefined,
701
+ { path, sourceName }
702
+ )
703
+ }
704
+
705
+ function compileDocument (
706
+ target: ConfigurationLayer,
707
+ document: Readonly<ConfigurationDocument>,
708
+ sourceName: string
709
+ ): void {
710
+ for (const [section, sectionValue] of uniqueEntries(document, '', sourceName)) {
711
+ switch (section.toLowerCase()) {
712
+ case 'connections':
713
+ compileDocumentConnections(target, sectionValue, section, sourceName)
714
+ break
715
+ case 'connectionsmap':
716
+ compileDocumentConnectionsMap(target, sectionValue, section, sourceName)
717
+ break
718
+ case 'cloudadapteroptions':
719
+ compileDocumentLeafObject(target, sectionValue, section, sourceName, 'cloudAdapterOptions')
720
+ break
721
+ case 'outboundhostvalidator':
722
+ compileDocumentLeafObject(target, sectionValue, section, sourceName, 'outboundHostValidator')
723
+ break
724
+ case 'agentapplication':
725
+ compileDocumentAgentApplication(target, sectionValue, section, sourceName)
726
+ break
727
+ default:
728
+ // Shared appsettings files may contain unrelated host configuration.
729
+ if (suggestConfigurationPath(section)) {
730
+ throw invalidConfigurationPath(section, sourceName)
731
+ }
732
+ break
733
+ }
734
+ }
735
+ }
736
+
737
+ function compileDocumentConnections (
738
+ target: ConfigurationLayer,
739
+ value: unknown,
740
+ path: string,
741
+ sourceName: string
742
+ ): void {
743
+ const connections = asDocumentObject(value, path, sourceName)
744
+ for (const [id, definitionValue] of uniqueEntries(connections, path, sourceName)) {
745
+ validateDynamicSegment(id, `${path}.${id}`, sourceName)
746
+ const definition = asDocumentObject(definitionValue, `${path}.${id}`, sourceName)
747
+ ensureConnection(target, id)
748
+ let settingsFound = false
749
+ for (const [key, entryValue] of uniqueEntries(definition, `${path}.${id}`, sourceName)) {
750
+ const entryPath = `${path}.${id}.${key}`
751
+ if (key.toLowerCase() === 'settings') {
752
+ settingsFound = true
753
+ compileDocumentLeafObject(target, entryValue, entryPath, sourceName, `connections.${id}.settings`)
754
+ } else if (key.toLowerCase() === 'assembly' || key.toLowerCase() === 'type') {
755
+ unsupportedRuntimeField(entryPath, sourceName)
756
+ } else {
757
+ throw invalidConfigurationPath(entryPath, sourceName)
758
+ }
759
+ }
760
+ if (!settingsFound) {
761
+ throw ExceptionHelper.generateException(TypeError, Errors.InvalidConfigurationValue, undefined, {
762
+ path: `${path}.${id}.settings`,
763
+ sourceName
764
+ })
765
+ }
766
+ }
767
+ }
768
+
769
+ function compileDocumentConnectionsMap (
770
+ target: ConfigurationLayer,
771
+ value: unknown,
772
+ path: string,
773
+ sourceName: string
774
+ ): void {
775
+ if (!Array.isArray(value)) {
776
+ throw ExceptionHelper.generateException(TypeError, Errors.InvalidConfigurationValue, undefined, { path, sourceName })
777
+ }
778
+ value.forEach((item, index) => {
779
+ compileDocumentLeafObject(
780
+ target,
781
+ item,
782
+ `${path}.${index}`,
783
+ sourceName,
784
+ `connectionsMap.${index}`
785
+ )
786
+ })
787
+ }
788
+
789
+ function compileDocumentAgentApplication (
790
+ target: ConfigurationLayer,
791
+ value: unknown,
792
+ path: string,
793
+ sourceName: string
794
+ ): void {
795
+ const application = asDocumentObject(value, path, sourceName)
796
+ for (const [key, entryValue] of uniqueEntries(application, path, sourceName)) {
797
+ const entryPath = `${path}.${key}`
798
+ if (key.toLowerCase() !== 'userauthorization') {
799
+ if (suggestConfigurationPath(entryPath)) {
800
+ throw invalidConfigurationPath(entryPath, sourceName)
801
+ }
802
+ continue
803
+ }
804
+ const authorization = asDocumentObject(entryValue, entryPath, sourceName)
805
+ for (const [authorizationKey, authorizationValue] of uniqueEntries(authorization, entryPath, sourceName)) {
806
+ const authorizationPath = `${entryPath}.${authorizationKey}`
807
+ if (authorizationKey.toLowerCase() === 'handlers') {
808
+ compileDocumentAuthorizationHandlers(target, authorizationValue, authorizationPath, sourceName)
809
+ } else if (suggestConfigurationPath(authorizationPath)) {
810
+ throw invalidConfigurationPath(authorizationPath, sourceName)
811
+ } else {
812
+ unsupportedRuntimeField(authorizationPath, sourceName)
813
+ }
814
+ }
815
+ }
816
+ }
817
+
818
+ function compileDocumentAuthorizationHandlers (
819
+ target: ConfigurationLayer,
820
+ value: unknown,
821
+ path: string,
822
+ sourceName: string
823
+ ): void {
824
+ const handlers = asDocumentObject(value, path, sourceName)
825
+ for (const [id, definitionValue] of uniqueEntries(handlers, path, sourceName)) {
826
+ validateDynamicSegment(id, `${path}.${id}`, sourceName)
827
+ const definition = asDocumentObject(definitionValue, `${path}.${id}`, sourceName)
828
+ ensureAuthorizationHandler(target, id)
829
+ let settingsFound = false
830
+ for (const [key, entryValue] of uniqueEntries(definition, `${path}.${id}`, sourceName)) {
831
+ const entryPath = `${path}.${id}.${key}`
832
+ if (key.toLowerCase() === 'settings') {
833
+ settingsFound = true
834
+ compileDocumentLeafObject(
835
+ target,
836
+ entryValue,
837
+ entryPath,
838
+ sourceName,
839
+ `agentApplication.userAuthorization.handlers.${id}.settings`
840
+ )
841
+ } else if (key.toLowerCase() === 'assembly' || key.toLowerCase() === 'type') {
842
+ unsupportedRuntimeField(entryPath, sourceName)
843
+ } else {
844
+ throw invalidConfigurationPath(entryPath, sourceName)
845
+ }
846
+ }
847
+ if (!settingsFound) {
848
+ throw ExceptionHelper.generateException(TypeError, Errors.InvalidConfigurationValue, undefined, {
849
+ path: `${path}.${id}.settings`,
850
+ sourceName
851
+ })
852
+ }
853
+ }
854
+ }
855
+
856
+ function compileDocumentLeafObject (
857
+ target: ConfigurationLayer,
858
+ value: unknown,
859
+ path: string,
860
+ sourceName: string,
861
+ canonicalPrefix: string
862
+ ): void {
863
+ const record = asDocumentObject(value, path, sourceName)
864
+ const normalizedProperties = new Set<string>()
865
+ for (const [key, entryValue] of uniqueEntries(record, path, sourceName)) {
866
+ const lookup = key.toLowerCase()
867
+ if (canonicalPrefix.startsWith('connections.') && dotNetOnlyConnectionSettings.has(lookup)) {
868
+ unsupportedRuntimeField(`${path}.${key}`, sourceName)
869
+ }
870
+ if (canonicalPrefix.startsWith('agentApplication.userAuthorization.handlers.') &&
871
+ dotNetOnlyAuthorizationSettings.has(lookup)) {
872
+ unsupportedRuntimeField(`${path}.${key}`, sourceName)
873
+ }
874
+ const normalizedProperty = canonicalPrefix.startsWith('connections.')
875
+ ? authPropertyAliases[lookup] ?? lookup
876
+ : canonicalPrefix.startsWith('agentApplication.userAuthorization.handlers.')
877
+ ? authorizationPropertyAliases[lookup] ?? lookup
878
+ : lookup
879
+ const normalizedLookup = normalizedProperty.toLowerCase()
880
+ if (normalizedProperties.has(normalizedLookup)) {
881
+ throw ExceptionHelper.generateException(Error, Errors.InvalidConfigurationPath, undefined, {
882
+ path: `${path}.${key}`,
883
+ sourceName
884
+ })
885
+ }
886
+ normalizedProperties.add(normalizedLookup)
887
+ setConfigurationValue(target, `${canonicalPrefix}.${key}`, entryValue, sourceName, 'document')
888
+ }
889
+ }
890
+
891
+ function compileSourceResult (
892
+ target: ConfigurationLayer,
893
+ result: ConfigurationSourceResult,
894
+ sourceName: string
895
+ ): void {
896
+ if (
897
+ result !== null &&
898
+ typeof result === 'object' &&
899
+ !Array.isArray(result) &&
900
+ !('format' in result) &&
901
+ Object.values(result).every(value => typeof value === 'string')
902
+ ) {
903
+ compileCanonicalEntries(target, Object.entries(result), sourceName)
904
+ return
905
+ }
906
+ if (result?.format === 'canonical' && result.values !== null && typeof result.values === 'object' && !Array.isArray(result.values)) {
907
+ compileCanonicalEntries(target, Object.entries(result.values), sourceName)
908
+ return
909
+ }
910
+ if (result?.format === 'document' && result.value !== null && typeof result.value === 'object' && !Array.isArray(result.value)) {
911
+ compileDocument(target, result.value, sourceName)
912
+ return
913
+ }
914
+ throw ExceptionHelper.generateException(Error, Errors.InvalidConfigurationSourceResult, undefined, { sourceName })
915
+ }
916
+
917
+ function compileCanonicalEntries (
918
+ target: ConfigurationLayer,
919
+ entries: readonly (readonly [string, string])[],
920
+ sourceName: string
921
+ ): void {
922
+ const destinations = new Set<string>()
923
+ for (const [path, value] of entries) {
924
+ const destination = normalizeCanonicalDestination(path)
925
+ if (destinations.has(destination)) {
926
+ const definition = {
927
+ ...Errors.InvalidConfigurationPath,
928
+ description: 'Configuration source `{sourceName}` returned duplicate canonical destination `{path}`.'
929
+ }
930
+ throw ExceptionHelper.generateException(Error, definition, undefined, { path, sourceName })
931
+ }
932
+ destinations.add(destination)
933
+ setConfigurationValue(target, path, value, sourceName, 'canonical')
934
+ }
935
+ }
936
+
937
+ function normalizeCanonicalDestination (path: string): string {
938
+ const parts = path.split('.').map(part => part.toLowerCase())
939
+ if (parts[0] === 'connectionsmap' && /^\d+$/.test(parts[1] ?? '')) {
940
+ parts[1] = Number.parseInt(parts[1], 10).toString()
941
+ }
942
+ return parts.join('.')
943
+ }
944
+
945
+ async function loadConfigurationSnapshot (
946
+ registrations: readonly ConfigurationSourceRegistration[]
947
+ ): Promise<ConfigurationSnapshot> {
948
+ const names = new Set<string>()
949
+ for (const { source, mode } of registrations) {
950
+ if (!source.name.trim()) {
951
+ throw ExceptionHelper.generateException(Error, Errors.ConfigurationSourceNameRequired)
952
+ }
953
+ if (!modes.includes(mode)) {
954
+ throw ExceptionHelper.generateException(
955
+ Error,
956
+ Errors.InvalidConfigurationSourceMode,
957
+ undefined,
958
+ { sourceName: source.name, mode: String(mode) }
959
+ )
960
+ }
961
+ if (names.has(source.name)) {
962
+ throw ExceptionHelper.generateException(Error, Errors.DuplicateConfigurationSource, undefined, { sourceName: source.name })
963
+ }
964
+ names.add(source.name)
965
+ }
966
+
967
+ const settled = await Promise.allSettled(registrations.map(async registration => {
968
+ try {
969
+ return {
970
+ ...registration,
971
+ result: await registration.source.load()
972
+ }
973
+ } catch (cause) {
974
+ throw ExceptionHelper.generateException(
975
+ Error,
976
+ Errors.ConfigurationSourceLoadFailed,
977
+ isSafeJsonFileConfigurationError(cause) ? cause : undefined,
978
+ { sourceName: registration.source.name }
979
+ )
980
+ }
981
+ }))
982
+ const loaded = settled.map(result => {
983
+ if (result.status === 'rejected') {
984
+ throw result.reason
985
+ }
986
+ return result.value
987
+ })
988
+
989
+ const mutable = Object.fromEntries(
990
+ modes.map(mode => [mode, createConfigurationLayer()])
991
+ ) as Record<ConfigurationSourceMode, ConfigurationLayer>
992
+ for (const { source, mode, result } of loaded) {
993
+ compileSourceResult(mutable[mode], result, source.name)
994
+ }
995
+
996
+ return Object.freeze(Object.fromEntries(
997
+ modes.map(mode => [mode, freezeConfigurationLayer(mutable[mode])])
998
+ )) as ConfigurationSnapshot
999
+ }
1000
+
1001
+ /**
1002
+ * Loads configuration sources into a new immutable, host-scoped context.
1003
+ *
1004
+ * @param registrations Configuration sources and their explicit resolution modes.
1005
+ * @returns A context that can be shared by consumers belonging to one host.
1006
+ */
1007
+ export async function createConfigurationContext (
1008
+ registrations: readonly ConfigurationSourceRegistration[]
1009
+ ): Promise<ConfigurationContext> {
1010
+ const snapshot = await loadConfigurationSnapshot(registrations)
1011
+ const context = Object.freeze(instantiateConfigurationContext()) as ConfigurationContext
1012
+ contextSnapshots.set(context, snapshot)
1013
+ return context
1014
+ }
1015
+
1016
+ /**
1017
+ * Preloads external configuration sources into an immutable process-level snapshot.
1018
+ *
1019
+ * @remarks
1020
+ * Call this function once, before constructing configuration consumers such as
1021
+ * `CloudAdapter` or `AgentApplication`. Sources in the same mode are applied in
1022
+ * registration order, with later values winning.
1023
+ *
1024
+ * @param registrations Configuration sources and their explicit resolution modes.
1025
+ */
1026
+ export async function preloadConfigurationSources (
1027
+ registrations: readonly ConfigurationSourceRegistration[]
1028
+ ): Promise<void> {
1029
+ if (state.kind === 'loading') {
1030
+ throw ExceptionHelper.generateException(Error, Errors.ConfigurationPreloadInProgress)
1031
+ }
1032
+ if (state.kind === 'ready') {
1033
+ throw ExceptionHelper.generateException(Error, Errors.ConfigurationAlreadyPreloaded)
1034
+ }
1035
+ if (state.kind === 'consumed') {
1036
+ throw ExceptionHelper.generateException(Error, Errors.ConfigurationAlreadyConsumed)
1037
+ }
1038
+
1039
+ state = { kind: 'loading' }
1040
+ try {
1041
+ state = {
1042
+ kind: 'ready',
1043
+ snapshot: await loadConfigurationSnapshot(registrations)
1044
+ }
1045
+ } catch (cause) {
1046
+ state = { kind: 'uninitialized' }
1047
+ throw cause
1048
+ }
1049
+ }
1050
+
1051
+ export function getConfigurationSnapshot (context?: ConfigurationContext): ConfigurationSnapshot {
1052
+ if (context) {
1053
+ const snapshot = contextSnapshots.get(context)
1054
+ if (!snapshot) {
1055
+ throw ExceptionHelper.generateException(Error, Errors.InvalidConfigurationContext)
1056
+ }
1057
+ return snapshot
1058
+ }
1059
+ if (state.kind === 'loading') {
1060
+ throw ExceptionHelper.generateException(Error, Errors.ConfigurationPreloadInProgress)
1061
+ }
1062
+ if (state.kind === 'uninitialized') {
1063
+ const snapshot = emptySnapshot()
1064
+ state = { kind: 'consumed', snapshot }
1065
+ return snapshot
1066
+ }
1067
+ if (state.kind === 'ready') {
1068
+ state = { kind: 'consumed', snapshot: state.snapshot }
1069
+ return state.snapshot
1070
+ }
1071
+ return state.snapshot
1072
+ }
1073
+
1074
+ export function resetConfigurationSourcesForTest (): void {
1075
+ state = { kind: 'uninitialized' }
1076
+ }