@microsoft/agents-hosting 1.10.0-beta.9.g18d68b903e → 1.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (169) hide show
  1. package/README.md +41 -2
  2. package/dist/package.json +3 -3
  3. package/dist/src/activityHandler.js +6 -3
  4. package/dist/src/activityHandler.js.map +1 -1
  5. package/dist/src/agent-client/agentResponseHandler.d.ts +7 -0
  6. package/dist/src/agent-client/agentResponseHandler.js.map +1 -1
  7. package/dist/src/agent-client/createAgentResponseHandler.d.ts +2 -0
  8. package/dist/src/agent-client/createAgentResponseHandler.js.map +1 -1
  9. package/dist/src/app/adaptiveCards/adaptiveCardsActions.d.ts +1 -0
  10. package/dist/src/app/adaptiveCards/adaptiveCardsActions.js +1 -0
  11. package/dist/src/app/adaptiveCards/adaptiveCardsActions.js.map +1 -1
  12. package/dist/src/app/attachmentDownloadHttpClient.d.ts +13 -0
  13. package/dist/src/app/attachmentDownloadHttpClient.js +80 -0
  14. package/dist/src/app/attachmentDownloadHttpClient.js.map +1 -0
  15. package/dist/src/app/attachmentDownloader.d.ts +2 -0
  16. package/dist/src/app/attachmentDownloader.js +6 -11
  17. package/dist/src/app/attachmentDownloader.js.map +1 -1
  18. package/dist/src/app/auth/authorization.d.ts +58 -0
  19. package/dist/src/app/auth/authorization.js.map +1 -1
  20. package/dist/src/app/proactive/conversation.d.ts +18 -0
  21. package/dist/src/app/proactive/conversation.js +6 -0
  22. package/dist/src/app/proactive/conversation.js.map +1 -1
  23. package/dist/src/app/proactive/conversationBuilder.js +2 -1
  24. package/dist/src/app/proactive/conversationBuilder.js.map +1 -1
  25. package/dist/src/app/proactive/conversationReferenceBuilder.js +8 -4
  26. package/dist/src/app/proactive/conversationReferenceBuilder.js.map +1 -1
  27. package/dist/src/app/proactive/createConversationOptionsBuilder.d.ts +1 -0
  28. package/dist/src/app/proactive/createConversationOptionsBuilder.js +2 -1
  29. package/dist/src/app/proactive/createConversationOptionsBuilder.js.map +1 -1
  30. package/dist/src/app/proactive/proactive.d.ts +6 -0
  31. package/dist/src/app/proactive/proactive.js +32 -1
  32. package/dist/src/app/proactive/proactive.js.map +1 -1
  33. package/dist/src/app/routeList.d.ts +21 -0
  34. package/dist/src/app/routeList.js +27 -3
  35. package/dist/src/app/routeList.js.map +1 -1
  36. package/dist/src/app/teamsAttachmentDownloader.d.ts +8 -0
  37. package/dist/src/app/teamsAttachmentDownloader.js +17 -12
  38. package/dist/src/app/teamsAttachmentDownloader.js.map +1 -1
  39. package/dist/src/auth/authConfiguration.d.ts +1 -0
  40. package/dist/src/auth/authConfiguration.js.map +1 -1
  41. package/dist/src/auth/authConstants.d.ts +7 -0
  42. package/dist/src/auth/authConstants.js +7 -0
  43. package/dist/src/auth/authConstants.js.map +1 -1
  44. package/dist/src/auth/authProvider.d.ts +36 -0
  45. package/dist/src/auth/connectionManager.js +2 -1
  46. package/dist/src/auth/connectionManager.js.map +1 -1
  47. package/dist/src/auth/connections.d.ts +1 -0
  48. package/dist/src/auth/msal/msalConnectionManager.d.ts +7 -0
  49. package/dist/src/auth/msal/msalConnectionManager.js +7 -0
  50. package/dist/src/auth/msal/msalConnectionManager.js.map +1 -1
  51. package/dist/src/auth/msal/msalTokenProvider.d.ts +44 -0
  52. package/dist/src/auth/msal/msalTokenProvider.js +43 -0
  53. package/dist/src/auth/msal/msalTokenProvider.js.map +1 -1
  54. package/dist/src/auth/settings.d.ts +28 -0
  55. package/dist/src/auth/settings.js +23 -0
  56. package/dist/src/auth/settings.js.map +1 -1
  57. package/dist/src/auth/sidecar/sidecarAuthProvider.d.ts +3 -0
  58. package/dist/src/auth/sidecar/sidecarAuthProvider.js.map +1 -1
  59. package/dist/src/auth/sidecar/sidecarHttpClient.d.ts +0 -8
  60. package/dist/src/auth/sidecar/sidecarHttpClient.js +2 -68
  61. package/dist/src/auth/sidecar/sidecarHttpClient.js.map +1 -1
  62. package/dist/src/cards/taskModuleAction.d.ts +6 -0
  63. package/dist/src/cards/taskModuleAction.js +6 -0
  64. package/dist/src/cards/taskModuleAction.js.map +1 -1
  65. package/dist/src/cloudAdapter.js +16 -8
  66. package/dist/src/cloudAdapter.js.map +1 -1
  67. package/dist/src/configuration/configuration.d.ts +1 -0
  68. package/dist/src/configuration/configuration.js +2 -1
  69. package/dist/src/configuration/configuration.js.map +1 -1
  70. package/dist/src/configuration/environmentConfiguration.js +1 -1
  71. package/dist/src/configuration/environmentConfiguration.js.map +1 -1
  72. package/dist/src/connector-client/connectorClient.d.ts +8 -0
  73. package/dist/src/connector-client/connectorClient.js +8 -0
  74. package/dist/src/connector-client/connectorClient.js.map +1 -1
  75. package/dist/src/createCloudAdapter.d.ts +2 -0
  76. package/dist/src/createCloudAdapter.js.map +1 -1
  77. package/dist/src/errorHelper.d.ts +1 -0
  78. package/dist/src/errorHelper.js +9 -0
  79. package/dist/src/errorHelper.js.map +1 -1
  80. package/dist/src/headerPropagation.d.ts +38 -0
  81. package/dist/src/headerPropagation.js +33 -0
  82. package/dist/src/headerPropagation.js.map +1 -1
  83. package/dist/src/httpClient.d.ts +58 -0
  84. package/dist/src/httpClient.js +40 -0
  85. package/dist/src/httpClient.js.map +1 -1
  86. package/dist/src/interfaces/webResponse.d.ts +7 -0
  87. package/dist/src/middlewareSet.d.ts +6 -0
  88. package/dist/src/middlewareSet.js.map +1 -1
  89. package/dist/src/oauth/userTokenClient.d.ts +6 -0
  90. package/dist/src/oauth/userTokenClient.js +12 -0
  91. package/dist/src/oauth/userTokenClient.js.map +1 -1
  92. package/dist/src/outboundHostValidator.d.ts +33 -1
  93. package/dist/src/outboundHostValidator.js +60 -25
  94. package/dist/src/outboundHostValidator.js.map +1 -1
  95. package/dist/src/storage/fileStorage.d.ts +26 -0
  96. package/dist/src/storage/fileStorage.js +35 -6
  97. package/dist/src/storage/fileStorage.js.map +1 -1
  98. package/dist/src/storage/memoryStorage.d.ts +34 -0
  99. package/dist/src/storage/memoryStorage.js +41 -4
  100. package/dist/src/storage/memoryStorage.js.map +1 -1
  101. package/dist/src/storage/storage.d.ts +20 -0
  102. package/dist/src/storage/storage.js +7 -0
  103. package/dist/src/storage/storage.js.map +1 -1
  104. package/dist/src/storage/storageCompatibility.js +12 -6
  105. package/dist/src/storage/storageCompatibility.js.map +1 -1
  106. package/dist/src/storage/storageExpiry.d.ts +7 -0
  107. package/dist/src/storage/storageExpiry.js +7 -0
  108. package/dist/src/storage/storageExpiry.js.map +1 -1
  109. package/dist/src/turnContext.d.ts +10 -0
  110. package/dist/src/turnContext.js +5 -0
  111. package/dist/src/turnContext.js.map +1 -1
  112. package/dist/src/utils/env.js +22 -11
  113. package/dist/src/utils/env.js.map +1 -1
  114. package/dist/src/utils/ipAddress.d.ts +6 -0
  115. package/dist/src/utils/ipAddress.js +64 -0
  116. package/dist/src/utils/ipAddress.js.map +1 -0
  117. package/dist/src/utils/outboundUrl.d.ts +7 -0
  118. package/dist/src/utils/outboundUrl.js +52 -0
  119. package/dist/src/utils/outboundUrl.js.map +1 -0
  120. package/dist/src/utils.js +2 -1
  121. package/dist/src/utils.js.map +1 -1
  122. package/package.json +3 -3
  123. package/src/activityHandler.ts +9 -3
  124. package/src/agent-client/agentResponseHandler.ts +7 -0
  125. package/src/agent-client/createAgentResponseHandler.ts +2 -0
  126. package/src/app/adaptiveCards/adaptiveCardsActions.ts +1 -0
  127. package/src/app/attachmentDownloadHttpClient.ts +80 -0
  128. package/src/app/attachmentDownloader.ts +6 -9
  129. package/src/app/auth/authorization.ts +58 -0
  130. package/src/app/proactive/conversation.ts +24 -0
  131. package/src/app/proactive/conversationBuilder.ts +3 -1
  132. package/src/app/proactive/conversationReferenceBuilder.ts +12 -4
  133. package/src/app/proactive/createConversationOptionsBuilder.ts +5 -1
  134. package/src/app/proactive/proactive.ts +33 -1
  135. package/src/app/routeList.ts +30 -3
  136. package/src/app/teamsAttachmentDownloader.ts +17 -10
  137. package/src/auth/authConfiguration.ts +1 -0
  138. package/src/auth/authConstants.ts +7 -0
  139. package/src/auth/authProvider.ts +36 -0
  140. package/src/auth/connectionManager.ts +3 -1
  141. package/src/auth/connections.ts +1 -0
  142. package/src/auth/msal/msalConnectionManager.ts +7 -0
  143. package/src/auth/msal/msalTokenProvider.ts +59 -0
  144. package/src/auth/settings.ts +28 -0
  145. package/src/auth/sidecar/sidecarAuthProvider.ts +4 -0
  146. package/src/auth/sidecar/sidecarHttpClient.ts +2 -62
  147. package/src/cards/taskModuleAction.ts +6 -0
  148. package/src/cloudAdapter.ts +30 -10
  149. package/src/configuration/configuration.ts +4 -2
  150. package/src/configuration/environmentConfiguration.ts +7 -1
  151. package/src/connector-client/connectorClient.ts +8 -0
  152. package/src/createCloudAdapter.ts +2 -0
  153. package/src/errorHelper.ts +11 -0
  154. package/src/headerPropagation.ts +38 -0
  155. package/src/httpClient.ts +58 -0
  156. package/src/interfaces/webResponse.ts +7 -0
  157. package/src/middlewareSet.ts +6 -0
  158. package/src/oauth/userTokenClient.ts +13 -0
  159. package/src/outboundHostValidator.ts +84 -23
  160. package/src/storage/fileStorage.ts +46 -6
  161. package/src/storage/memoryStorage.ts +45 -4
  162. package/src/storage/storage.ts +20 -0
  163. package/src/storage/storageCompatibility.ts +18 -6
  164. package/src/storage/storageExpiry.ts +7 -0
  165. package/src/turnContext.ts +11 -0
  166. package/src/utils/env.ts +33 -11
  167. package/src/utils/ipAddress.ts +63 -0
  168. package/src/utils/outboundUrl.ts +50 -0
  169. package/src/utils.ts +3 -1
@@ -17,14 +17,21 @@ export class HeaderPropagation implements HeaderPropagationCollection {
17
17
 
18
18
  private _protectedHeaders = ['x-ms-correlation-id']
19
19
 
20
+ /** Gets the normalized headers received on the incoming request. */
20
21
  public get incoming (): Record<string, string> {
21
22
  return this._incomingRequests
22
23
  }
23
24
 
25
+ /** Gets the headers selected for propagation to outgoing requests. */
24
26
  public get outgoing (): Record<string, string> {
25
27
  return this._outgoingHeaders
26
28
  }
27
29
 
30
+ /**
31
+ * Creates a header propagation collection from incoming request headers.
32
+ *
33
+ * @param headers The incoming headers, including optional multi-value headers.
34
+ */
28
35
  constructor (headers: Record<string, string | string[] | undefined>) {
29
36
  if (!headers) {
30
37
  throw ExceptionHelper.generateException(Error, Errors.HeadersRequired)
@@ -39,6 +46,11 @@ export class HeaderPropagation implements HeaderPropagationCollection {
39
46
  this._protectedHeaders = protectedHeaders // Restore protected headers list after propagation
40
47
  }
41
48
 
49
+ /**
50
+ * Copies existing incoming header values to the outgoing collection.
51
+ *
52
+ * @param headers The header names to propagate.
53
+ */
42
54
  propagate (headers: string[]) {
43
55
  this.process(headers.map(h => [h, '']), key => {
44
56
  if (this._incomingRequests[key] && !this._outgoingHeaders[key]) {
@@ -47,6 +59,11 @@ export class HeaderPropagation implements HeaderPropagationCollection {
47
59
  })
48
60
  }
49
61
 
62
+ /**
63
+ * Adds headers that do not already exist in either collection.
64
+ *
65
+ * @param headers The headers to add.
66
+ */
50
67
  add (headers: Record<string, string>) {
51
68
  this.process(Object.entries(headers ?? {}), (key, value) => {
52
69
  if (!this._incomingRequests[key] && !this._outgoingHeaders[key]) {
@@ -55,6 +72,11 @@ export class HeaderPropagation implements HeaderPropagationCollection {
55
72
  })
56
73
  }
57
74
 
75
+ /**
76
+ * Appends values to headers that already exist in either collection.
77
+ *
78
+ * @param headers The values to append.
79
+ */
58
80
  concat (headers: Record<string, string>) {
59
81
  this.process(Object.entries(headers ?? {}), (key, value) => {
60
82
  if (this._incomingRequests[key] || this._outgoingHeaders[key]) {
@@ -63,12 +85,23 @@ export class HeaderPropagation implements HeaderPropagationCollection {
63
85
  })
64
86
  }
65
87
 
88
+ /**
89
+ * Sets outgoing header values unless the headers are protected.
90
+ *
91
+ * @param headers The headers to set.
92
+ */
66
93
  override (headers: Record<string, string>) {
67
94
  this.process(Object.entries(headers ?? {}), (key, value) => {
68
95
  this._outgoingHeaders[key] = value
69
96
  })
70
97
  }
71
98
 
99
+ /**
100
+ * Finds a known header name without regard to casing.
101
+ *
102
+ * @param key The header name to find.
103
+ * @returns The original-cased header name, or `undefined` when it is unknown.
104
+ */
72
105
  key (key: string) {
73
106
  return this._keys.find(k => k.toLowerCase() === key.toLowerCase())
74
107
  }
@@ -113,6 +146,11 @@ export class HeaderPropagation implements HeaderPropagationCollection {
113
146
  * A function type that defines how headers should be propagated.
114
147
  */
115
148
  export interface HeaderPropagationDefinition {
149
+ /**
150
+ * Configures outgoing headers for a request.
151
+ *
152
+ * @param headers The collection to configure.
153
+ */
116
154
  (headers: HeaderPropagationCollection): void
117
155
  }
118
156
 
package/src/httpClient.ts CHANGED
@@ -11,13 +11,21 @@ import { Errors } from './errorHelper'
11
11
  * Configuration for an HTTP request.
12
12
  */
13
13
  export interface HttpRequestConfig {
14
+ /** The HTTP method. */
14
15
  method: string
16
+ /** The absolute URL or path relative to the client's base URL. */
15
17
  url: string
18
+ /** Headers to merge with the client's default headers. */
16
19
  headers?: Record<string, string>
20
+ /** The request body. */
17
21
  data?: unknown
22
+ /** Query parameters to append to the URL. */
18
23
  params?: Record<string, string | undefined>
24
+ /** The format used to read the response body. */
19
25
  responseType?: 'json' | 'arraybuffer' | 'stream'
26
+ /** The request timeout in milliseconds. */
20
27
  timeout?: number
28
+ /** A signal used to cancel the request. */
21
29
  signal?: AbortSignal
22
30
  }
23
31
 
@@ -25,10 +33,15 @@ export interface HttpRequestConfig {
25
33
  * Represents an HTTP response.
26
34
  */
27
35
  export interface HttpResponse<T = unknown> {
36
+ /** The parsed response body. */
28
37
  data: T
38
+ /** The HTTP status code. */
29
39
  status: number
40
+ /** The HTTP status text. */
30
41
  statusText: string
42
+ /** The response headers. */
31
43
  headers: Headers
44
+ /** The configuration used for the request. */
32
45
  config: HttpRequestConfig
33
46
  }
34
47
 
@@ -36,7 +49,9 @@ export interface HttpResponse<T = unknown> {
36
49
  * Options for creating an HttpClient instance.
37
50
  */
38
51
  export interface HttpClientOptions {
52
+ /** The base URL used for relative request URLs. */
39
53
  baseURL?: string
54
+ /** Default headers included with every request. */
40
55
  headers?: Record<string, string>
41
56
  }
42
57
 
@@ -47,27 +62,48 @@ export class HttpClient {
47
62
  private _baseURL: string
48
63
  private _defaultHeaders: Record<string, string>
49
64
 
65
+ /**
66
+ * Creates an HTTP client.
67
+ *
68
+ * @param options The base URL and default headers.
69
+ */
50
70
  constructor (options: HttpClientOptions = {}) {
51
71
  this._baseURL = options.baseURL ?? ''
52
72
  this._defaultHeaders = this.normalizeHeaders(options.headers)
53
73
  }
54
74
 
75
+ /** Gets the base URL used for relative requests. */
55
76
  get baseURL (): string {
56
77
  return this._baseURL
57
78
  }
58
79
 
80
+ /** Gets the normalized default request headers. */
59
81
  get defaultHeaders (): Record<string, string> {
60
82
  return this._defaultHeaders
61
83
  }
62
84
 
85
+ /** Replaces the default request headers. */
63
86
  set defaultHeaders (headers: Record<string, string>) {
64
87
  this._defaultHeaders = this.normalizeHeaders(headers)
65
88
  }
66
89
 
90
+ /**
91
+ * Sets a default request header.
92
+ *
93
+ * @param name The header name.
94
+ * @param value The header value.
95
+ */
67
96
  setHeader (name: string, value: string): void {
68
97
  this._defaultHeaders[name.toLowerCase()] = value
69
98
  }
70
99
 
100
+ /**
101
+ * Sends an HTTP request.
102
+ *
103
+ * @param config The request configuration.
104
+ * @returns The response with its parsed body.
105
+ * @throws An {@link HttpError} when the response status is not successful.
106
+ */
71
107
  async request<T = unknown> (config: HttpRequestConfig): Promise<HttpResponse<T>> {
72
108
  const url = this.buildUrl(config.url, config.params)
73
109
  const headers = this.normalizeHeaders({ ...this._defaultHeaders, ...config.headers })
@@ -142,6 +178,13 @@ export class HttpClient {
142
178
  return httpResponse
143
179
  }
144
180
 
181
+ /**
182
+ * Sends an HTTP request, defaulting to GET.
183
+ *
184
+ * @param url The absolute URL or path relative to the client's base URL.
185
+ * @param options Additional request options. The `method` and `url` options override the defaults.
186
+ * @returns The response with its parsed body.
187
+ */
145
188
  async get<T = unknown> (url: string, options?: Partial<HttpRequestConfig>): Promise<HttpResponse<T>> {
146
189
  return this.request<T>({ method: 'get', url, ...options })
147
190
  }
@@ -192,10 +235,20 @@ export class HttpClient {
192
235
  * Error thrown when an HTTP request fails.
193
236
  */
194
237
  export class HttpError extends Error {
238
+ /** The unsuccessful HTTP response. */
195
239
  public readonly response: HttpResponse
240
+ /** The configuration used for the request. */
196
241
  public readonly config: HttpRequestConfig
242
+ /** The HTTP status code. */
197
243
  public readonly status: number
198
244
 
245
+ /**
246
+ * Creates an HTTP request error.
247
+ *
248
+ * @param message The error message.
249
+ * @param response The unsuccessful response.
250
+ * @param config The request configuration.
251
+ */
199
252
  constructor (message: string, response: HttpResponse, config: HttpRequestConfig) {
200
253
  super(message)
201
254
  this.name = 'HttpError'
@@ -204,6 +257,11 @@ export class HttpError extends Error {
204
257
  this.status = response.status
205
258
  }
206
259
 
260
+ /**
261
+ * Returns a serializable representation of the error.
262
+ *
263
+ * @returns The error, request, and response details.
264
+ */
207
265
  toJSON (): Record<string, unknown> {
208
266
  return {
209
267
  message: this.message,
@@ -17,11 +17,17 @@
17
17
  * longer needs to import types from `express` at compile time.
18
18
  */
19
19
  export interface WebResponse {
20
+ /** Sets the HTTP status code. */
20
21
  status (code: number): this
22
+ /** Sets a response header. */
21
23
  setHeader (name: string, value: string): this
24
+ /** Sends an optional response body. */
22
25
  send (body?: unknown): this
26
+ /** Ends the response. */
23
27
  end (): this
28
+ /** Indicates whether response headers have been sent. */
24
29
  headersSent: boolean
30
+ /** Indicates whether the response stream has ended. */
25
31
  writableEnded: boolean
26
32
  }
27
33
 
@@ -40,5 +46,6 @@ export type NextFunction = (err?: any) => void
40
46
  * (`req.params.conversationId`) without coupling to Express's `Request` type.
41
47
  */
42
48
  export interface WebRequestParamsCarrier {
49
+ /** Route parameters parsed from the request URL. */
43
50
  params?: Record<string, string | undefined>
44
51
  }
@@ -10,6 +10,12 @@ const logger = debug('agents:middleware')
10
10
  * Interface for middleware.
11
11
  */
12
12
  export interface Middleware {
13
+ /**
14
+ * Processes a turn and optionally continues the middleware chain.
15
+ *
16
+ * @param context The current turn context.
17
+ * @param next The callback that continues the middleware chain.
18
+ */
13
19
  onTurn: (context: TurnContext, next: () => Promise<void>) => Promise<void>
14
20
  }
15
21
 
@@ -34,6 +34,7 @@ function formatHttpErrorMessage (error: HttpError): string {
34
34
  * Client for managing user tokens.
35
35
  */
36
36
  export class UserTokenClient {
37
+ /** The HTTP client used for token service requests. */
37
38
  client: HttpClient
38
39
  private msAppId: string = ''
39
40
  private authProvider?: AuthProvider
@@ -58,6 +59,13 @@ export class UserTokenClient {
58
59
  */
59
60
  constructor (httpClient: HttpClient, authProvider?: AuthProvider, authScope?: string)
60
61
 
62
+ /**
63
+ * Initializes the client from an application ID or an HTTP client.
64
+ *
65
+ * @param param The application ID or configured HTTP client.
66
+ * @param authProvider The optional provider used to acquire an access token lazily.
67
+ * @param authScope The optional access-token scope.
68
+ */
61
69
  constructor (param: string | HttpClient, authProvider?: AuthProvider, authScope?: string) {
62
70
  if (typeof param === 'string') {
63
71
  const baseURL = getTokenServiceEndpoint()
@@ -260,6 +268,11 @@ export class UserTokenClient {
260
268
  })
261
269
  }
262
270
 
271
+ /**
272
+ * Replaces the bearer token used for token service requests.
273
+ *
274
+ * @param token The access token.
275
+ */
263
276
  public updateAuthToken (token: string): void {
264
277
  this.client.setHeader('Authorization', `Bearer ${token}`)
265
278
  this.authInitialized = true
@@ -9,6 +9,7 @@ import {
9
9
  } from './configuration/configuration'
10
10
  import { loadModernEnvironmentConfiguration } from './configuration/environmentConfiguration'
11
11
  import { mergeDefined } from './utils'
12
+ import { getHttpsUrl, hasAllowedOutboundAddresses, isLocalOrBlockedHost } from './utils/outboundUrl'
12
13
 
13
14
  /** Hosts used by Microsoft channel callbacks and hosted attachments. */
14
15
  const DEFAULT_MICROSOFT_HOSTS = Object.freeze([
@@ -37,6 +38,12 @@ export interface OutboundHostValidatorOptions {
37
38
  */
38
39
  includeDefaultMicrosoftHosts?: boolean
39
40
 
41
+ /**
42
+ * Allows allowlisted HTTPS destinations to use private, shared (RFC 6598),
43
+ * loopback, link-local, or unspecified addresses. Defaults to `false`.
44
+ */
45
+ allowPrivateNetworkAddresses?: boolean
46
+
40
47
  /** Additional exact hosts or host suffixes to allow.
41
48
  * An entry matches a request host when the host equals the entry or is a subdomain of it (e.g. `contoso.com` matches `contoso.com` and `files.contoso.com`).
42
49
  * A leading `*.` is accepted and ignored (treated as a suffix). Ports and paths are ignored if provided.
@@ -46,7 +53,14 @@ export interface OutboundHostValidatorOptions {
46
53
 
47
54
  /** A policy that decides whether an outbound URL is safe to request. */
48
55
  export interface OutboundUrlPolicy {
56
+ /** Indicates whether URL validation is enforced. */
49
57
  readonly enabled: boolean
58
+ /**
59
+ * Determines whether an outbound URL is allowed.
60
+ *
61
+ * @param url The URL to validate.
62
+ * @returns `true` when the URL is allowed.
63
+ */
50
64
  isAllowed(url: string | URL | null | undefined): boolean
51
65
  }
52
66
 
@@ -54,43 +68,88 @@ export interface OutboundUrlPolicy {
54
68
  * Shared allowlist policy for server-side outbound requests.
55
69
  *
56
70
  * A configured suffix matches both the exact host and its subdomains. The
57
- * policy is disabled by default to preserve existing SDK behavior.
71
+ * policy is disabled by default to preserve existing SDK behavior. Enabled
72
+ * policies require HTTPS without userinfo and restrict private addresses.
73
+ * Attachment downloaders additionally validate DNS answers and redirect hops.
58
74
  */
59
75
  export class OutboundHostValidator implements OutboundUrlPolicy {
76
+ /** Indicates whether allowlist enforcement is enabled. */
60
77
  public readonly enabled: boolean
61
78
  private readonly suffixes: ReadonlySet<string>
79
+ private readonly allowPrivateNetworkAddresses: boolean
62
80
 
81
+ /**
82
+ * Creates an outbound host validator.
83
+ *
84
+ * @param options The allowlist configuration.
85
+ */
63
86
  public constructor (options: OutboundHostValidatorOptions = {}) {
64
87
  const resolved = options.configurationContext
65
88
  ? resolveOutboundHostValidatorOptions(options)
66
89
  : options
67
90
  this.enabled = resolved.enabled ?? false
91
+ this.allowPrivateNetworkAddresses = resolved.allowPrivateNetworkAddresses ?? false
68
92
 
69
93
  const suffixes = new Set<string>()
70
94
  if (resolved.includeDefaultMicrosoftHosts ?? true) {
71
- for (const host of DEFAULT_MICROSOFT_HOSTS) suffixes.add(host)
95
+ for (const host of DEFAULT_MICROSOFT_HOSTS) {
96
+ suffixes.add(host)
97
+ }
72
98
  }
73
99
 
74
100
  for (const host of resolved.hosts ?? []) {
75
101
  const normalized = normalizeConfiguredHost(host)
76
- if (normalized) suffixes.add(normalized)
102
+ if (normalized) {
103
+ suffixes.add(normalized)
104
+ }
77
105
  }
78
106
 
79
107
  this.suffixes = suffixes
80
108
  }
81
109
 
110
+ /**
111
+ * Determines whether a URL's host matches the configured allowlist.
112
+ *
113
+ * @param input The absolute URL to validate.
114
+ * @returns `true` when validation is disabled or the host is allowed.
115
+ */
82
116
  public isAllowed (input: string | URL | null | undefined): boolean {
83
- if (!this.enabled) return true
117
+ if (!this.enabled) {
118
+ return true
119
+ }
84
120
 
85
- const host = getUrlHost(input)
86
- if (!host) return false
121
+ const url = getHttpsUrl(input)
122
+ if (!url) {
123
+ return false
124
+ }
125
+ const host = normalizeHostname(url.hostname)
126
+ if (!host || (!this.allowPrivateNetworkAddresses && isLocalOrBlockedHost(host))) {
127
+ return false
128
+ }
87
129
 
88
130
  for (const suffix of this.suffixes) {
89
- if (host === suffix || host.endsWith(`.${suffix}`)) return true
131
+ if (host === suffix || host.endsWith(`.${suffix}`)) {
132
+ return true
133
+ }
90
134
  }
91
135
 
92
136
  return false
93
137
  }
138
+
139
+ /**
140
+ * Checks the host policy and all resolved destination addresses before an attachment request.
141
+ * DNS prevalidation does not pin the HTTP connection to the checked addresses.
142
+ */
143
+ public async isAllowedAsync (input: string | URL | null | undefined): Promise<boolean> {
144
+ if (!this.isAllowed(input)) {
145
+ return false
146
+ }
147
+ if (!this.enabled) {
148
+ return true
149
+ }
150
+ const url = getHttpsUrl(input)
151
+ return url !== undefined && await hasAllowedOutboundAddresses(url, this.allowPrivateNetworkAddresses)
152
+ }
94
153
  }
95
154
 
96
155
  /**
@@ -115,11 +174,18 @@ function resolveOutboundHostValidatorOptions (
115
174
  const external = getConfigurationSnapshot(options.configurationContext)
116
175
  const fromEnv = loadOutboundHostValidatorOptionsFromEnv()
117
176
  const direct: OutboundHostValidatorOptions = {}
118
- if (options.enabled !== undefined) direct.enabled = options.enabled
177
+ if (options.enabled !== undefined) {
178
+ direct.enabled = options.enabled
179
+ }
119
180
  if (options.includeDefaultMicrosoftHosts !== undefined) {
120
181
  direct.includeDefaultMicrosoftHosts = options.includeDefaultMicrosoftHosts
121
182
  }
122
- if (options.hosts !== undefined) direct.hosts = options.hosts
183
+ if (options.hosts !== undefined) {
184
+ direct.hosts = options.hosts
185
+ }
186
+ if (options.allowPrivateNetworkAddresses !== undefined) {
187
+ direct.allowPrivateNetworkAddresses = options.allowPrivateNetworkAddresses
188
+ }
123
189
  return mergeDefined<OutboundHostValidatorOptions>(
124
190
  external.fallback.outboundHostValidator,
125
191
  fromEnv,
@@ -129,25 +195,20 @@ function resolveOutboundHostValidatorOptions (
129
195
  )
130
196
  }
131
197
 
132
- function getUrlHost (input: string | URL | null | undefined): string | undefined {
133
- if (!input) return undefined
134
-
135
- try {
136
- const url = input instanceof URL ? input : new URL(input)
137
- return normalizeHostname(url.hostname)
138
- } catch {
139
- return undefined
140
- }
141
- }
142
-
143
198
  function normalizeConfiguredHost (input: string): string | undefined {
144
199
  let candidate = input?.trim()
145
- if (!candidate) return undefined
146
- if (candidate.startsWith('*.')) candidate = candidate.slice(2)
200
+ if (!candidate) {
201
+ return undefined
202
+ }
203
+ if (candidate.startsWith('*.')) {
204
+ candidate = candidate.slice(2)
205
+ }
147
206
 
148
207
  try {
149
208
  const absolute = new URL(candidate)
150
- if (absolute.hostname) return normalizeHostname(absolute.hostname)
209
+ if (absolute.hostname) {
210
+ return normalizeHostname(absolute.hostname)
211
+ }
151
212
  } catch {
152
213
  // A bare host, host:port, or host/path is handled below.
153
214
  }
@@ -30,16 +30,29 @@ class FileStorageInternals {
30
30
  private readonly statePath: string
31
31
  private readonly versionsPath: string
32
32
  private readonly expirationsPath: string
33
+
34
+ /** The stored values keyed by storage key. */
33
35
  readonly state: Record<string, unknown>
36
+
37
+ /** The current version associated with each storage key. */
34
38
  readonly versions: Record<string, string>
39
+
40
+ /** The expiration time associated with each storage key, expressed as Unix time in milliseconds. */
35
41
  readonly expirations: Record<string, number>
36
42
 
43
+ /**
44
+ * Creates the shared file storage implementation.
45
+ *
46
+ * @param folder The folder containing the storage files.
47
+ */
37
48
  constructor (folder: string) {
38
49
  fs.mkdirSync(folder, { recursive: true })
39
50
  this.statePath = path.join(folder, 'state.json')
40
51
  this.versionsPath = path.join(folder, 'state.versions.json')
41
52
  this.expirationsPath = path.join(folder, 'state.expirations.json')
42
- if (!fs.existsSync(this.statePath)) fs.writeFileSync(this.statePath, '{}')
53
+ if (!fs.existsSync(this.statePath)) {
54
+ fs.writeFileSync(this.statePath, '{}')
55
+ }
43
56
  this.state = JSON.parse(fs.readFileSync(this.statePath, 'utf8')) as Record<string, unknown>
44
57
  this.versions = fs.existsSync(this.versionsPath)
45
58
  ? JSON.parse(fs.readFileSync(this.versionsPath, 'utf8')) as Record<string, string>
@@ -49,6 +62,7 @@ class FileStorageInternals {
49
62
  : {}
50
63
  }
51
64
 
65
+ /** Writes the current storage state and metadata to disk. */
52
66
  flush (): void {
53
67
  fs.writeFileSync(this.statePath, JSON.stringify(this.state, null, 2))
54
68
  if (fs.existsSync(this.versionsPath) || Object.keys(this.versions).length > 0) {
@@ -59,17 +73,34 @@ class FileStorageInternals {
59
73
  }
60
74
  }
61
75
 
76
+ /**
77
+ * Gets the current version for a key, creating one when necessary.
78
+ *
79
+ * @param key The storage key.
80
+ * @returns The current version.
81
+ */
62
82
  getOrCreateVersion (key: string): string {
63
83
  const version = this.versions[key] ?? randomUUID()
64
84
  this.versions[key] = version
65
85
  return version
66
86
  }
67
87
 
88
+ /**
89
+ * Determines whether a stored value has expired.
90
+ *
91
+ * @param key The storage key.
92
+ * @returns `true` when the value has expired; otherwise, `false`.
93
+ */
68
94
  isExpired (key: string): boolean {
69
95
  const expiresAt = this.expirations[key]
70
96
  return expiresAt !== undefined && expiresAt <= Date.now()
71
97
  }
72
98
 
99
+ /**
100
+ * Removes a value and its version and expiration metadata.
101
+ *
102
+ * @param key The storage key.
103
+ */
73
104
  remove (key: string): void {
74
105
  delete this.state[key]
75
106
  delete this.versions[key]
@@ -232,7 +263,9 @@ export class FileStorageV2 extends StorageV2 {
232
263
  version: version ?? this.internals.getOrCreateVersion(key),
233
264
  }]
234
265
  }))
235
- if (createdVersion) this.internals.flush()
266
+ if (createdVersion) {
267
+ this.internals.flush()
268
+ }
236
269
  return results
237
270
  })
238
271
  }
@@ -277,13 +310,18 @@ export class FileStorageV2 extends StorageV2 {
277
310
  const version = randomUUID()
278
311
  this.internals.state[key] = structuredClone(value)
279
312
  this.internals.versions[key] = version
280
- if (expiresAt === undefined) delete this.internals.expirations[key]
281
- else this.internals.expirations[key] = expiresAt
313
+ if (expiresAt === undefined) {
314
+ delete this.internals.expirations[key]
315
+ } else {
316
+ this.internals.expirations[key] = expiresAt
317
+ }
282
318
  results[key] = { key, status: StorageOperationStatus.Succeeded, version }
283
319
  changed = true
284
320
  }
285
321
  }
286
- if (changed || createdVersion) this.internals.flush()
322
+ if (changed || createdVersion) {
323
+ this.internals.flush()
324
+ }
287
325
  return results
288
326
  })
289
327
  }
@@ -325,7 +363,9 @@ export class FileStorageV2 extends StorageV2 {
325
363
  changed = true
326
364
  }
327
365
  }
328
- if (changed || createdVersion) this.internals.flush()
366
+ if (changed || createdVersion) {
367
+ this.internals.flush()
368
+ }
329
369
  return results
330
370
  })
331
371
  }