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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/README.md +179 -9
  2. package/dist/package.json +4 -4
  3. package/dist/src/app/agentApplication.d.ts +3 -3
  4. package/dist/src/app/agentApplication.js +6 -16
  5. package/dist/src/app/agentApplication.js.map +1 -1
  6. package/dist/src/app/agentApplicationOptions.d.ts +6 -0
  7. package/dist/src/app/auth/authorization.d.ts +54 -0
  8. package/dist/src/app/auth/authorization.js +63 -2
  9. package/dist/src/app/auth/authorization.js.map +1 -1
  10. package/dist/src/app/auth/authorizationManager.d.ts +0 -8
  11. package/dist/src/app/auth/authorizationManager.js +104 -178
  12. package/dist/src/app/auth/authorizationManager.js.map +1 -1
  13. package/dist/src/auth/authConfiguration.d.ts +10 -3
  14. package/dist/src/auth/authConfiguration.js +292 -259
  15. package/dist/src/auth/authConfiguration.js.map +1 -1
  16. package/dist/src/auth/delegatedTokenCredential.d.ts +29 -0
  17. package/dist/src/auth/delegatedTokenCredential.js +49 -0
  18. package/dist/src/auth/delegatedTokenCredential.js.map +1 -0
  19. package/dist/src/auth/msal/msalTokenCredential.js +1 -1
  20. package/dist/src/auth/msal/msalTokenCredential.js.map +1 -1
  21. package/dist/src/auth/settings.d.ts +8 -70
  22. package/dist/src/auth/settings.js +5 -67
  23. package/dist/src/auth/settings.js.map +1 -1
  24. package/dist/src/cloudAdapter.d.ts +5 -0
  25. package/dist/src/cloudAdapter.js +13 -103
  26. package/dist/src/cloudAdapter.js.map +1 -1
  27. package/dist/src/configuration/botFrameworkEnvironmentCompatibility.d.ts +23 -0
  28. package/dist/src/configuration/botFrameworkEnvironmentCompatibility.js +183 -0
  29. package/dist/src/configuration/botFrameworkEnvironmentCompatibility.js.map +1 -0
  30. package/dist/src/configuration/configuration.d.ts +83 -0
  31. package/dist/src/configuration/configuration.js +836 -0
  32. package/dist/src/configuration/configuration.js.map +1 -0
  33. package/dist/src/configuration/configurationSource.d.ts +57 -0
  34. package/dist/src/configuration/configurationSource.js +7 -0
  35. package/dist/src/configuration/configurationSource.js.map +1 -0
  36. package/dist/src/configuration/environmentConfiguration.d.ts +14 -0
  37. package/dist/src/configuration/environmentConfiguration.js +271 -0
  38. package/dist/src/configuration/environmentConfiguration.js.map +1 -0
  39. package/dist/src/configuration/index.d.ts +7 -0
  40. package/dist/src/configuration/index.js +14 -0
  41. package/dist/src/configuration/index.js.map +1 -0
  42. package/dist/src/configuration/jsonFileConfigurationSource.d.ts +15 -0
  43. package/dist/src/configuration/jsonFileConfigurationSource.js +58 -0
  44. package/dist/src/configuration/jsonFileConfigurationSource.js.map +1 -0
  45. package/dist/src/createCloudAdapter.d.ts +30 -5
  46. package/dist/src/createCloudAdapter.js +17 -10
  47. package/dist/src/createCloudAdapter.js.map +1 -1
  48. package/dist/src/errorHelper.js +90 -2
  49. package/dist/src/errorHelper.js.map +1 -1
  50. package/dist/src/index.d.ts +1 -0
  51. package/dist/src/index.js +1 -0
  52. package/dist/src/index.js.map +1 -1
  53. package/dist/src/outboundHostValidator.d.ts +4 -1
  54. package/dist/src/outboundHostValidator.js +25 -48
  55. package/dist/src/outboundHostValidator.js.map +1 -1
  56. package/dist/src/storage/fileStorage.d.ts +5 -2
  57. package/dist/src/storage/fileStorage.js +41 -3
  58. package/dist/src/storage/fileStorage.js.map +1 -1
  59. package/dist/src/storage/index.d.ts +1 -0
  60. package/dist/src/storage/index.js +1 -0
  61. package/dist/src/storage/index.js.map +1 -1
  62. package/dist/src/storage/memoryStorage.d.ts +4 -2
  63. package/dist/src/storage/memoryStorage.js +32 -6
  64. package/dist/src/storage/memoryStorage.js.map +1 -1
  65. package/dist/src/storage/storage.d.ts +14 -1
  66. package/dist/src/storage/storageExpiry.d.ts +6 -0
  67. package/dist/src/storage/storageExpiry.js +20 -0
  68. package/dist/src/storage/storageExpiry.js.map +1 -0
  69. package/dist/src/utils/env.d.ts +43 -0
  70. package/dist/src/utils/env.js +42 -6
  71. package/dist/src/utils/env.js.map +1 -1
  72. package/dist/src/utils.d.ts +1 -0
  73. package/dist/src/utils.js +10 -1
  74. package/dist/src/utils.js.map +1 -1
  75. package/package.json +4 -4
  76. package/src/app/agentApplication.ts +11 -14
  77. package/src/app/agentApplicationOptions.ts +7 -0
  78. package/src/app/auth/authorization.ts +67 -2
  79. package/src/app/auth/authorizationManager.ts +126 -204
  80. package/src/auth/authConfiguration.ts +415 -260
  81. package/src/auth/delegatedTokenCredential.ts +49 -0
  82. package/src/auth/msal/msalTokenCredential.ts +1 -1
  83. package/src/auth/settings.ts +5 -82
  84. package/src/cloudAdapter.ts +38 -113
  85. package/src/configuration/botFrameworkEnvironmentCompatibility.ts +245 -0
  86. package/src/configuration/configuration.ts +1076 -0
  87. package/src/configuration/configurationSource.ts +76 -0
  88. package/src/configuration/environmentConfiguration.ts +377 -0
  89. package/src/configuration/index.ts +19 -0
  90. package/src/configuration/jsonFileConfigurationSource.ts +74 -0
  91. package/src/createCloudAdapter.ts +52 -9
  92. package/src/errorHelper.ts +110 -2
  93. package/src/index.ts +1 -0
  94. package/src/outboundHostValidator.ts +37 -44
  95. package/src/storage/fileStorage.ts +43 -5
  96. package/src/storage/index.ts +1 -0
  97. package/src/storage/memoryStorage.ts +34 -8
  98. package/src/storage/storage.ts +15 -1
  99. package/src/storage/storageExpiry.ts +21 -0
  100. package/src/utils/env.ts +63 -0
  101. package/src/utils.ts +10 -0
@@ -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 option is not available in app options.
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 options were configured.'
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 { parseBooleanEnv } from './utils/env'
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
- this.enabled = options.enabled ?? false
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 (options.includeDefaultMicrosoftHosts ?? true) {
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 options.hosts ?? []) {
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
- const result: OutboundHostValidatorOptions = {}
97
- const indexedHosts = new Map<number, string>()
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
- return result
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
- /** Creates the default validator, with explicit settings overriding the environment. */
130
- export function createOutboundHostValidator (options?: OutboundHostValidatorOptions): OutboundHostValidator {
112
+ function resolveOutboundHostValidatorOptions (
113
+ options: OutboundHostValidatorOptions
114
+ ): OutboundHostValidatorOptions {
115
+ const external = getConfigurationSnapshot(options.configurationContext)
131
116
  const fromEnv = loadOutboundHostValidatorOptionsFromEnv()
132
- return new OutboundHostValidator({
133
- enabled: options?.enabled ?? fromEnv.enabled,
134
- includeDefaultMicrosoftHosts: options?.includeDefaultMicrosoftHosts ?? fromEnv.includeDefaultMicrosoftHosts,
135
- hosts: options?.hosts ?? fromEnv.hosts
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 { trace } from '@microsoft/agents-telemetry'
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
- fs.writeFileSync(this._folder + '/state.json', JSON.stringify(this._stateFile, null, 2))
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
- fs.writeFileSync(this._folder + '/state.json', JSON.stringify(this._stateFile, null, 2))
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
  }
@@ -1,3 +1,4 @@
1
1
  export * from './storage'
2
+ export * from './storageExpiry'
2
3
  export * from './memoryStorage'
3
4
  export * from './fileStorage'
@@ -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
- if (!changes || changes.length === 0) {
110
+ const keys = changes ? Object.keys(changes) : []
111
+ if (!changes || keys.length === 0) {
103
112
  throw ExceptionHelper.generateException(ReferenceError, Errors.StorageWriteChangesRequired)
104
113
  }
105
- record({ keyCount: changes.length })
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
  }
@@ -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
+ }