@kodelyth/msteams 2026.5.42 → 2026.6.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 (177) hide show
  1. package/klaw.plugin.json +726 -2
  2. package/package.json +18 -6
  3. package/api.ts +0 -3
  4. package/channel-config-api.ts +0 -1
  5. package/channel-plugin-api.ts +0 -2
  6. package/config-api.ts +0 -4
  7. package/contract-api.ts +0 -4
  8. package/index.ts +0 -20
  9. package/runtime-api.ts +0 -66
  10. package/secret-contract-api.ts +0 -5
  11. package/setup-entry.ts +0 -13
  12. package/setup-plugin-api.ts +0 -3
  13. package/src/ai-entity.ts +0 -7
  14. package/src/approval-auth.ts +0 -44
  15. package/src/attachments/bot-framework.test.ts +0 -506
  16. package/src/attachments/bot-framework.ts +0 -348
  17. package/src/attachments/download.ts +0 -328
  18. package/src/attachments/graph.test.ts +0 -441
  19. package/src/attachments/graph.ts +0 -489
  20. package/src/attachments/html.ts +0 -122
  21. package/src/attachments/payload.ts +0 -14
  22. package/src/attachments/remote-media.test.ts +0 -187
  23. package/src/attachments/remote-media.ts +0 -86
  24. package/src/attachments/shared.test.ts +0 -547
  25. package/src/attachments/shared.ts +0 -655
  26. package/src/attachments/types.ts +0 -47
  27. package/src/attachments.graph.test.ts +0 -414
  28. package/src/attachments.helpers.test.ts +0 -245
  29. package/src/attachments.test-helpers.ts +0 -17
  30. package/src/attachments.test.ts +0 -754
  31. package/src/attachments.ts +0 -18
  32. package/src/block-streaming-config.test.ts +0 -61
  33. package/src/channel-api.ts +0 -1
  34. package/src/channel.actions.test.ts +0 -797
  35. package/src/channel.directory.test.ts +0 -176
  36. package/src/channel.message-adapter.test.ts +0 -227
  37. package/src/channel.runtime.ts +0 -56
  38. package/src/channel.setup.ts +0 -77
  39. package/src/channel.test.ts +0 -136
  40. package/src/channel.ts +0 -1176
  41. package/src/config-schema.ts +0 -6
  42. package/src/config-ui-hints.ts +0 -40
  43. package/src/conversation-store-fs.test.ts +0 -81
  44. package/src/conversation-store-fs.ts +0 -149
  45. package/src/conversation-store-helpers.test.ts +0 -202
  46. package/src/conversation-store-helpers.ts +0 -105
  47. package/src/conversation-store-memory.ts +0 -51
  48. package/src/conversation-store.shared.test.ts +0 -260
  49. package/src/conversation-store.ts +0 -71
  50. package/src/directory-live.test.ts +0 -156
  51. package/src/directory-live.ts +0 -111
  52. package/src/doctor.ts +0 -27
  53. package/src/errors.test.ts +0 -154
  54. package/src/errors.ts +0 -270
  55. package/src/feedback-reflection-prompt.ts +0 -117
  56. package/src/feedback-reflection-store.ts +0 -113
  57. package/src/feedback-reflection.test.ts +0 -237
  58. package/src/feedback-reflection.ts +0 -268
  59. package/src/file-consent-helpers.test.ts +0 -328
  60. package/src/file-consent-helpers.ts +0 -115
  61. package/src/file-consent-invoke.ts +0 -150
  62. package/src/file-consent.test.ts +0 -378
  63. package/src/file-consent.ts +0 -223
  64. package/src/graph-chat.ts +0 -36
  65. package/src/graph-group-management.test.ts +0 -332
  66. package/src/graph-group-management.ts +0 -168
  67. package/src/graph-members.test.ts +0 -89
  68. package/src/graph-members.ts +0 -48
  69. package/src/graph-messages.actions.test.ts +0 -253
  70. package/src/graph-messages.read.test.ts +0 -391
  71. package/src/graph-messages.search.test.ts +0 -227
  72. package/src/graph-messages.test-helpers.ts +0 -50
  73. package/src/graph-messages.ts +0 -534
  74. package/src/graph-teams.test.ts +0 -222
  75. package/src/graph-teams.ts +0 -114
  76. package/src/graph-thread.test.ts +0 -252
  77. package/src/graph-thread.ts +0 -146
  78. package/src/graph-upload.test.ts +0 -253
  79. package/src/graph-upload.ts +0 -531
  80. package/src/graph-users.ts +0 -29
  81. package/src/graph.test.ts +0 -540
  82. package/src/graph.ts +0 -308
  83. package/src/inbound.test.ts +0 -221
  84. package/src/inbound.ts +0 -148
  85. package/src/index.ts +0 -4
  86. package/src/media-helpers.test.ts +0 -220
  87. package/src/media-helpers.ts +0 -105
  88. package/src/mentions.test.ts +0 -254
  89. package/src/mentions.ts +0 -114
  90. package/src/messenger.test.ts +0 -961
  91. package/src/messenger.ts +0 -608
  92. package/src/monitor-handler/access.ts +0 -136
  93. package/src/monitor-handler/inbound-media.test.ts +0 -314
  94. package/src/monitor-handler/inbound-media.ts +0 -180
  95. package/src/monitor-handler/message-handler-mock-support.test-support.ts +0 -28
  96. package/src/monitor-handler/message-handler.authz.test.ts +0 -739
  97. package/src/monitor-handler/message-handler.dm-media.test.ts +0 -54
  98. package/src/monitor-handler/message-handler.test-support.ts +0 -99
  99. package/src/monitor-handler/message-handler.thread-parent.test.ts +0 -225
  100. package/src/monitor-handler/message-handler.thread-session.test.ts +0 -132
  101. package/src/monitor-handler/message-handler.ts +0 -1003
  102. package/src/monitor-handler/reaction-handler.test.ts +0 -325
  103. package/src/monitor-handler/reaction-handler.ts +0 -122
  104. package/src/monitor-handler/thread-session.ts +0 -30
  105. package/src/monitor-handler.adaptive-card.test.ts +0 -158
  106. package/src/monitor-handler.feedback-authz.test.ts +0 -357
  107. package/src/monitor-handler.file-consent.test.ts +0 -443
  108. package/src/monitor-handler.sso.test.ts +0 -576
  109. package/src/monitor-handler.test-helpers.ts +0 -181
  110. package/src/monitor-handler.ts +0 -538
  111. package/src/monitor-handler.types.ts +0 -27
  112. package/src/monitor-types.ts +0 -6
  113. package/src/monitor.lifecycle.test.ts +0 -457
  114. package/src/monitor.test.ts +0 -119
  115. package/src/monitor.ts +0 -476
  116. package/src/oauth.flow.ts +0 -77
  117. package/src/oauth.shared.ts +0 -37
  118. package/src/oauth.test.ts +0 -350
  119. package/src/oauth.token.ts +0 -162
  120. package/src/oauth.ts +0 -130
  121. package/src/outbound.test.ts +0 -400
  122. package/src/outbound.ts +0 -198
  123. package/src/pending-uploads-fs.test.ts +0 -261
  124. package/src/pending-uploads-fs.ts +0 -235
  125. package/src/pending-uploads.test.ts +0 -186
  126. package/src/pending-uploads.ts +0 -121
  127. package/src/policy.test.ts +0 -156
  128. package/src/policy.ts +0 -245
  129. package/src/polls-store-memory.ts +0 -32
  130. package/src/polls.test.ts +0 -169
  131. package/src/polls.ts +0 -312
  132. package/src/presentation.ts +0 -93
  133. package/src/probe.test.ts +0 -79
  134. package/src/probe.ts +0 -132
  135. package/src/reply-dispatcher.test.ts +0 -543
  136. package/src/reply-dispatcher.ts +0 -523
  137. package/src/reply-stream-controller.test.ts +0 -424
  138. package/src/reply-stream-controller.ts +0 -334
  139. package/src/resolve-allowlist.test.ts +0 -253
  140. package/src/resolve-allowlist.ts +0 -309
  141. package/src/revoked-context.ts +0 -17
  142. package/src/runtime.ts +0 -12
  143. package/src/sdk-types.ts +0 -59
  144. package/src/sdk.test.ts +0 -727
  145. package/src/sdk.ts +0 -916
  146. package/src/secret-contract.ts +0 -49
  147. package/src/secret-input.ts +0 -7
  148. package/src/send-context.test.ts +0 -93
  149. package/src/send-context.ts +0 -269
  150. package/src/send.test.ts +0 -588
  151. package/src/send.ts +0 -697
  152. package/src/sent-message-cache.test.ts +0 -106
  153. package/src/sent-message-cache.ts +0 -174
  154. package/src/session-route.ts +0 -40
  155. package/src/setup-core.ts +0 -162
  156. package/src/setup-surface.test.ts +0 -175
  157. package/src/setup-surface.ts +0 -319
  158. package/src/sso-token-store.test.ts +0 -74
  159. package/src/sso-token-store.ts +0 -166
  160. package/src/sso.ts +0 -300
  161. package/src/storage.ts +0 -25
  162. package/src/store-fs.ts +0 -42
  163. package/src/streaming-message.test.ts +0 -323
  164. package/src/streaming-message.ts +0 -327
  165. package/src/test-runtime.ts +0 -16
  166. package/src/thread-parent-context.test.ts +0 -224
  167. package/src/thread-parent-context.ts +0 -159
  168. package/src/token-response.ts +0 -11
  169. package/src/token.test.ts +0 -268
  170. package/src/token.ts +0 -194
  171. package/src/user-agent.test.ts +0 -121
  172. package/src/user-agent.ts +0 -53
  173. package/src/webhook-timeouts.ts +0 -27
  174. package/src/welcome-card.test.ts +0 -104
  175. package/src/welcome-card.ts +0 -57
  176. package/test-api.ts +0 -1
  177. package/tsconfig.json +0 -16
package/src/sdk.ts DELETED
@@ -1,916 +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 "klaw/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
- * Klaw 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/klaw/klaw/issues/55161
101
- * See: https://github.com/klaw/klaw/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 (err) {
880
- // Network-level failures (DNS, firewall, TLS) must be distinguished from
881
- // invalid tokens so callers can log them at an appropriate severity.
882
- // Rethrow so the JWT middleware can emit an actionable warning instead of
883
- // silently returning 401 (which looks identical to a bad credential).
884
- if (isJwksNetworkError(err)) {
885
- throw err;
886
- }
887
- return false;
888
- }
889
- },
890
- };
891
- }
892
-
893
- /**
894
- * Return true when the error originated from a network-level failure fetching
895
- * the JWKS endpoint (DNS resolution, connection refused, TLS handshake, etc.)
896
- * rather than from token verification logic.
897
- */
898
- function isJwksNetworkError(err: unknown): boolean {
899
- if (!(err instanceof Error)) {
900
- return false;
901
- }
902
- const code = (err as NodeJS.ErrnoException).code;
903
- if (
904
- code === "ECONNREFUSED" ||
905
- code === "ENOTFOUND" ||
906
- code === "EHOSTUNREACH" ||
907
- code === "ETIMEDOUT" ||
908
- code === "ECONNRESET"
909
- ) {
910
- return true;
911
- }
912
- // jwks-rsa wraps fetch failures with a message containing the URL or "key fetching"
913
- return (
914
- /jwks|key fetch|getSigningKey/i.test(err.message) && /network|fetch|connect/i.test(err.message)
915
- );
916
- }