@microsoft/agents-hosting 1.8.1 → 1.9.0-beta.18.g6b8111bba5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +179 -9
- package/dist/package.json +4 -4
- package/dist/src/app/agentApplication.d.ts +3 -3
- package/dist/src/app/agentApplication.js +6 -16
- package/dist/src/app/agentApplication.js.map +1 -1
- package/dist/src/app/agentApplicationOptions.d.ts +6 -0
- package/dist/src/app/auth/authorization.d.ts +54 -0
- package/dist/src/app/auth/authorization.js +63 -2
- package/dist/src/app/auth/authorization.js.map +1 -1
- 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/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/delegatedTokenCredential.d.ts +29 -0
- package/dist/src/auth/delegatedTokenCredential.js +49 -0
- package/dist/src/auth/delegatedTokenCredential.js.map +1 -0
- package/dist/src/auth/msal/msalTokenCredential.js +1 -1
- package/dist/src/auth/msal/msalTokenCredential.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 +90 -2
- 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/outboundHostValidator.d.ts +4 -1
- package/dist/src/outboundHostValidator.js +25 -48
- package/dist/src/outboundHostValidator.js.map +1 -1
- package/dist/src/storage/fileStorage.d.ts +5 -2
- package/dist/src/storage/fileStorage.js +41 -3
- 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 +4 -2
- package/dist/src/storage/memoryStorage.js +32 -6
- package/dist/src/storage/memoryStorage.js.map +1 -1
- package/dist/src/storage/storage.d.ts +14 -1
- 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 +4 -4
- package/src/app/agentApplication.ts +11 -14
- package/src/app/agentApplicationOptions.ts +7 -0
- package/src/app/auth/authorization.ts +67 -2
- package/src/app/auth/authorizationManager.ts +126 -204
- package/src/auth/authConfiguration.ts +415 -260
- package/src/auth/delegatedTokenCredential.ts +49 -0
- package/src/auth/msal/msalTokenCredential.ts +1 -1
- 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 +110 -2
- package/src/index.ts +1 -0
- package/src/outboundHostValidator.ts +37 -44
- package/src/storage/fileStorage.ts +43 -5
- package/src/storage/index.ts +1 -0
- package/src/storage/memoryStorage.ts +34 -8
- package/src/storage/storage.ts +15 -1
- package/src/storage/storageExpiry.ts +21 -0
- package/src/utils/env.ts +63 -0
- package/src/utils.ts +10 -0
package/src/errorHelper.ts
CHANGED
|
@@ -432,6 +432,14 @@ export const Errors: { [key: string]: AgentErrorDefinition } = {
|
|
|
432
432
|
description: 'MSAL HTTP request failed: {message}'
|
|
433
433
|
},
|
|
434
434
|
|
|
435
|
+
/**
|
|
436
|
+
* Error thrown when a delegated token credential's provider returns a response without a usable token.
|
|
437
|
+
*/
|
|
438
|
+
NullTokenResponse: {
|
|
439
|
+
code: -120393,
|
|
440
|
+
description: 'The token response provider returned a null response or a response without a token.'
|
|
441
|
+
},
|
|
442
|
+
|
|
435
443
|
/**
|
|
436
444
|
* Error thrown when an MSAL HTTP response body cannot be parsed as JSON.
|
|
437
445
|
*/
|
|
@@ -440,6 +448,14 @@ export const Errors: { [key: string]: AgentErrorDefinition } = {
|
|
|
440
448
|
description: 'MSAL returned an unparsable response body'
|
|
441
449
|
},
|
|
442
450
|
|
|
451
|
+
/**
|
|
452
|
+
* Error thrown when a security-sensitive boolean configuration value is invalid.
|
|
453
|
+
*/
|
|
454
|
+
InvalidBooleanConfigurationValue: {
|
|
455
|
+
code: -120394,
|
|
456
|
+
description: 'Configuration source "{sourceName}" returned an invalid boolean for "{path}". Expected one of true/false/1/0.'
|
|
457
|
+
},
|
|
458
|
+
|
|
443
459
|
FailedToAcquireTokenUsingCertificate: {
|
|
444
460
|
code: -120395,
|
|
445
461
|
description: 'Failed to acquire token using certificate'
|
|
@@ -570,11 +586,11 @@ export const Errors: { [key: string]: AgentErrorDefinition } = {
|
|
|
570
586
|
},
|
|
571
587
|
|
|
572
588
|
/**
|
|
573
|
-
* Error thrown when authorization
|
|
589
|
+
* Error thrown when no authorization handlers were resolved.
|
|
574
590
|
*/
|
|
575
591
|
AuthorizationOptionNotAvailable: {
|
|
576
592
|
code: -120530,
|
|
577
|
-
description: 'The Application.authorization property is unavailable because no authorization
|
|
593
|
+
description: 'The Application.authorization property is unavailable because no authorization handlers were resolved.'
|
|
578
594
|
},
|
|
579
595
|
|
|
580
596
|
/**
|
|
@@ -782,6 +798,14 @@ export const Errors: { [key: string]: AgentErrorDefinition } = {
|
|
|
782
798
|
description: 'Changes are required when writing.'
|
|
783
799
|
},
|
|
784
800
|
|
|
801
|
+
/**
|
|
802
|
+
* Error thrown when a storage TTL value is invalid.
|
|
803
|
+
*/
|
|
804
|
+
InvalidStorageTtl: {
|
|
805
|
+
code: -120703,
|
|
806
|
+
description: 'StorageWriteOptions.ttl must be a finite number greater than zero.'
|
|
807
|
+
},
|
|
808
|
+
|
|
785
809
|
/**
|
|
786
810
|
* Error thrown when storage option is not available in app options.
|
|
787
811
|
*/
|
|
@@ -1035,6 +1059,90 @@ export const Errors: { [key: string]: AgentErrorDefinition } = {
|
|
|
1035
1059
|
description: 'The Entra Agent ID sidecar returned an unsupported authorization scheme `{scheme}`. Only the Bearer scheme is supported'
|
|
1036
1060
|
},
|
|
1037
1061
|
|
|
1062
|
+
// ============================================================================
|
|
1063
|
+
// Configuration Source Errors (-120812 to -120827)
|
|
1064
|
+
// ============================================================================
|
|
1065
|
+
|
|
1066
|
+
ConfigurationAlreadyConsumed: {
|
|
1067
|
+
code: -120812,
|
|
1068
|
+
description: 'Configuration sources must be preloaded before the first configuration consumer is created.'
|
|
1069
|
+
},
|
|
1070
|
+
|
|
1071
|
+
ConfigurationSourceNameRequired: {
|
|
1072
|
+
code: -120813,
|
|
1073
|
+
description: 'Configuration source name must be a non-empty string.'
|
|
1074
|
+
},
|
|
1075
|
+
|
|
1076
|
+
DuplicateConfigurationSource: {
|
|
1077
|
+
code: -120814,
|
|
1078
|
+
description: 'Configuration source name `{sourceName}` is registered more than once.'
|
|
1079
|
+
},
|
|
1080
|
+
|
|
1081
|
+
ConfigurationSourceLoadFailed: {
|
|
1082
|
+
code: -120815,
|
|
1083
|
+
description: 'Configuration source `{sourceName}` failed to load.'
|
|
1084
|
+
},
|
|
1085
|
+
|
|
1086
|
+
InvalidConfigurationPath: {
|
|
1087
|
+
code: -120816,
|
|
1088
|
+
description: 'Configuration source `{sourceName}` returned unsupported canonical path `{path}`.'
|
|
1089
|
+
},
|
|
1090
|
+
|
|
1091
|
+
InvalidConfigurationValue: {
|
|
1092
|
+
code: -120817,
|
|
1093
|
+
description: 'Configuration source `{sourceName}` returned an invalid value for canonical path `{path}`.'
|
|
1094
|
+
},
|
|
1095
|
+
|
|
1096
|
+
ConfigurationPreloadInProgress: {
|
|
1097
|
+
code: -120818,
|
|
1098
|
+
description: 'Another configuration preload is already in progress.'
|
|
1099
|
+
},
|
|
1100
|
+
|
|
1101
|
+
InvalidConnectionMapEntry: {
|
|
1102
|
+
code: -120819,
|
|
1103
|
+
description: 'Connection map entry at index {index} must define both serviceUrl and connection.'
|
|
1104
|
+
},
|
|
1105
|
+
|
|
1106
|
+
ConfigurationAlreadyPreloaded: {
|
|
1107
|
+
code: -120820,
|
|
1108
|
+
description: 'Configuration sources have already been preloaded for this process.'
|
|
1109
|
+
},
|
|
1110
|
+
|
|
1111
|
+
UnsupportedRuntimeConfigurationField: {
|
|
1112
|
+
code: -120821,
|
|
1113
|
+
description: 'Configuration source `{sourceName}` returned field `{path}`, which is not supported by the JavaScript SDK.'
|
|
1114
|
+
},
|
|
1115
|
+
|
|
1116
|
+
InvalidConfigurationSourceResult: {
|
|
1117
|
+
code: -120822,
|
|
1118
|
+
description: 'Configuration source `{sourceName}` returned an invalid source result.'
|
|
1119
|
+
},
|
|
1120
|
+
|
|
1121
|
+
InvalidConfigurationContext: {
|
|
1122
|
+
code: -120823,
|
|
1123
|
+
description: 'The configuration context is invalid. Create contexts with createConfigurationContext().'
|
|
1124
|
+
},
|
|
1125
|
+
|
|
1126
|
+
InvalidConfigurationSourceMode: {
|
|
1127
|
+
code: -120824,
|
|
1128
|
+
description: 'Configuration source `{sourceName}` uses unsupported mode `{mode}`. Use `fallback`, `overrideEnvironment`, or `enforce`.'
|
|
1129
|
+
},
|
|
1130
|
+
|
|
1131
|
+
JsonConfigurationFileReadFailed: {
|
|
1132
|
+
code: -120825,
|
|
1133
|
+
description: 'JSON configuration file `{filePath}` could not be read.'
|
|
1134
|
+
},
|
|
1135
|
+
|
|
1136
|
+
InvalidJsonConfigurationFile: {
|
|
1137
|
+
code: -120826,
|
|
1138
|
+
description: 'JSON configuration file `{filePath}` does not contain valid JSON.'
|
|
1139
|
+
},
|
|
1140
|
+
|
|
1141
|
+
JsonConfigurationDocumentRequired: {
|
|
1142
|
+
code: -120827,
|
|
1143
|
+
description: 'JSON configuration file `{filePath}` must contain an object at the document root.'
|
|
1144
|
+
},
|
|
1145
|
+
|
|
1038
1146
|
// ============================================================================
|
|
1039
1147
|
// Application Configuration Errors (-120850 to -120902)
|
|
1040
1148
|
// ============================================================================
|
package/src/index.ts
CHANGED
|
@@ -8,6 +8,7 @@ export { authorizeJWT, buildJwksUri, clearJwksClients } from './auth/jwt-middlew
|
|
|
8
8
|
|
|
9
9
|
export * from './app'
|
|
10
10
|
export * from './cards'
|
|
11
|
+
export * from './configuration'
|
|
11
12
|
export * from './connector-client'
|
|
12
13
|
export { Errors as HostingErrors } from './errorHelper'
|
|
13
14
|
export * from './invoke'
|
|
@@ -3,7 +3,12 @@
|
|
|
3
3
|
* Licensed under the MIT License.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
ConfigurationContext,
|
|
8
|
+
getConfigurationSnapshot
|
|
9
|
+
} from './configuration/configuration'
|
|
10
|
+
import { loadModernEnvironmentConfiguration } from './configuration/environmentConfiguration'
|
|
11
|
+
import { mergeDefined } from './utils'
|
|
7
12
|
|
|
8
13
|
/** Hosts used by Microsoft channel callbacks and hosted attachments. */
|
|
9
14
|
const DEFAULT_MICROSOFT_HOSTS = Object.freeze([
|
|
@@ -17,11 +22,11 @@ const DEFAULT_MICROSOFT_HOSTS = Object.freeze([
|
|
|
17
22
|
'blob.core.windows.net'
|
|
18
23
|
])
|
|
19
24
|
|
|
20
|
-
const OUTBOUND_HOST_VALIDATOR_ENV_PREFIX = 'OutboundHostValidator__'
|
|
21
|
-
const OUTBOUND_HOST_VALIDATOR_ENV_PREFIX_UPPER = OUTBOUND_HOST_VALIDATOR_ENV_PREFIX.toUpperCase()
|
|
22
|
-
|
|
23
25
|
/** Configuration for the shared outbound-host allowlist. */
|
|
24
26
|
export interface OutboundHostValidatorOptions {
|
|
27
|
+
/** Optional host-scoped external configuration. */
|
|
28
|
+
configurationContext?: ConfigurationContext
|
|
29
|
+
|
|
25
30
|
/** Enables allowlist enforcement. Defaults to `false`. */
|
|
26
31
|
enabled?: boolean
|
|
27
32
|
|
|
@@ -56,14 +61,17 @@ export class OutboundHostValidator implements OutboundUrlPolicy {
|
|
|
56
61
|
private readonly suffixes: ReadonlySet<string>
|
|
57
62
|
|
|
58
63
|
public constructor (options: OutboundHostValidatorOptions = {}) {
|
|
59
|
-
|
|
64
|
+
const resolved = options.configurationContext
|
|
65
|
+
? resolveOutboundHostValidatorOptions(options)
|
|
66
|
+
: options
|
|
67
|
+
this.enabled = resolved.enabled ?? false
|
|
60
68
|
|
|
61
69
|
const suffixes = new Set<string>()
|
|
62
|
-
if (
|
|
70
|
+
if (resolved.includeDefaultMicrosoftHosts ?? true) {
|
|
63
71
|
for (const host of DEFAULT_MICROSOFT_HOSTS) suffixes.add(host)
|
|
64
72
|
}
|
|
65
73
|
|
|
66
|
-
for (const host of
|
|
74
|
+
for (const host of resolved.hosts ?? []) {
|
|
67
75
|
const normalized = normalizeConfiguredHost(host)
|
|
68
76
|
if (normalized) suffixes.add(normalized)
|
|
69
77
|
}
|
|
@@ -93,47 +101,32 @@ export class OutboundHostValidator implements OutboundUrlPolicy {
|
|
|
93
101
|
* indexed values such as `Hosts__0`, `Hosts__1`, and so on.
|
|
94
102
|
*/
|
|
95
103
|
export function loadOutboundHostValidatorOptionsFromEnv (): OutboundHostValidatorOptions {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
const hosts: string[] = []
|
|
99
|
-
|
|
100
|
-
for (const [envKey, rawValue] of Object.entries(process.env)) {
|
|
101
|
-
const upperKey = envKey.toUpperCase()
|
|
102
|
-
if (!upperKey.startsWith(OUTBOUND_HOST_VALIDATOR_ENV_PREFIX_UPPER)) continue
|
|
103
|
-
|
|
104
|
-
const property = envKey.substring(OUTBOUND_HOST_VALIDATOR_ENV_PREFIX.length)
|
|
105
|
-
const upperProperty = property.toUpperCase()
|
|
106
|
-
|
|
107
|
-
if (upperProperty === 'ENABLED') {
|
|
108
|
-
const value = parseBooleanEnv(rawValue)
|
|
109
|
-
if (value !== undefined) result.enabled = value
|
|
110
|
-
} else if (upperProperty === 'INCLUDEDEFAULTMICROSOFTHOSTS') {
|
|
111
|
-
const value = parseBooleanEnv(rawValue)
|
|
112
|
-
if (value !== undefined) result.includeDefaultMicrosoftHosts = value
|
|
113
|
-
} else if (upperProperty === 'HOSTS' && rawValue) {
|
|
114
|
-
hosts.push(...rawValue.split(',').map(host => host.trim()).filter(Boolean))
|
|
115
|
-
} else {
|
|
116
|
-
const match = /^HOSTS__(\d+)$/i.exec(property)
|
|
117
|
-
if (match && rawValue?.trim()) indexedHosts.set(Number(match[1]), rawValue.trim())
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
for (const [, host] of [...indexedHosts.entries()].sort(([left], [right]) => left - right)) {
|
|
122
|
-
hosts.push(host)
|
|
123
|
-
}
|
|
124
|
-
if (hosts.length > 0) result.hosts = hosts
|
|
104
|
+
return { ...loadModernEnvironmentConfiguration().outboundHostValidator }
|
|
105
|
+
}
|
|
125
106
|
|
|
126
|
-
|
|
107
|
+
/** Creates a validator using the shared configuration precedence. */
|
|
108
|
+
export function createOutboundHostValidator (options: OutboundHostValidatorOptions = {}): OutboundHostValidator {
|
|
109
|
+
return new OutboundHostValidator(resolveOutboundHostValidatorOptions(options))
|
|
127
110
|
}
|
|
128
111
|
|
|
129
|
-
|
|
130
|
-
|
|
112
|
+
function resolveOutboundHostValidatorOptions (
|
|
113
|
+
options: OutboundHostValidatorOptions
|
|
114
|
+
): OutboundHostValidatorOptions {
|
|
115
|
+
const external = getConfigurationSnapshot(options.configurationContext)
|
|
131
116
|
const fromEnv = loadOutboundHostValidatorOptionsFromEnv()
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
}
|
|
117
|
+
const direct: OutboundHostValidatorOptions = {}
|
|
118
|
+
if (options.enabled !== undefined) direct.enabled = options.enabled
|
|
119
|
+
if (options.includeDefaultMicrosoftHosts !== undefined) {
|
|
120
|
+
direct.includeDefaultMicrosoftHosts = options.includeDefaultMicrosoftHosts
|
|
121
|
+
}
|
|
122
|
+
if (options.hosts !== undefined) direct.hosts = options.hosts
|
|
123
|
+
return mergeDefined<OutboundHostValidatorOptions>(
|
|
124
|
+
external.fallback.outboundHostValidator,
|
|
125
|
+
fromEnv,
|
|
126
|
+
external.overrideEnvironment.outboundHostValidator,
|
|
127
|
+
direct,
|
|
128
|
+
external.enforce.outboundHostValidator
|
|
129
|
+
)
|
|
137
130
|
}
|
|
138
131
|
|
|
139
132
|
function getUrlHost (input: string | URL | null | undefined): string | undefined {
|
|
@@ -5,10 +5,13 @@
|
|
|
5
5
|
|
|
6
6
|
import path from 'path'
|
|
7
7
|
import fs from 'fs'
|
|
8
|
-
import { Storage, StoreItem } from './storage'
|
|
9
|
-
import {
|
|
8
|
+
import { Storage, StoreItem, StorageWriteOptions } from './storage'
|
|
9
|
+
import { getStorageWriteExpiry } from './storageExpiry'
|
|
10
|
+
import { debug, redactString, trace } from '@microsoft/agents-telemetry'
|
|
10
11
|
import { StorageTraceDefinitions } from '../observability'
|
|
11
12
|
|
|
13
|
+
const logger = debug('agents:file-storage')
|
|
14
|
+
|
|
12
15
|
/**
|
|
13
16
|
* A file-based storage implementation that persists data to the local filesystem.
|
|
14
17
|
*
|
|
@@ -53,6 +56,7 @@ import { StorageTraceDefinitions } from '../observability'
|
|
|
53
56
|
export class FileStorage implements Storage {
|
|
54
57
|
private _folder: string
|
|
55
58
|
private _stateFile: Record<string, string>
|
|
59
|
+
private _metadataFile: { expirations?: Record<string, number> }
|
|
56
60
|
|
|
57
61
|
/**
|
|
58
62
|
* Creates a new FileStorage instance that stores data in the specified folder.
|
|
@@ -75,8 +79,14 @@ export class FileStorage implements Storage {
|
|
|
75
79
|
if (!fs.existsSync(path.join(folder, 'state.json'))) {
|
|
76
80
|
fs.writeFileSync(path.join(folder, 'state.json'), '{}')
|
|
77
81
|
}
|
|
82
|
+
if (!fs.existsSync(path.join(folder, 'state.metadata.json'))) {
|
|
83
|
+
fs.writeFileSync(path.join(folder, 'state.metadata.json'), '{}')
|
|
84
|
+
}
|
|
78
85
|
const data = fs.readFileSync(path.join(folder, 'state.json'), 'utf8')
|
|
79
86
|
this._stateFile = JSON.parse(data)
|
|
87
|
+
const metadata = fs.readFileSync(path.join(folder, 'state.metadata.json'), 'utf8')
|
|
88
|
+
this._metadataFile = JSON.parse(metadata)
|
|
89
|
+
this._metadataFile.expirations ??= {}
|
|
80
90
|
}
|
|
81
91
|
|
|
82
92
|
/**
|
|
@@ -100,12 +110,23 @@ export class FileStorage implements Storage {
|
|
|
100
110
|
reject(new ReferenceError('Keys are required when reading.'))
|
|
101
111
|
} else {
|
|
102
112
|
const data: StoreItem = {}
|
|
113
|
+
let storageChanged = false
|
|
103
114
|
for (const key of keys) {
|
|
115
|
+
if (this.isExpired(key)) {
|
|
116
|
+
logger.info('Item expired, deleting from storage', { key: redactString(key, true) })
|
|
117
|
+
delete this._stateFile[key]
|
|
118
|
+
delete this._metadataFile.expirations![key]
|
|
119
|
+
storageChanged = true
|
|
120
|
+
continue
|
|
121
|
+
}
|
|
104
122
|
const item = this._stateFile[key]
|
|
105
123
|
if (item) {
|
|
106
124
|
data[key] = item
|
|
107
125
|
}
|
|
108
126
|
}
|
|
127
|
+
if (storageChanged) {
|
|
128
|
+
this.writeFiles()
|
|
129
|
+
}
|
|
109
130
|
resolve(data)
|
|
110
131
|
}
|
|
111
132
|
})
|
|
@@ -128,14 +149,20 @@ export class FileStorage implements Storage {
|
|
|
128
149
|
* > Any eTag values in the changes object are ignored.
|
|
129
150
|
*
|
|
130
151
|
*/
|
|
131
|
-
write (changes: StoreItem) : Promise<void> {
|
|
152
|
+
write (changes: StoreItem, options?: StorageWriteOptions) : Promise<void> {
|
|
132
153
|
return trace(StorageTraceDefinitions.write, async ({ record }) => {
|
|
154
|
+
const expiresAt = getStorageWriteExpiry(options)
|
|
133
155
|
const keys = Object.keys(changes)
|
|
134
156
|
record({ keyCount: keys?.length })
|
|
135
157
|
for (const key of keys) {
|
|
136
158
|
this._stateFile[key] = changes[key]
|
|
159
|
+
if (expiresAt === undefined) {
|
|
160
|
+
delete this._metadataFile.expirations![key]
|
|
161
|
+
} else {
|
|
162
|
+
this._metadataFile.expirations![key] = expiresAt
|
|
163
|
+
}
|
|
137
164
|
}
|
|
138
|
-
|
|
165
|
+
this.writeFiles()
|
|
139
166
|
})
|
|
140
167
|
}
|
|
141
168
|
|
|
@@ -162,11 +189,22 @@ export class FileStorage implements Storage {
|
|
|
162
189
|
} else {
|
|
163
190
|
for (const key of keys) {
|
|
164
191
|
delete this._stateFile[key]
|
|
192
|
+
delete this._metadataFile.expirations![key]
|
|
165
193
|
}
|
|
166
|
-
|
|
194
|
+
this.writeFiles()
|
|
167
195
|
resolve(undefined)
|
|
168
196
|
}
|
|
169
197
|
})
|
|
170
198
|
})
|
|
171
199
|
}
|
|
200
|
+
|
|
201
|
+
private isExpired (key: string): boolean {
|
|
202
|
+
const expiresAt = this._metadataFile.expirations?.[key]
|
|
203
|
+
return expiresAt !== undefined && expiresAt <= Date.now()
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
private writeFiles (): void {
|
|
207
|
+
fs.writeFileSync(path.join(this._folder, 'state.json'), JSON.stringify(this._stateFile, null, 2))
|
|
208
|
+
fs.writeFileSync(path.join(this._folder, 'state.metadata.json'), JSON.stringify(this._metadataFile, null, 2))
|
|
209
|
+
}
|
|
172
210
|
}
|
package/src/storage/index.ts
CHANGED
|
@@ -3,10 +3,11 @@
|
|
|
3
3
|
* Licensed under the MIT License.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { trace } from '@microsoft/agents-telemetry'
|
|
6
|
+
import { redactString, trace } from '@microsoft/agents-telemetry'
|
|
7
7
|
import { ExceptionHelper } from '@microsoft/agents-activity'
|
|
8
8
|
import { StorageTraceDefinitions } from '../observability'
|
|
9
|
-
import { Storage, StoreItem } from './storage'
|
|
9
|
+
import { Storage, StoreItem, StorageWriteOptions } from './storage'
|
|
10
|
+
import { getStorageWriteExpiry } from './storageExpiry'
|
|
10
11
|
import { debug } from '@microsoft/agents-telemetry'
|
|
11
12
|
import { Errors } from '../errorHelper'
|
|
12
13
|
|
|
@@ -28,6 +29,7 @@ const logger = debug('agents:memory-storage')
|
|
|
28
29
|
*/
|
|
29
30
|
export class MemoryStorage implements Storage {
|
|
30
31
|
private static instance: MemoryStorage
|
|
32
|
+
private expirations: { [k: string]: number } = {}
|
|
31
33
|
/**
|
|
32
34
|
* Counter used to generate unique eTags for stored items
|
|
33
35
|
*/
|
|
@@ -74,6 +76,12 @@ export class MemoryStorage implements Storage {
|
|
|
74
76
|
const data: StoreItem = {}
|
|
75
77
|
for (const key of keys) {
|
|
76
78
|
logger.debug(`Reading key: ${key}`)
|
|
79
|
+
if (this.isExpired(key)) {
|
|
80
|
+
logger.info('Item expired, deleting from memory', { key: redactString(key, true) })
|
|
81
|
+
delete this.memory[key]
|
|
82
|
+
delete this.expirations[key]
|
|
83
|
+
continue
|
|
84
|
+
}
|
|
77
85
|
const item = this.memory[key]
|
|
78
86
|
if (item) {
|
|
79
87
|
data[key] = JSON.parse(item)
|
|
@@ -97,22 +105,29 @@ export class MemoryStorage implements Storage {
|
|
|
97
105
|
* has the same eTag. If an item has an eTag of '*' or no eTag, it will
|
|
98
106
|
* always be written regardless of the current state.
|
|
99
107
|
*/
|
|
100
|
-
async write (changes: StoreItem): Promise<void> {
|
|
108
|
+
async write (changes: StoreItem, options?: StorageWriteOptions): Promise<void> {
|
|
101
109
|
return trace(StorageTraceDefinitions.write, async ({ record }) => {
|
|
102
|
-
|
|
110
|
+
const keys = changes ? Object.keys(changes) : []
|
|
111
|
+
if (!changes || keys.length === 0) {
|
|
103
112
|
throw ExceptionHelper.generateException(ReferenceError, Errors.StorageWriteChangesRequired)
|
|
104
113
|
}
|
|
105
|
-
|
|
114
|
+
const expiresAt = getStorageWriteExpiry(options)
|
|
115
|
+
record({ keyCount: keys.length })
|
|
106
116
|
|
|
107
117
|
for (const [key, newItem] of Object.entries(changes)) {
|
|
108
118
|
logger.debug(`Writing key: ${key}`)
|
|
119
|
+
if (this.isExpired(key)) {
|
|
120
|
+
logger.info('Item expired, deleting from memory', { key: redactString(key, true) })
|
|
121
|
+
delete this.memory[key]
|
|
122
|
+
delete this.expirations[key]
|
|
123
|
+
}
|
|
109
124
|
const oldItemStr = this.memory[key]
|
|
110
125
|
if (!oldItemStr || newItem.eTag === '*' || !newItem.eTag) {
|
|
111
|
-
this.saveItem(key, newItem)
|
|
126
|
+
this.saveItem(key, newItem, expiresAt)
|
|
112
127
|
} else {
|
|
113
128
|
const oldItem = JSON.parse(oldItemStr)
|
|
114
129
|
if (newItem.eTag === oldItem.eTag) {
|
|
115
|
-
this.saveItem(key, newItem)
|
|
130
|
+
this.saveItem(key, newItem, expiresAt)
|
|
116
131
|
} else {
|
|
117
132
|
throw ExceptionHelper.generateException(Error, Errors.StorageETagConflict, undefined, { key })
|
|
118
133
|
}
|
|
@@ -133,6 +148,7 @@ export class MemoryStorage implements Storage {
|
|
|
133
148
|
logger.debug(`Deleting keys: ${keys.join(', ')}`)
|
|
134
149
|
for (const key of keys) {
|
|
135
150
|
delete this.memory[key]
|
|
151
|
+
delete this.expirations[key]
|
|
136
152
|
}
|
|
137
153
|
})
|
|
138
154
|
}
|
|
@@ -151,8 +167,18 @@ export class MemoryStorage implements Storage {
|
|
|
151
167
|
*
|
|
152
168
|
* @private
|
|
153
169
|
*/
|
|
154
|
-
private saveItem (key: string, item: unknown): void {
|
|
170
|
+
private saveItem (key: string, item: unknown, expiresAt?: number): void {
|
|
155
171
|
const clone = Object.assign({}, item, { eTag: (this.etag++).toString() })
|
|
156
172
|
this.memory[key] = JSON.stringify(clone)
|
|
173
|
+
if (expiresAt === undefined) {
|
|
174
|
+
delete this.expirations[key]
|
|
175
|
+
} else {
|
|
176
|
+
this.expirations[key] = expiresAt
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
private isExpired (key: string): boolean {
|
|
181
|
+
const expiresAt = this.expirations[key]
|
|
182
|
+
return expiresAt !== undefined && expiresAt <= Date.now()
|
|
157
183
|
}
|
|
158
184
|
}
|
package/src/storage/storage.ts
CHANGED
|
@@ -50,6 +50,20 @@ export interface StoreItems {
|
|
|
50
50
|
[key: string]: any;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
/**
|
|
54
|
+
* Options for writing items to storage.
|
|
55
|
+
*/
|
|
56
|
+
export interface StorageWriteOptions {
|
|
57
|
+
/**
|
|
58
|
+
* Optional time to live, in seconds, for written items.
|
|
59
|
+
*
|
|
60
|
+
* @remarks
|
|
61
|
+
* When set, storage providers should stop returning the written items after
|
|
62
|
+
* this duration has elapsed. Values must be finite numbers greater than zero.
|
|
63
|
+
*/
|
|
64
|
+
ttl?: number;
|
|
65
|
+
}
|
|
66
|
+
|
|
53
67
|
/**
|
|
54
68
|
* A factory function to generate storage keys based on the conversation context.
|
|
55
69
|
*
|
|
@@ -91,7 +105,7 @@ export interface Storage {
|
|
|
91
105
|
* @returns A promise that resolves when the write operation is complete
|
|
92
106
|
* @throws If the changes object is empty or undefined, or if an eTag conflict occurs and optimistic concurrency is enabled
|
|
93
107
|
*/
|
|
94
|
-
write: (changes: StoreItem) => Promise<void>;
|
|
108
|
+
write: (changes: StoreItem, options?: StorageWriteOptions) => Promise<void>;
|
|
95
109
|
|
|
96
110
|
/**
|
|
97
111
|
* Deletes store items from storage.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
3
|
+
* Licensed under the MIT License.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { StorageWriteOptions } from './storage'
|
|
7
|
+
import { ExceptionHelper } from '@microsoft/agents-activity'
|
|
8
|
+
import { Errors } from '../errorHelper'
|
|
9
|
+
|
|
10
|
+
export function getStorageWriteExpiry (options?: StorageWriteOptions): number | undefined {
|
|
11
|
+
const ttl = options?.ttl
|
|
12
|
+
if (ttl === undefined) {
|
|
13
|
+
return undefined
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (!Number.isFinite(ttl) || ttl <= 0) {
|
|
17
|
+
throw ExceptionHelper.generateException(RangeError, Errors.InvalidStorageTtl)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return Date.now() + ttl * 1000
|
|
21
|
+
}
|
package/src/utils/env.ts
CHANGED
|
@@ -10,6 +10,69 @@
|
|
|
10
10
|
* normalize behavior across the package without growing the public API.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
+
export interface LoadEnv {
|
|
14
|
+
[upperKey: string]: { key: string, value: any }
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
type ParserSettings<K extends string> = {
|
|
18
|
+
[key in K]: (value: string) => { key?: string, value?: any } | undefined
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Indexes non-empty environment values case-insensitively while preserving
|
|
23
|
+
* their original key spelling.
|
|
24
|
+
*/
|
|
25
|
+
export function loadEnvSettings (
|
|
26
|
+
callback: (key: string, value: string) => void,
|
|
27
|
+
environment: NodeJS.ProcessEnv = process.env
|
|
28
|
+
): LoadEnv {
|
|
29
|
+
const env: LoadEnv = {}
|
|
30
|
+
for (const [envKey, envValue] of Object.entries(environment)) {
|
|
31
|
+
if (!envValue?.trim()) {
|
|
32
|
+
continue
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
env[envKey.toUpperCase()] = { key: envKey, value: envValue }
|
|
36
|
+
callback(envKey, envValue)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return env
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Creates a case-insensitive parser for a fixed set of environment keys.
|
|
44
|
+
*/
|
|
45
|
+
export function envParser<K extends string> (
|
|
46
|
+
settings: ParserSettings<K> & ThisType<ParserSettings<K>>
|
|
47
|
+
) {
|
|
48
|
+
const keys = Object.keys(settings) as K[]
|
|
49
|
+
const upperKeys = keys.reduce((acc, key) => {
|
|
50
|
+
acc[key.toUpperCase()] = key
|
|
51
|
+
return acc
|
|
52
|
+
}, {} as Record<string, K>)
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
keys,
|
|
56
|
+
parse (key: K, value: string) {
|
|
57
|
+
const match = upperKeys[key.toUpperCase()]
|
|
58
|
+
if (!match) {
|
|
59
|
+
return {}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const result = settings[match](value)
|
|
63
|
+
return { key: result?.key ?? match, value: result?.value }
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export const envParserUtils = {
|
|
69
|
+
bypass: (value: string) => ({ value }),
|
|
70
|
+
redirect: <Parser extends ReturnType<typeof envParser>>(
|
|
71
|
+
parser: Parser,
|
|
72
|
+
key: Parameters<Parser['parse']>[0]
|
|
73
|
+
) => (value: string) => parser.parse(key, value)
|
|
74
|
+
}
|
|
75
|
+
|
|
13
76
|
/**
|
|
14
77
|
* Parses an environment-variable string into a boolean.
|
|
15
78
|
*
|
package/src/utils.ts
CHANGED
|
@@ -12,3 +12,13 @@ export const prune = <T extends Record<string, any>>(obj: T) => {
|
|
|
12
12
|
const entries = Object.entries(obj).filter(([, value]) => value !== undefined)
|
|
13
13
|
return Object.fromEntries(entries) as T
|
|
14
14
|
}
|
|
15
|
+
|
|
16
|
+
export const mergeDefined = <T extends Record<string, any>>(
|
|
17
|
+
...layers: readonly (Partial<T> | undefined)[]
|
|
18
|
+
): T => {
|
|
19
|
+
const result: Partial<T> = {}
|
|
20
|
+
for (const layer of layers) {
|
|
21
|
+
if (layer) Object.assign(result, prune(layer))
|
|
22
|
+
}
|
|
23
|
+
return result as T
|
|
24
|
+
}
|