@microsoft/agents-hosting 1.7.0-beta.1.g8bcd4f11a7 → 1.7.0-beta.11.gb5d3e8c751

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 (110) hide show
  1. package/README.md +25 -0
  2. package/dist/package.json +3 -3
  3. package/dist/src/agent-client/agentClient.d.ts +4 -0
  4. package/dist/src/agent-client/agentClient.js +32 -18
  5. package/dist/src/agent-client/agentClient.js.map +1 -1
  6. package/dist/src/agent-client/agentResponseHandler.d.ts +27 -6
  7. package/dist/src/agent-client/agentResponseHandler.js +18 -55
  8. package/dist/src/agent-client/agentResponseHandler.js.map +1 -1
  9. package/dist/src/agent-client/createAgentResponseHandler.d.ts +48 -0
  10. package/dist/src/agent-client/createAgentResponseHandler.js +134 -0
  11. package/dist/src/agent-client/createAgentResponseHandler.js.map +1 -0
  12. package/dist/src/agent-client/index.d.ts +1 -0
  13. package/dist/src/agent-client/index.js +1 -0
  14. package/dist/src/agent-client/index.js.map +1 -1
  15. package/dist/src/app/adaptiveCards/activityValueParsers.js +2 -2
  16. package/dist/src/app/adaptiveCards/activityValueParsers.js.map +1 -1
  17. package/dist/src/app/adaptiveCards/adaptiveCardsActions.js +13 -12
  18. package/dist/src/app/adaptiveCards/adaptiveCardsActions.js.map +1 -1
  19. package/dist/src/app/agentApplication.d.ts +4 -0
  20. package/dist/src/app/agentApplication.js +20 -8
  21. package/dist/src/app/agentApplication.js.map +1 -1
  22. package/dist/src/app/streaming/streamingResponse.js +2 -2
  23. package/dist/src/app/streaming/streamingResponse.js.map +1 -1
  24. package/dist/src/app/turnState.d.ts +0 -1
  25. package/dist/src/app/turnState.js +15 -14
  26. package/dist/src/app/turnState.js.map +1 -1
  27. package/dist/src/app/turnStateProperty.js +4 -2
  28. package/dist/src/app/turnStateProperty.js.map +1 -1
  29. package/dist/src/auth/authConfiguration.js +5 -3
  30. package/dist/src/auth/authConfiguration.js.map +1 -1
  31. package/dist/src/auth/index.d.ts +1 -0
  32. package/dist/src/auth/index.js +1 -0
  33. package/dist/src/auth/index.js.map +1 -1
  34. package/dist/src/auth/jwt-middleware.d.ts +8 -2
  35. package/dist/src/auth/jwt-middleware.js +112 -14
  36. package/dist/src/auth/jwt-middleware.js.map +1 -1
  37. package/dist/src/auth/msal/msalTokenProvider.js +16 -16
  38. package/dist/src/auth/msal/msalTokenProvider.js.map +1 -1
  39. package/dist/src/baseAdapter.js +3 -1
  40. package/dist/src/baseAdapter.js.map +1 -1
  41. package/dist/src/cloudAdapter.d.ts +15 -2
  42. package/dist/src/cloudAdapter.js +28 -10
  43. package/dist/src/cloudAdapter.js.map +1 -1
  44. package/dist/src/createCloudAdapter.d.ts +40 -0
  45. package/dist/src/createCloudAdapter.js +44 -0
  46. package/dist/src/createCloudAdapter.js.map +1 -0
  47. package/dist/src/errorHelper.js +206 -79
  48. package/dist/src/errorHelper.js.map +1 -1
  49. package/dist/src/headerPropagation.js +3 -1
  50. package/dist/src/headerPropagation.js.map +1 -1
  51. package/dist/src/index.d.ts +2 -0
  52. package/dist/src/index.js +2 -0
  53. package/dist/src/index.js.map +1 -1
  54. package/dist/src/interfaces/index.d.ts +5 -0
  55. package/dist/src/interfaces/index.js +22 -0
  56. package/dist/src/interfaces/index.js.map +1 -0
  57. package/dist/src/interfaces/webResponse.d.ts +41 -0
  58. package/dist/src/interfaces/webResponse.js +7 -0
  59. package/dist/src/interfaces/webResponse.js.map +1 -0
  60. package/dist/src/middlewareSet.d.ts +0 -1
  61. package/dist/src/middlewareSet.js +4 -1
  62. package/dist/src/middlewareSet.js.map +1 -1
  63. package/dist/src/oauth/userTokenClient.js +2 -1
  64. package/dist/src/oauth/userTokenClient.js.map +1 -1
  65. package/dist/src/state/conversationState.js +4 -2
  66. package/dist/src/state/conversationState.js.map +1 -1
  67. package/dist/src/state/userState.js +4 -2
  68. package/dist/src/state/userState.js.map +1 -1
  69. package/dist/src/storage/memoryStorage.js +5 -3
  70. package/dist/src/storage/memoryStorage.js.map +1 -1
  71. package/dist/src/transcript/consoleTranscriptLogger.js +3 -1
  72. package/dist/src/transcript/consoleTranscriptLogger.js.map +1 -1
  73. package/dist/src/transcript/fileTranscriptLogger.js +5 -4
  74. package/dist/src/transcript/fileTranscriptLogger.js.map +1 -1
  75. package/dist/src/transcript/transcriptLoggerMiddleware.js +2 -1
  76. package/dist/src/transcript/transcriptLoggerMiddleware.js.map +1 -1
  77. package/dist/src/turnContext.js +2 -1
  78. package/dist/src/turnContext.js.map +1 -1
  79. package/package.json +3 -3
  80. package/src/agent-client/agentClient.ts +37 -21
  81. package/src/agent-client/agentResponseHandler.ts +45 -67
  82. package/src/agent-client/createAgentResponseHandler.ts +189 -0
  83. package/src/agent-client/index.ts +1 -0
  84. package/src/app/adaptiveCards/activityValueParsers.ts +3 -3
  85. package/src/app/adaptiveCards/adaptiveCardsActions.ts +5 -5
  86. package/src/app/agentApplication.ts +23 -16
  87. package/src/app/streaming/streamingResponse.ts +2 -2
  88. package/src/app/turnState.ts +15 -14
  89. package/src/app/turnStateProperty.ts +4 -2
  90. package/src/auth/authConfiguration.ts +5 -3
  91. package/src/auth/index.ts +1 -0
  92. package/src/auth/jwt-middleware.ts +127 -17
  93. package/src/auth/msal/msalTokenProvider.ts +16 -16
  94. package/src/baseAdapter.ts +3 -2
  95. package/src/cloudAdapter.ts +33 -12
  96. package/src/createCloudAdapter.ts +56 -0
  97. package/src/errorHelper.ts +244 -89
  98. package/src/headerPropagation.ts +4 -1
  99. package/src/index.ts +2 -0
  100. package/src/interfaces/index.ts +6 -0
  101. package/src/interfaces/webResponse.ts +44 -0
  102. package/src/middlewareSet.ts +3 -1
  103. package/src/oauth/userTokenClient.ts +3 -2
  104. package/src/state/conversationState.ts +4 -3
  105. package/src/state/userState.ts +4 -3
  106. package/src/storage/memoryStorage.ts +5 -3
  107. package/src/transcript/consoleTranscriptLogger.ts +3 -2
  108. package/src/transcript/fileTranscriptLogger.ts +6 -5
  109. package/src/transcript/transcriptLoggerMiddleware.ts +3 -2
  110. package/src/turnContext.ts +3 -2
@@ -8,6 +8,8 @@ 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 { ExceptionHelper } from '@microsoft/agents-activity'
12
+ import { Errors } from '../errorHelper'
11
13
 
12
14
  const logger = debug('agents:authConfiguration')
13
15
 
@@ -118,12 +120,12 @@ const connectionsEnv = {
118
120
  const map = connectionsMap ?? connectionsMapEnv.connectionsMap
119
121
  const name = map?.find((item) => item.serviceUrl === '*')?.connection
120
122
  if (!name) {
121
- throw new Error('No default connection found in environment connections.')
123
+ throw ExceptionHelper.generateException(Error, Errors.NoDefaultConnectionFound)
122
124
  }
123
125
 
124
126
  const connection = conn?.get(name ?? '')
125
127
  if (!connection) {
126
- throw new Error(`Connection "${name}" not found in environment connections.`)
128
+ throw ExceptionHelper.generateException(Error, Errors.ConnectionNotFoundInEnvironment, undefined, { connectionName: name })
127
129
  }
128
130
 
129
131
  return applyDefaultSettings({ ...connection, connections: conn, connectionsMap: map })
@@ -359,7 +361,7 @@ export const loadAuthConfigFromEnv = (cnxName?: string): AuthConfiguration => {
359
361
  // No connections provided, we need to populate the connections map with the old config settings
360
362
  const result = applyDefaultSettings(cnxName?.trim() ? legacyPrefixEnv.process(globalEnv.env, cnxName) : globalEnv.legacyPrefixSettings)
361
363
  if (cnxName && !result.clientId) {
362
- throw new Error(`ClientId not found for connection: ${cnxName}`)
364
+ throw ExceptionHelper.generateException(Error, Errors.ClientIdNotFoundForConnection, undefined, { connectionName: cnxName })
363
365
  }
364
366
 
365
367
  logger.info('Auth settings loaded from environment', {
package/src/auth/index.ts CHANGED
@@ -5,5 +5,6 @@ export * from './msal/msalTokenProvider'
5
5
  export * from './request'
6
6
  export * from './msal/msalTokenCredential'
7
7
  export * from './connectionManager'
8
+ export * from './connections'
8
9
  export * from './msal/msalConnectionManager'
9
10
  export * from './sidecar/sidecarAuthProvider'
@@ -4,8 +4,10 @@
4
4
  */
5
5
 
6
6
  import { AuthConfiguration, resolveAuthority } from './authConfiguration'
7
- import { Response, NextFunction } from 'express'
8
7
  import { Request } from './request'
8
+ import { WebResponse, NextFunction } from '../interfaces/webResponse'
9
+ import { Errors } from '../errorHelper'
10
+ import { ExceptionHelper } from '@microsoft/agents-activity'
9
11
  import jwksRsa, { JwksClient, SigningKey } from 'jwks-rsa'
10
12
  import jwt, { JwtHeader, JwtPayload, SignCallback, GetPublicKeyOrSecret } from 'jsonwebtoken'
11
13
  import { debug } from '@microsoft/agents-telemetry'
@@ -13,6 +15,25 @@ import { debug } from '@microsoft/agents-telemetry'
13
15
  const logger = debug('agents:jwt-middleware')
14
16
  const jwksClients = new Map<string, JwksClient>()
15
17
  const maxJwksClients = 100
18
+ const authorizedAudience = Symbol('authorizedAudience')
19
+
20
+ interface AuthorizedRequest extends Request {
21
+ [authorizedAudience]?: string
22
+ }
23
+
24
+ interface VerifiedToken {
25
+ payload: JwtPayload
26
+ audience: string
27
+ }
28
+
29
+ /**
30
+ * Gets the configured audience selected while authorizing a request.
31
+ * @param req The authorized request.
32
+ * @returns The matched configured client ID, or `undefined` for anonymous requests.
33
+ */
34
+ export function getAuthorizedAudience (req: Request): string | undefined {
35
+ return (req as AuthorizedRequest)[authorizedAudience]
36
+ }
16
37
 
17
38
  /**
18
39
  * Clears process-wide JWKS clients.
@@ -57,24 +78,30 @@ export function getJwksClient (jwksUri: string): JwksClient {
57
78
  * Verifies the JWT token.
58
79
  * @param raw The raw JWT token.
59
80
  * @param config The authentication configuration.
60
- * @returns A promise that resolves to the JWT payload.
81
+ * @returns A promise that resolves to the verified payload and matched configured audience.
61
82
  */
62
- const verifyToken = async (raw: string, config: AuthConfiguration): Promise<JwtPayload> => {
83
+ const verifyToken = async (raw: string, config: AuthConfiguration): Promise<VerifiedToken> => {
63
84
  const payload = jwt.decode(raw) as JwtPayload
64
85
  logger.debug('jwt.decode ', JSON.stringify(payload))
65
86
 
66
87
  if (!payload) {
67
- throw new Error('invalid token')
88
+ throw ExceptionHelper.generateException(Error, Errors.InvalidJwtToken)
68
89
  }
69
- const audience = payload.aud
90
+ const audiences = Array.isArray(payload.aud)
91
+ ? payload.aud
92
+ : typeof payload.aud === 'string'
93
+ ? [payload.aud]
94
+ : []
70
95
 
71
96
  const matchingEntry = config.connections && config.connections.size > 0
72
- ? [...config.connections.entries()].find(([_, configuration]) => configuration.clientId === audience)
97
+ ? [...config.connections.entries()].find(([_, configuration]) =>
98
+ typeof configuration.clientId === 'string' && audiences.includes(configuration.clientId)
99
+ )
73
100
  : undefined
74
101
 
75
102
  if (!matchingEntry) {
76
- const err = new Error('Audience mismatch')
77
- logger.error(err.message, audience)
103
+ const err = ExceptionHelper.generateException(Error, Errors.JwtAudienceMismatch)
104
+ logger.error(err.message, audiences)
78
105
  throw err
79
106
  }
80
107
 
@@ -107,7 +134,7 @@ const verifyToken = async (raw: string, config: AuthConfiguration): Promise<JwtP
107
134
  clockTolerance: 300
108
135
  }
109
136
 
110
- return await new Promise((resolve, reject) => {
137
+ const verifiedPayload = await new Promise<JwtPayload>((resolve, reject) => {
111
138
  jwt.verify(raw, getKey, verifyOptions, (err, user) => {
112
139
  if (err) {
113
140
  logger.error('jwt.verify ', JSON.stringify(err))
@@ -117,6 +144,75 @@ const verifyToken = async (raw: string, config: AuthConfiguration): Promise<JwtP
117
144
  resolve(user as JwtPayload)
118
145
  })
119
146
  })
147
+ return {
148
+ payload: verifiedPayload,
149
+ audience: authConfig.clientId!
150
+ }
151
+ }
152
+
153
+ /**
154
+ * Determines whether an `Authorization` header value is present (non-empty).
155
+ *
156
+ * The {@link Request} contract allows `string | string[] | undefined` because
157
+ * different web frameworks surface headers differently. This treats an empty
158
+ * string or empty array as "absent" so callers can distinguish a missing header
159
+ * (anonymous / 401) from a present-but-malformed one (always 401).
160
+ * @param authorization The raw `Authorization` header value.
161
+ * @returns `true` when a non-empty header value is present.
162
+ */
163
+ function hasAuthorizationHeader (authorization: string | string[] | undefined): boolean {
164
+ if (Array.isArray(authorization)) {
165
+ return authorization.some((value) => typeof value === 'string' && value.trim().length > 0)
166
+ }
167
+ return typeof authorization === 'string' && authorization.trim().length > 0
168
+ }
169
+
170
+ /**
171
+ * Parses a single `Authorization` header value and returns its bearer token.
172
+ *
173
+ * Validates the `Bearer <token>` scheme, returning `undefined` for anything
174
+ * malformed (wrong scheme, missing token, or extra whitespace-delimited parts).
175
+ * @param headerValue A single `Authorization` header value.
176
+ * @returns The bearer token, or `undefined` if the value is absent or malformed.
177
+ */
178
+ function parseBearerValue (headerValue: string | undefined): string | undefined {
179
+ if (typeof headerValue !== 'string') {
180
+ return undefined
181
+ }
182
+ const parts = headerValue.trim().split(/\s+/)
183
+ if (parts.length !== 2) {
184
+ return undefined
185
+ }
186
+ const [scheme, token] = parts
187
+ if (scheme.toLowerCase() !== 'bearer' || !token) {
188
+ return undefined
189
+ }
190
+ return token
191
+ }
192
+
193
+ /**
194
+ * Extracts the bearer token from a raw `Authorization` header value.
195
+ *
196
+ * On Node's core HTTP parser (which both Express and Fastify use) `Authorization`
197
+ * is always surfaced as a single string holding the *first* header line —
198
+ * duplicate `Authorization` headers are discarded, not comma-joined. The
199
+ * `string[]` case is handled only because the {@link Request} contract permits
200
+ * it for non-Node frameworks; when an array is supplied, every entry is inspected
201
+ * and the first valid `Bearer <token>` is returned (the bearer value is not
202
+ * assumed to be first). Returns `undefined` for anything malformed so the caller
203
+ * can emit a consistent 401 instead of throwing.
204
+ * @param authorization The raw `Authorization` header value.
205
+ * @returns The bearer token, or `undefined` if the header is absent or malformed.
206
+ */
207
+ function extractBearerToken (authorization: string | string[] | undefined): string | undefined {
208
+ const values = Array.isArray(authorization) ? authorization : [authorization]
209
+ for (const value of values) {
210
+ const token = parseBearerValue(value)
211
+ if (token) {
212
+ return token
213
+ }
214
+ }
215
+ return undefined
120
216
  }
121
217
 
122
218
  /**
@@ -125,7 +221,7 @@ const verifyToken = async (raw: string, config: AuthConfiguration): Promise<JwtP
125
221
  * @returns An Express middleware function.
126
222
  */
127
223
  export const authorizeJWT = (authConfig: AuthConfiguration) => {
128
- return async function (req: Request, res: Response, next: NextFunction) {
224
+ return async function (req: Request, res: WebResponse, next: NextFunction) {
129
225
  let failed = false
130
226
  logger.debug('authorizing jwt')
131
227
  if (req.method !== 'POST' && req.method !== 'GET') {
@@ -133,18 +229,32 @@ export const authorizeJWT = (authConfig: AuthConfiguration) => {
133
229
  logger.warn('Method not allowed', req.method)
134
230
  res.status(405).send({ 'jwt-auth-error': 'Method not allowed' })
135
231
  } else {
136
- const authHeader = req.headers.authorization as string
137
- if (authHeader) {
138
- const token: string = authHeader.split(' ')[1] // Extract the token from the Bearer string
232
+ const token = extractBearerToken(req.headers.authorization)
233
+ if (token) {
139
234
  try {
140
- const user = await verifyToken(token, authConfig)
141
- logger.debug('token verified for ', user)
142
- req.user = user
235
+ const verifiedToken = await verifyToken(token, authConfig)
236
+ logger.debug('token verified for ', verifiedToken.payload)
237
+ req.user = verifiedToken.payload
238
+ const authorizedRequest = req as AuthorizedRequest
239
+ authorizedRequest[authorizedAudience] = verifiedToken.audience
143
240
  } catch (err: Error | any) {
144
241
  failed = true
145
242
  logger.error(err)
146
- res.status(401).send({ 'jwt-auth-error': err.message })
243
+ // Emit only the human-readable description rather than the
244
+ // ExceptionHelper-formatted "[code] - description - helplink" string,
245
+ // so the wire format does not leak internal error codes or help links.
246
+ // Fall back to a stable string so a thrown non-Error (no description/
247
+ // message) never serializes to an empty `{}` and drops the detail.
248
+ const wireMessage: string = err?.description ?? err?.message ?? 'unauthorized'
249
+ res.status(401).send({ 'jwt-auth-error': wireMessage })
147
250
  }
251
+ } else if (hasAuthorizationHeader(req.headers.authorization)) {
252
+ // Header is present but not a well-formed `Bearer <token>` (e.g. wrong
253
+ // scheme, missing token, or an array value). Respond with a consistent
254
+ // 401 rather than letting malformed input throw before authorization.
255
+ failed = true
256
+ logger.warn('malformed authorization header')
257
+ res.status(401).send({ 'jwt-auth-error': 'invalid authorization header' })
148
258
  } else {
149
259
  if (!authConfig.clientId && process.env.NODE_ENV !== 'production') {
150
260
  logger.info('using anonymous auth')
@@ -284,7 +284,7 @@ export class MsalTokenProvider implements AuthProvider {
284
284
  if (typeof authConfigOrScope === 'string') {
285
285
  // Called as getAccessToken(scope)
286
286
  if (!this.connectionSettings) {
287
- throw new Error('Connection settings must be provided to constructor when calling getAccessToken(scope)')
287
+ throw ExceptionHelper.generateException(Error, Errors.ConnectionSettingsRequiredForGetAccessTokenScope)
288
288
  }
289
289
  authConfig = this.connectionSettings
290
290
  actualScope = authConfigOrScope
@@ -378,7 +378,7 @@ export class MsalTokenProvider implements AuthProvider {
378
378
  if (Array.isArray(authConfigOrScopes)) {
379
379
  // Called as acquireTokenOnBehalfOf(scopes, oboAssertion)
380
380
  if (!this.connectionSettings) {
381
- throw new Error('Connection settings must be provided to constructor when calling acquireTokenOnBehalfOf(scopes, oboAssertion)')
381
+ throw ExceptionHelper.generateException(Error, Errors.ConnectionSettingsRequiredForAcquireTokenOnBehalfOf)
382
382
  }
383
383
  authConfig = this.connectionSettings
384
384
  actualScopes = authConfigOrScopes
@@ -399,7 +399,7 @@ export class MsalTokenProvider implements AuthProvider {
399
399
  scopes: actualScopes
400
400
  })
401
401
  if (!token?.accessToken) {
402
- throw new Error('Failed to acquire token on behalf of user')
402
+ throw ExceptionHelper.generateException(Error, Errors.FailedToAcquireTokenOnBehalfOf)
403
403
  }
404
404
 
405
405
  return token.accessToken
@@ -412,7 +412,7 @@ export class MsalTokenProvider implements AuthProvider {
412
412
  record({ agenticInstanceId: agentAppInstanceId })
413
413
 
414
414
  if (!this.connectionSettings) {
415
- throw new Error('Connection settings must be provided when calling getAgenticInstanceToken')
415
+ throw ExceptionHelper.generateException(Error, Errors.ConnectionSettingsRequiredForGetAgenticInstanceToken)
416
416
  }
417
417
 
418
418
  const instanceTokenCacheKey = MsalTokenProvider.cacheKey(
@@ -444,7 +444,7 @@ export class MsalTokenProvider implements AuthProvider {
444
444
  })
445
445
 
446
446
  if (!token?.accessToken) {
447
- throw new Error(`Failed to acquire instance token for agent instance: ${agentAppInstanceId}`)
447
+ throw ExceptionHelper.generateException(Error, Errors.FailedToAcquireInstanceTokenForAgentInstance, undefined, { agentAppInstanceId })
448
448
  }
449
449
 
450
450
  this.cacheAgenticAuthenticationResult(instanceTokenCacheKey, token)
@@ -494,7 +494,7 @@ export class MsalTokenProvider implements AuthProvider {
494
494
  */
495
495
  private async acquireTokenForAgenticScenarios (tenantId: string, clientId: string, clientAssertion: string | undefined, scopes: string[], tokenBodyParameters: { [key: string]: any }): Promise<string | null> {
496
496
  if (!this.connectionSettings) {
497
- throw new Error('Connection settings must be provided when calling getAgenticInstanceToken')
497
+ throw ExceptionHelper.generateException(Error, Errors.ConnectionSettingsRequiredForGetAgenticInstanceToken)
498
498
  }
499
499
 
500
500
  logger.debug('acquireTokenForAgenticScenarios clientId=%s tenantId=%s scopes=%o grant_type=%s', clientId, tenantId, scopes, tokenBodyParameters.grant_type)
@@ -585,7 +585,7 @@ export class MsalTokenProvider implements AuthProvider {
585
585
  })
586
586
 
587
587
  if (!token) {
588
- throw new Error(`Failed to acquire instance token for user token: ${agentAppInstanceId}`)
588
+ throw ExceptionHelper.generateException(Error, Errors.FailedToAcquireInstanceTokenForUserToken, undefined, { agentAppInstanceId })
589
589
  }
590
590
 
591
591
  return token
@@ -594,7 +594,7 @@ export class MsalTokenProvider implements AuthProvider {
594
594
 
595
595
  public async getAgenticApplicationToken (tenantId: string, agentAppInstanceId: string): Promise<string> {
596
596
  if (!this.connectionSettings?.clientId) {
597
- throw new Error('Connection settings must be provided when calling getAgenticApplicationToken')
597
+ throw ExceptionHelper.generateException(Error, Errors.ConnectionSettingsRequiredForGetAgenticApplicationToken)
598
598
  }
599
599
  logger.debug('getAgenticApplicationToken clientId=%s tenantId=%s agentAppInstanceId=%s', this.connectionSettings.clientId, tenantId, agentAppInstanceId)
600
600
 
@@ -605,7 +605,7 @@ export class MsalTokenProvider implements AuthProvider {
605
605
  if (!this.connectionSettings.idpmResource) {
606
606
  resource = 'api://AzureAdTokenExchange/.default'
607
607
  } else if (!URL.canParse(this.connectionSettings.idpmResource)) {
608
- throw new Error('idpmResource must be a valid absolute URI')
608
+ throw ExceptionHelper.generateException(Error, Errors.IdpmResourceAbsoluteUriRequired)
609
609
  } else {
610
610
  resource = this.connectionSettings.idpmResource
611
611
  }
@@ -617,7 +617,7 @@ export class MsalTokenProvider implements AuthProvider {
617
617
  })
618
618
  const tokenResult = await msiApp.acquireToken({ resource })
619
619
  if (!tokenResult?.accessToken) {
620
- throw new Error(`Failed to acquire token via IdentityProxyManager for agent instance: ${agentAppInstanceId}`)
620
+ throw ExceptionHelper.generateException(Error, Errors.FailedToAcquireTokenViaIdentityProxyManagerForAgentInstance, undefined, { agentAppInstanceId })
621
621
  }
622
622
  logger.debug('getAgenticApplicationToken via IdentityProxyManager clientId=%s resource=%s', this.connectionSettings.clientId, resource)
623
623
  return tokenResult.accessToken
@@ -655,7 +655,7 @@ export class MsalTokenProvider implements AuthProvider {
655
655
  })
656
656
 
657
657
  if (!token) {
658
- throw new Error(`Failed to acquire token for agent instance: ${agentAppInstanceId}`)
658
+ throw ExceptionHelper.generateException(Error, Errors.FailedToAcquireInstanceTokenForAgentInstance, undefined, { agentAppInstanceId })
659
659
  }
660
660
 
661
661
  return token
@@ -780,7 +780,7 @@ export class MsalTokenProvider implements AuthProvider {
780
780
  azureRegion: authConfig.azureRegion
781
781
  })
782
782
  if (!token?.accessToken) {
783
- throw new Error('Failed to acquire token using certificate')
783
+ throw ExceptionHelper.generateException(Error, Errors.FailedToAcquireTokenUsingCertificate)
784
784
  }
785
785
  return token.accessToken
786
786
  }
@@ -799,7 +799,7 @@ export class MsalTokenProvider implements AuthProvider {
799
799
  azureRegion: authConfig.azureRegion
800
800
  })
801
801
  if (!token?.accessToken) {
802
- throw new Error('Failed to acquire token using client secret')
802
+ throw ExceptionHelper.generateException(Error, Errors.FailedToAcquireTokenUsingClientSecret)
803
803
  }
804
804
  return token.accessToken
805
805
  }
@@ -824,7 +824,7 @@ export class MsalTokenProvider implements AuthProvider {
824
824
  const token = await cca.acquireTokenByClientCredential({ scopes, azureRegion: authConfig.azureRegion })
825
825
  logger.debug('got token using FIC client assertion')
826
826
  if (!token?.accessToken) {
827
- throw new Error('Failed to acquire token using FIC client assertion')
827
+ throw ExceptionHelper.generateException(Error, Errors.FailedToAcquireTokenUsingFICClientAssertion)
828
828
  }
829
829
  return token.accessToken
830
830
  }
@@ -850,7 +850,7 @@ export class MsalTokenProvider implements AuthProvider {
850
850
  const token = await cca.acquireTokenByClientCredential({ scopes, azureRegion: authConfig.azureRegion })
851
851
  logger.debug('got token using WID client assertion')
852
852
  if (!token?.accessToken) {
853
- throw new Error('Failed to acquire token using WID client assertion')
853
+ throw ExceptionHelper.generateException(Error, Errors.FailedToAcquireTokenUsingWIDClientAssertion)
854
854
  }
855
855
  return token.accessToken
856
856
  }
@@ -874,7 +874,7 @@ export class MsalTokenProvider implements AuthProvider {
874
874
  })
875
875
  logger.debug('got token for FIC')
876
876
  if (!response?.accessToken) {
877
- throw new Error('Failed to acquire external token for FIC client assertion')
877
+ throw ExceptionHelper.generateException(Error, Errors.FailedToAcquireExternalTokenForFICClientAssertion)
878
878
  }
879
879
  return response.accessToken
880
880
  }
@@ -6,11 +6,12 @@
6
6
  import { Middleware, MiddlewareHandler, MiddlewareSet } from './middlewareSet'
7
7
  import { TurnContext } from './turnContext'
8
8
  import { debug } from '@microsoft/agents-telemetry'
9
- import { Activity, ConversationReference } from '@microsoft/agents-activity'
9
+ import { Activity, ConversationReference, ExceptionHelper } from '@microsoft/agents-activity'
10
10
  import { ResourceResponse } from './connector-client/resourceResponse'
11
11
  import { AttachmentData } from './connector-client/attachmentData'
12
12
  import { AttachmentInfo } from './connector-client/attachmentInfo'
13
13
  import { JwtPayload } from 'jsonwebtoken'
14
+ import { Errors } from './errorHelper'
14
15
 
15
16
  const logger = debug('agents:base-adapter')
16
17
 
@@ -202,7 +203,7 @@ export abstract class BaseAdapter {
202
203
  if (err instanceof Error) {
203
204
  await this.onTurnError(pContext.proxy, err)
204
205
  } else {
205
- throw new Error('Unknown error type: ' + err.message)
206
+ throw ExceptionHelper.generateException(Error, Errors.UnknownErrorType, undefined, { errorMessage: err.message })
206
207
  }
207
208
  } else {
208
209
  throw err
@@ -6,8 +6,8 @@
6
6
  import { AgentHandler, INVOKE_RESPONSE_KEY } from './activityHandler'
7
7
  import { BaseAdapter } from './baseAdapter'
8
8
  import { TurnContext } from './turnContext'
9
- import { Response } from 'express'
10
9
  import { Request } from './auth/request'
10
+ import { NextFunction, WebResponse } from './interfaces/webResponse'
11
11
  import { ConnectorClient } from './connector-client/connectorClient'
12
12
  import { AuthConfiguration, getAuthConfigWithDefaults } from './auth/authConfiguration'
13
13
  import { AuthProvider } from './auth/authProvider'
@@ -32,6 +32,7 @@ import { parseBooleanEnv, suggestClosest } from './utils/env'
32
32
  import { trace } from '@microsoft/agents-telemetry'
33
33
  import { AdapterTraceDefinitions } from './observability'
34
34
  import { applyAgenticHeaders } from './getProductInfo'
35
+ import { authorizeJWT } from './auth/jwt-middleware'
35
36
 
36
37
  const logger = debug('agents:cloud-adapter')
37
38
 
@@ -238,6 +239,7 @@ function truncateActivityForLog (activity: unknown, max = 1024): string {
238
239
 
239
240
  export class CloudAdapter extends BaseAdapter {
240
241
  protected readonly authConfig: AuthConfiguration
242
+ private readonly jwtMiddleware: ReturnType<typeof authorizeJWT>
241
243
  protected _agentName?: string
242
244
 
243
245
  /**
@@ -257,6 +259,7 @@ export class CloudAdapter extends BaseAdapter {
257
259
  constructor (authConfig?: AuthConfiguration, authProvider?: AuthProvider, userTokenClient?: UserTokenClient, options?: CloudAdapterOptions) {
258
260
  super()
259
261
  this.authConfig = authConfig = getAuthConfigWithDefaults(authConfig)
262
+ this.jwtMiddleware = authorizeJWT(authConfig)
260
263
  this.connectionManager = new MsalConnectionManager(undefined, undefined, authConfig)
261
264
  this._options = resolveCloudAdapterOptions(options)
262
265
 
@@ -289,7 +292,7 @@ export class CloudAdapter extends BaseAdapter {
289
292
  */
290
293
  protected resolveIfConnectorClientIsNeeded (activity: Activity): boolean {
291
294
  if (!activity) {
292
- throw new TypeError('`activity` parameter required')
295
+ throw ExceptionHelper.generateException(TypeError, Errors.ActivityParameterRequired)
293
296
  }
294
297
 
295
298
  switch (activity.deliveryMode) {
@@ -392,7 +395,7 @@ export class CloudAdapter extends BaseAdapter {
392
395
  headers
393
396
  )
394
397
  } else {
395
- throw new Error('Could not create connector client for agentic user')
398
+ throw ExceptionHelper.generateException(Error, Errors.CannotCreateConnectorClientForAgenticUser)
396
399
  }
397
400
  } else {
398
401
  // ABS tokens will not have an azp/appid so use the botframework scope.
@@ -425,6 +428,24 @@ export class CloudAdapter extends BaseAdapter {
425
428
  } as JwtPayload
426
429
  }
427
430
 
431
+ /**
432
+ * Authorizes an incoming web request using this adapter's resolved authentication configuration.
433
+ * @param req The incoming request.
434
+ * @param res The outgoing response.
435
+ * @param next Callback invoked after successful authorization.
436
+ */
437
+ public async authorizeRequest (req: Request, res: WebResponse, next: NextFunction): Promise<void> {
438
+ await this.jwtMiddleware(req, res, next)
439
+ }
440
+
441
+ /**
442
+ * Gets the client ID used by this adapter's default authentication connection.
443
+ * @returns The configured client ID, or `undefined` for anonymous development configuration.
444
+ */
445
+ public getClientId (): string | undefined {
446
+ return this.authConfig.clientId
447
+ }
448
+
428
449
  /**
429
450
  * Sets the agent name for M365 agent header propagation.
430
451
  * @param agentName The human-friendly agent name to set for header propagation.
@@ -580,7 +601,7 @@ export class CloudAdapter extends BaseAdapter {
580
601
  */
581
602
  public async process (
582
603
  request: Request,
583
- res: Response,
604
+ res: WebResponse,
584
605
  logic: (context: TurnContext) => Promise<void>,
585
606
  headerPropagation?: HeaderPropagationDefinition): Promise<void> {
586
607
  return trace(AdapterTraceDefinitions.process, async ({ record }) => {
@@ -609,7 +630,7 @@ export class CloudAdapter extends BaseAdapter {
609
630
  res.end()
610
631
  }
611
632
  if (!request.body) {
612
- throw new TypeError('`request.body` parameter required, make sure express.json() is used as middleware')
633
+ throw ExceptionHelper.generateException(TypeError, Errors.MissingRequestBody)
613
634
  }
614
635
  const incoming = normalizeIncomingActivity(request.body!)
615
636
  const activity = Activity.fromObject(incoming)
@@ -726,11 +747,11 @@ export class CloudAdapter extends BaseAdapter {
726
747
  async updateActivity (context: TurnContext, activity: Activity): Promise<ResourceResponse | void> {
727
748
  return trace(AdapterTraceDefinitions.updateActivity, async ({ record }) => {
728
749
  if (!context) {
729
- throw new TypeError('`context` parameter required')
750
+ throw ExceptionHelper.generateException(TypeError, Errors.ContextParameterRequired)
730
751
  }
731
752
 
732
753
  if (!activity) {
733
- throw new TypeError('`activity` parameter required')
754
+ throw ExceptionHelper.generateException(TypeError, Errors.ActivityParameterRequired)
734
755
  }
735
756
 
736
757
  record({ activity })
@@ -758,7 +779,7 @@ export class CloudAdapter extends BaseAdapter {
758
779
  async deleteActivity (context: TurnContext, reference: Partial<ConversationReference>): Promise<void> {
759
780
  return trace(AdapterTraceDefinitions.deleteActivity, async ({ record }) => {
760
781
  if (!context) {
761
- throw new TypeError('`context` parameter required')
782
+ throw ExceptionHelper.generateException(TypeError, Errors.ContextParameterRequired)
762
783
  }
763
784
 
764
785
  if (!reference || !reference.serviceUrl || (reference.conversation == null) || !reference.conversation.id || !reference.activityId) {
@@ -791,7 +812,7 @@ export class CloudAdapter extends BaseAdapter {
791
812
  }
792
813
 
793
814
  if (!botAppIdOrIdentity) {
794
- throw new TypeError('continueConversation: botAppIdOrIdentity is required')
815
+ throw ExceptionHelper.generateException(TypeError, Errors.ContinueConversationBotAppIdOrIdentityRequired)
795
816
  }
796
817
  const botAppId = typeof botAppIdOrIdentity === 'string' ? botAppIdOrIdentity : botAppIdOrIdentity.aud as string
797
818
 
@@ -911,10 +932,10 @@ export class CloudAdapter extends BaseAdapter {
911
932
  logic: (context: TurnContext) => Promise<void>
912
933
  ): Promise<void> {
913
934
  if (typeof serviceUrl !== 'string' || !serviceUrl) {
914
- throw new TypeError('`serviceUrl` must be a non-empty string')
935
+ throw ExceptionHelper.generateException(TypeError, Errors.ServiceUrlRequired)
915
936
  }
916
- if (!conversationParameters) throw new TypeError('`conversationParameters` must be defined')
917
- if (!logic) throw new TypeError('`logic` must be defined')
937
+ if (!conversationParameters) throw ExceptionHelper.generateException(TypeError, Errors.ConversationParametersRequired)
938
+ if (!logic) throw ExceptionHelper.generateException(TypeError, Errors.LogicParameterRequired)
918
939
 
919
940
  const identity = CloudAdapter.createIdentity(audience)
920
941
  const restClient = await this.createConnectorClient(serviceUrl, audience, identity)
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Copyright (c) Microsoft Corporation. All rights reserved.
3
+ * Licensed under the MIT License.
4
+ */
5
+
6
+ import { ActivityHandler } from './activityHandler'
7
+ import { AgentApplication } from './app/agentApplication'
8
+ import { AuthConfiguration } from './auth/authConfiguration'
9
+ import { CloudAdapter } from './cloudAdapter'
10
+ import { HeaderPropagationDefinition } from './headerPropagation'
11
+ import { TurnState } from './app/turnState'
12
+
13
+ /**
14
+ * Result of creating a CloudAdapter from an agent.
15
+ */
16
+ export interface CloudAdapterResult {
17
+ adapter: CloudAdapter
18
+ headerPropagation: HeaderPropagationDefinition | undefined
19
+ }
20
+
21
+ /**
22
+ * Creates a CloudAdapter for the given agent.
23
+ *
24
+ * If the agent is an AgentApplication with a pre-configured adapter, that adapter is reused.
25
+ * Otherwise, a new CloudAdapter is created.
26
+ *
27
+ * @param agent - The AgentApplication or ActivityHandler instance.
28
+ * @param authConfig - Optional auth configuration used when creating a new CloudAdapter.
29
+ * If the agent already has an adapter, that adapter is reused and this value is ignored.
30
+ * @returns An object containing the CloudAdapter and optional header propagation configuration.
31
+ *
32
+ * @example
33
+ * ```typescript
34
+ * import { AgentApplication, TurnState, createCloudAdapter } from '@microsoft/agents-hosting';
35
+ *
36
+ * const app = new AgentApplication<TurnState>();
37
+ * const { adapter, headerPropagation } = createCloudAdapter(app, { clientId: process.env.CLIENT_ID });
38
+ *
39
+ * // Use the adapter directly with request/response objects compatible with CloudAdapter.process
40
+ * adapter.process(req, res, (context) => app.run(context), headerPropagation);
41
+ * ```
42
+ */
43
+ export const createCloudAdapter = (
44
+ agent: AgentApplication<TurnState<any, any>> | ActivityHandler,
45
+ authConfig?: AuthConfiguration
46
+ ): CloudAdapterResult => {
47
+ if (agent instanceof ActivityHandler) {
48
+ return { adapter: new CloudAdapter(authConfig), headerPropagation: undefined }
49
+ }
50
+
51
+ // AgentApplication: reuse its pre-configured adapter when present, otherwise create one.
52
+ // headerPropagation is an application-level option, so read it regardless of adapter source.
53
+ const headerPropagation = agent.options?.headerPropagation
54
+ const adapter = agent.adapter ?? new CloudAdapter(authConfig)
55
+ return { adapter, headerPropagation }
56
+ }