@microsoft/agents-hosting 1.7.2 → 1.8.0-beta.36.gb033f0abc1

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 (102) hide show
  1. package/README.md +64 -2
  2. package/dist/package.json +3 -3
  3. package/dist/src/agent-client/agentClient.d.ts +18 -6
  4. package/dist/src/agent-client/agentClient.js +34 -18
  5. package/dist/src/agent-client/agentClient.js.map +1 -1
  6. package/dist/src/agent-client/agentResponseHandler.d.ts +14 -9
  7. package/dist/src/agent-client/agentResponseHandler.js +10 -5
  8. package/dist/src/agent-client/agentResponseHandler.js.map +1 -1
  9. package/dist/src/agent-client/createAgentResponseHandler.d.ts +7 -2
  10. package/dist/src/agent-client/createAgentResponseHandler.js +55 -8
  11. package/dist/src/agent-client/createAgentResponseHandler.js.map +1 -1
  12. package/dist/src/app/adaptiveCards/activityValueParsers.d.ts +1 -4
  13. package/dist/src/app/adaptiveCards/activityValueParsers.js +2 -5
  14. package/dist/src/app/adaptiveCards/activityValueParsers.js.map +1 -1
  15. package/dist/src/app/adaptiveCards/adaptiveCardsActions.d.ts +18 -0
  16. package/dist/src/app/adaptiveCards/adaptiveCardsActions.js +53 -23
  17. package/dist/src/app/adaptiveCards/adaptiveCardsActions.js.map +1 -1
  18. package/dist/src/app/agentApplication.d.ts +3 -0
  19. package/dist/src/app/agentApplication.js +140 -81
  20. package/dist/src/app/agentApplication.js.map +1 -1
  21. package/dist/src/app/attachmentDownloader.d.ts +4 -1
  22. package/dist/src/app/attachmentDownloader.js +27 -5
  23. package/dist/src/app/attachmentDownloader.js.map +1 -1
  24. package/dist/src/app/auth/handlers/agenticAuthorization.d.ts +1 -1
  25. package/dist/src/app/auth/handlers/azureBotAuthorization.d.ts +1 -1
  26. package/dist/src/app/auth/types.d.ts +3 -4
  27. package/dist/src/app/index.d.ts +2 -0
  28. package/dist/src/app/index.js.map +1 -1
  29. package/dist/src/app/streaming/streamingResponse.d.ts +56 -0
  30. package/dist/src/app/streaming/streamingResponse.js +250 -26
  31. package/dist/src/app/streaming/streamingResponse.js.map +1 -1
  32. package/dist/src/app/teamsAttachmentDownloader.d.ts +3 -1
  33. package/dist/src/app/teamsAttachmentDownloader.js +12 -1
  34. package/dist/src/app/teamsAttachmentDownloader.js.map +1 -1
  35. package/dist/src/auth/authConfiguration.js +12 -0
  36. package/dist/src/auth/authConfiguration.js.map +1 -1
  37. package/dist/src/auth/index.d.ts +2 -1
  38. package/dist/src/auth/index.js +2 -1
  39. package/dist/src/auth/index.js.map +1 -1
  40. package/dist/src/auth/jwt-middleware.d.ts +6 -0
  41. package/dist/src/auth/jwt-middleware.js +263 -13
  42. package/dist/src/auth/jwt-middleware.js.map +1 -1
  43. package/dist/src/auth/msal/msalConnectionSettings.d.ts +8 -0
  44. package/dist/src/auth/msal/msalHttpRetryHandlerHelper.d.ts +34 -0
  45. package/dist/src/auth/msal/msalHttpRetryHandlerHelper.js +126 -0
  46. package/dist/src/auth/msal/msalHttpRetryHandlerHelper.js.map +1 -0
  47. package/dist/src/auth/msal/msalTokenProvider.d.ts +1 -0
  48. package/dist/src/auth/msal/msalTokenProvider.js +23 -14
  49. package/dist/src/auth/msal/msalTokenProvider.js.map +1 -1
  50. package/dist/src/auth/settings.d.ts +18 -0
  51. package/dist/src/auth/settings.js +8 -2
  52. package/dist/src/auth/settings.js.map +1 -1
  53. package/dist/src/cloudAdapter.d.ts +29 -2
  54. package/dist/src/cloudAdapter.js +44 -2
  55. package/dist/src/cloudAdapter.js.map +1 -1
  56. package/dist/src/errorHelper.js +137 -101
  57. package/dist/src/errorHelper.js.map +1 -1
  58. package/dist/src/index.d.ts +1 -0
  59. package/dist/src/index.js +1 -0
  60. package/dist/src/index.js.map +1 -1
  61. package/dist/src/invoke/adaptiveCardInvokeValue.d.ts +5 -0
  62. package/dist/src/oauth/userTokenClient.d.ts +0 -1
  63. package/dist/src/oauth/userTokenClient.js +0 -1
  64. package/dist/src/oauth/userTokenClient.js.map +1 -1
  65. package/dist/src/observability/traces.d.ts +6 -0
  66. package/dist/src/observability/traces.js +20 -2
  67. package/dist/src/observability/traces.js.map +1 -1
  68. package/dist/src/outboundHostValidator.d.ts +47 -0
  69. package/dist/src/outboundHostValidator.js +149 -0
  70. package/dist/src/outboundHostValidator.js.map +1 -0
  71. package/dist/src/state/agentState.d.ts +5 -0
  72. package/dist/src/state/agentState.js +16 -3
  73. package/dist/src/state/agentState.js.map +1 -1
  74. package/package.json +3 -3
  75. package/src/agent-client/agentClient.ts +46 -23
  76. package/src/agent-client/agentResponseHandler.ts +14 -9
  77. package/src/agent-client/createAgentResponseHandler.ts +70 -9
  78. package/src/app/adaptiveCards/activityValueParsers.ts +3 -9
  79. package/src/app/adaptiveCards/adaptiveCardsActions.ts +52 -24
  80. package/src/app/agentApplication.ts +149 -77
  81. package/src/app/attachmentDownloader.ts +25 -5
  82. package/src/app/auth/handlers/agenticAuthorization.ts +1 -1
  83. package/src/app/auth/handlers/azureBotAuthorization.ts +1 -1
  84. package/src/app/auth/types.ts +3 -3
  85. package/src/app/index.ts +8 -0
  86. package/src/app/streaming/streamingResponse.ts +276 -22
  87. package/src/app/teamsAttachmentDownloader.ts +11 -1
  88. package/src/auth/authConfiguration.ts +12 -0
  89. package/src/auth/index.ts +2 -1
  90. package/src/auth/jwt-middleware.ts +298 -14
  91. package/src/auth/msal/msalConnectionSettings.ts +10 -0
  92. package/src/auth/msal/msalHttpRetryHandlerHelper.ts +161 -0
  93. package/src/auth/msal/msalTokenProvider.ts +32 -14
  94. package/src/auth/settings.ts +18 -3
  95. package/src/cloudAdapter.ts +56 -3
  96. package/src/errorHelper.ts +155 -114
  97. package/src/index.ts +1 -0
  98. package/src/invoke/adaptiveCardInvokeValue.ts +5 -0
  99. package/src/oauth/userTokenClient.ts +0 -1
  100. package/src/observability/traces.ts +19 -1
  101. package/src/outboundHostValidator.ts +172 -0
  102. package/src/state/agentState.ts +24 -6
@@ -11,6 +11,8 @@ import { Attachment } from '@microsoft/agents-activity'
11
11
  import { AuthProvider } from '../auth/authProvider'
12
12
  import { debug } from '@microsoft/agents-telemetry'
13
13
  import { loadAuthConfigFromEnv, MsalTokenProvider } from '../auth'
14
+ import { createOutboundHostValidator, OutboundUrlPolicy } from '../outboundHostValidator'
15
+ import { Connections } from '../auth/connections'
14
16
 
15
17
  const logger = debug('agents:attachmentDownloader')
16
18
 
@@ -27,16 +29,19 @@ const logger = debug('agents:attachmentDownloader')
27
29
  export class AttachmentDownloader<TState extends TurnState = TurnState> implements InputFileDownloader<TState> {
28
30
  private _httpClient: HttpClient
29
31
  private _stateKey: string
32
+ private readonly _hostValidator: OutboundUrlPolicy
30
33
 
31
34
  /**
32
35
  * Creates an instance of AttachmentDownloader.
33
36
  * This class is responsible for downloading input files from attachments.
34
37
  *
35
38
  * @param stateKey The key to store files in state. Defaults to 'inputFiles'.
39
+ * @param outboundHostValidator Optional shared policy used to validate outbound attachment URLs.
36
40
  */
37
- public constructor (stateKey: string = 'inputFiles') {
41
+ public constructor (stateKey: string = 'inputFiles', outboundHostValidator?: OutboundUrlPolicy) {
38
42
  this._httpClient = new HttpClient()
39
43
  this._stateKey = stateKey
44
+ this._hostValidator = outboundHostValidator ?? createOutboundHostValidator()
40
45
  }
41
46
 
42
47
  /**
@@ -52,10 +57,18 @@ export class AttachmentDownloader<TState extends TurnState = TurnState> implemen
52
57
  return Promise.resolve([])
53
58
  }
54
59
 
55
- // TODO: from adapter
56
- const authProvider: AuthProvider = new MsalTokenProvider()
57
-
58
- const accessToken = await authProvider.getAccessToken(loadAuthConfigFromEnv(), 'https://api.botframework.com')
60
+ let accessToken = ''
61
+ const connectionManager = (context.adapter as { connectionManager?: Connections })?.connectionManager
62
+ const identity = context.identity
63
+ if (connectionManager && identity) {
64
+ const scope = identity.azp ?? identity.appid ?? 'https://api.botframework.com'
65
+ logger.debug(`Using adapter connection manager token provider for attachment downloads with scope ${scope}`)
66
+ accessToken = await connectionManager.getTokenProviderFromActivity(identity, context.activity).getAccessToken(scope)
67
+ } else {
68
+ logger.debug('Using MsalTokenProvider fallback for attachment downloads')
69
+ const authProvider: AuthProvider = new MsalTokenProvider()
70
+ accessToken = await authProvider.getAccessToken(loadAuthConfigFromEnv(), 'https://api.botframework.com')
71
+ }
59
72
 
60
73
  const files: InputFile[] = []
61
74
  for (const attachment of attachments) {
@@ -90,6 +103,13 @@ export class AttachmentDownloader<TState extends TurnState = TurnState> implemen
90
103
  if (accessToken.length > 0) {
91
104
  headers.Authorization = `Bearer ${accessToken}`
92
105
  }
106
+ if (!this._hostValidator.isAllowed(attachment.contentUrl)) {
107
+ let host = '[invalid-url]'
108
+ try { host = new URL(attachment.contentUrl).hostname } catch { /* invalid */ }
109
+ logger.warn(`Attachment contentUrl host is not in the configured allowed hosts. Host='${host}'`)
110
+ return undefined
111
+ }
112
+
93
113
  const response = await this._httpClient.get(attachment.contentUrl, {
94
114
  headers,
95
115
  responseType: 'arraybuffer'
@@ -22,7 +22,7 @@ const logger = debug('agents:authorization:agentic')
22
22
  * where `{handlerId}` is the handler's unique identifier and `{propertyName}` matches the property name.
23
23
  *
24
24
  * @example
25
- * ```env
25
+ * ```
26
26
  * # For a handler with id "myAuth":
27
27
  * AgentApplication__UserAuthorization__handlers__myAuth__settings__type=AgenticUserAuthorization
28
28
  * AgentApplication__UserAuthorization__handlers__myAuth__settings__scopes=api://scope1 api://scope2
@@ -48,7 +48,7 @@ interface AzureBotActiveHandler extends ActiveAuthorizationHandler {
48
48
  * where `{handlerId}` is the handler's unique identifier and `{propertyName}` matches the property name.
49
49
  *
50
50
  * @example
51
- * ```env
51
+ * ```
52
52
  * # For a handler with id "myAuth":
53
53
  * AgentApplication__UserAuthorization__handlers__myAuth__settings__azureBotOAuthConnectionName=MyConnection
54
54
  * AgentApplication__UserAuthorization__handlers__myAuth__settings__oboScopes=api://scope1 api://scope2
@@ -126,7 +126,7 @@ export interface AuthorizationHandlerTokenOptions {
126
126
  * where `{handlerId}` is the handler's unique identifier and `{propertyName}` matches the property name.
127
127
  *
128
128
  * @example
129
- * ```env
129
+ * ```
130
130
  * # For a handler with id "myAuth":
131
131
  * AgentApplication__UserAuthorization__handlers__myAuth__settings__azureBotOAuthConnectionName=MyConnection
132
132
  * AgentApplication__UserAuthorization__handlers__myAuth__settings__oboScopes=api://scope1 api://scope2
@@ -170,7 +170,7 @@ export interface AzureBotAuthorizationOptionsLegacy {
170
170
  * @deprecated
171
171
  * Messages configuration for the AzureBotAuthorization handler.
172
172
  */
173
- interface AzureBotAuthorizationOptionsMessages {
173
+ export interface AzureBotAuthorizationOptionsMessages {
174
174
  /**
175
175
  * @deprecated Use `invalidSignInRetryMessage` instead.
176
176
  * Message displayed when an invalid code is entered.
@@ -198,7 +198,7 @@ interface AzureBotAuthorizationOptionsMessages {
198
198
  * @deprecated
199
199
  * Settings for on-behalf-of token acquisition.
200
200
  */
201
- interface AzureBotAuthorizationOptionsOBO {
201
+ export interface AzureBotAuthorizationOptionsOBO {
202
202
  /**
203
203
  * @deprecated Use `oboConnectionName` instead.
204
204
  * Connection name to use for on-behalf-of token acquisition.
package/src/app/index.ts CHANGED
@@ -3,6 +3,14 @@ export * from './agentApplicationBuilder'
3
3
  export * from './agentApplicationOptions'
4
4
  export * from './appRoute'
5
5
  export { Authorization } from './auth/authorization'
6
+ export type { AgenticAuthorizationOptions, AzureBotAuthorizationOptions } from './auth/handlers'
7
+ export type {
8
+ AuthorizationOptions,
9
+ AuthorizationHandlerTokenOptions,
10
+ AzureBotAuthorizationOptionsLegacy,
11
+ AzureBotAuthorizationOptionsMessages,
12
+ AzureBotAuthorizationOptionsOBO
13
+ } from './auth/types'
6
14
  export * from './routeHandler'
7
15
  export * from './routeList'
8
16
  export * from './routeRank'
@@ -11,6 +11,10 @@ import { debug } from '@microsoft/agents-telemetry'
11
11
  import { Errors } from '../../errorHelper'
12
12
 
13
13
  const logger = debug('agents:streamingResponse')
14
+ const teamsStreamTimedOut = 'Content stream finished due to exceeded streaming time.'
15
+ const defaultStreamingTakingTooLongMessage = 'The response is taking longer than expected. Please wait while we continue to generate the response.'
16
+ const m365StreamingTimeoutMs = 105000
17
+ const m365WorkingNoticeIntervalMs = 35000
14
18
 
15
19
  /**
16
20
  * Results for streaming response operations.
@@ -57,6 +61,13 @@ export class StreamingResponse {
57
61
  private _finalMessage?: Activity
58
62
  private _canceled = false
59
63
  private _userCanceled = false
64
+ private _streamTimedOut = false
65
+ private _streamTimeoutNotificationSent = false
66
+ private _lastInformationalMessageSent = ''
67
+ private _streamStartedAt?: number
68
+ private _keepAliveTimer?: ReturnType<typeof setTimeout>
69
+ private _streamTimeoutTimer?: ReturnType<typeof setTimeout>
70
+ private _streamingTakingTooLongMessage = defaultStreamingTakingTooLongMessage
60
71
 
61
72
  // Queue for outgoing activities
62
73
  private _queue: Array<() => Activity> = []
@@ -127,12 +138,30 @@ export class StreamingResponse {
127
138
  return this._isStreamingChannel
128
139
  }
129
140
 
141
+ /**
142
+ * Gets the message sent when streaming takes longer than the channel allows.
143
+ */
144
+ public get streamingTakingTooLongMessage (): string {
145
+ return this._streamingTakingTooLongMessage
146
+ }
147
+
148
+ /**
149
+ * Sets the message sent when streaming takes longer than the channel allows.
150
+ */
151
+ public set streamingTakingTooLongMessage (message: string) {
152
+ this._streamingTakingTooLongMessage = message
153
+ }
154
+
130
155
  /**
131
156
  * Queues an informative update to be sent to the client.
132
157
  *
133
158
  * @param {string} text Text of the update to send.
134
159
  */
135
160
  public queueInformativeUpdate (text: string): void {
161
+ if (this.isM365Copilot()) {
162
+ this._lastInformationalMessageSent = text
163
+ }
164
+
136
165
  if (!this.isStreamingChannel || !text.trim() || this._canceled) {
137
166
  return
138
167
  }
@@ -203,9 +232,23 @@ export class StreamingResponse {
203
232
 
204
233
  // Queue final message
205
234
  this._ended = true
235
+ this.clearStreamTimers()
206
236
 
207
237
  if (!this.isStreamingChannel) {
208
- await this.sendActivity(this.createFinalMessage())
238
+ // A failed streaming send may still be updating the timed-out activity.
239
+ // Preserve ordering so that the completed response is always the last update.
240
+ await this.waitForQueue()
241
+
242
+ if (this._streamTimeoutNotificationSent && !this._message && !this._finalMessage && !this._attachments?.length) {
243
+ return StreamingResponseResult.Success
244
+ }
245
+
246
+ const finalMessage = this.createFinalMessage()
247
+ if (this._streamTimedOut) {
248
+ await this.updateActivity(finalMessage)
249
+ } else {
250
+ await this.sendActivity(finalMessage)
251
+ }
209
252
  return StreamingResponseResult.Success
210
253
  }
211
254
 
@@ -214,6 +257,13 @@ export class StreamingResponse {
214
257
 
215
258
  // Wait for the queue to drain
216
259
  await this.waitForQueue()
260
+
261
+ // The final streaming send itself can report that the channel timed out. In
262
+ // that case the queued final activity is discarded and must be applied as
263
+ // an update after the timeout recovery has completed.
264
+ if (this._streamTimedOut) {
265
+ await this.updateActivity(this.createFinalMessage())
266
+ }
217
267
  return StreamingResponseResult.Success
218
268
  }
219
269
 
@@ -223,6 +273,7 @@ export class StreamingResponse {
223
273
  */
224
274
  public async reset () : Promise<void> {
225
275
  await this.waitForQueue()
276
+ this.clearStreamTimers()
226
277
 
227
278
  this._queueSync = undefined
228
279
  this._queue = []
@@ -230,6 +281,10 @@ export class StreamingResponse {
230
281
  this._ended = false
231
282
  this._canceled = false
232
283
  this._userCanceled = false
284
+ this._streamTimedOut = false
285
+ this._streamTimeoutNotificationSent = false
286
+ this._lastInformationalMessageSent = ''
287
+ this._streamStartedAt = undefined
233
288
  this._message = ''
234
289
  this._nextSequence = 1
235
290
  this._streamId = undefined
@@ -358,6 +413,29 @@ export class StreamingResponse {
358
413
  return this._message
359
414
  }
360
415
 
416
+ /**
417
+ * Ends the streaming portion of a response while allowing the underlying operation to continue.
418
+ *
419
+ * @param message Message sent to notify the user that streaming has stopped.
420
+ * @returns Whether the streaming response was stopped.
421
+ */
422
+ public async sendStreamTimedOutNotification (message: string): Promise<boolean> {
423
+ if (this._ended || this._canceled || !this.isStreamingChannel) {
424
+ return false
425
+ }
426
+
427
+ // Ensure any in-flight queued sends complete before we send the timeout notification.
428
+ await this.waitForQueue()
429
+
430
+ await this.sendActivity(this.createStreamStoppedMessage(message))
431
+ this._streamTimeoutNotificationSent = true
432
+ this._isStreamingChannel = false
433
+ this._queue = []
434
+ this._chunkQueued = false
435
+ this.clearStreamTimers()
436
+ return true
437
+ }
438
+
361
439
  /**
362
440
  * Waits for the outgoing activity queue to be empty.
363
441
  *
@@ -446,6 +524,10 @@ export class StreamingResponse {
446
524
  activity.attachments = this._attachments
447
525
  this._nextSequence++ // Increment sequence for final message, even if not streaming.
448
526
 
527
+ if (this._streamTimedOut && this._streamId) {
528
+ activity.id = this._streamId
529
+ }
530
+
449
531
  if (this.isStreamingChannel) {
450
532
  activity.entities.push({
451
533
  type: 'streaminfo',
@@ -457,6 +539,72 @@ export class StreamingResponse {
457
539
  return activity
458
540
  }
459
541
 
542
+ /**
543
+ * Creates the final streaming notification used when streaming stops but processing continues.
544
+ */
545
+ private createStreamStoppedMessage (message: string): Activity {
546
+ return Activity.fromObject({
547
+ type: 'message',
548
+ text: message || 'No text was streamed',
549
+ entities: [{
550
+ type: 'streaminfo',
551
+ streamType: 'final',
552
+ streamResult: this._message ? 'success' : 'error',
553
+ streamSequence: this._nextSequence++
554
+ }]
555
+ })
556
+ }
557
+
558
+ /**
559
+ * Creates a checkpoint or final notification for a channel streaming timeout.
560
+ */
561
+ private createStreamTimedOutMessage (addStreamFinal = false): Activity {
562
+ const text = this._message
563
+ ? `${this._message}\n\n${this.streamingTakingTooLongMessage}\n`
564
+ : this.streamingTakingTooLongMessage
565
+ const activity = Activity.fromObject({
566
+ type: 'message',
567
+ text,
568
+ entities: []
569
+ })
570
+
571
+ if (this._streamId) {
572
+ activity.id = this._streamId
573
+ }
574
+
575
+ if (addStreamFinal) {
576
+ activity.entities!.push({
577
+ type: 'streaminfo',
578
+ streamType: 'final',
579
+ streamId: this._streamId,
580
+ streamResult: this._message ? 'success' : 'error',
581
+ streamSequence: this._nextSequence++
582
+ })
583
+ }
584
+
585
+ return activity
586
+ }
587
+
588
+ /**
589
+ * Creates the last streaming update sent before proactively finalizing an
590
+ * M365 Copilot stream.
591
+ */
592
+ private createStreamTimedOutStreamingUpdate (): Activity {
593
+ const text = this._message
594
+ ? `${this._message}\n\n${this.streamingTakingTooLongMessage}\n`
595
+ : this.streamingTakingTooLongMessage
596
+
597
+ return Activity.fromObject({
598
+ type: 'typing',
599
+ text,
600
+ entities: [{
601
+ type: 'streaminfo',
602
+ streamType: 'streaming',
603
+ streamSequence: this._nextSequence++
604
+ }]
605
+ })
606
+ }
607
+
460
608
  /**
461
609
  * Sends an activity to the client and saves the stream ID returned.
462
610
  *
@@ -465,16 +613,15 @@ export class StreamingResponse {
465
613
  * @private
466
614
  */
467
615
  private async sendActivity (activity: Activity): Promise<void> {
468
- // Set activity ID to the assigned stream ID
469
- if (this._streamId) {
616
+ this.startStreamTimers()
617
+
618
+ // Only streaming activities should carry stream metadata. Once streaming
619
+ // has stopped, the completed response must be sent as an independent
620
+ // message without an orphaned streamId entity.
621
+ const streamInfo = activity.entities?.find(entity => entity.type?.toLowerCase() === 'streaminfo')
622
+ if (this._streamId && streamInfo) {
470
623
  activity.id = this._streamId
471
- if (!activity.entities) {
472
- activity.entities = []
473
- }
474
- if (!activity.entities[0]) {
475
- activity.entities[0] = {} as Entity
476
- }
477
- activity.entities[0].streamId = this._streamId
624
+ streamInfo.streamId = this._streamId
478
625
  }
479
626
 
480
627
  if (this._citations && this._citations.length > 0 && !this._ended) {
@@ -507,25 +654,138 @@ export class StreamingResponse {
507
654
  if (!this._streamId) {
508
655
  this._streamId = response?.id
509
656
  }
657
+ if (this.isM365Copilot() && this.isStreamingChannel && !this._ended) {
658
+ this.scheduleKeepAlive()
659
+ }
510
660
  await new Promise((resolve) => setTimeout(resolve, this.delayInMs))
511
661
  } catch (error) {
512
- const { message } = error as Error
662
+ const message = error instanceof Error ? error.message : String(error)
513
663
  this._canceled = true
514
- this._queueSync = undefined
515
664
  this._queue = []
665
+ this._chunkQueued = false
666
+ this.clearStreamTimers()
516
667
 
517
668
  // MS Teams code list: https://learn.microsoft.com/en-us/microsoftteams/platform/bots/streaming-ux?tabs=jsts#error-codes
518
- if (message.includes('ContentStreamNotAllowed')) {
669
+ const normalizedMessage = message.toLowerCase()
670
+ if (normalizedMessage.includes('contentstreamnotallowed') && normalizedMessage.includes(teamsStreamTimedOut.toLowerCase())) {
671
+ logger.warn('Client canceled due to exceeded allowed streaming time.', { originalError: message })
672
+ this._streamTimedOut = true
673
+ this._canceled = false
674
+ this._isStreamingChannel = false
675
+ await this.updateActivity(this.createStreamTimedOutMessage())
676
+ } else if (normalizedMessage.includes('contentstreamnotallowed')) {
519
677
  logger.warn('Streaming content is not allowed by the client side.', { originalError: message })
520
678
  this._userCanceled = true
521
- } else if (message.includes('BadArgument') && message.toLowerCase().includes('streaming api is not enabled')) {
679
+ } else if (normalizedMessage.includes('badargument') && normalizedMessage.includes('streaming api is not enabled')) {
522
680
  logger.warn('Interaction does not support streaming. Defaulting to non-streaming response.', { originalError: message })
523
681
  this._canceled = false
524
682
  this._isStreamingChannel = false
683
+ } else {
684
+ logger.error('Exception during StreamingResponse sendActivity.', { originalError: message })
525
685
  }
526
686
  }
527
687
  }
528
688
 
689
+ /**
690
+ * Updates the existing streaming activity without allowing failures to escape the queue.
691
+ */
692
+ private async updateActivity (activity: Activity): Promise<void> {
693
+ try {
694
+ await this._context.updateActivity(activity)
695
+ } catch (error) {
696
+ logger.warn('Exception during StreamingResponse updateActivity.', { originalError: (error as Error).message })
697
+ }
698
+ }
699
+
700
+ /**
701
+ * Starts M365 Copilot keep-alive and maximum-duration timers.
702
+ */
703
+ private startStreamTimers (): void {
704
+ if (!this.isM365Copilot() || !this.isStreamingChannel || this._streamStartedAt !== undefined) {
705
+ return
706
+ }
707
+
708
+ this._streamStartedAt = Date.now()
709
+ this.scheduleKeepAlive()
710
+ this._streamTimeoutTimer = setTimeout(() => {
711
+ this.handleM365StreamTimeout().catch(error => {
712
+ logger.error('Error handling the M365 Copilot streaming timeout.', { originalError: (error as Error).message })
713
+ })
714
+ }, m365StreamingTimeoutMs)
715
+ }
716
+
717
+ /**
718
+ * Schedules an informative update before M365 Copilot's idle timeout.
719
+ */
720
+ private scheduleKeepAlive (): void {
721
+ if (!this.isM365Copilot() || !this.isStreamingChannel || this._ended) {
722
+ return
723
+ }
724
+
725
+ if (this._keepAliveTimer) {
726
+ clearTimeout(this._keepAliveTimer)
727
+ }
728
+
729
+ this._keepAliveTimer = setTimeout(() => {
730
+ this._keepAliveTimer = undefined
731
+ if (!this._ended && this.isStreamingChannel && this._queue.length === 0) {
732
+ this.queueInformativeUpdate(this._lastInformationalMessageSent.trim()
733
+ ? this._lastInformationalMessageSent
734
+ : this.streamingTakingTooLongMessage)
735
+ }
736
+ }, m365WorkingNoticeIntervalMs)
737
+ }
738
+
739
+ /**
740
+ * Finalizes an M365 Copilot stream that reached the channel duration limit.
741
+ */
742
+ private async handleM365StreamTimeout (): Promise<void> {
743
+ if (this._ended || this._canceled || !this.isStreamingChannel || !this.isM365Copilot()) {
744
+ return
745
+ }
746
+
747
+ logger.warn('M365 Copilot streaming response reached the maximum duration. Finalizing the stream and continuing in non-streaming mode.', {
748
+ streamId: this._streamId,
749
+ elapsedMs: this._streamStartedAt === undefined ? undefined : Date.now() - this._streamStartedAt
750
+ })
751
+
752
+ const timedOutActivities = this._message
753
+ ? [this.createStreamTimedOutStreamingUpdate(), this.createStreamTimedOutMessage(true)]
754
+ : [this.createStreamTimedOutMessage(true)]
755
+
756
+ this._streamTimeoutNotificationSent = true
757
+ this._isStreamingChannel = false
758
+ this._queue = []
759
+ this._chunkQueued = false
760
+ this.clearStreamTimers()
761
+
762
+ // Queue these behind any in-flight send. Clearing pending activities and
763
+ // using the same drain prevents out-of-order stream sequences.
764
+ for (const activity of timedOutActivities) {
765
+ this.queueActivity(() => activity)
766
+ }
767
+
768
+ await this.waitForQueue()
769
+ }
770
+
771
+ /**
772
+ * Stops active channel timeout timers.
773
+ */
774
+ private clearStreamTimers (): void {
775
+ if (this._keepAliveTimer) {
776
+ clearTimeout(this._keepAliveTimer)
777
+ this._keepAliveTimer = undefined
778
+ }
779
+ if (this._streamTimeoutTimer) {
780
+ clearTimeout(this._streamTimeoutTimer)
781
+ this._streamTimeoutTimer = undefined
782
+ }
783
+ }
784
+
785
+ private isM365Copilot (): boolean {
786
+ return this._context.activity.channelId?.toLocaleLowerCase() === Channels.M365Copilot.toLocaleLowerCase()
787
+ }
788
+
529
789
  /**
530
790
  * Loads default values for the streaming response.
531
791
  */
@@ -537,14 +797,8 @@ export class StreamingResponse {
537
797
  if (activity.deliveryMode === DeliveryModes.ExpectReplies) {
538
798
  this._isStreamingChannel = false
539
799
  } else if (Channels.Msteams === activity.channelIdChannel) {
540
- if (activity.isAgenticRequest()) {
541
- // Agentic requests do not support streaming responses at this time.
542
- // TODO: Enable streaming for agentic requests when supported.
543
- this._isStreamingChannel = false
544
- } else {
545
- this._isStreamingChannel = true
546
- this._delayInMs = 1000
547
- }
800
+ this._isStreamingChannel = true
801
+ this._delayInMs = 1000
548
802
  } else if (Channels.Webchat === activity.channelId || Channels.Directline === activity.channelId || Channels.Emulator === activity.channelId) {
549
803
  this._isStreamingChannel = true
550
804
  this._delayInMs = 500
@@ -11,6 +11,7 @@ import { TurnContext } from '../turnContext'
11
11
  import { TurnState } from './turnState'
12
12
  import { HttpClient } from '../httpClient'
13
13
  import { z } from 'zod'
14
+ import { createOutboundHostValidator, OutboundUrlPolicy } from '../outboundHostValidator'
14
15
 
15
16
  const logger = debug('agents:M365AttachmentDownloader')
16
17
 
@@ -20,10 +21,12 @@ const logger = debug('agents:M365AttachmentDownloader')
20
21
  export class M365AttachmentDownloader<TState extends TurnState = TurnState> implements InputFileDownloader<TState> {
21
22
  private _httpClient: HttpClient
22
23
  private _stateKey: string
24
+ private readonly _hostValidator: OutboundUrlPolicy
23
25
 
24
- public constructor (stateKey: string = 'inputFiles') {
26
+ public constructor (stateKey: string = 'inputFiles', outboundHostValidator?: OutboundUrlPolicy) {
25
27
  this._httpClient = new HttpClient()
26
28
  this._stateKey = stateKey
29
+ this._hostValidator = outboundHostValidator ?? createOutboundHostValidator()
27
30
  }
28
31
 
29
32
  /**
@@ -69,6 +72,13 @@ export class M365AttachmentDownloader<TState extends TurnState = TurnState> impl
69
72
  const contentSchema = z.object({ downloadUrl: z.string().url() })
70
73
  const parsed = contentSchema.safeParse(attachment.content)
71
74
  const downloadUrl = parsed.success ? parsed.data.downloadUrl : attachment.contentUrl
75
+ if (!this._hostValidator.isAllowed(downloadUrl)) {
76
+ let host = '[invalid-url]'
77
+ try { host = new URL(downloadUrl).hostname } catch { /* invalid */ }
78
+ logger.warn(`Attachment downloadUrl host is not in the configured allowed hosts. Host='${host}'`)
79
+ return undefined
80
+ }
81
+
72
82
  const response = await this._httpClient.get(downloadUrl, { responseType: 'arraybuffer' })
73
83
 
74
84
  const content = Buffer.from(response.data as ArrayBuffer)
@@ -8,6 +8,7 @@ import { loadEnvSettings, AuthConfiguration, envParser, envParserUtils, LoadEnv,
8
8
 
9
9
  export { type AuthConfiguration, type ConnectionSettings, type ConnectionSettingsBase, type MsalConnectionSettings, type SidecarConnectionSettings, AuthType, resolveAuthority, type ConnectionMapItem, resolveAuthType } from './settings'
10
10
  import { prune } from '../utils'
11
+ import { parseBooleanEnv } from '../utils/env'
11
12
  import { ExceptionHelper } from '@microsoft/agents-activity'
12
13
  import { Errors } from '../errorHelper'
13
14
 
@@ -28,6 +29,7 @@ function summarizeAuthConfiguration (authConfig: AuthConfiguration) {
28
29
  authorityEndpoint: config.authorityEndpoint ? redactUrl(config.authorityEndpoint) : undefined,
29
30
  scopes: (config.scopes ? redactScopes(config.scopes) : undefined) as any,
30
31
  issuers: config.issuers?.map(redactUrl).filter(e => e !== undefined),
32
+ validateIssuer: config.validateIssuer,
31
33
  federatedClientId: redactString(config.federatedClientId, true),
32
34
  certPemFile: redactString(config.certPemFile),
33
35
  certKeyFile: redactString(config.certKeyFile),
@@ -40,6 +42,7 @@ function summarizeAuthConfiguration (authConfig: AuthConfiguration) {
40
42
  alternateBlueprintConnectionName: undefined, // Alias of altBlueprintConnectionName, avoid logging duplicate info
41
43
  azureRegion: config.azureRegion,
42
44
  sendX5C: config.sendX5C,
45
+ msalRetryCount: config.msalRetryCount,
43
46
  sidecarBaseUrl: config.sidecarBaseUrl ? redactUrl(config.sidecarBaseUrl) : undefined,
44
47
  serviceName: config.serviceName,
45
48
  blueprintServiceName: config.blueprintServiceName,
@@ -96,6 +99,10 @@ const connectionsEnv = {
96
99
  idpmResource: envParserUtils.bypass,
97
100
  azureRegion: envParserUtils.bypass,
98
101
  sendX5C: (value) => ({ value: value === 'true' }),
102
+ msalRetryCount: (value) => {
103
+ const n = parseInt(value, 10)
104
+ return { value: Number.isFinite(n) && n >= 0 ? n : undefined }
105
+ },
99
106
  sidecarBaseUrl: envParserUtils.bypass,
100
107
  serviceName: envParserUtils.bypass,
101
108
  blueprintServiceName: envParserUtils.bypass,
@@ -114,6 +121,7 @@ const connectionsEnv = {
114
121
  }
115
122
  return { value: value.split(/\s+/).filter(Boolean) }
116
123
  },
124
+ validateIssuer: (value) => ({ value: parseBooleanEnv(value) }),
117
125
  }),
118
126
  default (connections?: AuthConfiguration['connections'], connectionsMap?: AuthConfiguration['connectionsMap']) {
119
127
  const conn = connections ?? this.connections
@@ -230,6 +238,7 @@ const legacyBotFrameworkEnv = {
230
238
  WIDAssertionFile: envParserUtils.redirect(connectionsEnv.parser, 'WIDAssertionFile'),
231
239
  azureRegion: envParserUtils.redirect(connectionsEnv.parser, 'azureRegion'),
232
240
  sendX5C: envParserUtils.redirect(connectionsEnv.parser, 'sendX5C'),
241
+ msalRetryCount: envParserUtils.redirect(connectionsEnv.parser, 'msalRetryCount'),
233
242
  authType: envParserUtils.redirect(connectionsEnv.parser, 'authType'),
234
243
  federatedTokenFile: envParserUtils.redirect(connectionsEnv.parser, 'federatedTokenFile'),
235
244
  idpmResource: envParserUtils.redirect(connectionsEnv.parser, 'idpmResource'),
@@ -239,6 +248,7 @@ const legacyBotFrameworkEnv = {
239
248
  bypassLocalNetworkRestriction: envParserUtils.redirect(connectionsEnv.parser, 'bypassLocalNetworkRestriction'),
240
249
  requestTimeout: envParserUtils.redirect(connectionsEnv.parser, 'requestTimeout'),
241
250
  retryCount: envParserUtils.redirect(connectionsEnv.parser, 'retryCount'),
251
+ validateIssuer: envParserUtils.redirect(connectionsEnv.parser, 'validateIssuer'),
242
252
  }),
243
253
  process (env: LoadEnv) {
244
254
  return legacyPrefixEnv.process.call(this, env)
@@ -265,6 +275,7 @@ const legacyPrefixEnv = {
265
275
  WIDAssertionFile: envParserUtils.redirect(connectionsEnv.parser, 'WIDAssertionFile'),
266
276
  azureRegion: envParserUtils.redirect(connectionsEnv.parser, 'azureRegion'),
267
277
  sendX5C: envParserUtils.redirect(connectionsEnv.parser, 'sendX5C'),
278
+ msalRetryCount: envParserUtils.redirect(connectionsEnv.parser, 'msalRetryCount'),
268
279
  authType: envParserUtils.redirect(connectionsEnv.parser, 'authType'),
269
280
  federatedTokenFile: envParserUtils.redirect(connectionsEnv.parser, 'federatedTokenFile'),
270
281
  idpmResource: envParserUtils.redirect(connectionsEnv.parser, 'idpmResource'),
@@ -274,6 +285,7 @@ const legacyPrefixEnv = {
274
285
  bypassLocalNetworkRestriction: envParserUtils.redirect(connectionsEnv.parser, 'bypassLocalNetworkRestriction'),
275
286
  requestTimeout: envParserUtils.redirect(connectionsEnv.parser, 'requestTimeout'),
276
287
  retryCount: envParserUtils.redirect(connectionsEnv.parser, 'retryCount'),
288
+ validateIssuer: envParserUtils.redirect(connectionsEnv.parser, 'validateIssuer'),
277
289
  }),
278
290
  process (env: LoadEnv, prefix?: string) {
279
291
  const settings: Partial<AuthConfiguration> = {}
package/src/auth/index.ts CHANGED
@@ -1,8 +1,9 @@
1
1
  export * from './authConfiguration'
2
2
  export * from './authConstants'
3
3
  export * from './authProvider'
4
- export * from './msal/msalTokenProvider'
4
+ export * from './connections'
5
5
  export * from './request'
6
+ export * from './msal/msalTokenProvider'
6
7
  export * from './msal/msalTokenCredential'
7
8
  export * from './connectionManager'
8
9
  export * from './connections'