@microsoft/agents-hosting 1.7.2 → 1.8.0-beta.34.g5d543f96c0

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 (100) 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 +126 -78
  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.js +2 -9
  30. package/dist/src/app/streaming/streamingResponse.js.map +1 -1
  31. package/dist/src/app/teamsAttachmentDownloader.d.ts +3 -1
  32. package/dist/src/app/teamsAttachmentDownloader.js +12 -1
  33. package/dist/src/app/teamsAttachmentDownloader.js.map +1 -1
  34. package/dist/src/auth/authConfiguration.js +12 -0
  35. package/dist/src/auth/authConfiguration.js.map +1 -1
  36. package/dist/src/auth/index.d.ts +2 -1
  37. package/dist/src/auth/index.js +2 -1
  38. package/dist/src/auth/index.js.map +1 -1
  39. package/dist/src/auth/jwt-middleware.d.ts +6 -0
  40. package/dist/src/auth/jwt-middleware.js +263 -13
  41. package/dist/src/auth/jwt-middleware.js.map +1 -1
  42. package/dist/src/auth/msal/msalConnectionSettings.d.ts +8 -0
  43. package/dist/src/auth/msal/msalHttpRetryHandlerHelper.d.ts +34 -0
  44. package/dist/src/auth/msal/msalHttpRetryHandlerHelper.js +126 -0
  45. package/dist/src/auth/msal/msalHttpRetryHandlerHelper.js.map +1 -0
  46. package/dist/src/auth/msal/msalTokenProvider.d.ts +1 -0
  47. package/dist/src/auth/msal/msalTokenProvider.js +23 -14
  48. package/dist/src/auth/msal/msalTokenProvider.js.map +1 -1
  49. package/dist/src/auth/settings.d.ts +18 -0
  50. package/dist/src/auth/settings.js +8 -2
  51. package/dist/src/auth/settings.js.map +1 -1
  52. package/dist/src/cloudAdapter.d.ts +29 -2
  53. package/dist/src/cloudAdapter.js +44 -2
  54. package/dist/src/cloudAdapter.js.map +1 -1
  55. package/dist/src/errorHelper.js +137 -101
  56. package/dist/src/errorHelper.js.map +1 -1
  57. package/dist/src/index.d.ts +1 -0
  58. package/dist/src/index.js +1 -0
  59. package/dist/src/index.js.map +1 -1
  60. package/dist/src/invoke/adaptiveCardInvokeValue.d.ts +5 -0
  61. package/dist/src/oauth/userTokenClient.d.ts +0 -1
  62. package/dist/src/oauth/userTokenClient.js +0 -1
  63. package/dist/src/oauth/userTokenClient.js.map +1 -1
  64. package/dist/src/observability/traces.js +3 -2
  65. package/dist/src/observability/traces.js.map +1 -1
  66. package/dist/src/outboundHostValidator.d.ts +47 -0
  67. package/dist/src/outboundHostValidator.js +149 -0
  68. package/dist/src/outboundHostValidator.js.map +1 -0
  69. package/dist/src/state/agentState.d.ts +5 -0
  70. package/dist/src/state/agentState.js +16 -3
  71. package/dist/src/state/agentState.js.map +1 -1
  72. package/package.json +3 -3
  73. package/src/agent-client/agentClient.ts +46 -23
  74. package/src/agent-client/agentResponseHandler.ts +14 -9
  75. package/src/agent-client/createAgentResponseHandler.ts +70 -9
  76. package/src/app/adaptiveCards/activityValueParsers.ts +3 -9
  77. package/src/app/adaptiveCards/adaptiveCardsActions.ts +52 -24
  78. package/src/app/agentApplication.ts +135 -74
  79. package/src/app/attachmentDownloader.ts +25 -5
  80. package/src/app/auth/handlers/agenticAuthorization.ts +1 -1
  81. package/src/app/auth/handlers/azureBotAuthorization.ts +1 -1
  82. package/src/app/auth/types.ts +3 -3
  83. package/src/app/index.ts +8 -0
  84. package/src/app/streaming/streamingResponse.ts +2 -8
  85. package/src/app/teamsAttachmentDownloader.ts +11 -1
  86. package/src/auth/authConfiguration.ts +12 -0
  87. package/src/auth/index.ts +2 -1
  88. package/src/auth/jwt-middleware.ts +298 -14
  89. package/src/auth/msal/msalConnectionSettings.ts +10 -0
  90. package/src/auth/msal/msalHttpRetryHandlerHelper.ts +161 -0
  91. package/src/auth/msal/msalTokenProvider.ts +32 -14
  92. package/src/auth/settings.ts +18 -3
  93. package/src/cloudAdapter.ts +56 -3
  94. package/src/errorHelper.ts +155 -114
  95. package/src/index.ts +1 -0
  96. package/src/invoke/adaptiveCardInvokeValue.ts +5 -0
  97. package/src/oauth/userTokenClient.ts +0 -1
  98. package/src/observability/traces.ts +2 -1
  99. package/src/outboundHostValidator.ts +172 -0
  100. package/src/state/agentState.ts +24 -6
@@ -156,6 +156,14 @@ export const Errors: { [key: string]: AgentErrorDefinition } = {
156
156
  description: 'serviceUrl must be a non-empty string'
157
157
  },
158
158
 
159
+ /**
160
+ * Error thrown when the outbound service URL is denied by the configured host policy.
161
+ */
162
+ OutboundServiceUrlNotAllowed: {
163
+ code: -120175,
164
+ description: 'serviceUrl host is not in the configured allowed hosts.'
165
+ },
166
+
159
167
  /**
160
168
  * Error thrown when conversationParameters must be defined.
161
169
  */
@@ -416,6 +424,22 @@ export const Errors: { [key: string]: AgentErrorDefinition } = {
416
424
  description: 'Identity is required to get the token provider.'
417
425
  },
418
426
 
427
+ /**
428
+ * Error thrown when an MSAL HTTP request cannot be completed.
429
+ */
430
+ MsalHttpRequestFailed: {
431
+ code: -120391,
432
+ description: 'MSAL HTTP request failed: {message}'
433
+ },
434
+
435
+ /**
436
+ * Error thrown when an MSAL HTTP response body cannot be parsed as JSON.
437
+ */
438
+ MsalResponseUnparsable: {
439
+ code: -120392,
440
+ description: 'MSAL returned an unparsable response body'
441
+ },
442
+
419
443
  FailedToAcquireTokenUsingCertificate: {
420
444
  code: -120395,
421
445
  description: 'Failed to acquire token using certificate'
@@ -673,108 +697,6 @@ export const Errors: { [key: string]: AgentErrorDefinition } = {
673
697
  description: 'Token request timed out after {timeoutMs} ms'
674
698
  },
675
699
 
676
- // ============================================================================
677
- // Entra Agent ID Sidecar Authentication Errors (-120800 to -120849)
678
- // ============================================================================
679
-
680
- /**
681
- * Error thrown when the Entra sidecar auth provider is used without connection settings.
682
- */
683
- SidecarConnectionSettingsRequired: {
684
- code: -120800,
685
- description: 'Connection settings must be provided when using the Entra sidecar auth provider'
686
- },
687
-
688
- /**
689
- * Error thrown when the resolved sidecar base URL is not a valid absolute URL.
690
- */
691
- SidecarBaseUrlInvalid: {
692
- code: -120801,
693
- description: 'The resolved sidecar base URL `{url}` is not a valid absolute URL'
694
- },
695
-
696
- /**
697
- * Error thrown when the resolved sidecar base URL does not use the http or https scheme.
698
- */
699
- SidecarBaseUrlInsecureScheme: {
700
- code: -120802,
701
- description: 'The resolved sidecar base URL `{url}` must use the http or https scheme'
702
- },
703
-
704
- /**
705
- * Error thrown when the resolved sidecar base URL contains userinfo (credentials).
706
- */
707
- SidecarBaseUrlUserInfo: {
708
- code: -120803,
709
- description: 'The resolved sidecar base URL `{url}` must not contain userinfo (credentials)'
710
- },
711
-
712
- /**
713
- * Error thrown when the resolved sidecar base URL points to a non-loopback, non-private address.
714
- */
715
- SidecarBaseUrlNotLocal: {
716
- code: -120804,
717
- description: 'The resolved sidecar base URL `{url}` points to a non-loopback, non-private address. The Entra Agent ID sidecar (agent container) must be reachable only from within the agent\'s network boundary. Set `bypassLocalNetworkRestriction` to true in the connection configuration to override this safety check (UNSAFE: only for carefully validated private-network configurations)'
718
- },
719
-
720
- /**
721
- * Error thrown when both AgentUsername and AgentUserId are supplied on a sidecar request (mutually exclusive).
722
- */
723
- SidecarUserIdentityMutuallyExclusive: {
724
- code: -120805,
725
- description: 'AgentUsername and AgentUserId are mutually exclusive; set only one on the sidecar request options'
726
- },
727
-
728
- /**
729
- * Error thrown when a sidecar request fails after exhausting all retry attempts.
730
- */
731
- SidecarRequestFailed: {
732
- code: -120806,
733
- description: 'Sidecar request failed after {attempts} attempt(s): {message}'
734
- },
735
-
736
- /**
737
- * Error thrown when the sidecar responds with a non-success status code.
738
- */
739
- SidecarErrorResponse: {
740
- code: -120807,
741
- description: 'Sidecar returned error status {status}: {message}'
742
- },
743
-
744
- /**
745
- * Error thrown when the sidecar response is missing the authorizationHeader field.
746
- */
747
- SidecarResponseMissingAuthorizationHeader: {
748
- code: -120808,
749
- description: 'Sidecar response missing `authorizationHeader` field'
750
- },
751
-
752
- /**
753
- * Error thrown when the sidecar response body cannot be parsed.
754
- */
755
- SidecarResponseUnparsable: {
756
- code: -120809,
757
- description: 'Sidecar returned an unparsable response body'
758
- },
759
-
760
- /**
761
- * Error thrown when acquireTokenOnBehalfOf is called on the sidecar provider, which does not support OBO in Phase 1.
762
- */
763
- OnBehalfOfNotSupportedBySidecar: {
764
- code: -120810,
765
- description: 'acquireTokenOnBehalfOf is not supported by the Entra sidecar auth provider in Phase 1'
766
- },
767
-
768
- /**
769
- * Error thrown when the sidecar returns a token with an authorization scheme other than Bearer.
770
- * The hosting stack always transmits tokens as `Bearer {token}`, so a non-Bearer scheme cannot be
771
- * honored and is rejected rather than silently sent as an invalid Authorization header.
772
- */
773
- SidecarUnsupportedAuthScheme: {
774
- code: -120811,
775
- description: 'The Entra Agent ID sidecar returned an unsupported authorization scheme `{scheme}`. Only the Bearer scheme is supported'
776
- },
777
-
778
700
  // ============================================================================
779
701
  // Agent and Client Errors (-120600 to -120650)
780
702
  // ============================================================================
@@ -1012,32 +934,105 @@ export const Errors: { [key: string]: AgentErrorDefinition } = {
1012
934
  },
1013
935
 
1014
936
  // ============================================================================
1015
- // Hosting / Web layer errors (-120800 to -120840)
937
+ // Entra Agent ID Sidecar Authentication Errors (-120800 to -120811)
1016
938
  // ============================================================================
1017
939
 
1018
940
  /**
1019
- * Error thrown by CloudAdapter.process when the incoming request lacks a parsed body.
1020
- * Indicates the hosting layer (e.g. express.json() or Fastify's JSON parser) was not configured.
941
+ * Error thrown when the Entra sidecar auth provider is used without connection settings.
1021
942
  */
1022
- MissingRequestBody: {
943
+ SidecarConnectionSettingsRequired: {
1023
944
  code: -120800,
1024
- description: '`request.body` parameter required; ensure your hosting layer parses JSON request bodies before invoking the adapter (e.g., express.json() with Express or Fastify\'s built-in JSON parser).'
945
+ description: 'Connection settings must be provided when using the Entra sidecar auth provider'
1025
946
  },
1026
947
 
1027
948
  /**
1028
- * Error thrown by verifyToken when the provided JWT cannot be decoded.
949
+ * Error thrown when the resolved sidecar base URL is not a valid absolute URL.
1029
950
  */
1030
- InvalidJwtToken: {
951
+ SidecarBaseUrlInvalid: {
952
+ code: -120801,
953
+ description: 'The resolved sidecar base URL `{url}` is not a valid absolute URL'
954
+ },
955
+
956
+ /**
957
+ * Error thrown when the resolved sidecar base URL does not use the http or https scheme.
958
+ */
959
+ SidecarBaseUrlInsecureScheme: {
960
+ code: -120802,
961
+ description: 'The resolved sidecar base URL `{url}` must use the http or https scheme'
962
+ },
963
+
964
+ /**
965
+ * Error thrown when the resolved sidecar base URL contains userinfo (credentials).
966
+ */
967
+ SidecarBaseUrlUserInfo: {
968
+ code: -120803,
969
+ description: 'The resolved sidecar base URL `{url}` must not contain userinfo (credentials)'
970
+ },
971
+
972
+ /**
973
+ * Error thrown when the resolved sidecar base URL points to a non-loopback, non-private address.
974
+ */
975
+ SidecarBaseUrlNotLocal: {
976
+ code: -120804,
977
+ description: 'The resolved sidecar base URL `{url}` points to a non-loopback, non-private address. The Entra Agent ID sidecar (agent container) must be reachable only from within the agent\'s network boundary. Set `bypassLocalNetworkRestriction` to true in the connection configuration to override this safety check (UNSAFE: only for carefully validated private-network configurations)'
978
+ },
979
+
980
+ /**
981
+ * Error thrown when both AgentUsername and AgentUserId are supplied on a sidecar request (mutually exclusive).
982
+ */
983
+ SidecarUserIdentityMutuallyExclusive: {
984
+ code: -120805,
985
+ description: 'AgentUsername and AgentUserId are mutually exclusive; set only one on the sidecar request options'
986
+ },
987
+
988
+ /**
989
+ * Error thrown when a sidecar request fails after exhausting all retry attempts.
990
+ */
991
+ SidecarRequestFailed: {
992
+ code: -120806,
993
+ description: 'Sidecar request failed after {attempts} attempt(s): {message}'
994
+ },
995
+
996
+ /**
997
+ * Error thrown when the sidecar responds with a non-success status code.
998
+ */
999
+ SidecarErrorResponse: {
1000
+ code: -120807,
1001
+ description: 'Sidecar returned error status {status}: {message}'
1002
+ },
1003
+
1004
+ /**
1005
+ * Error thrown when the sidecar response is missing the authorizationHeader field.
1006
+ */
1007
+ SidecarResponseMissingAuthorizationHeader: {
1008
+ code: -120808,
1009
+ description: 'Sidecar response missing `authorizationHeader` field'
1010
+ },
1011
+
1012
+ /**
1013
+ * Error thrown when the sidecar response body cannot be parsed.
1014
+ */
1015
+ SidecarResponseUnparsable: {
1016
+ code: -120809,
1017
+ description: 'Sidecar returned an unparsable response body'
1018
+ },
1019
+
1020
+ /**
1021
+ * Error thrown when acquireTokenOnBehalfOf is called on the sidecar provider, which does not support OBO in Phase 1.
1022
+ */
1023
+ OnBehalfOfNotSupportedBySidecar: {
1031
1024
  code: -120810,
1032
- description: 'invalid token'
1025
+ description: 'acquireTokenOnBehalfOf is not supported by the Entra sidecar auth provider in Phase 1'
1033
1026
  },
1034
1027
 
1035
1028
  /**
1036
- * Error thrown by verifyToken when the token audience does not match any configured connection clientId.
1029
+ * Error thrown when the sidecar returns a token with an authorization scheme other than Bearer.
1030
+ * The hosting stack always transmits tokens as `Bearer {token}`, so a non-Bearer scheme cannot be
1031
+ * honored and is rejected rather than silently sent as an invalid Authorization header.
1037
1032
  */
1038
- JwtAudienceMismatch: {
1039
- code: -120820,
1040
- description: 'Audience mismatch'
1033
+ SidecarUnsupportedAuthScheme: {
1034
+ code: -120811,
1035
+ description: 'The Entra Agent ID sidecar returned an unsupported authorization scheme `{scheme}`. Only the Bearer scheme is supported'
1041
1036
  },
1042
1037
 
1043
1038
  // ============================================================================
@@ -1102,6 +1097,52 @@ export const Errors: { [key: string]: AgentErrorDefinition } = {
1102
1097
  description: "You must configure the Application with an 'adapter' before calling Application.continueConversationAsync()"
1103
1098
  },
1104
1099
 
1100
+ // ============================================================================
1101
+ // Hosting / Web layer errors (-120910 to -120940)
1102
+ // ============================================================================
1103
+
1104
+ /**
1105
+ * Error thrown by CloudAdapter.process when the incoming request lacks a parsed body.
1106
+ * Indicates the hosting layer (e.g. express.json() or Fastify's JSON parser) was not configured.
1107
+ */
1108
+ MissingRequestBody: {
1109
+ code: -120910,
1110
+ description: '`request.body` parameter required; ensure your hosting layer parses JSON request bodies before invoking the adapter (e.g., express.json() with Express or Fastify\'s built-in JSON parser).'
1111
+ },
1112
+
1113
+ /**
1114
+ * Error thrown by verifyToken when the provided JWT cannot be decoded.
1115
+ */
1116
+ InvalidJwtToken: {
1117
+ code: -120920,
1118
+ description: 'invalid token'
1119
+ },
1120
+
1121
+ /**
1122
+ * Error thrown by verifyToken when the token audience does not match any configured connection clientId.
1123
+ */
1124
+ JwtAudienceMismatch: {
1125
+ code: -120930,
1126
+ description: 'Audience mismatch'
1127
+ },
1128
+
1129
+ /**
1130
+ * Error thrown by verifyToken when the token issuer is not in the connection's allowed issuer list.
1131
+ */
1132
+ JwtIssuerMismatch: {
1133
+ code: -120830,
1134
+ description: 'Issuer mismatch'
1135
+ },
1136
+
1137
+ /**
1138
+ * Error thrown by verifyToken when an Entra token's tenant id (`tid`) claim does not match the
1139
+ * tenant GUID embedded in its issuer (`iss`) claim.
1140
+ */
1141
+ JwtTenantMismatch: {
1142
+ code: -120840,
1143
+ description: 'Tenant mismatch'
1144
+ },
1145
+
1105
1146
  // ============================================================================
1106
1147
  // Adaptive Cards Errors (-120950 to -120980)
1107
1148
  // ============================================================================
package/src/index.ts CHANGED
@@ -32,5 +32,6 @@ export * from './storage/storage'
32
32
  export * from './headerPropagation'
33
33
  export * from './interfaces'
34
34
  export * from './httpClient'
35
+ export * from './outboundHostValidator'
35
36
 
36
37
  export * from './agent-client'
@@ -22,4 +22,9 @@ export interface AdaptiveCardInvokeValue {
22
22
  * The state of the adaptive card.
23
23
  */
24
24
  state: string
25
+ /**
26
+ * Indicates whether Teams initiated the action while rendering a refreshable
27
+ * card or a user explicitly selected an action.
28
+ */
29
+ trigger?: 'automatic' | 'manual'
25
30
  }
@@ -80,7 +80,6 @@ export class UserTokenClient {
80
80
  /**
81
81
  * Creates a new instance of UserTokenClient with authentication.
82
82
  * @param baseURL - The base URL for the API.
83
- * @param authConfig - The authentication configuration.
84
83
  * @param authProvider - The authentication provider.
85
84
  * @param scope - The scope for the authentication token.
86
85
  * @param headers - Optional headers to propagate in the request.
@@ -26,7 +26,8 @@ export const AgentApplicationTraceDefinitions = {
26
26
  span.setAttributes({
27
27
  'route.authorized': record.authorized,
28
28
  'route.matched': record.routeMatched,
29
- ...attributes
29
+ ...attributes,
30
+ 'activity.name': activity.name ?? 'unknown'
30
31
  })
31
32
 
32
33
  HostingMetrics.turnsTotalCounter.add(1, attributes)
@@ -0,0 +1,172 @@
1
+ /**
2
+ * Copyright (c) Microsoft Corporation. All rights reserved.
3
+ * Licensed under the MIT License.
4
+ */
5
+
6
+ import { parseBooleanEnv } from './utils/env'
7
+
8
+ /** Hosts used by Microsoft channel callbacks and hosted attachments. */
9
+ const DEFAULT_MICROSOFT_HOSTS = Object.freeze([
10
+ 'botframework.com',
11
+ 'smba.trafficmanager.net',
12
+ 'teams.microsoft.com',
13
+ 'teams.microsoft.us',
14
+ 'graph.microsoft.com',
15
+ 'sharepoint.com',
16
+ 'svc.ms',
17
+ 'blob.core.windows.net'
18
+ ])
19
+
20
+ const OUTBOUND_HOST_VALIDATOR_ENV_PREFIX = 'OutboundHostValidator__'
21
+ const OUTBOUND_HOST_VALIDATOR_ENV_PREFIX_UPPER = OUTBOUND_HOST_VALIDATOR_ENV_PREFIX.toUpperCase()
22
+
23
+ /** Configuration for the shared outbound-host allowlist. */
24
+ export interface OutboundHostValidatorOptions {
25
+ /** Enables allowlist enforcement. Defaults to `false`. */
26
+ enabled?: boolean
27
+
28
+ /**
29
+ * Indicates whether the built-in list of Microsoft first-party hosts
30
+ * (Bot Connector, Graph, SharePoint, Azure Blob/AMS) is included when enforcement is enabled.
31
+ * Defaults to `true`.
32
+ */
33
+ includeDefaultMicrosoftHosts?: boolean
34
+
35
+ /** Additional exact hosts or host suffixes to allow.
36
+ * 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`).
37
+ * A leading `*.` is accepted and ignored (treated as a suffix). Ports and paths are ignored if provided.
38
+ */
39
+ hosts?: readonly string[]
40
+ }
41
+
42
+ /** A policy that decides whether an outbound URL is safe to request. */
43
+ export interface OutboundUrlPolicy {
44
+ readonly enabled: boolean
45
+ isAllowed(url: string | URL | null | undefined): boolean
46
+ }
47
+
48
+ /**
49
+ * Shared allowlist policy for server-side outbound requests.
50
+ *
51
+ * A configured suffix matches both the exact host and its subdomains. The
52
+ * policy is disabled by default to preserve existing SDK behavior.
53
+ */
54
+ export class OutboundHostValidator implements OutboundUrlPolicy {
55
+ public readonly enabled: boolean
56
+ private readonly suffixes: ReadonlySet<string>
57
+
58
+ public constructor (options: OutboundHostValidatorOptions = {}) {
59
+ this.enabled = options.enabled ?? false
60
+
61
+ const suffixes = new Set<string>()
62
+ if (options.includeDefaultMicrosoftHosts ?? true) {
63
+ for (const host of DEFAULT_MICROSOFT_HOSTS) suffixes.add(host)
64
+ }
65
+
66
+ for (const host of options.hosts ?? []) {
67
+ const normalized = normalizeConfiguredHost(host)
68
+ if (normalized) suffixes.add(normalized)
69
+ }
70
+
71
+ this.suffixes = suffixes
72
+ }
73
+
74
+ public isAllowed (input: string | URL | null | undefined): boolean {
75
+ if (!this.enabled) return true
76
+
77
+ const host = getUrlHost(input)
78
+ if (!host) return false
79
+
80
+ for (const suffix of this.suffixes) {
81
+ if (host === suffix || host.endsWith(`.${suffix}`)) return true
82
+ }
83
+
84
+ return false
85
+ }
86
+ }
87
+
88
+ /**
89
+ * Loads validator options from environment variables compatible with the
90
+ * .NET `OutboundHostValidator` configuration section.
91
+ *
92
+ * Hosts can be supplied either as a comma-separated `Hosts` value or as
93
+ * indexed values such as `Hosts__0`, `Hosts__1`, and so on.
94
+ */
95
+ export function loadOutboundHostValidatorOptionsFromEnv (): OutboundHostValidatorOptions {
96
+ const result: OutboundHostValidatorOptions = {}
97
+ const indexedHosts = new Map<number, string>()
98
+ const hosts: string[] = []
99
+
100
+ for (const [envKey, rawValue] of Object.entries(process.env)) {
101
+ const upperKey = envKey.toUpperCase()
102
+ if (!upperKey.startsWith(OUTBOUND_HOST_VALIDATOR_ENV_PREFIX_UPPER)) continue
103
+
104
+ const property = envKey.substring(OUTBOUND_HOST_VALIDATOR_ENV_PREFIX.length)
105
+ const upperProperty = property.toUpperCase()
106
+
107
+ if (upperProperty === 'ENABLED') {
108
+ const value = parseBooleanEnv(rawValue)
109
+ if (value !== undefined) result.enabled = value
110
+ } else if (upperProperty === 'INCLUDEDEFAULTMICROSOFTHOSTS') {
111
+ const value = parseBooleanEnv(rawValue)
112
+ if (value !== undefined) result.includeDefaultMicrosoftHosts = value
113
+ } else if (upperProperty === 'HOSTS' && rawValue) {
114
+ hosts.push(...rawValue.split(',').map(host => host.trim()).filter(Boolean))
115
+ } else {
116
+ const match = /^HOSTS__(\d+)$/i.exec(property)
117
+ if (match && rawValue?.trim()) indexedHosts.set(Number(match[1]), rawValue.trim())
118
+ }
119
+ }
120
+
121
+ for (const [, host] of [...indexedHosts.entries()].sort(([left], [right]) => left - right)) {
122
+ hosts.push(host)
123
+ }
124
+ if (hosts.length > 0) result.hosts = hosts
125
+
126
+ return result
127
+ }
128
+
129
+ /** Creates the default validator, with explicit settings overriding the environment. */
130
+ export function createOutboundHostValidator (options?: OutboundHostValidatorOptions): OutboundHostValidator {
131
+ const fromEnv = loadOutboundHostValidatorOptionsFromEnv()
132
+ return new OutboundHostValidator({
133
+ enabled: options?.enabled ?? fromEnv.enabled,
134
+ includeDefaultMicrosoftHosts: options?.includeDefaultMicrosoftHosts ?? fromEnv.includeDefaultMicrosoftHosts,
135
+ hosts: options?.hosts ?? fromEnv.hosts
136
+ })
137
+ }
138
+
139
+ function getUrlHost (input: string | URL | null | undefined): string | undefined {
140
+ if (!input) return undefined
141
+
142
+ try {
143
+ const url = input instanceof URL ? input : new URL(input)
144
+ return normalizeHostname(url.hostname)
145
+ } catch {
146
+ return undefined
147
+ }
148
+ }
149
+
150
+ function normalizeConfiguredHost (input: string): string | undefined {
151
+ let candidate = input?.trim()
152
+ if (!candidate) return undefined
153
+ if (candidate.startsWith('*.')) candidate = candidate.slice(2)
154
+
155
+ try {
156
+ const absolute = new URL(candidate)
157
+ if (absolute.hostname) return normalizeHostname(absolute.hostname)
158
+ } catch {
159
+ // A bare host, host:port, or host/path is handled below.
160
+ }
161
+
162
+ try {
163
+ return normalizeHostname(new URL(`http://${candidate}`).hostname)
164
+ } catch {
165
+ return undefined
166
+ }
167
+ }
168
+
169
+ function normalizeHostname (host: string): string | undefined {
170
+ const normalized = host.trim().toLowerCase().replace(/\.$/, '')
171
+ return normalized || undefined
172
+ }
@@ -44,7 +44,10 @@ export interface CustomKey {
44
44
  * The ID of the conversation where the state should be stored
45
45
  */
46
46
  conversationId: string;
47
- // TODO: namespace needs to be added
47
+ /**
48
+ * Optional namespace appended to the generated storage key
49
+ */
50
+ namespace?: string;
48
51
  }
49
52
 
50
53
  /**
@@ -152,8 +155,10 @@ export class AgentState {
152
155
  private async getStorageOrCustomKey (customKey: CustomKey | undefined, context: TurnContext) {
153
156
  let key: string | undefined
154
157
  if (customKey && customKey.channelId && customKey.conversationId) {
155
- // TODO check ConversationState.ts line 40. This line below should follow the same pattern
156
- key = `${customKey!.channelId}/conversations/${customKey!.conversationId}`
158
+ key = `${customKey.channelId}/conversations/${customKey.conversationId}`
159
+ if (customKey.namespace) {
160
+ key = `${key}/${customKey.namespace}`
161
+ }
157
162
  } else {
158
163
  key = await this.storageKey(context)
159
164
  }
@@ -217,13 +222,26 @@ export class AgentState {
217
222
  */
218
223
  private readonly calculateChangeHash = (item: StoreItem): string => {
219
224
  const { eTag, ...rest } = item
220
-
221
- // TODO review circular json structure
222
- const result = JSON.stringify(rest)
225
+ const result = this.stringifyForHash(rest)
223
226
 
224
227
  const hash = createHash('sha256', { encoding: 'utf-8' })
225
228
  const hashed = hash.update(result).digest('hex')
226
229
 
227
230
  return hashed
228
231
  }
232
+
233
+ private readonly stringifyForHash = (item: StoreItem): string => {
234
+ const seen = new WeakSet<object>()
235
+
236
+ return JSON.stringify(item, (_key, value) => {
237
+ if (value && typeof value === 'object') {
238
+ if (seen.has(value)) {
239
+ return '[Circular]'
240
+ }
241
+ seen.add(value)
242
+ }
243
+
244
+ return value
245
+ })
246
+ }
229
247
  }