@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.
- package/README.md +182 -4
- package/dist/package.json +3 -3
- package/dist/src/app/agentApplication.d.ts +4 -3
- package/dist/src/app/agentApplication.js +24 -21
- package/dist/src/app/agentApplication.js.map +1 -1
- package/dist/src/app/agentApplicationBuilder.d.ts +9 -2
- package/dist/src/app/agentApplicationBuilder.js +9 -0
- package/dist/src/app/agentApplicationBuilder.js.map +1 -1
- package/dist/src/app/agentApplicationOptions.d.ts +16 -4
- package/dist/src/app/auth/authorizationManager.d.ts +0 -8
- package/dist/src/app/auth/authorizationManager.js +104 -178
- package/dist/src/app/auth/authorizationManager.js.map +1 -1
- package/dist/src/app/auth/handlerStorage.d.ts +5 -3
- package/dist/src/app/auth/handlerStorage.js +36 -10
- package/dist/src/app/auth/handlerStorage.js.map +1 -1
- package/dist/src/app/auth/types.d.ts +2 -2
- package/dist/src/app/index.d.ts +1 -0
- package/dist/src/app/proactive/proactive.js +10 -6
- package/dist/src/app/proactive/proactive.js.map +1 -1
- package/dist/src/app/proactive/proactiveOptions.d.ts +2 -2
- package/dist/src/app/rateLimit.d.ts +315 -0
- package/dist/src/app/rateLimit.js +333 -0
- package/dist/src/app/rateLimit.js.map +1 -0
- package/dist/src/app/turnState.d.ts +6 -3
- package/dist/src/app/turnState.js +43 -4
- package/dist/src/app/turnState.js.map +1 -1
- package/dist/src/auth/authConfiguration.d.ts +10 -3
- package/dist/src/auth/authConfiguration.js +292 -259
- package/dist/src/auth/authConfiguration.js.map +1 -1
- package/dist/src/auth/settings.d.ts +8 -70
- package/dist/src/auth/settings.js +5 -67
- package/dist/src/auth/settings.js.map +1 -1
- package/dist/src/cloudAdapter.d.ts +5 -0
- package/dist/src/cloudAdapter.js +13 -103
- package/dist/src/cloudAdapter.js.map +1 -1
- package/dist/src/configuration/botFrameworkEnvironmentCompatibility.d.ts +23 -0
- package/dist/src/configuration/botFrameworkEnvironmentCompatibility.js +183 -0
- package/dist/src/configuration/botFrameworkEnvironmentCompatibility.js.map +1 -0
- package/dist/src/configuration/configuration.d.ts +83 -0
- package/dist/src/configuration/configuration.js +836 -0
- package/dist/src/configuration/configuration.js.map +1 -0
- package/dist/src/configuration/configurationSource.d.ts +57 -0
- package/dist/src/configuration/configurationSource.js +7 -0
- package/dist/src/configuration/configurationSource.js.map +1 -0
- package/dist/src/configuration/environmentConfiguration.d.ts +14 -0
- package/dist/src/configuration/environmentConfiguration.js +271 -0
- package/dist/src/configuration/environmentConfiguration.js.map +1 -0
- package/dist/src/configuration/index.d.ts +7 -0
- package/dist/src/configuration/index.js +14 -0
- package/dist/src/configuration/index.js.map +1 -0
- package/dist/src/configuration/jsonFileConfigurationSource.d.ts +15 -0
- package/dist/src/configuration/jsonFileConfigurationSource.js +58 -0
- package/dist/src/configuration/jsonFileConfigurationSource.js.map +1 -0
- package/dist/src/createCloudAdapter.d.ts +30 -5
- package/dist/src/createCloudAdapter.js +17 -10
- package/dist/src/createCloudAdapter.js.map +1 -1
- package/dist/src/errorHelper.js +117 -5
- package/dist/src/errorHelper.js.map +1 -1
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +1 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/observability/traces.d.ts +6 -4
- package/dist/src/observability/traces.js +10 -9
- package/dist/src/observability/traces.js.map +1 -1
- package/dist/src/outboundHostValidator.d.ts +4 -1
- package/dist/src/outboundHostValidator.js +25 -48
- package/dist/src/outboundHostValidator.js.map +1 -1
- package/dist/src/state/agentState.d.ts +8 -3
- package/dist/src/state/agentState.js +26 -8
- package/dist/src/state/agentState.js.map +1 -1
- package/dist/src/state/conversationState.d.ts +2 -2
- package/dist/src/state/conversationState.js.map +1 -1
- package/dist/src/state/userState.d.ts +2 -2
- package/dist/src/state/userState.js.map +1 -1
- package/dist/src/statusCodes.d.ts +5 -0
- package/dist/src/statusCodes.js +5 -0
- package/dist/src/statusCodes.js.map +1 -1
- package/dist/src/storage/fileStorage.d.ts +83 -68
- package/dist/src/storage/fileStorage.js +279 -105
- package/dist/src/storage/fileStorage.js.map +1 -1
- package/dist/src/storage/index.d.ts +1 -0
- package/dist/src/storage/index.js +1 -0
- package/dist/src/storage/index.js.map +1 -1
- package/dist/src/storage/memoryStorage.d.ts +85 -60
- package/dist/src/storage/memoryStorage.js +244 -87
- package/dist/src/storage/memoryStorage.js.map +1 -1
- package/dist/src/storage/storage.d.ts +100 -2
- package/dist/src/storage/storage.js +25 -0
- package/dist/src/storage/storage.js.map +1 -1
- package/dist/src/storage/storageCompatibility.d.ts +17 -0
- package/dist/src/storage/storageCompatibility.js +157 -0
- package/dist/src/storage/storageCompatibility.js.map +1 -0
- package/dist/src/storage/storageExpiry.d.ts +6 -0
- package/dist/src/storage/storageExpiry.js +20 -0
- package/dist/src/storage/storageExpiry.js.map +1 -0
- package/dist/src/utils/env.d.ts +43 -0
- package/dist/src/utils/env.js +42 -6
- package/dist/src/utils/env.js.map +1 -1
- package/dist/src/utils.d.ts +1 -0
- package/dist/src/utils.js +10 -1
- package/dist/src/utils.js.map +1 -1
- package/package.json +3 -3
- package/src/app/agentApplication.ts +27 -14
- package/src/app/agentApplicationBuilder.ts +13 -2
- package/src/app/agentApplicationOptions.ts +18 -4
- package/src/app/auth/authorizationManager.ts +126 -204
- package/src/app/auth/handlerStorage.ts +51 -7
- package/src/app/auth/types.ts +2 -2
- package/src/app/index.ts +7 -0
- package/src/app/proactive/proactive.ts +19 -9
- package/src/app/proactive/proactiveOptions.ts +2 -2
- package/src/app/rateLimit.ts +659 -0
- package/src/app/turnState.ts +50 -7
- package/src/auth/authConfiguration.ts +415 -260
- package/src/auth/settings.ts +5 -82
- package/src/cloudAdapter.ts +38 -113
- package/src/configuration/botFrameworkEnvironmentCompatibility.ts +245 -0
- package/src/configuration/configuration.ts +1076 -0
- package/src/configuration/configurationSource.ts +76 -0
- package/src/configuration/environmentConfiguration.ts +377 -0
- package/src/configuration/index.ts +19 -0
- package/src/configuration/jsonFileConfigurationSource.ts +74 -0
- package/src/createCloudAdapter.ts +52 -9
- package/src/errorHelper.ts +143 -5
- package/src/index.ts +1 -0
- package/src/observability/traces.ts +8 -7
- package/src/outboundHostValidator.ts +37 -44
- package/src/state/agentState.ts +39 -13
- package/src/state/conversationState.ts +2 -2
- package/src/state/userState.ts +2 -2
- package/src/statusCodes.ts +6 -0
- package/src/storage/fileStorage.ts +299 -106
- package/src/storage/index.ts +1 -0
- package/src/storage/memoryStorage.ts +268 -85
- package/src/storage/storage.ts +114 -2
- package/src/storage/storageCompatibility.ts +185 -0
- package/src/storage/storageExpiry.ts +21 -0
- package/src/utils/env.ts +63 -0
- 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
|
+
}
|