@openclaw/msteams 2026.5.2 → 2026.5.3-beta.2

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 (197) hide show
  1. package/dist/api.js +3 -0
  2. package/dist/channel-D7hdreTh.js +984 -0
  3. package/dist/channel-config-api.js +2 -0
  4. package/dist/channel-plugin-api.js +2 -0
  5. package/dist/channel.runtime-BC1ruIfN.js +573 -0
  6. package/dist/config-schema-B8QezH6t.js +15 -0
  7. package/dist/contract-api.js +2 -0
  8. package/dist/graph-users-9uQJepqr.js +1354 -0
  9. package/dist/index.js +22 -0
  10. package/dist/oauth-BWJyilR1.js +114 -0
  11. package/dist/oauth.token-xxpoLWy5.js +115 -0
  12. package/dist/policy-DTnU2GR7.js +142 -0
  13. package/dist/probe-D_H8yFps.js +2194 -0
  14. package/dist/resolve-allowlist-D41JSziq.js +219 -0
  15. package/dist/runtime-api-DV1iVMn1.js +28 -0
  16. package/dist/runtime-api.js +2 -0
  17. package/dist/secret-contract-BuoEXmPS.js +35 -0
  18. package/dist/secret-contract-api.js +2 -0
  19. package/dist/setup-entry.js +15 -0
  20. package/dist/setup-plugin-api.js +64 -0
  21. package/dist/setup-surface-BLkFQYIQ.js +313 -0
  22. package/dist/src-CFp1QpFd.js +4064 -0
  23. package/dist/test-api.js +2 -0
  24. package/package.json +14 -6
  25. package/api.ts +0 -3
  26. package/channel-config-api.ts +0 -1
  27. package/channel-plugin-api.ts +0 -2
  28. package/config-api.ts +0 -4
  29. package/contract-api.ts +0 -4
  30. package/index.ts +0 -20
  31. package/runtime-api.ts +0 -73
  32. package/secret-contract-api.ts +0 -5
  33. package/setup-entry.ts +0 -13
  34. package/setup-plugin-api.ts +0 -3
  35. package/src/ai-entity.ts +0 -7
  36. package/src/approval-auth.ts +0 -44
  37. package/src/attachments/bot-framework.test.ts +0 -461
  38. package/src/attachments/bot-framework.ts +0 -362
  39. package/src/attachments/download.ts +0 -311
  40. package/src/attachments/graph.test.ts +0 -416
  41. package/src/attachments/graph.ts +0 -484
  42. package/src/attachments/html.ts +0 -122
  43. package/src/attachments/payload.ts +0 -14
  44. package/src/attachments/remote-media.test.ts +0 -137
  45. package/src/attachments/remote-media.ts +0 -112
  46. package/src/attachments/shared.test.ts +0 -530
  47. package/src/attachments/shared.ts +0 -626
  48. package/src/attachments/types.ts +0 -47
  49. package/src/attachments.graph.test.ts +0 -342
  50. package/src/attachments.helpers.test.ts +0 -246
  51. package/src/attachments.test-helpers.ts +0 -17
  52. package/src/attachments.test.ts +0 -687
  53. package/src/attachments.ts +0 -18
  54. package/src/block-streaming-config.test.ts +0 -61
  55. package/src/channel-api.ts +0 -1
  56. package/src/channel.actions.test.ts +0 -742
  57. package/src/channel.directory.test.ts +0 -200
  58. package/src/channel.runtime.ts +0 -56
  59. package/src/channel.setup.ts +0 -77
  60. package/src/channel.test.ts +0 -128
  61. package/src/channel.ts +0 -1136
  62. package/src/config-schema.ts +0 -6
  63. package/src/config-ui-hints.ts +0 -12
  64. package/src/conversation-store-fs.test.ts +0 -74
  65. package/src/conversation-store-fs.ts +0 -149
  66. package/src/conversation-store-helpers.test.ts +0 -202
  67. package/src/conversation-store-helpers.ts +0 -105
  68. package/src/conversation-store-memory.ts +0 -51
  69. package/src/conversation-store.shared.test.ts +0 -225
  70. package/src/conversation-store.ts +0 -71
  71. package/src/directory-live.test.ts +0 -156
  72. package/src/directory-live.ts +0 -111
  73. package/src/doctor.ts +0 -27
  74. package/src/errors.test.ts +0 -133
  75. package/src/errors.ts +0 -246
  76. package/src/feedback-reflection-prompt.ts +0 -117
  77. package/src/feedback-reflection-store.ts +0 -114
  78. package/src/feedback-reflection.test.ts +0 -237
  79. package/src/feedback-reflection.ts +0 -283
  80. package/src/file-consent-helpers.test.ts +0 -326
  81. package/src/file-consent-helpers.ts +0 -126
  82. package/src/file-consent-invoke.ts +0 -150
  83. package/src/file-consent.test.ts +0 -363
  84. package/src/file-consent.ts +0 -287
  85. package/src/graph-chat.ts +0 -55
  86. package/src/graph-group-management.test.ts +0 -318
  87. package/src/graph-group-management.ts +0 -168
  88. package/src/graph-members.test.ts +0 -89
  89. package/src/graph-members.ts +0 -48
  90. package/src/graph-messages.actions.test.ts +0 -243
  91. package/src/graph-messages.read.test.ts +0 -391
  92. package/src/graph-messages.search.test.ts +0 -213
  93. package/src/graph-messages.test-helpers.ts +0 -50
  94. package/src/graph-messages.ts +0 -534
  95. package/src/graph-teams.test.ts +0 -215
  96. package/src/graph-teams.ts +0 -114
  97. package/src/graph-thread.test.ts +0 -246
  98. package/src/graph-thread.ts +0 -146
  99. package/src/graph-upload.test.ts +0 -258
  100. package/src/graph-upload.ts +0 -531
  101. package/src/graph-users.ts +0 -29
  102. package/src/graph.test.ts +0 -516
  103. package/src/graph.ts +0 -293
  104. package/src/inbound.test.ts +0 -221
  105. package/src/inbound.ts +0 -148
  106. package/src/index.ts +0 -4
  107. package/src/media-helpers.test.ts +0 -202
  108. package/src/media-helpers.ts +0 -105
  109. package/src/mentions.test.ts +0 -244
  110. package/src/mentions.ts +0 -114
  111. package/src/messenger.test.ts +0 -865
  112. package/src/messenger.ts +0 -605
  113. package/src/monitor-handler/access.ts +0 -125
  114. package/src/monitor-handler/inbound-media.test.ts +0 -289
  115. package/src/monitor-handler/inbound-media.ts +0 -180
  116. package/src/monitor-handler/message-handler-mock-support.test-support.ts +0 -28
  117. package/src/monitor-handler/message-handler.authz.test.ts +0 -669
  118. package/src/monitor-handler/message-handler.dm-media.test.ts +0 -54
  119. package/src/monitor-handler/message-handler.test-support.ts +0 -100
  120. package/src/monitor-handler/message-handler.thread-parent.test.ts +0 -223
  121. package/src/monitor-handler/message-handler.thread-session.test.ts +0 -77
  122. package/src/monitor-handler/message-handler.ts +0 -1000
  123. package/src/monitor-handler/reaction-handler.test.ts +0 -267
  124. package/src/monitor-handler/reaction-handler.ts +0 -210
  125. package/src/monitor-handler/thread-session.ts +0 -17
  126. package/src/monitor-handler.adaptive-card.test.ts +0 -162
  127. package/src/monitor-handler.feedback-authz.test.ts +0 -314
  128. package/src/monitor-handler.file-consent.test.ts +0 -423
  129. package/src/monitor-handler.sso.test.ts +0 -563
  130. package/src/monitor-handler.test-helpers.ts +0 -180
  131. package/src/monitor-handler.ts +0 -534
  132. package/src/monitor-handler.types.ts +0 -27
  133. package/src/monitor-types.ts +0 -6
  134. package/src/monitor.lifecycle.test.ts +0 -278
  135. package/src/monitor.test.ts +0 -119
  136. package/src/monitor.ts +0 -442
  137. package/src/oauth.flow.ts +0 -77
  138. package/src/oauth.shared.ts +0 -37
  139. package/src/oauth.test.ts +0 -305
  140. package/src/oauth.token.ts +0 -158
  141. package/src/oauth.ts +0 -130
  142. package/src/outbound.test.ts +0 -130
  143. package/src/outbound.ts +0 -71
  144. package/src/pending-uploads-fs.test.ts +0 -246
  145. package/src/pending-uploads-fs.ts +0 -235
  146. package/src/pending-uploads.test.ts +0 -173
  147. package/src/pending-uploads.ts +0 -121
  148. package/src/policy.test.ts +0 -240
  149. package/src/policy.ts +0 -262
  150. package/src/polls-store-memory.ts +0 -32
  151. package/src/polls.test.ts +0 -160
  152. package/src/polls.ts +0 -323
  153. package/src/presentation.ts +0 -68
  154. package/src/probe.test.ts +0 -77
  155. package/src/probe.ts +0 -132
  156. package/src/reply-dispatcher.test.ts +0 -437
  157. package/src/reply-dispatcher.ts +0 -346
  158. package/src/reply-stream-controller.test.ts +0 -235
  159. package/src/reply-stream-controller.ts +0 -147
  160. package/src/resolve-allowlist.test.ts +0 -250
  161. package/src/resolve-allowlist.ts +0 -309
  162. package/src/revoked-context.ts +0 -17
  163. package/src/runtime.ts +0 -9
  164. package/src/sdk-types.ts +0 -59
  165. package/src/sdk.test.ts +0 -666
  166. package/src/sdk.ts +0 -884
  167. package/src/secret-contract.ts +0 -49
  168. package/src/secret-input.ts +0 -7
  169. package/src/send-context.ts +0 -231
  170. package/src/send.test.ts +0 -493
  171. package/src/send.ts +0 -637
  172. package/src/sent-message-cache.test.ts +0 -15
  173. package/src/sent-message-cache.ts +0 -56
  174. package/src/session-route.ts +0 -40
  175. package/src/setup-core.ts +0 -160
  176. package/src/setup-surface.test.ts +0 -202
  177. package/src/setup-surface.ts +0 -320
  178. package/src/sso-token-store.test.ts +0 -72
  179. package/src/sso-token-store.ts +0 -166
  180. package/src/sso.ts +0 -300
  181. package/src/storage.ts +0 -25
  182. package/src/store-fs.ts +0 -44
  183. package/src/streaming-message.test.ts +0 -262
  184. package/src/streaming-message.ts +0 -297
  185. package/src/test-runtime.ts +0 -16
  186. package/src/thread-parent-context.test.ts +0 -224
  187. package/src/thread-parent-context.ts +0 -159
  188. package/src/token-response.ts +0 -11
  189. package/src/token.test.ts +0 -259
  190. package/src/token.ts +0 -195
  191. package/src/user-agent.test.ts +0 -86
  192. package/src/user-agent.ts +0 -53
  193. package/src/webhook-timeouts.ts +0 -27
  194. package/src/welcome-card.test.ts +0 -81
  195. package/src/welcome-card.ts +0 -57
  196. package/test-api.ts +0 -1
  197. package/tsconfig.json +0 -16
package/src/sdk.ts DELETED
@@ -1,884 +0,0 @@
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";
7
- import type { MSTeamsAdapter } from "./messenger.js";
8
- import type { MSTeamsCredentials, MSTeamsFederatedCredentials } from "./token.js";
9
- import { buildUserAgent } from "./user-agent.js";
10
-
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
- };
19
-
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
- };
107
- }
108
-
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(
117
- creds: MSTeamsCredentials,
118
- sdk: MSTeamsTeamsSdk,
119
- ): Promise<MSTeamsApp> {
120
- if (creds.type === "federated") {
121
- return createFederatedApp(creds, sdk);
122
- }
123
- return new sdk.App({
124
- clientId: creds.appId,
125
- clientSecret: creds.appPassword,
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,
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
- };
437
- }
438
-
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
- };
625
- }
626
-
627
- export async function loadMSTeamsSdkWithAuth(creds: MSTeamsCredentials) {
628
- const sdk = await loadMSTeamsSdk();
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
- };
884
- }