@openclaw/msteams 2026.3.13 → 2026.5.2-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (175) hide show
  1. package/api.ts +3 -0
  2. package/channel-config-api.ts +1 -0
  3. package/channel-plugin-api.ts +2 -0
  4. package/config-api.ts +4 -0
  5. package/contract-api.ts +4 -0
  6. package/index.ts +15 -12
  7. package/openclaw.plugin.json +553 -1
  8. package/package.json +46 -12
  9. package/runtime-api.ts +73 -0
  10. package/secret-contract-api.ts +5 -0
  11. package/setup-entry.ts +13 -0
  12. package/setup-plugin-api.ts +3 -0
  13. package/src/ai-entity.ts +7 -0
  14. package/src/approval-auth.ts +44 -0
  15. package/src/attachments/bot-framework.test.ts +461 -0
  16. package/src/attachments/bot-framework.ts +362 -0
  17. package/src/attachments/download.ts +63 -19
  18. package/src/attachments/graph.test.ts +416 -0
  19. package/src/attachments/graph.ts +163 -72
  20. package/src/attachments/html.ts +33 -1
  21. package/src/attachments/payload.ts +1 -1
  22. package/src/attachments/remote-media.test.ts +137 -0
  23. package/src/attachments/remote-media.ts +75 -8
  24. package/src/attachments/shared.test.ts +138 -1
  25. package/src/attachments/shared.ts +193 -26
  26. package/src/attachments/types.ts +10 -0
  27. package/src/attachments.graph.test.ts +342 -0
  28. package/src/attachments.helpers.test.ts +246 -0
  29. package/src/attachments.test-helpers.ts +17 -0
  30. package/src/attachments.test.ts +163 -418
  31. package/src/attachments.ts +5 -5
  32. package/src/block-streaming-config.test.ts +61 -0
  33. package/src/channel-api.ts +1 -0
  34. package/src/channel.actions.test.ts +742 -0
  35. package/src/channel.directory.test.ts +145 -4
  36. package/src/channel.runtime.ts +56 -0
  37. package/src/channel.setup.ts +77 -0
  38. package/src/channel.test.ts +128 -0
  39. package/src/channel.ts +1077 -395
  40. package/src/config-schema.ts +6 -0
  41. package/src/config-ui-hints.ts +12 -0
  42. package/src/conversation-store-fs.test.ts +4 -5
  43. package/src/conversation-store-fs.ts +35 -51
  44. package/src/conversation-store-helpers.test.ts +202 -0
  45. package/src/conversation-store-helpers.ts +105 -0
  46. package/src/conversation-store-memory.ts +27 -23
  47. package/src/conversation-store.shared.test.ts +225 -0
  48. package/src/conversation-store.ts +30 -0
  49. package/src/directory-live.test.ts +156 -0
  50. package/src/directory-live.ts +7 -4
  51. package/src/doctor.ts +27 -0
  52. package/src/errors.test.ts +64 -1
  53. package/src/errors.ts +50 -9
  54. package/src/feedback-reflection-prompt.ts +117 -0
  55. package/src/feedback-reflection-store.ts +114 -0
  56. package/src/feedback-reflection.test.ts +237 -0
  57. package/src/feedback-reflection.ts +283 -0
  58. package/src/file-consent-helpers.test.ts +83 -0
  59. package/src/file-consent-helpers.ts +64 -11
  60. package/src/file-consent-invoke.ts +150 -0
  61. package/src/file-consent.test.ts +363 -0
  62. package/src/file-consent.ts +165 -4
  63. package/src/graph-chat.ts +5 -3
  64. package/src/graph-group-management.test.ts +318 -0
  65. package/src/graph-group-management.ts +168 -0
  66. package/src/graph-members.test.ts +89 -0
  67. package/src/graph-members.ts +48 -0
  68. package/src/graph-messages.actions.test.ts +243 -0
  69. package/src/graph-messages.read.test.ts +391 -0
  70. package/src/graph-messages.search.test.ts +213 -0
  71. package/src/graph-messages.test-helpers.ts +50 -0
  72. package/src/graph-messages.ts +534 -0
  73. package/src/graph-teams.test.ts +215 -0
  74. package/src/graph-teams.ts +114 -0
  75. package/src/graph-thread.test.ts +246 -0
  76. package/src/graph-thread.ts +146 -0
  77. package/src/graph-upload.test.ts +161 -4
  78. package/src/graph-upload.ts +147 -56
  79. package/src/graph.test.ts +516 -0
  80. package/src/graph.ts +233 -21
  81. package/src/inbound.test.ts +156 -1
  82. package/src/inbound.ts +101 -1
  83. package/src/media-helpers.ts +1 -1
  84. package/src/mentions.test.ts +27 -18
  85. package/src/mentions.ts +2 -2
  86. package/src/messenger.test.ts +504 -23
  87. package/src/messenger.ts +133 -52
  88. package/src/monitor-handler/access.ts +125 -0
  89. package/src/monitor-handler/inbound-media.test.ts +289 -0
  90. package/src/monitor-handler/inbound-media.ts +57 -5
  91. package/src/monitor-handler/message-handler-mock-support.test-support.ts +28 -0
  92. package/src/monitor-handler/message-handler.authz.test.ts +588 -74
  93. package/src/monitor-handler/message-handler.dm-media.test.ts +54 -0
  94. package/src/monitor-handler/message-handler.test-support.ts +100 -0
  95. package/src/monitor-handler/message-handler.thread-parent.test.ts +223 -0
  96. package/src/monitor-handler/message-handler.thread-session.test.ts +77 -0
  97. package/src/monitor-handler/message-handler.ts +470 -164
  98. package/src/monitor-handler/reaction-handler.test.ts +267 -0
  99. package/src/monitor-handler/reaction-handler.ts +210 -0
  100. package/src/monitor-handler/thread-session.ts +17 -0
  101. package/src/monitor-handler.adaptive-card.test.ts +162 -0
  102. package/src/monitor-handler.feedback-authz.test.ts +314 -0
  103. package/src/monitor-handler.file-consent.test.ts +281 -79
  104. package/src/monitor-handler.sso.test.ts +563 -0
  105. package/src/monitor-handler.test-helpers.ts +180 -0
  106. package/src/monitor-handler.ts +459 -115
  107. package/src/monitor-handler.types.ts +27 -0
  108. package/src/monitor-types.ts +1 -0
  109. package/src/monitor.lifecycle.test.ts +74 -10
  110. package/src/monitor.test.ts +35 -1
  111. package/src/monitor.ts +143 -46
  112. package/src/oauth.flow.ts +77 -0
  113. package/src/oauth.shared.ts +37 -0
  114. package/src/oauth.test.ts +305 -0
  115. package/src/oauth.token.ts +158 -0
  116. package/src/oauth.ts +130 -0
  117. package/src/outbound.test.ts +10 -11
  118. package/src/outbound.ts +62 -44
  119. package/src/pending-uploads-fs.test.ts +246 -0
  120. package/src/pending-uploads-fs.ts +235 -0
  121. package/src/pending-uploads.test.ts +173 -0
  122. package/src/pending-uploads.ts +34 -2
  123. package/src/policy.test.ts +11 -5
  124. package/src/policy.ts +5 -5
  125. package/src/polls.test.ts +106 -5
  126. package/src/polls.ts +15 -7
  127. package/src/presentation.ts +68 -0
  128. package/src/probe.test.ts +27 -8
  129. package/src/probe.ts +43 -9
  130. package/src/reply-dispatcher.test.ts +437 -0
  131. package/src/reply-dispatcher.ts +259 -73
  132. package/src/reply-stream-controller.test.ts +235 -0
  133. package/src/reply-stream-controller.ts +147 -0
  134. package/src/resolve-allowlist.test.ts +105 -1
  135. package/src/resolve-allowlist.ts +112 -7
  136. package/src/runtime.ts +6 -3
  137. package/src/sdk-types.ts +43 -3
  138. package/src/sdk.test.ts +666 -0
  139. package/src/sdk.ts +867 -16
  140. package/src/secret-contract.ts +49 -0
  141. package/src/secret-input.ts +1 -1
  142. package/src/send-context.ts +76 -9
  143. package/src/send.test.ts +389 -5
  144. package/src/send.ts +140 -32
  145. package/src/sent-message-cache.ts +30 -18
  146. package/src/session-route.ts +40 -0
  147. package/src/setup-core.ts +160 -0
  148. package/src/setup-surface.test.ts +202 -0
  149. package/src/setup-surface.ts +320 -0
  150. package/src/sso-token-store.test.ts +72 -0
  151. package/src/sso-token-store.ts +166 -0
  152. package/src/sso.ts +300 -0
  153. package/src/storage.ts +1 -1
  154. package/src/store-fs.ts +2 -2
  155. package/src/streaming-message.test.ts +262 -0
  156. package/src/streaming-message.ts +297 -0
  157. package/src/test-runtime.ts +1 -1
  158. package/src/thread-parent-context.test.ts +224 -0
  159. package/src/thread-parent-context.ts +159 -0
  160. package/src/token.test.ts +237 -50
  161. package/src/token.ts +162 -7
  162. package/src/user-agent.test.ts +86 -0
  163. package/src/user-agent.ts +53 -0
  164. package/src/webhook-timeouts.ts +27 -0
  165. package/src/welcome-card.test.ts +81 -0
  166. package/src/welcome-card.ts +57 -0
  167. package/test-api.ts +1 -0
  168. package/tsconfig.json +16 -0
  169. package/CHANGELOG.md +0 -107
  170. package/src/file-lock.ts +0 -1
  171. package/src/graph-users.test.ts +0 -66
  172. package/src/onboarding.ts +0 -381
  173. package/src/polls-store.test.ts +0 -38
  174. package/src/revoked-context.test.ts +0 -39
  175. package/src/token-response.test.ts +0 -23
package/src/sdk.ts CHANGED
@@ -1,33 +1,884 @@
1
+ import * as fs from "node:fs";
2
+ // IHttpServerAdapter is re-exported via the public barrel (`export * from './http'`)
3
+ // but tsgo cannot resolve the chain. Use the dist subpath directly (type-only import).
4
+ import type { IHttpServerAdapter } from "@microsoft/teams.apps/dist/http/index.js";
5
+ import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/ssrf-runtime";
6
+ import { formatUnknownError } from "./errors.js";
1
7
  import type { MSTeamsAdapter } from "./messenger.js";
2
- import type { MSTeamsCredentials } from "./token.js";
8
+ import type { MSTeamsCredentials, MSTeamsFederatedCredentials } from "./token.js";
9
+ import { buildUserAgent } from "./user-agent.js";
3
10
 
4
- export type MSTeamsSdk = typeof import("@microsoft/agents-hosting");
5
- export type MSTeamsAuthConfig = ReturnType<MSTeamsSdk["getAuthConfigWithDefaults"]>;
11
+ /**
12
+ * Resolved Teams SDK modules loaded lazily to avoid importing when the
13
+ * provider is disabled.
14
+ */
15
+ export type MSTeamsTeamsSdk = {
16
+ App: typeof import("@microsoft/teams.apps").App;
17
+ Client: typeof import("@microsoft/teams.api").Client;
18
+ };
6
19
 
7
- export async function loadMSTeamsSdk(): Promise<MSTeamsSdk> {
8
- return await import("@microsoft/agents-hosting");
20
+ /**
21
+ * A Teams SDK App instance used for token management and proactive messaging.
22
+ */
23
+ type MSTeamsApp = InstanceType<MSTeamsTeamsSdk["App"]>;
24
+
25
+ /**
26
+ * Token provider compatible with the existing codebase, wrapping the Teams
27
+ * SDK App's token methods.
28
+ */
29
+ type MSTeamsTokenProvider = {
30
+ getAccessToken: (scope: string) => Promise<string>;
31
+ };
32
+
33
+ type MSTeamsBotIdentity = {
34
+ id?: string;
35
+ name?: string;
36
+ };
37
+
38
+ type MSTeamsSendContext = {
39
+ sendActivity: (textOrActivity: string | object) => Promise<unknown>;
40
+ updateActivity: (activityUpdate: object) => Promise<{ id?: string } | void>;
41
+ deleteActivity: (activityId: string) => Promise<void>;
42
+ };
43
+
44
+ type MSTeamsProcessContext = MSTeamsSendContext & {
45
+ activity: Record<string, unknown> | undefined;
46
+ sendActivities: (
47
+ activities: Array<{ type: string } & Record<string, unknown>>,
48
+ ) => Promise<unknown[]>;
49
+ };
50
+
51
+ type AzureAccessToken = {
52
+ token?: string;
53
+ } | null;
54
+
55
+ type AzureTokenCredential = {
56
+ getToken: (scope: string | string[]) => Promise<AzureAccessToken>;
57
+ };
58
+
59
+ type AzureIdentityModule = {
60
+ ClientCertificateCredential: new (
61
+ tenantId: string,
62
+ clientId: string,
63
+ options: { certificate: string },
64
+ ) => AzureTokenCredential;
65
+ ManagedIdentityCredential: new (clientId?: string) => AzureTokenCredential;
66
+ };
67
+
68
+ const AZURE_IDENTITY_MODULE = "@azure/identity";
69
+
70
+ let azureIdentityModulePromise: Promise<AzureIdentityModule> | null = null;
71
+
72
+ async function loadAzureIdentity(): Promise<AzureIdentityModule> {
73
+ azureIdentityModulePromise ??= import(AZURE_IDENTITY_MODULE) as Promise<AzureIdentityModule>;
74
+ return azureIdentityModulePromise;
75
+ }
76
+
77
+ let msTeamsSdkPromise: Promise<MSTeamsTeamsSdk> | null = null;
78
+
79
+ async function loadMSTeamsSdk(): Promise<MSTeamsTeamsSdk> {
80
+ msTeamsSdkPromise ??= Promise.all([
81
+ import("@microsoft/teams.apps"),
82
+ import("@microsoft/teams.api"),
83
+ ]).then(([appsModule, apiModule]) => ({
84
+ App: appsModule.App,
85
+ Client: apiModule.Client,
86
+ }));
87
+ return msTeamsSdkPromise;
88
+ }
89
+
90
+ /**
91
+ * Create a no-op HTTP server adapter that satisfies the Teams SDK's
92
+ * IHttpServerAdapter interface without spinning up an Express server.
93
+ *
94
+ * OpenClaw manages its own Express server for the Teams webhook endpoint, so
95
+ * the SDK's built-in HTTP server is unnecessary. Passing this adapter via the
96
+ * `httpServerAdapter` option prevents the SDK from creating the default
97
+ * HttpPlugin (which uses the deprecated `plugins` array and registers an
98
+ * Express middleware with the pattern `/api*` — invalid in Express 5).
99
+ *
100
+ * See: https://github.com/openclaw/openclaw/issues/55161
101
+ * See: https://github.com/openclaw/openclaw/issues/60732
102
+ */
103
+ function createNoOpHttpServerAdapter(): IHttpServerAdapter {
104
+ return {
105
+ registerRoute() {},
106
+ };
9
107
  }
10
108
 
11
- export function buildMSTeamsAuthConfig(
109
+ /**
110
+ * Create a Teams SDK App instance from credentials. The App manages token
111
+ * acquisition, JWT validation, and the HTTP server lifecycle.
112
+ *
113
+ * This replaces the previous CloudAdapter + MsalTokenProvider + authorizeJWT
114
+ * from @microsoft/agents-hosting.
115
+ */
116
+ export async function createMSTeamsApp(
12
117
  creds: MSTeamsCredentials,
13
- sdk: MSTeamsSdk,
14
- ): MSTeamsAuthConfig {
15
- return sdk.getAuthConfigWithDefaults({
118
+ sdk: MSTeamsTeamsSdk,
119
+ ): Promise<MSTeamsApp> {
120
+ if (creds.type === "federated") {
121
+ return createFederatedApp(creds, sdk);
122
+ }
123
+ return new sdk.App({
16
124
  clientId: creds.appId,
17
125
  clientSecret: creds.appPassword,
18
126
  tenantId: creds.tenantId,
127
+ httpServerAdapter: createNoOpHttpServerAdapter(),
128
+ } as ConstructorParameters<MSTeamsTeamsSdk["App"]>[0]);
129
+ }
130
+
131
+ function createFederatedApp(creds: MSTeamsFederatedCredentials, sdk: MSTeamsTeamsSdk): MSTeamsApp {
132
+ if (creds.useManagedIdentity) {
133
+ return createManagedIdentityApp(creds, sdk);
134
+ }
135
+
136
+ // Certificate-based auth
137
+ if (!creds.certificatePath) {
138
+ throw new Error("Federated credentials require either a certificate path or managed identity.");
139
+ }
140
+
141
+ let privateKey: string;
142
+ try {
143
+ privateKey = fs.readFileSync(creds.certificatePath, "utf-8");
144
+ } catch (err: unknown) {
145
+ const msg = err instanceof Error ? err.message : String(err);
146
+ throw new Error(`Failed to read certificate file at '${creds.certificatePath}': ${msg}`, {
147
+ cause: err,
148
+ });
149
+ }
150
+
151
+ return createCertificateApp(creds, privateKey, sdk);
152
+ }
153
+
154
+ function createCertificateApp(
155
+ creds: MSTeamsFederatedCredentials,
156
+ privateKey: string,
157
+ sdk: MSTeamsTeamsSdk,
158
+ ): MSTeamsApp {
159
+ // Lazily create and cache the credential so the token cache is reused.
160
+ let credentialPromise: Promise<AzureTokenCredential> | null = null;
161
+
162
+ const getCredential = async () => {
163
+ if (!credentialPromise) {
164
+ credentialPromise = loadAzureIdentity().then(
165
+ (az) =>
166
+ new az.ClientCertificateCredential(creds.tenantId, creds.appId, {
167
+ certificate: privateKey,
168
+ }),
169
+ );
170
+ }
171
+ return credentialPromise;
172
+ };
173
+
174
+ const tokenProvider = async (scope: string | string[]): Promise<string> => {
175
+ const credential = await getCredential();
176
+ const token = await credential.getToken(scope);
177
+
178
+ if (!token?.token) {
179
+ throw new Error("Failed to acquire token via certificate credential.");
180
+ }
181
+
182
+ return token.token;
183
+ };
184
+
185
+ return new sdk.App({
186
+ clientId: creds.appId,
187
+ tenantId: creds.tenantId,
188
+ token: tokenProvider,
189
+ httpServerAdapter: createNoOpHttpServerAdapter(),
190
+ } as unknown as ConstructorParameters<MSTeamsTeamsSdk["App"]>[0]);
191
+ }
192
+
193
+ function createManagedIdentityApp(
194
+ creds: MSTeamsFederatedCredentials,
195
+ sdk: MSTeamsTeamsSdk,
196
+ ): MSTeamsApp {
197
+ // Lazily create and cache the credential instance so the token cache is
198
+ // reused across calls instead of hitting IMDS/AAD on every message.
199
+ let credentialPromise: Promise<AzureTokenCredential> | null = null;
200
+
201
+ const getCredential = async () => {
202
+ if (!credentialPromise) {
203
+ credentialPromise = loadAzureIdentity().then((az) =>
204
+ creds.managedIdentityClientId
205
+ ? new az.ManagedIdentityCredential(creds.managedIdentityClientId)
206
+ : new az.ManagedIdentityCredential(),
207
+ );
208
+ }
209
+ return credentialPromise;
210
+ };
211
+
212
+ const tokenProvider = async (scope: string | string[]): Promise<string> => {
213
+ const credential = await getCredential();
214
+ const token = await credential.getToken(scope);
215
+
216
+ if (!token?.token) {
217
+ throw new Error("Failed to acquire token via managed identity.");
218
+ }
219
+
220
+ return token.token;
221
+ };
222
+
223
+ return new sdk.App({
224
+ clientId: creds.appId,
225
+ tenantId: creds.tenantId,
226
+ token: tokenProvider,
227
+ httpServerAdapter: createNoOpHttpServerAdapter(),
228
+ } as unknown as ConstructorParameters<MSTeamsTeamsSdk["App"]>[0]);
229
+ }
230
+
231
+ /**
232
+ * Build a token provider that uses the Teams SDK App for token acquisition.
233
+ */
234
+ export function createMSTeamsTokenProvider(app: MSTeamsApp): MSTeamsTokenProvider {
235
+ return {
236
+ async getAccessToken(scope: string): Promise<string> {
237
+ if (scope.includes("graph.microsoft.com")) {
238
+ const token = await (
239
+ app as unknown as { getAppGraphToken(): Promise<{ toString(): string } | null> }
240
+ ).getAppGraphToken();
241
+ return token ? String(token) : "";
242
+ }
243
+ const token = await (
244
+ app as unknown as { getBotToken(): Promise<{ toString(): string } | null> }
245
+ ).getBotToken();
246
+ return token ? String(token) : "";
247
+ },
248
+ };
249
+ }
250
+
251
+ function createBotTokenGetter(app: MSTeamsApp): () => Promise<string | undefined> {
252
+ return async () => {
253
+ const token = await (
254
+ app as unknown as { getBotToken(): Promise<{ toString(): string } | null> }
255
+ ).getBotToken();
256
+ return token ? String(token) : undefined;
257
+ };
258
+ }
259
+
260
+ function createApiClient(
261
+ sdk: MSTeamsTeamsSdk,
262
+ serviceUrl: string,
263
+ getToken: () => Promise<string | undefined>,
264
+ ) {
265
+ return new sdk.Client(serviceUrl, {
266
+ token: async () => (await getToken()) || undefined,
267
+ headers: { "User-Agent": buildUserAgent() },
268
+ } as Record<string, unknown>);
269
+ }
270
+
271
+ function normalizeOutboundActivity(textOrActivity: string | object): Record<string, unknown> {
272
+ return typeof textOrActivity === "string"
273
+ ? ({ type: "message", text: textOrActivity } as Record<string, unknown>)
274
+ : (textOrActivity as Record<string, unknown>);
275
+ }
276
+
277
+ function createSendContext(params: {
278
+ sdk: MSTeamsTeamsSdk;
279
+ serviceUrl?: string;
280
+ conversationId?: string;
281
+ conversationType?: string;
282
+ bot?: MSTeamsBotIdentity;
283
+ replyToActivityId?: string;
284
+ getToken: () => Promise<string | undefined>;
285
+ treatInvokeResponseAsNoop?: boolean;
286
+ /**
287
+ * Azure AD tenant ID for the target conversation. Bot Framework requires this
288
+ * on outbound proactive activities so the connector can route them to the
289
+ * correct tenant. Missing `tenantId` causes HTTP 403 on proactive sends.
290
+ */
291
+ tenantId?: string;
292
+ /** Target user's Teams user ID (e.g. `29:xxx`); included on the recipient field for routing. */
293
+ recipientId?: string;
294
+ /** Target user's Azure AD object ID; included as the recipient on personal DMs. */
295
+ recipientAadObjectId?: string;
296
+ }): MSTeamsSendContext {
297
+ const apiClient =
298
+ params.serviceUrl && params.conversationId
299
+ ? createApiClient(params.sdk, params.serviceUrl, params.getToken)
300
+ : undefined;
301
+
302
+ return {
303
+ async sendActivity(textOrActivity: string | object): Promise<unknown> {
304
+ const msg = normalizeOutboundActivity(textOrActivity);
305
+ if (params.treatInvokeResponseAsNoop && msg.type === "invokeResponse") {
306
+ return { id: "invokeResponse" };
307
+ }
308
+ if (!apiClient || !params.conversationId) {
309
+ return { id: "unknown" };
310
+ }
311
+
312
+ // Merge caller-provided channelData with the tenant metadata so Bot
313
+ // Framework receives `channelData.tenant.id` (the canonical source it
314
+ // uses to route proactive sends). Preserve any existing channelData
315
+ // fields the caller set (e.g. feedbackLoopEnabled).
316
+ const existingChannelData =
317
+ msg.channelData && typeof msg.channelData === "object"
318
+ ? (msg.channelData as Record<string, unknown>)
319
+ : undefined;
320
+ const channelData = params.tenantId
321
+ ? {
322
+ ...existingChannelData,
323
+ tenant: { id: params.tenantId },
324
+ }
325
+ : existingChannelData;
326
+
327
+ return await apiClient.conversations.activities(params.conversationId).create({
328
+ type: "message",
329
+ ...msg,
330
+ ...(channelData ? { channelData } : {}),
331
+ from: params.bot?.id
332
+ ? { id: params.bot.id, name: params.bot.name ?? "", role: "bot" }
333
+ : undefined,
334
+ conversation: {
335
+ id: params.conversationId,
336
+ conversationType: params.conversationType ?? "personal",
337
+ ...(params.tenantId ? { tenantId: params.tenantId } : {}),
338
+ },
339
+ ...(params.recipientId || params.recipientAadObjectId
340
+ ? {
341
+ recipient: {
342
+ ...(params.recipientId ? { id: params.recipientId } : {}),
343
+ ...(params.recipientAadObjectId
344
+ ? { aadObjectId: params.recipientAadObjectId }
345
+ : {}),
346
+ },
347
+ }
348
+ : {}),
349
+ ...(params.replyToActivityId && !msg.replyToId
350
+ ? { replyToId: params.replyToActivityId }
351
+ : {}),
352
+ } as Parameters<
353
+ typeof apiClient.conversations.activities extends (id: string) => {
354
+ create: (a: infer _T) => unknown;
355
+ }
356
+ ? never
357
+ : never
358
+ >[0]);
359
+ },
360
+
361
+ async updateActivity(activityUpdate: object): Promise<{ id?: string } | void> {
362
+ const nextActivity = activityUpdate as { id?: string } & Record<string, unknown>;
363
+ const activityId = nextActivity.id;
364
+ if (!activityId) {
365
+ throw new Error("updateActivity requires an activity id");
366
+ }
367
+ if (!params.serviceUrl || !params.conversationId) {
368
+ return { id: "unknown" };
369
+ }
370
+ return await updateActivityViaRest({
371
+ serviceUrl: params.serviceUrl,
372
+ conversationId: params.conversationId,
373
+ activityId,
374
+ activity: nextActivity,
375
+ token: await params.getToken(),
376
+ });
377
+ },
378
+
379
+ async deleteActivity(activityId: string): Promise<void> {
380
+ if (!activityId) {
381
+ throw new Error("deleteActivity requires an activity id");
382
+ }
383
+ if (!params.serviceUrl || !params.conversationId) {
384
+ return;
385
+ }
386
+ await deleteActivityViaRest({
387
+ serviceUrl: params.serviceUrl,
388
+ conversationId: params.conversationId,
389
+ activityId,
390
+ token: await params.getToken(),
391
+ });
392
+ },
393
+ };
394
+ }
395
+
396
+ function createProcessContext(params: {
397
+ sdk: MSTeamsTeamsSdk;
398
+ activity: Record<string, unknown> | undefined;
399
+ getToken: () => Promise<string | undefined>;
400
+ }): MSTeamsProcessContext {
401
+ const serviceUrl = params.activity?.serviceUrl as string | undefined;
402
+ const conversationId = (params.activity?.conversation as Record<string, unknown>)?.id as
403
+ | string
404
+ | undefined;
405
+ const conversationType = (params.activity?.conversation as Record<string, unknown>)
406
+ ?.conversationType as string | undefined;
407
+ const replyToActivityId = params.activity?.id as string | undefined;
408
+ const bot: MSTeamsBotIdentity | undefined =
409
+ params.activity?.recipient && typeof params.activity.recipient === "object"
410
+ ? {
411
+ id: (params.activity.recipient as Record<string, unknown>).id as string | undefined,
412
+ name: (params.activity.recipient as Record<string, unknown>).name as string | undefined,
413
+ }
414
+ : undefined;
415
+ const sendContext = createSendContext({
416
+ sdk: params.sdk,
417
+ serviceUrl,
418
+ conversationId,
419
+ conversationType,
420
+ bot,
421
+ replyToActivityId,
422
+ getToken: params.getToken,
423
+ treatInvokeResponseAsNoop: true,
19
424
  });
425
+
426
+ return {
427
+ activity: params.activity,
428
+ ...sendContext,
429
+ async sendActivities(activities: Array<{ type: string } & Record<string, unknown>>) {
430
+ const results = [];
431
+ for (const activity of activities) {
432
+ results.push(await sendContext.sendActivity(activity));
433
+ }
434
+ return results;
435
+ },
436
+ };
20
437
  }
21
438
 
22
- export function createMSTeamsAdapter(
23
- authConfig: MSTeamsAuthConfig,
24
- sdk: MSTeamsSdk,
25
- ): MSTeamsAdapter {
26
- return new sdk.CloudAdapter(authConfig) as unknown as MSTeamsAdapter;
439
+ /**
440
+ * Update an existing activity via the Bot Framework REST API.
441
+ * PUT /v3/conversations/{conversationId}/activities/{activityId}
442
+ */
443
+ async function updateActivityViaRest(params: {
444
+ serviceUrl: string;
445
+ conversationId: string;
446
+ activityId: string;
447
+ activity: Record<string, unknown>;
448
+ token?: string;
449
+ }): Promise<{ id?: string }> {
450
+ const { serviceUrl, conversationId, activityId, activity, token } = params;
451
+ const baseUrl = serviceUrl.replace(/\/+$/, "");
452
+ const url = `${baseUrl}/v3/conversations/${encodeURIComponent(conversationId)}/activities/${encodeURIComponent(activityId)}`;
453
+
454
+ const headers: Record<string, string> = {
455
+ "Content-Type": "application/json",
456
+ "User-Agent": buildUserAgent(),
457
+ };
458
+ if (token) {
459
+ headers.Authorization = `Bearer ${token}`;
460
+ }
461
+
462
+ const currentFetch = globalThis.fetch;
463
+ const { response, release } = await fetchWithSsrFGuard({
464
+ url,
465
+ fetchImpl: async (input, guardedInit) => await currentFetch(input, guardedInit),
466
+ init: {
467
+ method: "PUT",
468
+ headers,
469
+ body: JSON.stringify({
470
+ type: "message",
471
+ ...activity,
472
+ id: activityId,
473
+ }),
474
+ },
475
+ auditContext: "msteams-update-activity",
476
+ });
477
+
478
+ try {
479
+ if (!response.ok) {
480
+ const body = await response.text().catch(() => "");
481
+ throw Object.assign(new Error(`updateActivity failed: HTTP ${response.status} ${body}`), {
482
+ statusCode: response.status,
483
+ });
484
+ }
485
+
486
+ return await response.json().catch(() => ({ id: activityId }));
487
+ } finally {
488
+ await release();
489
+ }
490
+ }
491
+
492
+ /**
493
+ * Delete an existing activity via the Bot Framework REST API.
494
+ * DELETE /v3/conversations/{conversationId}/activities/{activityId}
495
+ */
496
+ async function deleteActivityViaRest(params: {
497
+ serviceUrl: string;
498
+ conversationId: string;
499
+ activityId: string;
500
+ token?: string;
501
+ }): Promise<void> {
502
+ const { serviceUrl, conversationId, activityId, token } = params;
503
+ const baseUrl = serviceUrl.replace(/\/+$/, "");
504
+ const url = `${baseUrl}/v3/conversations/${encodeURIComponent(conversationId)}/activities/${encodeURIComponent(activityId)}`;
505
+
506
+ const headers: Record<string, string> = {
507
+ "User-Agent": buildUserAgent(),
508
+ };
509
+ if (token) {
510
+ headers.Authorization = `Bearer ${token}`;
511
+ }
512
+
513
+ const currentFetch = globalThis.fetch;
514
+ const { response, release } = await fetchWithSsrFGuard({
515
+ url,
516
+ fetchImpl: async (input, guardedInit) => await currentFetch(input, guardedInit),
517
+ init: {
518
+ method: "DELETE",
519
+ headers,
520
+ },
521
+ auditContext: "msteams-delete-activity",
522
+ });
523
+
524
+ try {
525
+ if (!response.ok) {
526
+ const body = await response.text().catch(() => "");
527
+ throw Object.assign(new Error(`deleteActivity failed: HTTP ${response.status} ${body}`), {
528
+ statusCode: response.status,
529
+ });
530
+ }
531
+ } finally {
532
+ await release();
533
+ }
534
+ }
535
+
536
+ /**
537
+ * Build a CloudAdapter-compatible adapter using the Teams SDK REST client.
538
+ *
539
+ * This replaces the previous CloudAdapter from @microsoft/agents-hosting.
540
+ * For incoming requests: the App's HTTP server handles JWT validation.
541
+ * For proactive sends: uses the Bot Framework REST API via
542
+ * @microsoft/teams.api Client.
543
+ */
544
+ export function createMSTeamsAdapter(app: MSTeamsApp, sdk: MSTeamsTeamsSdk): MSTeamsAdapter {
545
+ return {
546
+ async continueConversation(_appId, reference, logic) {
547
+ const serviceUrl = reference.serviceUrl;
548
+ if (!serviceUrl) {
549
+ throw new Error("Missing serviceUrl in conversation reference");
550
+ }
551
+
552
+ const conversationId = reference.conversation?.id;
553
+ if (!conversationId) {
554
+ throw new Error("Missing conversation.id in conversation reference");
555
+ }
556
+
557
+ // Bot Framework requires `tenantId` on proactive sends so the connector
558
+ // can route them to the correct Azure AD tenant. Without it, requests
559
+ // fail with HTTP 403. Prefer the top-level `reference.tenantId` (captured
560
+ // from `activity.channelData.tenant.id` at inbound time) and fall back
561
+ // to `conversation.tenantId` for older stored references.
562
+ const tenantId = reference.tenantId ?? reference.conversation?.tenantId;
563
+ const recipientAadObjectId = reference.aadObjectId ?? reference.user?.aadObjectId;
564
+
565
+ const recipientId = reference.user?.id;
566
+
567
+ const sendContext = createSendContext({
568
+ sdk,
569
+ serviceUrl,
570
+ conversationId,
571
+ conversationType: reference.conversation?.conversationType,
572
+ bot: reference.agent ?? undefined,
573
+ getToken: createBotTokenGetter(app),
574
+ tenantId,
575
+ recipientId,
576
+ recipientAadObjectId,
577
+ });
578
+
579
+ await logic(sendContext);
580
+ },
581
+
582
+ async process(req, res, logic) {
583
+ const request = req as { body?: Record<string, unknown> };
584
+ const response = res as {
585
+ status: (code: number) => { send: (body?: unknown) => void };
586
+ };
587
+
588
+ const activity = request.body;
589
+ const isInvoke = (activity as Record<string, unknown>)?.type === "invoke";
590
+
591
+ try {
592
+ const context = createProcessContext({
593
+ sdk,
594
+ activity,
595
+ getToken: createBotTokenGetter(app),
596
+ });
597
+
598
+ // For invoke activities, send HTTP 200 immediately before running
599
+ // handler logic so slow operations (file uploads, reflections) don't
600
+ // hit Teams invoke timeouts ("unable to reach app").
601
+ if (isInvoke) {
602
+ response.status(200).send();
603
+ }
604
+
605
+ await logic(context);
606
+
607
+ if (!isInvoke) {
608
+ response.status(200).send();
609
+ }
610
+ } catch (err) {
611
+ if (!isInvoke) {
612
+ response.status(500).send({ error: formatUnknownError(err) });
613
+ }
614
+ }
615
+ },
616
+
617
+ async updateActivity(_context, _activity) {
618
+ // No-op: updateActivity is handled via REST in streaming-message.ts
619
+ },
620
+
621
+ async deleteActivity(_context, _reference) {
622
+ // No-op: deleteActivity not yet implemented for Teams SDK adapter
623
+ },
624
+ };
27
625
  }
28
626
 
29
627
  export async function loadMSTeamsSdkWithAuth(creds: MSTeamsCredentials) {
30
628
  const sdk = await loadMSTeamsSdk();
31
- const authConfig = buildMSTeamsAuthConfig(creds, sdk);
32
- return { sdk, authConfig };
629
+ const app = await createMSTeamsApp(creds, sdk);
630
+ return { sdk, app };
631
+ }
632
+
633
+ /**
634
+ * Bot Framework issuer → JWKS mapping.
635
+ * During Microsoft's transition, inbound service tokens can be signed by either
636
+ * the legacy Bot Framework issuer or the Entra issuer. Each gets its own JWKS
637
+ * endpoint so we verify signatures with the correct key set.
638
+ */
639
+ const BOT_FRAMEWORK_ISSUERS: ReadonlyArray<{
640
+ issuer: string | ((tenantId: string) => string);
641
+ jwksUri: string;
642
+ }> = [
643
+ {
644
+ issuer: "https://api.botframework.com",
645
+ jwksUri: "https://login.botframework.com/v1/.well-known/keys",
646
+ },
647
+ {
648
+ issuer: (tenantId: string) => `https://login.microsoftonline.com/${tenantId}/v2.0`,
649
+ jwksUri: "https://login.microsoftonline.com/common/discovery/v2.0/keys",
650
+ },
651
+ {
652
+ // SingleTenant bot deployments (Microsoft's default since 2025-07-31) get
653
+ // tokens signed by the Azure AD v1 endpoint, whose issuer is scoped to the
654
+ // bot's tenant. This must be a function so each deployment accepts its own
655
+ // tenant rather than a single hardcoded one (#64270).
656
+ issuer: (tenantId: string) => `https://sts.windows.net/${tenantId}/`,
657
+ jwksUri: "https://login.microsoftonline.com/common/discovery/v2.0/keys",
658
+ },
659
+ ];
660
+
661
+ type BotFrameworkJwtDeps = {
662
+ jwt: Pick<typeof import("jsonwebtoken"), "decode" | "verify">;
663
+ JwksClient: typeof import("jwks-rsa").JwksClient;
664
+ };
665
+ type JsonwebtokenRuntime = BotFrameworkJwtDeps["jwt"];
666
+ type JwksClientCtor = BotFrameworkJwtDeps["JwksClient"];
667
+
668
+ const BOT_FRAMEWORK_GLOBAL_AUDIENCE = "https://api.botframework.com";
669
+
670
+ function isJwtPayloadObject(
671
+ value: unknown,
672
+ ): value is { iss?: unknown; aud?: unknown; appid?: unknown; azp?: unknown } {
673
+ return !!value && typeof value === "object" && !Array.isArray(value);
674
+ }
675
+
676
+ function getAudienceClaims(payload: unknown): string[] {
677
+ if (!isJwtPayloadObject(payload)) {
678
+ return [];
679
+ }
680
+ const audience = payload.aud;
681
+ if (typeof audience === "string") {
682
+ const trimmed = audience.trim();
683
+ return trimmed ? [trimmed] : [];
684
+ }
685
+ if (Array.isArray(audience)) {
686
+ return audience
687
+ .filter((value): value is string => typeof value === "string")
688
+ .map((value) => value.trim())
689
+ .filter(Boolean);
690
+ }
691
+ return [];
692
+ }
693
+
694
+ function normalizeBotIdentityClaim(value: unknown): string | null {
695
+ if (typeof value !== "string") {
696
+ return null;
697
+ }
698
+ const normalized = value.trim().toLowerCase();
699
+ return normalized || null;
700
+ }
701
+
702
+ function hasExpectedBotIdentity(payload: unknown, expectedAppId: string): boolean {
703
+ if (!isJwtPayloadObject(payload)) {
704
+ return false;
705
+ }
706
+ const expected = normalizeBotIdentityClaim(expectedAppId);
707
+ if (!expected) {
708
+ return false;
709
+ }
710
+ return (
711
+ normalizeBotIdentityClaim(payload.appid) === expected ||
712
+ normalizeBotIdentityClaim(payload.azp) === expected
713
+ );
714
+ }
715
+
716
+ let botFrameworkJwtDepsPromise: Promise<BotFrameworkJwtDeps> | null = null;
717
+
718
+ function hasDefaultExport(value: unknown): value is { default?: unknown } {
719
+ return !!value && typeof value === "object" && "default" in value;
720
+ }
721
+
722
+ function isJsonwebtokenRuntime(value: unknown): value is JsonwebtokenRuntime {
723
+ return (
724
+ !!value &&
725
+ typeof value === "object" &&
726
+ typeof (value as { decode?: unknown }).decode === "function" &&
727
+ typeof (value as { verify?: unknown }).verify === "function"
728
+ );
729
+ }
730
+
731
+ function loadJsonwebtokenRuntime(jwtModule: unknown): JsonwebtokenRuntime {
732
+ const jwt = hasDefaultExport(jwtModule) ? (jwtModule.default ?? jwtModule) : jwtModule;
733
+ if (!isJsonwebtokenRuntime(jwt)) {
734
+ throw new Error("jsonwebtoken did not export decode/verify");
735
+ }
736
+ return jwt;
737
+ }
738
+
739
+ function isJwksClientRuntime(value: unknown): value is JwksClientCtor {
740
+ return typeof value === "function";
741
+ }
742
+
743
+ function loadJwksClientRuntime(jwksModule: unknown): JwksClientCtor {
744
+ const direct =
745
+ jwksModule && typeof jwksModule === "object"
746
+ ? (jwksModule as { JwksClient?: unknown }).JwksClient
747
+ : undefined;
748
+ const fallback =
749
+ hasDefaultExport(jwksModule) && jwksModule.default && typeof jwksModule.default === "object"
750
+ ? (jwksModule.default as { JwksClient?: unknown }).JwksClient
751
+ : undefined;
752
+ const JwksClient = direct ?? fallback;
753
+ if (!isJwksClientRuntime(JwksClient)) {
754
+ throw new Error("jwks-rsa did not export JwksClient");
755
+ }
756
+ return JwksClient;
757
+ }
758
+
759
+ async function loadBotFrameworkJwtDeps(): Promise<BotFrameworkJwtDeps> {
760
+ botFrameworkJwtDepsPromise ??= Promise.all([import("jsonwebtoken"), import("jwks-rsa")]).then(
761
+ ([jwtModule, jwksModule]) => {
762
+ return {
763
+ jwt: loadJsonwebtokenRuntime(jwtModule),
764
+ JwksClient: loadJwksClientRuntime(jwksModule),
765
+ };
766
+ },
767
+ );
768
+ return botFrameworkJwtDepsPromise;
769
+ }
770
+
771
+ /**
772
+ * Create a Bot Framework JWT validator using jsonwebtoken + jwks-rsa directly.
773
+ *
774
+ * The @microsoft/teams.apps JwtValidator hardcodes audience to [clientId, api://clientId],
775
+ * which rejects valid Bot Framework tokens that carry aud: "https://api.botframework.com".
776
+ * This implementation uses jsonwebtoken directly with the correct audience list, matching
777
+ * the behavior of the legacy @microsoft/agents-hosting authorizeJWT middleware.
778
+ *
779
+ * Security invariants:
780
+ * - signature verification via issuer-specific JWKS endpoints
781
+ * - audience validation: appId, api://appId, and https://api.botframework.com
782
+ * - issuer validation: strict allowlist (Bot Framework + tenant-scoped Entra)
783
+ * - expiration validation with 5-minute clock tolerance
784
+ */
785
+ export async function createBotFrameworkJwtValidator(creds: MSTeamsCredentials): Promise<{
786
+ validate: (authHeader: string) => Promise<boolean>;
787
+ }> {
788
+ const { jwt, JwksClient } = await loadBotFrameworkJwtDeps();
789
+
790
+ const allowedAudiences: [string, ...string[]] = [
791
+ creds.appId,
792
+ `api://${creds.appId}`,
793
+ BOT_FRAMEWORK_GLOBAL_AUDIENCE,
794
+ ];
795
+
796
+ const allowedIssuers = BOT_FRAMEWORK_ISSUERS.map((entry) =>
797
+ typeof entry.issuer === "function" ? entry.issuer(creds.tenantId) : entry.issuer,
798
+ ) as [string, ...string[]];
799
+
800
+ // One JWKS client per distinct endpoint, cached for the validator lifetime.
801
+ const jwksClients = new Map<string, InstanceType<typeof JwksClient>>();
802
+ function getJwksClient(uri: string): InstanceType<typeof JwksClient> {
803
+ let client = jwksClients.get(uri);
804
+ if (!client) {
805
+ client = new JwksClient({
806
+ jwksUri: uri,
807
+ cache: true,
808
+ cacheMaxAge: 600_000,
809
+ rateLimit: true,
810
+ });
811
+ jwksClients.set(uri, client);
812
+ }
813
+ return client;
814
+ }
815
+
816
+ /** Decode the token header without verification to determine the kid. */
817
+ function decodeHeader(token: string): { kid?: string } | null {
818
+ const decoded = jwt.decode(token, { complete: true });
819
+ return decoded && typeof decoded === "object" ? (decoded.header as { kid?: string }) : null;
820
+ }
821
+
822
+ /** Resolve the issuer entry for a token's issuer claim (pre-verification). */
823
+ function resolveIssuerEntry(issuerClaim: string | undefined) {
824
+ if (!issuerClaim) {
825
+ return undefined;
826
+ }
827
+ return BOT_FRAMEWORK_ISSUERS.find((entry) => {
828
+ const expected =
829
+ typeof entry.issuer === "function" ? entry.issuer(creds.tenantId) : entry.issuer;
830
+ return expected === issuerClaim;
831
+ });
832
+ }
833
+
834
+ return {
835
+ async validate(authHeader: string, _serviceUrl?: string): Promise<boolean> {
836
+ const token = authHeader.startsWith("Bearer ") ? authHeader.slice(7) : authHeader;
837
+ if (!token) {
838
+ return false;
839
+ }
840
+
841
+ // Decode without verification to extract issuer and kid for key lookup.
842
+ const header = decodeHeader(token);
843
+ const unverifiedPayload = jwt.decode(token);
844
+ if (
845
+ !header?.kid ||
846
+ !isJwtPayloadObject(unverifiedPayload) ||
847
+ typeof unverifiedPayload.iss !== "string"
848
+ ) {
849
+ return false;
850
+ }
851
+
852
+ // Resolve which JWKS endpoint to use based on the issuer claim.
853
+ const issuerEntry = resolveIssuerEntry(unverifiedPayload.iss);
854
+ if (!issuerEntry) {
855
+ return false;
856
+ }
857
+
858
+ const client = getJwksClient(issuerEntry.jwksUri);
859
+ try {
860
+ const signingKey = await client.getSigningKey(header.kid);
861
+ const publicKey = signingKey.getPublicKey();
862
+ const verifiedPayload = jwt.verify(token, publicKey, {
863
+ audience: allowedAudiences,
864
+ issuer: allowedIssuers,
865
+ algorithms: ["RS256"],
866
+ clockTolerance: 300,
867
+ });
868
+ if (!isJwtPayloadObject(verifiedPayload)) {
869
+ return false;
870
+ }
871
+ const audiences = getAudienceClaims(verifiedPayload);
872
+ if (
873
+ audiences.includes(BOT_FRAMEWORK_GLOBAL_AUDIENCE) &&
874
+ !hasExpectedBotIdentity(verifiedPayload, creds.appId)
875
+ ) {
876
+ return false;
877
+ }
878
+ return true;
879
+ } catch {
880
+ return false;
881
+ }
882
+ },
883
+ };
33
884
  }