@openclaw/msteams 2026.3.12 → 2026.5.1-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 +161 -9
  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 +174 -437
  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 +148 -14
  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 +258 -0
  78. package/src/graph-upload.ts +87 -8
  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 +522 -45
  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 +477 -174
  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 +301 -106
  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 +34 -40
  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 -101
  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
@@ -0,0 +1,563 @@
1
+ import { beforeAll, describe, expect, it, vi } from "vitest";
2
+ import type { OpenClawConfig } from "../runtime-api.js";
3
+ import {
4
+ type MSTeamsActivityHandler,
5
+ type MSTeamsMessageHandlerDeps,
6
+ registerMSTeamsHandlers,
7
+ } from "./monitor-handler.js";
8
+ import {
9
+ createActivityHandler as baseCreateActivityHandler,
10
+ createMSTeamsMessageHandlerDeps,
11
+ installMSTeamsTestRuntime,
12
+ } from "./monitor-handler.test-helpers.js";
13
+ import type { MSTeamsTurnContext } from "./sdk-types.js";
14
+ import { createMSTeamsSsoTokenStoreMemory } from "./sso-token-store.js";
15
+ import {
16
+ type MSTeamsSsoFetch,
17
+ handleSigninTokenExchangeInvoke,
18
+ handleSigninVerifyStateInvoke,
19
+ parseSigninTokenExchangeValue,
20
+ parseSigninVerifyStateValue,
21
+ } from "./sso.js";
22
+
23
+ function createActivityHandler() {
24
+ const run = vi.fn(async () => undefined);
25
+ const handler = baseCreateActivityHandler(run);
26
+ return { handler, run };
27
+ }
28
+
29
+ function createDepsWithoutSso(
30
+ overrides: Partial<MSTeamsMessageHandlerDeps> = {},
31
+ ): MSTeamsMessageHandlerDeps {
32
+ const base = createMSTeamsMessageHandlerDeps();
33
+ return { ...base, ...overrides };
34
+ }
35
+
36
+ function createSsoDeps(params: { fetchImpl: MSTeamsSsoFetch }) {
37
+ const tokenStore = createMSTeamsSsoTokenStoreMemory();
38
+ const tokenProvider = {
39
+ getAccessToken: vi.fn(async () => "bf-service-token"),
40
+ };
41
+ return {
42
+ sso: {
43
+ tokenProvider,
44
+ tokenStore,
45
+ connectionName: "GraphConnection",
46
+ fetchImpl: params.fetchImpl,
47
+ },
48
+ tokenStore,
49
+ tokenProvider,
50
+ };
51
+ }
52
+
53
+ function createRegisteredSsoHandler(sso: MSTeamsMessageHandlerDeps["sso"]) {
54
+ const deps = createDepsWithoutSso({ sso });
55
+ const { handler } = createActivityHandler();
56
+ const registered = registerMSTeamsHandlers(handler, deps) as MSTeamsActivityHandler & {
57
+ run: NonNullable<MSTeamsActivityHandler["run"]>;
58
+ };
59
+ return { deps, registered };
60
+ }
61
+
62
+ function createSigninInvokeContext(params: {
63
+ name: "signin/tokenExchange" | "signin/verifyState";
64
+ value: unknown;
65
+ userAadId?: string;
66
+ userBfId?: string;
67
+ conversationId?: string;
68
+ conversationType?: "personal" | "groupChat" | "channel";
69
+ teamId?: string;
70
+ channelName?: string;
71
+ }): MSTeamsTurnContext & { sendActivity: ReturnType<typeof vi.fn> } {
72
+ const conversationType = params.conversationType ?? "personal";
73
+ const conversationId =
74
+ params.conversationId ??
75
+ (conversationType === "personal"
76
+ ? "19:personal-chat"
77
+ : conversationType === "channel"
78
+ ? "19:channel@thread.tacv2"
79
+ : "19:group@thread.tacv2");
80
+
81
+ return {
82
+ activity: {
83
+ id: "invoke-1",
84
+ type: "invoke",
85
+ name: params.name,
86
+ channelId: "msteams",
87
+ serviceUrl: "https://service.example.test",
88
+ from: {
89
+ id: params.userBfId ?? "bf-user",
90
+ aadObjectId: params.userAadId ?? "aad-user-guid",
91
+ name: "Test User",
92
+ },
93
+ recipient: { id: "bot-id", name: "Bot" },
94
+ conversation: {
95
+ id: conversationId,
96
+ conversationType,
97
+ tenantId: params.teamId ? "tenant-1" : undefined,
98
+ },
99
+ channelData: params.teamId
100
+ ? {
101
+ team: { id: params.teamId, name: "Team 1" },
102
+ channel: params.channelName ? { name: params.channelName } : undefined,
103
+ }
104
+ : {},
105
+ attachments: [],
106
+ value: params.value,
107
+ },
108
+ sendActivity: vi.fn(async () => ({ id: "ack-id" })),
109
+ sendActivities: vi.fn(async () => []),
110
+ updateActivity: vi.fn(async () => ({ id: "update" })),
111
+ deleteActivity: vi.fn(async () => {}),
112
+ } as unknown as MSTeamsTurnContext & {
113
+ sendActivity: ReturnType<typeof vi.fn>;
114
+ };
115
+ }
116
+
117
+ function createFakeFetch(handlers: Array<(url: string, init?: unknown) => unknown>) {
118
+ const calls: Array<{ url: string; init?: unknown }> = [];
119
+ const fetchImpl: MSTeamsSsoFetch = async (url, init) => {
120
+ calls.push({ url, init });
121
+ const handler = handlers.shift();
122
+ if (!handler) {
123
+ throw new Error("unexpected fetch call");
124
+ }
125
+ const response = handler(url, init) as {
126
+ ok: boolean;
127
+ status: number;
128
+ body: unknown;
129
+ };
130
+ return {
131
+ ok: response.ok,
132
+ status: response.status,
133
+ json: async () => response.body,
134
+ text: async () =>
135
+ typeof response.body === "string" ? response.body : JSON.stringify(response.body ?? ""),
136
+ };
137
+ };
138
+ return { fetchImpl, calls };
139
+ }
140
+
141
+ function createBlockedSigninScenarios() {
142
+ return [
143
+ {
144
+ name: "DM sender outside allowlist",
145
+ cfg: {
146
+ channels: {
147
+ msteams: {
148
+ dmPolicy: "allowlist",
149
+ allowFrom: ["owner-aad"],
150
+ },
151
+ },
152
+ } as OpenClawConfig,
153
+ context: {
154
+ userAadId: "blocked-dm-aad",
155
+ },
156
+ expectedDropLog: "dropping signin invoke (dm sender not allowlisted)",
157
+ },
158
+ {
159
+ name: "channel outside route allowlist",
160
+ cfg: {
161
+ channels: {
162
+ msteams: {
163
+ groupPolicy: "allowlist",
164
+ groupAllowFrom: ["blocked-channel-aad"],
165
+ teams: {
166
+ "team-allowlisted": {
167
+ channels: {
168
+ "19:allowlisted@thread.tacv2": { requireMention: false },
169
+ },
170
+ },
171
+ },
172
+ },
173
+ },
174
+ } as OpenClawConfig,
175
+ context: {
176
+ userAadId: "blocked-channel-aad",
177
+ conversationType: "channel" as const,
178
+ conversationId: "19:blocked-channel@thread.tacv2",
179
+ teamId: "team-blocked",
180
+ channelName: "General",
181
+ },
182
+ expectedDropLog: "dropping signin invoke (not in team/channel allowlist)",
183
+ },
184
+ {
185
+ name: "group sender outside group allowlist",
186
+ cfg: {
187
+ channels: {
188
+ msteams: {
189
+ groupPolicy: "allowlist",
190
+ groupAllowFrom: ["owner-aad"],
191
+ },
192
+ },
193
+ } as OpenClawConfig,
194
+ context: {
195
+ userAadId: "blocked-group-aad",
196
+ conversationType: "groupChat" as const,
197
+ conversationId: "19:group-chat@thread.v2",
198
+ },
199
+ expectedDropLog: "dropping signin invoke (group sender not allowlisted)",
200
+ },
201
+ ];
202
+ }
203
+
204
+ describe("msteams signin invoke value parsers", () => {
205
+ it("parses signin/tokenExchange values", () => {
206
+ expect(
207
+ parseSigninTokenExchangeValue({
208
+ id: "flow-1",
209
+ connectionName: "Graph",
210
+ token: "eyJ...",
211
+ }),
212
+ ).toEqual({ id: "flow-1", connectionName: "Graph", token: "eyJ..." });
213
+ });
214
+
215
+ it("rejects non-object signin/tokenExchange values", () => {
216
+ expect(parseSigninTokenExchangeValue(null)).toBeNull();
217
+ expect(parseSigninTokenExchangeValue("nope")).toBeNull();
218
+ });
219
+
220
+ it("parses signin/verifyState values", () => {
221
+ expect(parseSigninVerifyStateValue({ state: "123456" })).toEqual({ state: "123456" });
222
+ expect(parseSigninVerifyStateValue({})).toEqual({ state: undefined });
223
+ expect(parseSigninVerifyStateValue(null)).toBeNull();
224
+ });
225
+ });
226
+
227
+ describe("handleSigninTokenExchangeInvoke", () => {
228
+ it("exchanges the Teams token and persists the result", async () => {
229
+ const { fetchImpl, calls } = createFakeFetch([
230
+ () => ({
231
+ ok: true,
232
+ status: 200,
233
+ body: {
234
+ channelId: "msteams",
235
+ connectionName: "GraphConnection",
236
+ token: "delegated-graph-token",
237
+ expiration: "2030-01-01T00:00:00Z",
238
+ },
239
+ }),
240
+ ]);
241
+ const { sso, tokenStore } = createSsoDeps({ fetchImpl });
242
+
243
+ const result = await handleSigninTokenExchangeInvoke({
244
+ value: { id: "flow-1", connectionName: "GraphConnection", token: "exchangeable-token" },
245
+ user: { userId: "aad-user-guid", channelId: "msteams" },
246
+ deps: sso,
247
+ });
248
+
249
+ expect(result).toEqual({
250
+ ok: true,
251
+ token: "delegated-graph-token",
252
+ expiresAt: "2030-01-01T00:00:00Z",
253
+ });
254
+ expect(calls).toHaveLength(1);
255
+ expect(calls[0]?.url).toContain("/api/usertoken/exchange");
256
+ expect(calls[0]?.url).toContain("userId=aad-user-guid");
257
+ expect(calls[0]?.url).toContain("connectionName=GraphConnection");
258
+ expect(calls[0]?.url).toContain("channelId=msteams");
259
+
260
+ const init = calls[0]?.init as {
261
+ method?: string;
262
+ headers?: Record<string, string>;
263
+ body?: string;
264
+ };
265
+ expect(init?.method).toBe("POST");
266
+ expect(init?.headers?.Authorization).toBe("Bearer bf-service-token");
267
+ expect(JSON.parse(init?.body ?? "{}")).toEqual({ token: "exchangeable-token" });
268
+
269
+ const stored = await tokenStore.get({
270
+ connectionName: "GraphConnection",
271
+ userId: "aad-user-guid",
272
+ });
273
+ expect(stored?.token).toBe("delegated-graph-token");
274
+ expect(stored?.expiresAt).toBe("2030-01-01T00:00:00Z");
275
+ });
276
+
277
+ it("returns a service error when the User Token service rejects the exchange", async () => {
278
+ const { fetchImpl } = createFakeFetch([
279
+ () => ({ ok: false, status: 502, body: "bad gateway" }),
280
+ ]);
281
+ const { sso, tokenStore } = createSsoDeps({ fetchImpl });
282
+
283
+ const result = await handleSigninTokenExchangeInvoke({
284
+ value: { id: "flow-1", connectionName: "GraphConnection", token: "exchangeable-token" },
285
+ user: { userId: "aad-user-guid", channelId: "msteams" },
286
+ deps: sso,
287
+ });
288
+
289
+ expect(result.ok).toBe(false);
290
+ if (!result.ok) {
291
+ expect(result.code).toBe("service_error");
292
+ expect(result.status).toBe(502);
293
+ expect(result.message).toContain("bad gateway");
294
+ }
295
+ const stored = await tokenStore.get({
296
+ connectionName: "GraphConnection",
297
+ userId: "aad-user-guid",
298
+ });
299
+ expect(stored).toBeNull();
300
+ });
301
+
302
+ it("refuses to exchange without a user id", async () => {
303
+ const { fetchImpl, calls } = createFakeFetch([]);
304
+ const { sso } = createSsoDeps({ fetchImpl });
305
+
306
+ const result = await handleSigninTokenExchangeInvoke({
307
+ value: { id: "flow-1", connectionName: "GraphConnection", token: "exchangeable-token" },
308
+ user: { userId: "", channelId: "msteams" },
309
+ deps: sso,
310
+ });
311
+ expect(result.ok).toBe(false);
312
+ if (!result.ok) {
313
+ expect(result.code).toBe("missing_user");
314
+ }
315
+ expect(calls).toHaveLength(0);
316
+ });
317
+ });
318
+
319
+ describe("handleSigninVerifyStateInvoke", () => {
320
+ it("fetches the user token for the magic code and persists it", async () => {
321
+ const { fetchImpl, calls } = createFakeFetch([
322
+ () => ({
323
+ ok: true,
324
+ status: 200,
325
+ body: {
326
+ channelId: "msteams",
327
+ connectionName: "GraphConnection",
328
+ token: "delegated-token-2",
329
+ expiration: "2031-02-03T04:05:06Z",
330
+ },
331
+ }),
332
+ ]);
333
+ const { sso, tokenStore } = createSsoDeps({ fetchImpl });
334
+
335
+ const result = await handleSigninVerifyStateInvoke({
336
+ value: { state: "654321" },
337
+ user: { userId: "aad-user-guid", channelId: "msteams" },
338
+ deps: sso,
339
+ });
340
+
341
+ expect(result.ok).toBe(true);
342
+ expect(calls[0]?.url).toContain("/api/usertoken/GetToken");
343
+ expect(calls[0]?.url).toContain("code=654321");
344
+ const init = calls[0]?.init as { method?: string };
345
+ expect(init?.method).toBe("GET");
346
+
347
+ const stored = await tokenStore.get({
348
+ connectionName: "GraphConnection",
349
+ userId: "aad-user-guid",
350
+ });
351
+ expect(stored?.token).toBe("delegated-token-2");
352
+ });
353
+
354
+ it("rejects invocations without a state code", async () => {
355
+ const { fetchImpl, calls } = createFakeFetch([]);
356
+ const { sso } = createSsoDeps({ fetchImpl });
357
+ const result = await handleSigninVerifyStateInvoke({
358
+ value: { state: " " },
359
+ user: { userId: "aad-user-guid", channelId: "msteams" },
360
+ deps: sso,
361
+ });
362
+ expect(result.ok).toBe(false);
363
+ if (!result.ok) {
364
+ expect(result.code).toBe("missing_state");
365
+ }
366
+ expect(calls).toHaveLength(0);
367
+ });
368
+ });
369
+
370
+ describe("msteams signin invoke handler registration", () => {
371
+ beforeAll(() => {
372
+ installMSTeamsTestRuntime();
373
+ });
374
+
375
+ const blockedSigninScenarios = createBlockedSigninScenarios();
376
+ const invokeVariants = [
377
+ {
378
+ name: "signin/tokenExchange" as const,
379
+ value: { id: "x", connectionName: "GraphConnection", token: "exchangeable" },
380
+ },
381
+ {
382
+ name: "signin/verifyState" as const,
383
+ value: { state: "112233" },
384
+ },
385
+ ];
386
+
387
+ it("acks signin invokes even when sso is not configured", async () => {
388
+ const deps = createDepsWithoutSso();
389
+ const { handler, run } = createActivityHandler();
390
+ const registered = registerMSTeamsHandlers(handler, deps) as MSTeamsActivityHandler & {
391
+ run: NonNullable<MSTeamsActivityHandler["run"]>;
392
+ };
393
+
394
+ const ctx = createSigninInvokeContext({
395
+ name: "signin/tokenExchange",
396
+ value: { id: "x", connectionName: "Graph", token: "exchangeable" },
397
+ });
398
+
399
+ await registered.run(ctx);
400
+
401
+ expect(ctx.sendActivity).toHaveBeenCalledWith(
402
+ expect.objectContaining({
403
+ type: "invokeResponse",
404
+ value: expect.objectContaining({ status: 200 }),
405
+ }),
406
+ );
407
+ expect(run).not.toHaveBeenCalled();
408
+ expect(deps.log.debug).toHaveBeenCalledWith(
409
+ "signin invoke received but msteams.sso is not configured",
410
+ expect.objectContaining({ name: "signin/tokenExchange" }),
411
+ );
412
+ });
413
+
414
+ for (const invoke of invokeVariants) {
415
+ for (const scenario of blockedSigninScenarios) {
416
+ it(`does not process ${invoke.name} for ${scenario.name}`, async () => {
417
+ const { fetchImpl, calls } = createFakeFetch([
418
+ () => ({
419
+ ok: true,
420
+ status: 200,
421
+ body: {
422
+ channelId: "msteams",
423
+ connectionName: "GraphConnection",
424
+ token: "delegated-graph-token",
425
+ expiration: "2030-01-01T00:00:00Z",
426
+ },
427
+ }),
428
+ ]);
429
+ const { sso, tokenStore } = createSsoDeps({ fetchImpl });
430
+ const deps = createDepsWithoutSso({ cfg: scenario.cfg, sso });
431
+ const { handler } = createActivityHandler();
432
+ const registered = registerMSTeamsHandlers(handler, deps) as MSTeamsActivityHandler & {
433
+ run: NonNullable<MSTeamsActivityHandler["run"]>;
434
+ };
435
+
436
+ const ctx = createSigninInvokeContext({
437
+ name: invoke.name,
438
+ value: invoke.value,
439
+ ...scenario.context,
440
+ });
441
+
442
+ await registered.run(ctx);
443
+
444
+ expect(ctx.sendActivity).toHaveBeenCalledWith(
445
+ expect.objectContaining({
446
+ type: "invokeResponse",
447
+ value: expect.objectContaining({ status: 200 }),
448
+ }),
449
+ );
450
+ expect(calls).toHaveLength(0);
451
+ const stored = await tokenStore.get({
452
+ connectionName: "GraphConnection",
453
+ userId: scenario.context.userAadId ?? "aad-user-guid",
454
+ });
455
+ expect(stored).toBeNull();
456
+ expect(deps.log.debug).toHaveBeenCalledWith(
457
+ scenario.expectedDropLog,
458
+ expect.objectContaining({ name: invoke.name }),
459
+ );
460
+ });
461
+ }
462
+ }
463
+
464
+ it("invokes the token exchange handler when sso is configured", async () => {
465
+ const { fetchImpl } = createFakeFetch([
466
+ () => ({
467
+ ok: true,
468
+ status: 200,
469
+ body: {
470
+ channelId: "msteams",
471
+ connectionName: "GraphConnection",
472
+ token: "delegated-graph-token",
473
+ expiration: "2030-01-01T00:00:00Z",
474
+ },
475
+ }),
476
+ ]);
477
+ const { sso, tokenStore } = createSsoDeps({ fetchImpl });
478
+ const { deps, registered } = createRegisteredSsoHandler(sso);
479
+
480
+ const ctx = createSigninInvokeContext({
481
+ name: "signin/tokenExchange",
482
+ value: { id: "x", connectionName: "GraphConnection", token: "exchangeable" },
483
+ });
484
+
485
+ await registered.run(ctx);
486
+
487
+ expect(ctx.sendActivity).toHaveBeenCalledWith(
488
+ expect.objectContaining({
489
+ type: "invokeResponse",
490
+ value: expect.objectContaining({ status: 200 }),
491
+ }),
492
+ );
493
+ expect(deps.log.info).toHaveBeenCalledWith(
494
+ "msteams sso token exchanged",
495
+ expect.objectContaining({ userId: "aad-user-guid", hasExpiry: true }),
496
+ );
497
+ const stored = await tokenStore.get({
498
+ connectionName: "GraphConnection",
499
+ userId: "aad-user-guid",
500
+ });
501
+ expect(stored?.token).toBe("delegated-graph-token");
502
+ });
503
+
504
+ it("logs an error when the token exchange fails", async () => {
505
+ const { fetchImpl } = createFakeFetch([
506
+ () => ({ ok: false, status: 400, body: "bad request" }),
507
+ ]);
508
+ const { sso } = createSsoDeps({ fetchImpl });
509
+ const { deps, registered } = createRegisteredSsoHandler(sso);
510
+
511
+ const ctx = createSigninInvokeContext({
512
+ name: "signin/tokenExchange",
513
+ value: { id: "x", connectionName: "GraphConnection", token: "exchangeable" },
514
+ });
515
+
516
+ await registered.run(ctx);
517
+
518
+ expect(ctx.sendActivity).toHaveBeenCalledWith(
519
+ expect.objectContaining({ type: "invokeResponse" }),
520
+ );
521
+ expect(deps.log.error).toHaveBeenCalledWith(
522
+ "msteams sso token exchange failed",
523
+ expect.objectContaining({ code: "unexpected_response", status: 400 }),
524
+ );
525
+ });
526
+
527
+ it("handles signin/verifyState via the magic-code flow", async () => {
528
+ const { fetchImpl } = createFakeFetch([
529
+ () => ({
530
+ ok: true,
531
+ status: 200,
532
+ body: {
533
+ channelId: "msteams",
534
+ connectionName: "GraphConnection",
535
+ token: "delegated-token-3",
536
+ },
537
+ }),
538
+ ]);
539
+ const { sso, tokenStore } = createSsoDeps({ fetchImpl });
540
+ const deps = createDepsWithoutSso({ sso });
541
+ const { handler } = createActivityHandler();
542
+ const registered = registerMSTeamsHandlers(handler, deps) as MSTeamsActivityHandler & {
543
+ run: NonNullable<MSTeamsActivityHandler["run"]>;
544
+ };
545
+
546
+ const ctx = createSigninInvokeContext({
547
+ name: "signin/verifyState",
548
+ value: { state: "112233" },
549
+ });
550
+
551
+ await registered.run(ctx);
552
+
553
+ expect(deps.log.info).toHaveBeenCalledWith(
554
+ "msteams sso verifyState succeeded",
555
+ expect.objectContaining({ userId: "aad-user-guid" }),
556
+ );
557
+ const stored = await tokenStore.get({
558
+ connectionName: "GraphConnection",
559
+ userId: "aad-user-guid",
560
+ });
561
+ expect(stored?.token).toBe("delegated-token-3");
562
+ });
563
+ });