@openclaw/msteams 2026.3.13 → 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 +138 -1
  25. package/src/attachments/shared.ts +193 -26
  26. package/src/attachments/types.ts +10 -0
  27. package/src/attachments.graph.test.ts +342 -0
  28. package/src/attachments.helpers.test.ts +246 -0
  29. package/src/attachments.test-helpers.ts +17 -0
  30. package/src/attachments.test.ts +163 -418
  31. package/src/attachments.ts +5 -5
  32. package/src/block-streaming-config.test.ts +61 -0
  33. package/src/channel-api.ts +1 -0
  34. package/src/channel.actions.test.ts +742 -0
  35. package/src/channel.directory.test.ts +145 -4
  36. package/src/channel.runtime.ts +56 -0
  37. package/src/channel.setup.ts +77 -0
  38. package/src/channel.test.ts +128 -0
  39. package/src/channel.ts +1077 -395
  40. package/src/config-schema.ts +6 -0
  41. package/src/config-ui-hints.ts +12 -0
  42. package/src/conversation-store-fs.test.ts +4 -5
  43. package/src/conversation-store-fs.ts +35 -51
  44. package/src/conversation-store-helpers.test.ts +202 -0
  45. package/src/conversation-store-helpers.ts +105 -0
  46. package/src/conversation-store-memory.ts +27 -23
  47. package/src/conversation-store.shared.test.ts +225 -0
  48. package/src/conversation-store.ts +30 -0
  49. package/src/directory-live.test.ts +156 -0
  50. package/src/directory-live.ts +7 -4
  51. package/src/doctor.ts +27 -0
  52. package/src/errors.test.ts +64 -1
  53. package/src/errors.ts +50 -9
  54. package/src/feedback-reflection-prompt.ts +117 -0
  55. package/src/feedback-reflection-store.ts +114 -0
  56. package/src/feedback-reflection.test.ts +237 -0
  57. package/src/feedback-reflection.ts +283 -0
  58. package/src/file-consent-helpers.test.ts +83 -0
  59. package/src/file-consent-helpers.ts +64 -11
  60. package/src/file-consent-invoke.ts +150 -0
  61. package/src/file-consent.test.ts +363 -0
  62. package/src/file-consent.ts +165 -4
  63. package/src/graph-chat.ts +5 -3
  64. package/src/graph-group-management.test.ts +318 -0
  65. package/src/graph-group-management.ts +168 -0
  66. package/src/graph-members.test.ts +89 -0
  67. package/src/graph-members.ts +48 -0
  68. package/src/graph-messages.actions.test.ts +243 -0
  69. package/src/graph-messages.read.test.ts +391 -0
  70. package/src/graph-messages.search.test.ts +213 -0
  71. package/src/graph-messages.test-helpers.ts +50 -0
  72. package/src/graph-messages.ts +534 -0
  73. package/src/graph-teams.test.ts +215 -0
  74. package/src/graph-teams.ts +114 -0
  75. package/src/graph-thread.test.ts +246 -0
  76. package/src/graph-thread.ts +146 -0
  77. package/src/graph-upload.test.ts +161 -4
  78. package/src/graph-upload.ts +147 -56
  79. package/src/graph.test.ts +516 -0
  80. package/src/graph.ts +233 -21
  81. package/src/inbound.test.ts +156 -1
  82. package/src/inbound.ts +101 -1
  83. package/src/media-helpers.ts +1 -1
  84. package/src/mentions.test.ts +27 -18
  85. package/src/mentions.ts +2 -2
  86. package/src/messenger.test.ts +504 -23
  87. package/src/messenger.ts +133 -52
  88. package/src/monitor-handler/access.ts +125 -0
  89. package/src/monitor-handler/inbound-media.test.ts +289 -0
  90. package/src/monitor-handler/inbound-media.ts +57 -5
  91. package/src/monitor-handler/message-handler-mock-support.test-support.ts +28 -0
  92. package/src/monitor-handler/message-handler.authz.test.ts +588 -74
  93. package/src/monitor-handler/message-handler.dm-media.test.ts +54 -0
  94. package/src/monitor-handler/message-handler.test-support.ts +100 -0
  95. package/src/monitor-handler/message-handler.thread-parent.test.ts +223 -0
  96. package/src/monitor-handler/message-handler.thread-session.test.ts +77 -0
  97. package/src/monitor-handler/message-handler.ts +470 -164
  98. package/src/monitor-handler/reaction-handler.test.ts +267 -0
  99. package/src/monitor-handler/reaction-handler.ts +210 -0
  100. package/src/monitor-handler/thread-session.ts +17 -0
  101. package/src/monitor-handler.adaptive-card.test.ts +162 -0
  102. package/src/monitor-handler.feedback-authz.test.ts +314 -0
  103. package/src/monitor-handler.file-consent.test.ts +281 -79
  104. package/src/monitor-handler.sso.test.ts +563 -0
  105. package/src/monitor-handler.test-helpers.ts +180 -0
  106. package/src/monitor-handler.ts +459 -115
  107. package/src/monitor-handler.types.ts +27 -0
  108. package/src/monitor-types.ts +1 -0
  109. package/src/monitor.lifecycle.test.ts +74 -10
  110. package/src/monitor.test.ts +35 -1
  111. package/src/monitor.ts +143 -46
  112. package/src/oauth.flow.ts +77 -0
  113. package/src/oauth.shared.ts +37 -0
  114. package/src/oauth.test.ts +305 -0
  115. package/src/oauth.token.ts +158 -0
  116. package/src/oauth.ts +130 -0
  117. package/src/outbound.test.ts +10 -11
  118. package/src/outbound.ts +62 -44
  119. package/src/pending-uploads-fs.test.ts +246 -0
  120. package/src/pending-uploads-fs.ts +235 -0
  121. package/src/pending-uploads.test.ts +173 -0
  122. package/src/pending-uploads.ts +34 -2
  123. package/src/policy.test.ts +11 -5
  124. package/src/policy.ts +5 -5
  125. package/src/polls.test.ts +106 -5
  126. package/src/polls.ts +15 -7
  127. package/src/presentation.ts +68 -0
  128. package/src/probe.test.ts +27 -8
  129. package/src/probe.ts +43 -9
  130. package/src/reply-dispatcher.test.ts +437 -0
  131. package/src/reply-dispatcher.ts +259 -73
  132. package/src/reply-stream-controller.test.ts +235 -0
  133. package/src/reply-stream-controller.ts +147 -0
  134. package/src/resolve-allowlist.test.ts +105 -1
  135. package/src/resolve-allowlist.ts +112 -7
  136. package/src/runtime.ts +6 -3
  137. package/src/sdk-types.ts +43 -3
  138. package/src/sdk.test.ts +666 -0
  139. package/src/sdk.ts +867 -16
  140. package/src/secret-contract.ts +49 -0
  141. package/src/secret-input.ts +1 -1
  142. package/src/send-context.ts +76 -9
  143. package/src/send.test.ts +389 -5
  144. package/src/send.ts +140 -32
  145. package/src/sent-message-cache.ts +30 -18
  146. package/src/session-route.ts +40 -0
  147. package/src/setup-core.ts +160 -0
  148. package/src/setup-surface.test.ts +202 -0
  149. package/src/setup-surface.ts +320 -0
  150. package/src/sso-token-store.test.ts +72 -0
  151. package/src/sso-token-store.ts +166 -0
  152. package/src/sso.ts +300 -0
  153. package/src/storage.ts +1 -1
  154. package/src/store-fs.ts +2 -2
  155. package/src/streaming-message.test.ts +262 -0
  156. package/src/streaming-message.ts +297 -0
  157. package/src/test-runtime.ts +1 -1
  158. package/src/thread-parent-context.test.ts +224 -0
  159. package/src/thread-parent-context.ts +159 -0
  160. package/src/token.test.ts +237 -50
  161. package/src/token.ts +162 -7
  162. package/src/user-agent.test.ts +86 -0
  163. package/src/user-agent.ts +53 -0
  164. package/src/webhook-timeouts.ts +27 -0
  165. package/src/welcome-card.test.ts +81 -0
  166. package/src/welcome-card.ts +57 -0
  167. package/test-api.ts +1 -0
  168. package/tsconfig.json +16 -0
  169. package/CHANGELOG.md +0 -107
  170. package/src/file-lock.ts +0 -1
  171. package/src/graph-users.test.ts +0 -66
  172. package/src/onboarding.ts +0 -381
  173. package/src/polls-store.test.ts +0 -38
  174. package/src/revoked-context.test.ts +0 -39
  175. package/src/token-response.test.ts +0 -23
@@ -1,6 +1,7 @@
1
1
  import { EventEmitter } from "node:events";
2
- import type { OpenClawConfig, RuntimeEnv } from "openclaw/plugin-sdk/msteams";
2
+ import type { Request, Response } from "express";
3
3
  import { afterEach, describe, expect, it, vi } from "vitest";
4
+ import type { OpenClawConfig, RuntimeEnv } from "../runtime-api.js";
4
5
  import type { MSTeamsConversationStore } from "./conversation-store.js";
5
6
  import type { MSTeamsPollStore } from "./polls.js";
6
7
 
@@ -13,9 +14,14 @@ type FakeServer = EventEmitter & {
13
14
 
14
15
  const expressControl = vi.hoisted(() => ({
15
16
  mode: { value: "listening" as "listening" | "error" },
17
+ apps: [] as Array<{
18
+ use: ReturnType<typeof vi.fn>;
19
+ post: ReturnType<typeof vi.fn>;
20
+ listen: ReturnType<typeof vi.fn>;
21
+ }>,
16
22
  }));
17
23
 
18
- vi.mock("openclaw/plugin-sdk/msteams", () => ({
24
+ vi.mock("../runtime-api.js", () => ({
19
25
  DEFAULT_WEBHOOK_MAX_BODY_BYTES: 1024 * 1024,
20
26
  normalizeSecretInputString: (value: unknown) =>
21
27
  typeof value === "string" && value.trim() ? value.trim() : undefined,
@@ -72,8 +78,14 @@ vi.mock("express", () => {
72
78
  }),
73
79
  });
74
80
 
81
+ const wrappedFactory = () => {
82
+ const app = factory();
83
+ expressControl.apps.push(app);
84
+ return app;
85
+ };
86
+
75
87
  return {
76
- default: factory,
88
+ default: wrappedFactory,
77
89
  json,
78
90
  };
79
91
  });
@@ -88,11 +100,12 @@ const createMSTeamsAdapter = vi.hoisted(() =>
88
100
  process: vi.fn(async () => {}),
89
101
  })),
90
102
  );
103
+ const jwtValidate = vi.hoisted(() => vi.fn().mockResolvedValue(true));
91
104
  const loadMSTeamsSdkWithAuth = vi.hoisted(() =>
92
105
  vi.fn(async () => ({
93
106
  sdk: {
94
- ActivityHandler: class {},
95
- MsalTokenProvider: class {},
107
+ ActivityHandler: function ActivityHandler() {},
108
+ MsalTokenProvider: function MsalTokenProvider() {},
96
109
  authorizeJWT:
97
110
  () => (_req: unknown, _res: unknown, next: ((err?: unknown) => void) | undefined) =>
98
111
  next?.(),
@@ -113,6 +126,12 @@ vi.mock("./resolve-allowlist.js", () => ({
113
126
  vi.mock("./sdk.js", () => ({
114
127
  createMSTeamsAdapter: () => createMSTeamsAdapter(),
115
128
  loadMSTeamsSdkWithAuth: () => loadMSTeamsSdkWithAuth(),
129
+ createMSTeamsTokenProvider: () => ({
130
+ getAccessToken: vi.fn().mockResolvedValue("mock-token"),
131
+ }),
132
+ createBotFrameworkJwtValidator: vi.fn().mockResolvedValue({
133
+ validate: jwtValidate,
134
+ }),
116
135
  }));
117
136
 
118
137
  vi.mock("./runtime.js", () => ({
@@ -172,6 +191,8 @@ describe("monitorMSTeamsProvider lifecycle", () => {
172
191
  afterEach(() => {
173
192
  vi.clearAllMocks();
174
193
  expressControl.mode.value = "listening";
194
+ expressControl.apps.length = 0;
195
+ jwtValidate.mockReset().mockResolvedValue(true);
175
196
  });
176
197
 
177
198
  it("stays active until aborted", async () => {
@@ -192,11 +213,9 @@ describe("monitorMSTeamsProvider lifecycle", () => {
192
213
  expect(early).toBe("pending");
193
214
 
194
215
  abort.abort();
195
- await expect(task).resolves.toEqual(
196
- expect.objectContaining({
197
- shutdown: expect.any(Function),
198
- }),
199
- );
216
+ const result = await task;
217
+ expect(result.app).not.toBeNull();
218
+ await expect(result.shutdown()).resolves.toBeUndefined();
200
219
  });
201
220
 
202
221
  it("rejects startup when webhook port is already in use", async () => {
@@ -211,4 +230,49 @@ describe("monitorMSTeamsProvider lifecycle", () => {
211
230
  }),
212
231
  ).rejects.toThrow(/EADDRINUSE/);
213
232
  });
233
+
234
+ it("runs JWT validation before JSON body parsing", async () => {
235
+ const abort = new AbortController();
236
+ const task = monitorMSTeamsProvider({
237
+ cfg: createConfig(0),
238
+ runtime: createRuntime(),
239
+ abortSignal: abort.signal,
240
+ conversationStore: createStores().conversationStore,
241
+ pollStore: createStores().pollStore,
242
+ });
243
+
244
+ await new Promise<void>((resolve) => setTimeout(resolve, 0));
245
+
246
+ const app = expressControl.apps.at(-1);
247
+ expect(app).toBeDefined();
248
+ expect(app!.use).toHaveBeenCalledTimes(4);
249
+
250
+ const jsonMiddleware = vi.mocked((await import("express")).json).mock.results[0]?.value;
251
+ expect(jsonMiddleware).toBeDefined();
252
+ expect(app!.use.mock.calls[1]?.[0]).not.toBe(jsonMiddleware);
253
+ expect(app!.use.mock.calls[2]?.[0]).toBe(jsonMiddleware);
254
+
255
+ const jwtMiddleware = app!.use.mock.calls[1]?.[0] as (
256
+ req: Request,
257
+ res: Response,
258
+ next: (err?: unknown) => void,
259
+ ) => void;
260
+ const next = vi.fn();
261
+ jwtMiddleware(
262
+ { headers: { authorization: "Bearer token" } } as Request,
263
+ {
264
+ status: vi.fn().mockReturnThis(),
265
+ json: vi.fn(),
266
+ } as unknown as Response,
267
+ next,
268
+ );
269
+
270
+ await vi.waitFor(() => {
271
+ expect(jwtValidate).toHaveBeenCalledWith("Bearer token");
272
+ expect(next).toHaveBeenCalledTimes(1);
273
+ });
274
+
275
+ abort.abort();
276
+ await task;
277
+ });
214
278
  });
@@ -3,7 +3,7 @@ import type { Server } from "node:http";
3
3
  import { createConnection, type AddressInfo } from "node:net";
4
4
  import express from "express";
5
5
  import { describe, expect, it } from "vitest";
6
- import { applyMSTeamsWebhookTimeouts } from "./monitor.js";
6
+ import { applyMSTeamsWebhookTimeouts } from "./webhook-timeouts.js";
7
7
 
8
8
  async function closeServer(server: Server): Promise<void> {
9
9
  await new Promise<void>((resolve) => {
@@ -37,6 +37,21 @@ async function waitForSlowBodySocketClose(port: number, timeoutMs: number): Prom
37
37
  }
38
38
 
39
39
  describe("msteams monitor webhook hardening", () => {
40
+ it("applies default timeouts and header clamp", async () => {
41
+ const app = express();
42
+ const server = app.listen(0, "127.0.0.1");
43
+ await once(server, "listening");
44
+ try {
45
+ applyMSTeamsWebhookTimeouts(server);
46
+
47
+ expect(server.timeout).toBe(30_000);
48
+ expect(server.requestTimeout).toBe(30_000);
49
+ expect(server.headersTimeout).toBe(15_000);
50
+ } finally {
51
+ await closeServer(server);
52
+ }
53
+ });
54
+
40
55
  it("applies explicit webhook timeout values", async () => {
41
56
  const app = express();
42
57
  const server = app.listen(0, "127.0.0.1");
@@ -56,6 +71,25 @@ describe("msteams monitor webhook hardening", () => {
56
71
  }
57
72
  });
58
73
 
74
+ it("clamps headers timeout when explicit value exceeds request timeout", async () => {
75
+ const app = express();
76
+ const server = app.listen(0, "127.0.0.1");
77
+ await once(server, "listening");
78
+ try {
79
+ applyMSTeamsWebhookTimeouts(server, {
80
+ inactivityTimeoutMs: 12_000,
81
+ requestTimeoutMs: 9_000,
82
+ headersTimeoutMs: 15_000,
83
+ });
84
+
85
+ expect(server.timeout).toBe(12_000);
86
+ expect(server.requestTimeout).toBe(9_000);
87
+ expect(server.headersTimeout).toBe(9_000);
88
+ } finally {
89
+ await closeServer(server);
90
+ }
91
+ });
92
+
59
93
  it("drops slow-body webhook requests within configured inactivity timeout", async () => {
60
94
  const app = express();
61
95
  app.use(express.json({ limit: "1mb" }));
package/src/monitor.ts CHANGED
@@ -1,4 +1,3 @@
1
- import type { Server } from "node:http";
2
1
  import type { Request, Response } from "express";
3
2
  import {
4
3
  DEFAULT_WEBHOOK_MAX_BODY_BYTES,
@@ -7,7 +6,7 @@ import {
7
6
  summarizeMapping,
8
7
  type OpenClawConfig,
9
8
  type RuntimeEnv,
10
- } from "openclaw/plugin-sdk/msteams";
9
+ } from "../runtime-api.js";
11
10
  import { createMSTeamsConversationStoreFs } from "./conversation-store-fs.js";
12
11
  import type { MSTeamsConversationStore } from "./conversation-store.js";
13
12
  import { formatUnknownError } from "./errors.js";
@@ -19,10 +18,18 @@ import {
19
18
  resolveMSTeamsUserAllowlist,
20
19
  } from "./resolve-allowlist.js";
21
20
  import { getMSTeamsRuntime } from "./runtime.js";
22
- import { createMSTeamsAdapter, loadMSTeamsSdkWithAuth } from "./sdk.js";
21
+ import {
22
+ createBotFrameworkJwtValidator,
23
+ createMSTeamsAdapter,
24
+ createMSTeamsTokenProvider,
25
+ loadMSTeamsSdkWithAuth,
26
+ } from "./sdk.js";
27
+ import { createMSTeamsSsoTokenStoreFs } from "./sso-token-store.js";
28
+ import type { MSTeamsSsoDeps } from "./sso.js";
23
29
  import { resolveMSTeamsCredentials } from "./token.js";
30
+ import { applyMSTeamsWebhookTimeouts } from "./webhook-timeouts.js";
24
31
 
25
- export type MonitorMSTeamsOpts = {
32
+ type MonitorMSTeamsOpts = {
26
33
  cfg: OpenClawConfig;
27
34
  runtime?: RuntimeEnv;
28
35
  abortSignal?: AbortSignal;
@@ -30,38 +37,12 @@ export type MonitorMSTeamsOpts = {
30
37
  pollStore?: MSTeamsPollStore;
31
38
  };
32
39
 
33
- export type MonitorMSTeamsResult = {
40
+ type MonitorMSTeamsResult = {
34
41
  app: unknown;
35
42
  shutdown: () => Promise<void>;
36
43
  };
37
44
 
38
45
  const MSTEAMS_WEBHOOK_MAX_BODY_BYTES = DEFAULT_WEBHOOK_MAX_BODY_BYTES;
39
- const MSTEAMS_WEBHOOK_INACTIVITY_TIMEOUT_MS = 30_000;
40
- const MSTEAMS_WEBHOOK_REQUEST_TIMEOUT_MS = 30_000;
41
- const MSTEAMS_WEBHOOK_HEADERS_TIMEOUT_MS = 15_000;
42
-
43
- export type ApplyMSTeamsWebhookTimeoutsOpts = {
44
- inactivityTimeoutMs?: number;
45
- requestTimeoutMs?: number;
46
- headersTimeoutMs?: number;
47
- };
48
-
49
- export function applyMSTeamsWebhookTimeouts(
50
- httpServer: Server,
51
- opts?: ApplyMSTeamsWebhookTimeoutsOpts,
52
- ): void {
53
- const inactivityTimeoutMs = opts?.inactivityTimeoutMs ?? MSTEAMS_WEBHOOK_INACTIVITY_TIMEOUT_MS;
54
- const requestTimeoutMs = opts?.requestTimeoutMs ?? MSTEAMS_WEBHOOK_REQUEST_TIMEOUT_MS;
55
- const headersTimeoutMs = Math.min(
56
- opts?.headersTimeoutMs ?? MSTEAMS_WEBHOOK_HEADERS_TIMEOUT_MS,
57
- requestTimeoutMs,
58
- );
59
-
60
- httpServer.setTimeout(inactivityTimeoutMs);
61
- httpServer.requestTimeout = requestTimeoutMs;
62
- httpServer.headersTimeout = headersTimeoutMs;
63
- }
64
-
65
46
  export async function monitorMSTeamsProvider(
66
47
  opts: MonitorMSTeamsOpts,
67
48
  ): Promise<MonitorMSTeamsResult> {
@@ -122,9 +103,8 @@ export async function monitorMSTeamsProvider(
122
103
 
123
104
  try {
124
105
  const allowEntries =
125
- allowFrom
126
- ?.map((entry) => cleanAllowEntry(String(entry)))
127
- .filter((entry) => entry && entry !== "*") ?? [];
106
+ allowFrom?.map((entry) => cleanAllowEntry(entry)).filter((entry) => entry && entry !== "*") ??
107
+ [];
128
108
  if (allowEntries.length > 0) {
129
109
  const { additions } = await resolveAllowlistUsers("msteams users", allowEntries);
130
110
  allowFrom = mergeAllowlist({ existing: allowFrom, additions });
@@ -132,7 +112,7 @@ export async function monitorMSTeamsProvider(
132
112
 
133
113
  if (Array.isArray(groupAllowFrom) && groupAllowFrom.length > 0) {
134
114
  const groupEntries = groupAllowFrom
135
- .map((entry) => cleanAllowEntry(String(entry)))
115
+ .map((entry) => cleanAllowEntry(entry))
136
116
  .filter((entry) => entry && entry !== "*");
137
117
  if (groupEntries.length > 0) {
138
118
  const { additions } = await resolveAllowlistUsers("msteams group users", groupEntries);
@@ -214,7 +194,7 @@ export async function monitorMSTeamsProvider(
214
194
  }
215
195
  }
216
196
  } catch (err) {
217
- runtime.log?.(`msteams resolve failed; using config entries. ${String(err)}`);
197
+ runtime.log?.(`msteams resolve failed; using config entries. ${formatUnknownError(err)}`);
218
198
  }
219
199
 
220
200
  msteamsCfg = {
@@ -247,14 +227,32 @@ export async function monitorMSTeamsProvider(
247
227
  // Dynamic import to avoid loading SDK when provider is disabled
248
228
  const express = await import("express");
249
229
 
250
- const { sdk, authConfig } = await loadMSTeamsSdkWithAuth(creds);
251
- const { ActivityHandler, MsalTokenProvider, authorizeJWT } = sdk;
230
+ const { sdk, app } = await loadMSTeamsSdkWithAuth(creds);
252
231
 
253
- // Auth configuration - create early so adapter is available for deliverReplies
254
- const tokenProvider = new MsalTokenProvider(authConfig);
255
- const adapter = createMSTeamsAdapter(authConfig, sdk);
232
+ // Build a token provider adapter for Graph API operations
233
+ const tokenProvider = createMSTeamsTokenProvider(app);
234
+
235
+ const adapter = createMSTeamsAdapter(app, sdk);
236
+
237
+ // Build SSO deps when the operator has opted in and a connection name
238
+ // is configured. Leaving `sso` undefined matches the pre-SSO behavior
239
+ // (the plugin will still ack signin invokes, but will not attempt a
240
+ // Bot Framework token exchange or persist anything).
241
+ let ssoDeps: MSTeamsSsoDeps | undefined;
242
+ if (msteamsCfg.sso?.enabled && msteamsCfg.sso.connectionName) {
243
+ ssoDeps = {
244
+ tokenProvider,
245
+ tokenStore: createMSTeamsSsoTokenStoreFs(),
246
+ connectionName: msteamsCfg.sso.connectionName,
247
+ };
248
+ log.debug?.("msteams sso enabled", {
249
+ connectionName: msteamsCfg.sso.connectionName,
250
+ });
251
+ }
256
252
 
257
- const handler = registerMSTeamsHandlers(new ActivityHandler() as MSTeamsActivityHandler, {
253
+ // Build a simple ActivityHandler-compatible object
254
+ const handler = buildActivityHandler();
255
+ registerMSTeamsHandlers(handler, {
258
256
  cfg,
259
257
  runtime,
260
258
  appId,
@@ -265,10 +263,48 @@ export async function monitorMSTeamsProvider(
265
263
  conversationStore,
266
264
  pollStore,
267
265
  log,
266
+ sso: ssoDeps,
268
267
  });
269
268
 
270
269
  // Create Express server
271
270
  const expressApp = express.default();
271
+
272
+ // Cheap pre-parse auth gate: reject requests without a Bearer token before
273
+ // spending CPU/memory on JSON body parsing. This prevents unauthenticated
274
+ // request floods from forcing body parsing on internet-exposed webhooks.
275
+ expressApp.use((req: Request, res: Response, next: (err?: unknown) => void) => {
276
+ const auth = req.headers.authorization;
277
+ if (!auth || !auth.startsWith("Bearer ")) {
278
+ res.status(401).json({ error: "Unauthorized" });
279
+ return;
280
+ }
281
+ next();
282
+ });
283
+
284
+ // JWT validation — verify Bot Framework tokens using the Teams SDK's
285
+ // JwtValidator (validates signature via JWKS, audience, issuer, expiration).
286
+ const jwtValidator = await createBotFrameworkJwtValidator(creds);
287
+ expressApp.use((req: Request, res: Response, next: (err?: unknown) => void) => {
288
+ // Authorization header is guaranteed by the pre-parse auth gate above.
289
+ // `serviceUrl` is optional, so authenticate from headers alone before body
290
+ // I/O to avoid spending memory and CPU on unauthenticated requests.
291
+ const authHeader = req.headers.authorization!;
292
+ jwtValidator
293
+ .validate(authHeader)
294
+ .then((valid) => {
295
+ if (!valid) {
296
+ log.debug?.("JWT validation failed");
297
+ res.status(401).json({ error: "Unauthorized" });
298
+ return;
299
+ }
300
+ next();
301
+ })
302
+ .catch((err) => {
303
+ log.debug?.(`JWT validation error: ${formatUnknownError(err)}`);
304
+ res.status(401).json({ error: "Unauthorized" });
305
+ });
306
+ });
307
+
272
308
  expressApp.use(express.json({ limit: MSTEAMS_WEBHOOK_MAX_BODY_BYTES }));
273
309
  expressApp.use((err: unknown, _req: Request, res: Response, next: (err?: unknown) => void) => {
274
310
  if (err && typeof err === "object" && "status" in err && err.status === 413) {
@@ -277,7 +313,6 @@ export async function monitorMSTeamsProvider(
277
313
  }
278
314
  next(err);
279
315
  });
280
- expressApp.use(authorizeJWT(authConfig));
281
316
 
282
317
  // Set up the messages endpoint - use configured path and /api/messages as fallback
283
318
  const configuredPath = msteamsCfg.webhook?.path ?? "/api/messages";
@@ -310,7 +345,7 @@ export async function monitorMSTeamsProvider(
310
345
  };
311
346
  const onError = (err: unknown) => {
312
347
  httpServer.off("listening", onListening);
313
- log.error("msteams server error", { error: String(err) });
348
+ log.error("msteams server error", { error: formatUnknownError(err) });
314
349
  reject(err);
315
350
  };
316
351
  httpServer.once("listening", onListening);
@@ -319,7 +354,7 @@ export async function monitorMSTeamsProvider(
319
354
  applyMSTeamsWebhookTimeouts(httpServer);
320
355
 
321
356
  httpServer.on("error", (err) => {
322
- log.error("msteams server error", { error: String(err) });
357
+ log.error("msteams server error", { error: formatUnknownError(err) });
323
358
  });
324
359
 
325
360
  const shutdown = async () => {
@@ -327,7 +362,7 @@ export async function monitorMSTeamsProvider(
327
362
  return new Promise<void>((resolve) => {
328
363
  httpServer.close((err) => {
329
364
  if (err) {
330
- log.debug?.("msteams server close error", { error: String(err) });
365
+ log.debug?.("msteams server close error", { error: formatUnknownError(err) });
331
366
  }
332
367
  resolve();
333
368
  });
@@ -343,3 +378,65 @@ export async function monitorMSTeamsProvider(
343
378
 
344
379
  return { app: expressApp, shutdown };
345
380
  }
381
+
382
+ /**
383
+ * Build a minimal ActivityHandler-compatible object that supports
384
+ * onMessage / onMembersAdded registration and a run() method.
385
+ */
386
+ function buildActivityHandler(): MSTeamsActivityHandler {
387
+ type Handler = (context: unknown, next: () => Promise<void>) => Promise<void>;
388
+ const messageHandlers: Handler[] = [];
389
+ const membersAddedHandlers: Handler[] = [];
390
+ const reactionsAddedHandlers: Handler[] = [];
391
+ const reactionsRemovedHandlers: Handler[] = [];
392
+
393
+ const handler: MSTeamsActivityHandler = {
394
+ onMessage(cb) {
395
+ messageHandlers.push(cb);
396
+ return handler;
397
+ },
398
+ onMembersAdded(cb) {
399
+ membersAddedHandlers.push(cb);
400
+ return handler;
401
+ },
402
+ onReactionsAdded(cb) {
403
+ reactionsAddedHandlers.push(cb);
404
+ return handler;
405
+ },
406
+ onReactionsRemoved(cb) {
407
+ reactionsRemovedHandlers.push(cb);
408
+ return handler;
409
+ },
410
+ async run(context: unknown) {
411
+ const ctx = context as { activity?: { type?: string } };
412
+ const activityType = ctx?.activity?.type;
413
+ const noop = async () => {};
414
+
415
+ if (activityType === "message") {
416
+ for (const h of messageHandlers) {
417
+ await h(context, noop);
418
+ }
419
+ } else if (activityType === "conversationUpdate") {
420
+ for (const h of membersAddedHandlers) {
421
+ await h(context, noop);
422
+ }
423
+ } else if (activityType === "messageReaction") {
424
+ const activity = (
425
+ ctx as { activity?: { reactionsAdded?: unknown[]; reactionsRemoved?: unknown[] } }
426
+ )?.activity;
427
+ if (activity?.reactionsAdded?.length) {
428
+ for (const h of reactionsAddedHandlers) {
429
+ await h(context, noop);
430
+ }
431
+ }
432
+ if (activity?.reactionsRemoved?.length) {
433
+ for (const h of reactionsRemovedHandlers) {
434
+ await h(context, noop);
435
+ }
436
+ }
437
+ }
438
+ },
439
+ };
440
+
441
+ return handler;
442
+ }
@@ -0,0 +1,77 @@
1
+ import { generateHexPkceVerifierChallenge } from "openclaw/plugin-sdk/provider-auth";
2
+ import {
3
+ generateOAuthState,
4
+ parseOAuthCallbackInput,
5
+ waitForLocalOAuthCallback,
6
+ } from "openclaw/plugin-sdk/provider-auth-runtime";
7
+ import { isWSL2Sync } from "openclaw/plugin-sdk/runtime-env";
8
+ import {
9
+ MSTEAMS_DEFAULT_DELEGATED_SCOPES,
10
+ MSTEAMS_OAUTH_CALLBACK_PATH,
11
+ MSTEAMS_OAUTH_CALLBACK_PORT,
12
+ MSTEAMS_OAUTH_REDIRECT_URI,
13
+ buildMSTeamsAuthEndpoint,
14
+ } from "./oauth.shared.js";
15
+
16
+ export function shouldUseManualOAuthFlow(isRemote: boolean): boolean {
17
+ return isRemote || isWSL2Sync();
18
+ }
19
+
20
+ export function generatePkce(): { verifier: string; challenge: string } {
21
+ return generateHexPkceVerifierChallenge();
22
+ }
23
+
24
+ export { generateOAuthState };
25
+
26
+ export function buildMSTeamsAuthUrl(params: {
27
+ tenantId: string;
28
+ clientId: string;
29
+ challenge: string;
30
+ /** Opaque CSRF state token — must NOT be the PKCE verifier. */
31
+ state: string;
32
+ scopes?: readonly string[];
33
+ }): string {
34
+ const scopes = params.scopes ?? MSTEAMS_DEFAULT_DELEGATED_SCOPES;
35
+ const endpoint = buildMSTeamsAuthEndpoint(params.tenantId);
36
+ const query = new URLSearchParams({
37
+ client_id: params.clientId,
38
+ response_type: "code",
39
+ redirect_uri: MSTEAMS_OAUTH_REDIRECT_URI,
40
+ scope: scopes.join(" "),
41
+ code_challenge: params.challenge,
42
+ code_challenge_method: "S256",
43
+ state: params.state,
44
+ prompt: "consent",
45
+ });
46
+ return `${endpoint}?${query.toString()}`;
47
+ }
48
+
49
+ export function parseCallbackInput(
50
+ input: string,
51
+ // Kept in the signature for API symmetry with the caller's CSRF verify step.
52
+ // The caller compares the parsed `state` against the expected value.
53
+ _expectedState: string,
54
+ ): { code: string; state: string } | { error: string } {
55
+ return parseOAuthCallbackInput(input, {
56
+ missingState: "Missing 'state' parameter in URL. Paste the full redirect URL.",
57
+ invalidInput:
58
+ "Paste the full redirect URL (including code and state parameters), not just the authorization code.",
59
+ });
60
+ }
61
+
62
+ export async function waitForLocalCallback(params: {
63
+ expectedState: string;
64
+ timeoutMs: number;
65
+ onProgress?: (message: string) => void;
66
+ }): Promise<{ code: string; state: string }> {
67
+ return await waitForLocalOAuthCallback({
68
+ expectedState: params.expectedState,
69
+ timeoutMs: params.timeoutMs,
70
+ port: MSTEAMS_OAUTH_CALLBACK_PORT,
71
+ callbackPath: MSTEAMS_OAUTH_CALLBACK_PATH,
72
+ redirectUri: MSTEAMS_OAUTH_REDIRECT_URI,
73
+ successTitle: "MSTeams Delegated OAuth complete",
74
+ progressMessage: `Waiting for OAuth callback on ${MSTEAMS_OAUTH_REDIRECT_URI}...`,
75
+ onProgress: params.onProgress,
76
+ });
77
+ }
@@ -0,0 +1,37 @@
1
+ export const MSTEAMS_OAUTH_REDIRECT_URI = "http://localhost:8086/oauth2callback";
2
+ export const MSTEAMS_OAUTH_CALLBACK_PORT = 8086;
3
+ export const MSTEAMS_OAUTH_CALLBACK_PATH = "/oauth2callback";
4
+ export const MSTEAMS_DEFAULT_TOKEN_FETCH_TIMEOUT_MS = 10_000;
5
+
6
+ export const MSTEAMS_DEFAULT_DELEGATED_SCOPES = [
7
+ "ChatMessage.Send",
8
+ "ChannelMessage.Send",
9
+ "Chat.ReadWrite",
10
+ "offline_access",
11
+ ] as const;
12
+
13
+ export function buildMSTeamsAuthEndpoint(tenantId: string): string {
14
+ return `https://login.microsoftonline.com/${encodeURIComponent(tenantId)}/oauth2/v2.0/authorize`;
15
+ }
16
+
17
+ export function buildMSTeamsTokenEndpoint(tenantId: string): string {
18
+ return `https://login.microsoftonline.com/${encodeURIComponent(tenantId)}/oauth2/v2.0/token`;
19
+ }
20
+
21
+ export type MSTeamsDelegatedTokens = {
22
+ accessToken: string;
23
+ refreshToken: string;
24
+ /** Unix ms, 5-min buffer pre-applied */
25
+ expiresAt: number;
26
+ scopes: string[];
27
+ userPrincipalName?: string;
28
+ };
29
+
30
+ export type MSTeamsDelegatedOAuthContext = {
31
+ isRemote: boolean;
32
+ openUrl: (url: string) => Promise<void>;
33
+ log: (msg: string) => void;
34
+ note: (message: string, title?: string) => Promise<void>;
35
+ prompt: (message: string) => Promise<string>;
36
+ progress: { update: (msg: string) => void; stop: (msg?: string) => void };
37
+ };