@microsoft/agents-hosting 1.6.0-beta.9.gce9d8facd2 → 1.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/package.json +4 -9
- package/dist/src/agent-client/agentClient.js +2 -2
- package/dist/src/agent-client/agentClient.js.map +1 -1
- package/dist/src/agent-client/agentResponseHandler.js +2 -2
- package/dist/src/agent-client/agentResponseHandler.js.map +1 -1
- package/dist/src/app/agentApplication.js +28 -2
- package/dist/src/app/agentApplication.js.map +1 -1
- package/dist/src/app/agentApplicationOptions.d.ts +10 -0
- package/dist/src/app/attachmentDownloader.js +5 -10
- package/dist/src/app/attachmentDownloader.js.map +1 -1
- package/dist/src/app/auth/authorizationManager.d.ts +4 -0
- package/dist/src/app/auth/authorizationManager.js +94 -67
- package/dist/src/app/auth/authorizationManager.js.map +1 -1
- package/dist/src/app/auth/handlers/azureBotAuthorization.js +2 -2
- package/dist/src/app/auth/handlers/azureBotAuthorization.js.map +1 -1
- package/dist/src/app/proactive/createConversationOptionsBuilder.js +10 -6
- package/dist/src/app/proactive/createConversationOptionsBuilder.js.map +1 -1
- package/dist/src/app/proactive/proactive.d.ts +1 -0
- package/dist/src/app/proactive/proactive.js +30 -15
- package/dist/src/app/proactive/proactive.js.map +1 -1
- package/dist/src/app/teamsAttachmentDownloader.js +6 -14
- package/dist/src/app/teamsAttachmentDownloader.js.map +1 -1
- package/dist/src/auth/authConfiguration.d.ts +2 -143
- package/dist/src/auth/authConfiguration.js +301 -257
- package/dist/src/auth/authConfiguration.js.map +1 -1
- package/dist/src/auth/jwt-middleware.js +2 -1
- package/dist/src/auth/jwt-middleware.js.map +1 -1
- package/dist/src/auth/msalConnectionManager.js +27 -14
- package/dist/src/auth/msalConnectionManager.js.map +1 -1
- package/dist/src/auth/msalTokenProvider.d.ts +7 -0
- package/dist/src/auth/msalTokenProvider.js +188 -44
- package/dist/src/auth/msalTokenProvider.js.map +1 -1
- package/dist/src/auth/settings.d.ts +327 -0
- package/dist/src/auth/settings.js +158 -0
- package/dist/src/auth/settings.js.map +1 -0
- package/dist/src/cloudAdapter.d.ts +58 -1
- package/dist/src/cloudAdapter.js +232 -52
- package/dist/src/cloudAdapter.js.map +1 -1
- package/dist/src/connector-client/connectorClient.d.ts +12 -8
- package/dist/src/connector-client/connectorClient.js +99 -82
- package/dist/src/connector-client/connectorClient.js.map +1 -1
- package/dist/src/errorHelper.js +85 -7
- package/dist/src/errorHelper.js.map +1 -1
- package/dist/src/getProductInfo.d.ts +23 -0
- package/dist/src/getProductInfo.js +71 -1
- package/dist/src/getProductInfo.js.map +1 -1
- package/dist/src/headerPropagation.d.ts +17 -2
- package/dist/src/headerPropagation.js +46 -24
- package/dist/src/headerPropagation.js.map +1 -1
- package/dist/src/httpClient.d.ts +60 -0
- package/dist/src/httpClient.js +173 -0
- package/dist/src/httpClient.js.map +1 -0
- package/dist/src/index.d.ts +2 -1
- package/dist/src/index.js +7 -2
- package/dist/src/index.js.map +1 -1
- package/dist/src/oauth/userTokenClient.d.ts +5 -4
- package/dist/src/oauth/userTokenClient.js +97 -78
- package/dist/src/oauth/userTokenClient.js.map +1 -1
- package/dist/src/observability/traces.d.ts +11 -4
- package/dist/src/observability/traces.js +39 -7
- package/dist/src/observability/traces.js.map +1 -1
- package/dist/src/utils/env.d.ts +50 -0
- package/dist/src/utils/env.js +113 -0
- package/dist/src/utils/env.js.map +1 -0
- package/dist/src/utils.d.ts +10 -0
- package/dist/src/utils.js +18 -0
- package/dist/src/utils.js.map +1 -0
- package/package.json +4 -9
- package/src/agent-client/agentClient.ts +2 -2
- package/src/agent-client/agentResponseHandler.ts +2 -2
- package/src/app/agentApplication.ts +30 -3
- package/src/app/agentApplicationOptions.ts +11 -0
- package/src/app/attachmentDownloader.ts +6 -8
- package/src/app/auth/authorizationManager.ts +84 -41
- package/src/app/auth/handlers/azureBotAuthorization.ts +2 -2
- package/src/app/proactive/createConversationOptionsBuilder.ts +8 -4
- package/src/app/proactive/proactive.ts +39 -23
- package/src/app/teamsAttachmentDownloader.ts +7 -11
- package/src/auth/authConfiguration.ts +317 -356
- package/src/auth/jwt-middleware.ts +1 -1
- package/src/auth/msalConnectionManager.ts +27 -10
- package/src/auth/msalTokenProvider.ts +180 -42
- package/src/auth/settings.ts +323 -0
- package/src/cloudAdapter.ts +291 -12
- package/src/connector-client/connectorClient.ts +116 -94
- package/src/errorHelper.ts +96 -7
- package/src/getProductInfo.ts +79 -1
- package/src/headerPropagation.ts +55 -23
- package/src/httpClient.ts +223 -0
- package/src/index.ts +5 -1
- package/src/oauth/userTokenClient.ts +103 -84
- package/src/observability/traces.ts +39 -8
- package/src/utils/env.ts +105 -0
- package/src/utils.ts +14 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Copyright (c) Microsoft Corporation. All rights reserved.
|
|
3
|
+
// Licensed under the MIT License.
|
|
4
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5
|
+
exports.parseBooleanEnv = parseBooleanEnv;
|
|
6
|
+
exports.parseIntEnv = parseIntEnv;
|
|
7
|
+
exports.suggestClosest = suggestClosest;
|
|
8
|
+
/**
|
|
9
|
+
* Internal helpers for parsing environment-variable strings into typed values.
|
|
10
|
+
*
|
|
11
|
+
* These helpers are intentionally NOT exported from the package's public
|
|
12
|
+
* `index.ts`; they are an internal utility shared between the adapter,
|
|
13
|
+
* authentication, and connector-client modules. Keeping them internal lets us
|
|
14
|
+
* normalize behavior across the package without growing the public API.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* Parses an environment-variable string into a boolean.
|
|
18
|
+
*
|
|
19
|
+
* Recognized truthy values (case-insensitive, trimmed): `"true"`, `"1"`.
|
|
20
|
+
* Recognized falsy values (case-insensitive, trimmed): `"false"`, `"0"`.
|
|
21
|
+
* Returns `undefined` for unset (`undefined`), empty strings, or any other
|
|
22
|
+
* value, so callers can distinguish "not specified" from explicit `false`.
|
|
23
|
+
*
|
|
24
|
+
* @param value - The raw environment variable value (typically
|
|
25
|
+
* `process.env.X`).
|
|
26
|
+
* @returns `true`, `false`, or `undefined` when the value is unset or
|
|
27
|
+
* unrecognized.
|
|
28
|
+
*/
|
|
29
|
+
function parseBooleanEnv(value) {
|
|
30
|
+
if (value === undefined)
|
|
31
|
+
return undefined;
|
|
32
|
+
const v = value.trim().toLowerCase();
|
|
33
|
+
if (v === 'true' || v === '1')
|
|
34
|
+
return true;
|
|
35
|
+
if (v === '')
|
|
36
|
+
return undefined;
|
|
37
|
+
if (v === 'false' || v === '0')
|
|
38
|
+
return false;
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Parses an environment-variable string into an integer, returning the
|
|
43
|
+
* supplied fallback for unset, empty, or non-numeric values.
|
|
44
|
+
*
|
|
45
|
+
* @param value - The raw environment variable value.
|
|
46
|
+
* @param fallback - The value to return when the input is unset or invalid.
|
|
47
|
+
* @returns A finite integer, or `fallback` when the input cannot be parsed.
|
|
48
|
+
*/
|
|
49
|
+
function parseIntEnv(value, fallback) {
|
|
50
|
+
if (value === undefined || value.trim() === '')
|
|
51
|
+
return fallback;
|
|
52
|
+
const parsed = parseInt(value, 10);
|
|
53
|
+
return Number.isNaN(parsed) ? fallback : parsed;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Returns the candidate from `candidates` that is closest to `input` by
|
|
57
|
+
* Levenshtein edit distance, provided the distance is no greater than
|
|
58
|
+
* `maxDistance`. Returns `undefined` when no candidate is close enough or the
|
|
59
|
+
* input is empty.
|
|
60
|
+
*
|
|
61
|
+
* Intended for "did you mean X?" hints on unknown configuration keys.
|
|
62
|
+
* Comparison is case-insensitive so casing differences alone never count as
|
|
63
|
+
* an edit. Ties are broken by the order in which candidates appear.
|
|
64
|
+
*
|
|
65
|
+
* In addition to `maxDistance`, the match must be within 50% of the input
|
|
66
|
+
* length (rounded up). This prevents confident-but-wrong suggestions on
|
|
67
|
+
* short inputs (e.g., a 4-char typo matching a 6-char canonical name).
|
|
68
|
+
*
|
|
69
|
+
* @param input - The unknown name supplied by the user.
|
|
70
|
+
* @param candidates - The set of known valid names.
|
|
71
|
+
* @param maxDistance - Maximum edit distance to consider a match (default 2).
|
|
72
|
+
*/
|
|
73
|
+
function suggestClosest(input, candidates, maxDistance = 2) {
|
|
74
|
+
if (!input)
|
|
75
|
+
return undefined;
|
|
76
|
+
const a = input.toLowerCase();
|
|
77
|
+
const cutoff = Math.min(maxDistance, Math.max(1, Math.ceil(a.length / 2)));
|
|
78
|
+
let best;
|
|
79
|
+
for (const candidate of candidates) {
|
|
80
|
+
const d = levenshtein(a, candidate.toLowerCase());
|
|
81
|
+
if (d <= cutoff && (!best || d < best.distance)) {
|
|
82
|
+
best = { name: candidate, distance: d };
|
|
83
|
+
if (d === 0)
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return best === null || best === void 0 ? void 0 : best.name;
|
|
88
|
+
}
|
|
89
|
+
/** Standard Levenshtein edit distance using a single-row dynamic-programming table. */
|
|
90
|
+
function levenshtein(a, b) {
|
|
91
|
+
if (a === b)
|
|
92
|
+
return 0;
|
|
93
|
+
if (a.length === 0)
|
|
94
|
+
return b.length;
|
|
95
|
+
if (b.length === 0)
|
|
96
|
+
return a.length;
|
|
97
|
+
const row = new Array(b.length + 1);
|
|
98
|
+
for (let j = 0; j <= b.length; j++)
|
|
99
|
+
row[j] = j;
|
|
100
|
+
for (let i = 1; i <= a.length; i++) {
|
|
101
|
+
let prev = i - 1;
|
|
102
|
+
row[0] = i;
|
|
103
|
+
for (let j = 1; j <= b.length; j++) {
|
|
104
|
+
const temp = row[j];
|
|
105
|
+
row[j] = a[i - 1] === b[j - 1]
|
|
106
|
+
? prev
|
|
107
|
+
: 1 + Math.min(prev, row[j - 1], row[j]);
|
|
108
|
+
prev = temp;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return row[b.length];
|
|
112
|
+
}
|
|
113
|
+
//# sourceMappingURL=env.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"env.js","sourceRoot":"","sources":["../../../src/utils/env.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
3
|
+
* Licensed under the MIT License.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Utility function to prune undefined values from an object. This is useful for cleaning up configuration objects before logging or processing, ensuring that only defined values are included.
|
|
7
|
+
* @param obj The object to prune
|
|
8
|
+
* @returns A new object with all undefined values removed
|
|
9
|
+
*/
|
|
10
|
+
export declare const prune: <T extends Record<string, any>>(obj: T) => T;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
4
|
+
* Licensed under the MIT License.
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.prune = void 0;
|
|
8
|
+
/**
|
|
9
|
+
* Utility function to prune undefined values from an object. This is useful for cleaning up configuration objects before logging or processing, ensuring that only defined values are included.
|
|
10
|
+
* @param obj The object to prune
|
|
11
|
+
* @returns A new object with all undefined values removed
|
|
12
|
+
*/
|
|
13
|
+
const prune = (obj) => {
|
|
14
|
+
const entries = Object.entries(obj).filter(([, value]) => value !== undefined);
|
|
15
|
+
return Object.fromEntries(entries);
|
|
16
|
+
};
|
|
17
|
+
exports.prune = prune;
|
|
18
|
+
//# sourceMappingURL=utils.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils.js","sourceRoot":"","sources":["../../src/utils.ts"],"names":[],"mappings":";AAAA;;;GAGG;;;AAEH;;;;GAIG;AACI,MAAM,KAAK,GAAG,CAAgC,GAAM,EAAE,EAAE;IAC7D,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,KAAK,KAAK,SAAS,CAAC,CAAA;IAC9E,OAAO,MAAM,CAAC,WAAW,CAAC,OAAO,CAAM,CAAA;AACzC,CAAC,CAAA;AAHY,QAAA,KAAK,SAGjB"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package.json",
|
|
3
3
|
"name": "@microsoft/agents-hosting",
|
|
4
|
-
"version": "1.6.
|
|
4
|
+
"version": "1.6.1",
|
|
5
5
|
"homepage": "https://github.com/microsoft/Agents-for-js",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -20,13 +20,11 @@
|
|
|
20
20
|
"types": "dist/src/index.d.ts",
|
|
21
21
|
"dependencies": {
|
|
22
22
|
"@azure/core-auth": "1.10.1",
|
|
23
|
-
"@azure/msal-node": "5.
|
|
24
|
-
"@microsoft/agents-activity": "1.6.
|
|
25
|
-
"@microsoft/agents-telemetry": "1.6.
|
|
26
|
-
"axios": "1.15.1",
|
|
23
|
+
"@azure/msal-node": "5.1.5",
|
|
24
|
+
"@microsoft/agents-activity": "1.6.1",
|
|
25
|
+
"@microsoft/agents-telemetry": "1.6.1",
|
|
27
26
|
"jsonwebtoken": "9.0.3",
|
|
28
27
|
"jwks-rsa": "4.0.1",
|
|
29
|
-
"object-path": "0.11.8",
|
|
30
28
|
"zod": "3.25.75"
|
|
31
29
|
},
|
|
32
30
|
"license": "MIT",
|
|
@@ -46,8 +44,5 @@
|
|
|
46
44
|
},
|
|
47
45
|
"engines": {
|
|
48
46
|
"node": ">=20.0.0"
|
|
49
|
-
},
|
|
50
|
-
"devDependencies": {
|
|
51
|
-
"@types/object-path": "0.11.4"
|
|
52
47
|
}
|
|
53
48
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { AuthConfiguration, MsalTokenProvider } from '../auth'
|
|
2
2
|
import { Activity, ConversationReference, RoleTypes } from '@microsoft/agents-activity'
|
|
3
|
-
import {
|
|
3
|
+
import { randomUUID } from 'crypto'
|
|
4
4
|
import { debug } from '@microsoft/agents-telemetry'
|
|
5
5
|
import { ConversationState } from '../state'
|
|
6
6
|
import { TurnContext } from '../turnContext'
|
|
@@ -88,7 +88,7 @@ export class AgentClient {
|
|
|
88
88
|
...activityCopy.conversation
|
|
89
89
|
}
|
|
90
90
|
}
|
|
91
|
-
activityCopy.conversation!.id =
|
|
91
|
+
activityCopy.conversation!.id = randomUUID()
|
|
92
92
|
|
|
93
93
|
const conversationDataAccessor = conversationState.createProperty<ConversationData>(activityCopy.conversation!.id)
|
|
94
94
|
const convRef = await conversationDataAccessor.set(context,
|
|
@@ -3,7 +3,7 @@ import { ActivityHandler } from '../activityHandler'
|
|
|
3
3
|
import { CloudAdapter } from '../cloudAdapter'
|
|
4
4
|
import { Request, Response, Application } from 'express'
|
|
5
5
|
import { TurnContext } from '../turnContext'
|
|
6
|
-
import {
|
|
6
|
+
import { randomUUID } from 'crypto'
|
|
7
7
|
import { normalizeIncomingActivity } from '../activityWireCompat'
|
|
8
8
|
import { debug } from '@microsoft/agents-telemetry'
|
|
9
9
|
import { ConversationState } from '../state'
|
|
@@ -78,7 +78,7 @@ const handleResponse = (adapter: CloudAdapter, handler: ActivityHandler, convers
|
|
|
78
78
|
applyActivityToTurnContext(turnContext, activity)
|
|
79
79
|
await handler.run(turnContext)
|
|
80
80
|
|
|
81
|
-
response =
|
|
81
|
+
response = randomUUID().replace(/-/g, '')
|
|
82
82
|
} else {
|
|
83
83
|
response = await turnContext.sendActivity(activity)
|
|
84
84
|
}
|
|
@@ -21,7 +21,7 @@ import { CloudAdapter } from '../cloudAdapter'
|
|
|
21
21
|
import { Authorization, UserAuthorization, AuthorizationManager } from './auth'
|
|
22
22
|
import { Proactive } from './proactive'
|
|
23
23
|
import { JwtPayload } from 'jsonwebtoken'
|
|
24
|
-
import { trace, debug } from '@microsoft/agents-telemetry'
|
|
24
|
+
import { trace, debug, redactString } from '@microsoft/agents-telemetry'
|
|
25
25
|
import { AgentApplicationTraceDefinitions } from '../observability'
|
|
26
26
|
import { Errors } from '../errorHelper'
|
|
27
27
|
|
|
@@ -144,6 +144,8 @@ export class AgentApplication<TState extends TurnState> {
|
|
|
144
144
|
this._adapter = new CloudAdapter()
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
+
this._adapter.setAgentName(this._options.agentName)
|
|
148
|
+
|
|
147
149
|
// Create Proactive whenever proactive options are explicitly configured or a storage
|
|
148
150
|
// backend is available — no explicit `proactive` option is required.
|
|
149
151
|
if (this._options.proactive !== undefined || this._options.storage !== undefined) {
|
|
@@ -164,10 +166,35 @@ export class AgentApplication<TState extends TurnState> {
|
|
|
164
166
|
}
|
|
165
167
|
}
|
|
166
168
|
|
|
167
|
-
logger.debug('AgentApplication created with options:', this._options)
|
|
168
|
-
|
|
169
169
|
this._authorizationManager = new AuthorizationManager(this, this._adapter.connectionManager)
|
|
170
170
|
this._authorization = new UserAuthorization(this._authorizationManager)
|
|
171
|
+
|
|
172
|
+
const className = (obj: any) => obj?.constructor?.name ?? (obj ? 'custom' : undefined)
|
|
173
|
+
logger.info('AgentApplication settings loaded', {
|
|
174
|
+
agentAppId: redactString(this._options.agentAppId, true),
|
|
175
|
+
adapter: {
|
|
176
|
+
className: className(this._adapter),
|
|
177
|
+
source: this._options.adapter ? 'consumed' : 'created',
|
|
178
|
+
},
|
|
179
|
+
storage: className(this._options.storage),
|
|
180
|
+
authorization: this._authorizationManager.handlers.map(e => e.id),
|
|
181
|
+
proactive: {
|
|
182
|
+
...this.options.proactive,
|
|
183
|
+
storage: className(this._options.proactive?.storage ?? this._options.storage),
|
|
184
|
+
source: this._options.proactive ? 'options' : 'storage',
|
|
185
|
+
},
|
|
186
|
+
transcriptLogger: className(this._options.transcriptLogger),
|
|
187
|
+
typing: {
|
|
188
|
+
startTimer: this._options.startTypingTimer,
|
|
189
|
+
options: this._options.typing,
|
|
190
|
+
},
|
|
191
|
+
longRunningMessages: this._options.longRunningMessages,
|
|
192
|
+
removeRecipientMention: this._options.removeRecipientMention,
|
|
193
|
+
normalizeMentions: this._options.normalizeMentions,
|
|
194
|
+
fileDownloaders: this._options.fileDownloaders ? 'configured' : undefined,
|
|
195
|
+
turnStateFactory: options?.turnStateFactory ? 'custom' : 'default',
|
|
196
|
+
adaptiveCardsOptions: this._options.adaptiveCardsOptions,
|
|
197
|
+
})
|
|
171
198
|
}
|
|
172
199
|
|
|
173
200
|
/**
|
|
@@ -178,6 +178,17 @@ export interface AgentApplicationOptions<TState extends TurnState> {
|
|
|
178
178
|
*/
|
|
179
179
|
headerPropagation?: HeaderPropagationDefinition
|
|
180
180
|
|
|
181
|
+
/**
|
|
182
|
+
* Optional. Human-friendly agent name sent in the `AgentName` header for
|
|
183
|
+
* SDK-managed outbound requests created during inbound turn processing.
|
|
184
|
+
*
|
|
185
|
+
* @remarks
|
|
186
|
+
* When omitted, the SDK uses the default value `Agent-SDK-JS`.
|
|
187
|
+
* The resolved value is trimmed and validated by the SDK. Only letters,
|
|
188
|
+
* numbers, spaces, hyphen, and underscore are allowed.
|
|
189
|
+
*/
|
|
190
|
+
agentName?: string
|
|
191
|
+
|
|
181
192
|
/**
|
|
182
193
|
* Optional. Configuration for managing multiple authentication connections within the agent.
|
|
183
194
|
* This allows the agent to handle authentication across different services or
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Licensed under the MIT License.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import
|
|
6
|
+
import { HttpClient } from '../httpClient'
|
|
7
7
|
import { InputFile, InputFileDownloader } from './inputFileDownloader'
|
|
8
8
|
import { TurnState } from './turnState'
|
|
9
9
|
import { TurnContext } from '../turnContext'
|
|
@@ -25,7 +25,7 @@ const logger = debug('agents:attachmentDownloader')
|
|
|
25
25
|
*
|
|
26
26
|
*/
|
|
27
27
|
export class AttachmentDownloader<TState extends TurnState = TurnState> implements InputFileDownloader<TState> {
|
|
28
|
-
private _httpClient:
|
|
28
|
+
private _httpClient: HttpClient
|
|
29
29
|
private _stateKey: string
|
|
30
30
|
|
|
31
31
|
/**
|
|
@@ -35,7 +35,7 @@ export class AttachmentDownloader<TState extends TurnState = TurnState> implemen
|
|
|
35
35
|
* @param stateKey The key to store files in state. Defaults to 'inputFiles'.
|
|
36
36
|
*/
|
|
37
37
|
public constructor (stateKey: string = 'inputFiles') {
|
|
38
|
-
this._httpClient =
|
|
38
|
+
this._httpClient = new HttpClient()
|
|
39
39
|
this._stateKey = stateKey
|
|
40
40
|
}
|
|
41
41
|
|
|
@@ -86,18 +86,16 @@ export class AttachmentDownloader<TState extends TurnState = TurnState> implemen
|
|
|
86
86
|
(attachment.contentUrl && attachment.contentUrl.startsWith('https://')) ||
|
|
87
87
|
(attachment.contentUrl && attachment.contentUrl.startsWith('http://localhost'))
|
|
88
88
|
) {
|
|
89
|
-
|
|
89
|
+
const headers: Record<string, string> = {}
|
|
90
90
|
if (accessToken.length > 0) {
|
|
91
|
-
headers = {
|
|
92
|
-
Authorization: `Bearer ${accessToken}`
|
|
93
|
-
}
|
|
91
|
+
headers.Authorization = `Bearer ${accessToken}`
|
|
94
92
|
}
|
|
95
93
|
const response = await this._httpClient.get(attachment.contentUrl, {
|
|
96
94
|
headers,
|
|
97
95
|
responseType: 'arraybuffer'
|
|
98
96
|
})
|
|
99
97
|
|
|
100
|
-
const content = Buffer.from(response.data
|
|
98
|
+
const content = Buffer.from(response.data as ArrayBuffer)
|
|
101
99
|
|
|
102
100
|
let contentType = attachment.contentType
|
|
103
101
|
if (contentType === 'image/*') {
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { Activity, ExceptionHelper } from '@microsoft/agents-activity'
|
|
7
|
-
import { debug } from '@microsoft/agents-telemetry'
|
|
7
|
+
import { debug, redactScopes } from '@microsoft/agents-telemetry'
|
|
8
8
|
import { AgentApplication } from '../agentApplication'
|
|
9
9
|
import { AgenticAuthorization, AzureBotAuthorization } from './handlers'
|
|
10
10
|
import { TurnContext } from '../../turnContext'
|
|
@@ -13,7 +13,8 @@ import { Errors } from '../../errorHelper'
|
|
|
13
13
|
import { ActiveAuthorizationHandler, AuthorizationHandlerStatus, AuthorizationHandler, AuthorizationHandlerSettings, AuthorizationOptions } from './types'
|
|
14
14
|
import { Connections } from '../../auth/connections'
|
|
15
15
|
import { sendInvokeResponse } from './utils'
|
|
16
|
-
import { envParser, envParserUtils } from '../../auth'
|
|
16
|
+
import { envParser, envParserUtils } from '../../auth/settings'
|
|
17
|
+
import { prune } from '../../utils'
|
|
17
18
|
|
|
18
19
|
const logger = debug('agents:authorization:manager')
|
|
19
20
|
|
|
@@ -155,10 +156,6 @@ export class AuthorizationManager {
|
|
|
155
156
|
if (this.handlers.length === 0 && app.options.authorization !== undefined) {
|
|
156
157
|
throw ExceptionHelper.generateException(Error, Errors.NoAuthHandlersConfigured)
|
|
157
158
|
}
|
|
158
|
-
|
|
159
|
-
for (const [id, handler] of Object.entries(this._handlers)) {
|
|
160
|
-
logger.debug('auth handler "%s" type=%s scopes=%o', id, handler.type, handler.scopes)
|
|
161
|
-
}
|
|
162
159
|
}
|
|
163
160
|
|
|
164
161
|
/**
|
|
@@ -320,7 +317,7 @@ export class AuthorizationManager {
|
|
|
320
317
|
private createHandlers () {
|
|
321
318
|
let legacyMessage = ''
|
|
322
319
|
const settings: AuthorizationHandlerSettings = { storage: this.app.options.storage!, connections: this.connections }
|
|
323
|
-
|
|
320
|
+
const runtimeOptionEntries = Object.entries(this.app.options.authorization ?? {})
|
|
324
321
|
const result = { latest: {}, legacy: {} } as {
|
|
325
322
|
latest: Record<string, Record<string, any> | undefined>;
|
|
326
323
|
legacy: Record<string, Record<string, any> | undefined>;
|
|
@@ -335,7 +332,7 @@ export class AuthorizationManager {
|
|
|
335
332
|
|
|
336
333
|
// Legacy: extract handler ID, handler options key and its value, and assign it to the correct runtime handler ID.
|
|
337
334
|
if (!upperEnvKey.startsWith(this._envLatest.key.prefix.toUpperCase())) {
|
|
338
|
-
const [id] =
|
|
335
|
+
const [id] = runtimeOptionEntries.find(([id]) => upperEnvKey.startsWith(`${id.toUpperCase()}${this._envLegacy.key.separator}`)) ?? []
|
|
339
336
|
if (!id) {
|
|
340
337
|
continue
|
|
341
338
|
}
|
|
@@ -386,7 +383,7 @@ export class AuthorizationManager {
|
|
|
386
383
|
const fixTypes = (entries: [string, any][]) => entries
|
|
387
384
|
.map(([id, options]) => [id, { ...options, type: this.fixType(id, options?.type) }] as [string, AuthorizationOptions[string]])
|
|
388
385
|
|
|
389
|
-
runtimeEntries = fixTypes(
|
|
386
|
+
const runtimeEntries = fixTypes(runtimeOptionEntries)
|
|
390
387
|
const latestEntries = fixTypes(Object.entries(result.latest))
|
|
391
388
|
const legacyEntries = fixTypes(Object.entries(result.legacy))
|
|
392
389
|
|
|
@@ -396,39 +393,27 @@ export class AuthorizationManager {
|
|
|
396
393
|
continue
|
|
397
394
|
}
|
|
398
395
|
|
|
399
|
-
|
|
400
|
-
const [, runtime] = runtimeEntries.find(([key]) => key.toLowerCase() === id.toLowerCase()) ?? []
|
|
401
|
-
const [, latest] = latestEntries.find(([key]) => key.toLowerCase() === id.toLowerCase()) ?? []
|
|
402
|
-
const [, legacy] = legacyEntries.find(([key]) => key.toLowerCase() === id.toLowerCase()) ?? []
|
|
396
|
+
const { options, format } = this.resolveHandlerConfiguration(id, runtimeEntries, latestEntries, legacyEntries)
|
|
403
397
|
|
|
404
|
-
if (
|
|
405
|
-
|
|
398
|
+
if (options.type === AZURE_BOT) {
|
|
399
|
+
// Set default values if not provided
|
|
400
|
+
options.title ||= 'Sign-in'
|
|
401
|
+
options.text ||= 'Please sign-in to continue'
|
|
402
|
+
options.oboScopes ??= []
|
|
403
|
+
options.enableSso = options.enableSso !== false // default value is true if undefined.
|
|
406
404
|
}
|
|
407
405
|
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
runtime.invalidSignInRetryMessage ??= runtime.messages?.invalidCode
|
|
413
|
-
runtime.invalidSignInRetryMessageFormat ??= runtime.messages?.invalidCodeFormat
|
|
414
|
-
runtime.invalidSignInRetryMaxExceededMessage ??= runtime.messages?.maxAttemptsExceeded
|
|
415
|
-
runtime.oboConnectionName ??= runtime.obo?.connection
|
|
416
|
-
runtime.oboScopes ??= runtime.obo?.scopes
|
|
417
|
-
delete runtime.name
|
|
418
|
-
delete runtime.maxAttempts
|
|
419
|
-
delete runtime.messages
|
|
420
|
-
delete runtime.obo
|
|
406
|
+
const maskedOptions: any = {}
|
|
407
|
+
|
|
408
|
+
if ('oboScopes' in options && options.oboScopes && options.oboScopes.length > 0) {
|
|
409
|
+
maskedOptions.oboScopes = redactScopes(options.oboScopes)
|
|
421
410
|
}
|
|
422
411
|
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
const entries = Object.entries(obj).filter(([, e]) => e !== undefined)
|
|
426
|
-
return Object.fromEntries(entries) as T
|
|
412
|
+
if ('scopes' in options && options.scopes && options.scopes.length > 0) {
|
|
413
|
+
maskedOptions.scopes = redactScopes(options.scopes)
|
|
427
414
|
}
|
|
428
415
|
|
|
429
|
-
|
|
430
|
-
// Pruning done individually to avoid overwriting with undefined when merging.
|
|
431
|
-
const options: AuthorizationOptions[string] = runtime ? { ...prune(legacy || {}), ...prune(runtime) } : prune(latest || {})
|
|
416
|
+
logger.info(this.prefix(id, 'settings loaded from \'%s\''), format, { ...options, ...maskedOptions })
|
|
432
417
|
|
|
433
418
|
if (!settings.storage) {
|
|
434
419
|
throw ExceptionHelper.generateException(Error, Errors.StorageRequiredForAuthorization)
|
|
@@ -437,12 +422,6 @@ export class AuthorizationManager {
|
|
|
437
422
|
if (options.type === AGENTIC) {
|
|
438
423
|
this._handlers[id] = new AgenticAuthorization(id, options, settings)
|
|
439
424
|
} else if (options.type === AZURE_BOT) {
|
|
440
|
-
// Set default values if not provided
|
|
441
|
-
options.title ||= 'Sign-in'
|
|
442
|
-
options.text ||= 'Please sign-in to continue'
|
|
443
|
-
options.oboScopes ??= []
|
|
444
|
-
options.enableSso = options.enableSso !== false // default value is true if undefined.
|
|
445
|
-
|
|
446
425
|
this._handlers[id] = new AzureBotAuthorization(id, options, settings)
|
|
447
426
|
}
|
|
448
427
|
|
|
@@ -473,4 +452,68 @@ export class AuthorizationManager {
|
|
|
473
452
|
|
|
474
453
|
throw ExceptionHelper.generateException(Error, Errors.UnsupportedAuthHandlerType, undefined, { handlerType: type })
|
|
475
454
|
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Resolves the effective handler configuration for a given handler ID.
|
|
458
|
+
*/
|
|
459
|
+
private resolveHandlerConfiguration (
|
|
460
|
+
id: string,
|
|
461
|
+
runtimeEntries: Array<[string, AuthorizationOptions[string]]>,
|
|
462
|
+
latestEntries: Array<[string, AuthorizationOptions[string]]>,
|
|
463
|
+
legacyEntries: Array<[string, AuthorizationOptions[string]]>
|
|
464
|
+
): {
|
|
465
|
+
options: AuthorizationOptions[string];
|
|
466
|
+
format: string;
|
|
467
|
+
} {
|
|
468
|
+
const matchesId = ([_id]: [string, AuthorizationOptions[string]]) => _id.toLowerCase() === id.toLowerCase()
|
|
469
|
+
|
|
470
|
+
// Find entries case-insensitively for later processing
|
|
471
|
+
const [, runtime] = runtimeEntries.find(matchesId) ?? []
|
|
472
|
+
const [, latest] = latestEntries.find(matchesId) ?? []
|
|
473
|
+
const [, legacy] = legacyEntries.find(matchesId) ?? []
|
|
474
|
+
|
|
475
|
+
if (runtime !== undefined && latest !== undefined) {
|
|
476
|
+
logger.warn(this.prefix(id, 'Both runtime options and latest environment variable configurations detected. Runtime configuration will take precedence over latest environment variables.'))
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
const runtimeLegacyKeys = ['name', 'maxAttempts', 'messages', 'obo']
|
|
480
|
+
const isRuntimeLegacy = runtime !== undefined && Object.keys(runtime).some(key => runtimeLegacyKeys.includes(key))
|
|
481
|
+
|
|
482
|
+
let runtimeLegacyFormat = ''
|
|
483
|
+
if (isRuntimeLegacy && runtime.type === AZURE_BOT) {
|
|
484
|
+
runtime.azureBotOAuthConnectionName ??= runtime.name
|
|
485
|
+
runtime.invalidSignInRetryMax ??= runtime.maxAttempts
|
|
486
|
+
runtime.invalidSignInRetryMessage ??= runtime.messages?.invalidCode
|
|
487
|
+
runtime.invalidSignInRetryMessageFormat ??= runtime.messages?.invalidCodeFormat
|
|
488
|
+
runtime.invalidSignInRetryMaxExceededMessage ??= runtime.messages?.maxAttemptsExceeded
|
|
489
|
+
runtime.oboConnectionName ??= runtime.obo?.connection
|
|
490
|
+
runtime.oboScopes ??= runtime.obo?.scopes
|
|
491
|
+
delete runtime.name
|
|
492
|
+
delete runtime.maxAttempts
|
|
493
|
+
delete runtime.messages
|
|
494
|
+
delete runtime.obo
|
|
495
|
+
runtimeLegacyFormat = 'runtime options (legacy)'
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
if (runtime !== undefined && legacy !== undefined) {
|
|
499
|
+
return { format: `${runtimeLegacyFormat || 'runtime options'} + .env variables (legacy)`, options: { ...prune(legacy), ...prune(runtime) } }
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
if (runtime !== undefined) {
|
|
503
|
+
return { format: runtimeLegacyFormat || 'runtime options', options: prune(runtime) }
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
if (latest !== undefined) {
|
|
507
|
+
return { format: '.env variables', options: prune(latest) }
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
if (legacy !== undefined) {
|
|
511
|
+
// Note: this should never happen, as legacy requires runtime to be present.
|
|
512
|
+
// However, we show a warning just in case to detect if something goes wrong.
|
|
513
|
+
logger.warn(this.prefix(id, 'Legacy environment variable configuration detected without a corresponding runtime options configuration.'))
|
|
514
|
+
return { format: '.env variables (legacy)', options: prune(legacy) }
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
return { format: 'empty options', options: {} }
|
|
518
|
+
}
|
|
476
519
|
}
|
|
@@ -334,7 +334,7 @@ export class AzureBotAuthorization implements AuthorizationHandler {
|
|
|
334
334
|
const { activity } = context
|
|
335
335
|
const userTokenClient = await this.getUserTokenClient(context)
|
|
336
336
|
// Using getTokenOrSignInResource instead of getUserToken to avoid HTTP 404 errors.
|
|
337
|
-
const { tokenResponse } = await userTokenClient.getTokenOrSignInResource(activity.from?.id!, this.options.azureBotOAuthConnectionName!, activity.channelId!, activity.getConversationReference(), activity.relatesTo!, '')
|
|
337
|
+
const { tokenResponse } = await userTokenClient.getTokenOrSignInResource(activity.from?.id!, this.options.azureBotOAuthConnectionName!, activity.channelId!, activity.getConversationReference({ forceBaseChannel: true }), activity.relatesTo!, '')
|
|
338
338
|
return tokenResponse?.token
|
|
339
339
|
}
|
|
340
340
|
|
|
@@ -399,7 +399,7 @@ export class AzureBotAuthorization implements AuthorizationHandler {
|
|
|
399
399
|
const { activity } = context
|
|
400
400
|
|
|
401
401
|
const userTokenClient = await this.getUserTokenClient(context)
|
|
402
|
-
const { tokenResponse, signInResource } = await userTokenClient.getTokenOrSignInResource(activity.from?.id!, this.options.azureBotOAuthConnectionName!, activity.channelId!, activity.getConversationReference(), activity.relatesTo!, code ?? '')
|
|
402
|
+
const { tokenResponse, signInResource } = await userTokenClient.getTokenOrSignInResource(activity.from?.id!, this.options.azureBotOAuthConnectionName!, activity.channelId!, activity.getConversationReference({ forceBaseChannel: true }), activity.relatesTo!, code ?? '')
|
|
403
403
|
|
|
404
404
|
if (!tokenResponse && active && code) {
|
|
405
405
|
reason.message = 'Invalid code entered. Restarting sign-in flow'
|
|
@@ -66,6 +66,9 @@ export class CreateConversationOptionsBuilder {
|
|
|
66
66
|
const builder = new CreateConversationOptionsBuilder(claims, channelId, serviceUrl)
|
|
67
67
|
if (parameters) {
|
|
68
68
|
builder._parameters = { ...builder._parameters, ...parameters }
|
|
69
|
+
if (parameters.activity) {
|
|
70
|
+
builder._activity = parameters.activity
|
|
71
|
+
}
|
|
69
72
|
}
|
|
70
73
|
|
|
71
74
|
// Set parameters.agent if not already provided — matches C# behavior
|
|
@@ -161,9 +164,10 @@ export class CreateConversationOptionsBuilder {
|
|
|
161
164
|
throw ExceptionHelper.generateException(Error, Errors.CreateConversationBuilderMembersRequired)
|
|
162
165
|
}
|
|
163
166
|
|
|
164
|
-
const
|
|
165
|
-
|
|
166
|
-
|
|
167
|
+
const isTeamsChannel = this._channelId === Channels.Msteams &&
|
|
168
|
+
Boolean((this._parameters.channelData as Record<string, any>)?.channel?.id)
|
|
169
|
+
if (isTeamsChannel && !this._activity) {
|
|
170
|
+
throw ExceptionHelper.generateException(Error, Errors.CreateConversationBuilderChannelActivityRequired)
|
|
167
171
|
}
|
|
168
172
|
|
|
169
173
|
return {
|
|
@@ -174,7 +178,7 @@ export class CreateConversationOptionsBuilder {
|
|
|
174
178
|
storeConversation: this._storeConversation,
|
|
175
179
|
parameters: {
|
|
176
180
|
...this._parameters,
|
|
177
|
-
activity:
|
|
181
|
+
activity: this._activity ? { type: 'message', ...this._activity } as Activity : undefined,
|
|
178
182
|
},
|
|
179
183
|
}
|
|
180
184
|
}
|