@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.
- package/api.ts +3 -0
- package/channel-config-api.ts +1 -0
- package/channel-plugin-api.ts +2 -0
- package/config-api.ts +4 -0
- package/contract-api.ts +4 -0
- package/index.ts +15 -12
- package/openclaw.plugin.json +553 -1
- package/package.json +46 -12
- package/runtime-api.ts +73 -0
- package/secret-contract-api.ts +5 -0
- package/setup-entry.ts +13 -0
- package/setup-plugin-api.ts +3 -0
- package/src/ai-entity.ts +7 -0
- package/src/approval-auth.ts +44 -0
- package/src/attachments/bot-framework.test.ts +461 -0
- package/src/attachments/bot-framework.ts +362 -0
- package/src/attachments/download.ts +63 -19
- package/src/attachments/graph.test.ts +416 -0
- package/src/attachments/graph.ts +163 -72
- package/src/attachments/html.ts +33 -1
- package/src/attachments/payload.ts +1 -1
- package/src/attachments/remote-media.test.ts +137 -0
- package/src/attachments/remote-media.ts +75 -8
- package/src/attachments/shared.test.ts +138 -1
- package/src/attachments/shared.ts +193 -26
- package/src/attachments/types.ts +10 -0
- package/src/attachments.graph.test.ts +342 -0
- package/src/attachments.helpers.test.ts +246 -0
- package/src/attachments.test-helpers.ts +17 -0
- package/src/attachments.test.ts +163 -418
- package/src/attachments.ts +5 -5
- package/src/block-streaming-config.test.ts +61 -0
- package/src/channel-api.ts +1 -0
- package/src/channel.actions.test.ts +742 -0
- package/src/channel.directory.test.ts +145 -4
- package/src/channel.runtime.ts +56 -0
- package/src/channel.setup.ts +77 -0
- package/src/channel.test.ts +128 -0
- package/src/channel.ts +1077 -395
- package/src/config-schema.ts +6 -0
- package/src/config-ui-hints.ts +12 -0
- package/src/conversation-store-fs.test.ts +4 -5
- package/src/conversation-store-fs.ts +35 -51
- package/src/conversation-store-helpers.test.ts +202 -0
- package/src/conversation-store-helpers.ts +105 -0
- package/src/conversation-store-memory.ts +27 -23
- package/src/conversation-store.shared.test.ts +225 -0
- package/src/conversation-store.ts +30 -0
- package/src/directory-live.test.ts +156 -0
- package/src/directory-live.ts +7 -4
- package/src/doctor.ts +27 -0
- package/src/errors.test.ts +64 -1
- package/src/errors.ts +50 -9
- package/src/feedback-reflection-prompt.ts +117 -0
- package/src/feedback-reflection-store.ts +114 -0
- package/src/feedback-reflection.test.ts +237 -0
- package/src/feedback-reflection.ts +283 -0
- package/src/file-consent-helpers.test.ts +83 -0
- package/src/file-consent-helpers.ts +64 -11
- package/src/file-consent-invoke.ts +150 -0
- package/src/file-consent.test.ts +363 -0
- package/src/file-consent.ts +165 -4
- package/src/graph-chat.ts +5 -3
- package/src/graph-group-management.test.ts +318 -0
- package/src/graph-group-management.ts +168 -0
- package/src/graph-members.test.ts +89 -0
- package/src/graph-members.ts +48 -0
- package/src/graph-messages.actions.test.ts +243 -0
- package/src/graph-messages.read.test.ts +391 -0
- package/src/graph-messages.search.test.ts +213 -0
- package/src/graph-messages.test-helpers.ts +50 -0
- package/src/graph-messages.ts +534 -0
- package/src/graph-teams.test.ts +215 -0
- package/src/graph-teams.ts +114 -0
- package/src/graph-thread.test.ts +246 -0
- package/src/graph-thread.ts +146 -0
- package/src/graph-upload.test.ts +161 -4
- package/src/graph-upload.ts +147 -56
- package/src/graph.test.ts +516 -0
- package/src/graph.ts +233 -21
- package/src/inbound.test.ts +156 -1
- package/src/inbound.ts +101 -1
- package/src/media-helpers.ts +1 -1
- package/src/mentions.test.ts +27 -18
- package/src/mentions.ts +2 -2
- package/src/messenger.test.ts +504 -23
- package/src/messenger.ts +133 -52
- package/src/monitor-handler/access.ts +125 -0
- package/src/monitor-handler/inbound-media.test.ts +289 -0
- package/src/monitor-handler/inbound-media.ts +57 -5
- package/src/monitor-handler/message-handler-mock-support.test-support.ts +28 -0
- package/src/monitor-handler/message-handler.authz.test.ts +588 -74
- package/src/monitor-handler/message-handler.dm-media.test.ts +54 -0
- package/src/monitor-handler/message-handler.test-support.ts +100 -0
- package/src/monitor-handler/message-handler.thread-parent.test.ts +223 -0
- package/src/monitor-handler/message-handler.thread-session.test.ts +77 -0
- package/src/monitor-handler/message-handler.ts +470 -164
- package/src/monitor-handler/reaction-handler.test.ts +267 -0
- package/src/monitor-handler/reaction-handler.ts +210 -0
- package/src/monitor-handler/thread-session.ts +17 -0
- package/src/monitor-handler.adaptive-card.test.ts +162 -0
- package/src/monitor-handler.feedback-authz.test.ts +314 -0
- package/src/monitor-handler.file-consent.test.ts +281 -79
- package/src/monitor-handler.sso.test.ts +563 -0
- package/src/monitor-handler.test-helpers.ts +180 -0
- package/src/monitor-handler.ts +459 -115
- package/src/monitor-handler.types.ts +27 -0
- package/src/monitor-types.ts +1 -0
- package/src/monitor.lifecycle.test.ts +74 -10
- package/src/monitor.test.ts +35 -1
- package/src/monitor.ts +143 -46
- package/src/oauth.flow.ts +77 -0
- package/src/oauth.shared.ts +37 -0
- package/src/oauth.test.ts +305 -0
- package/src/oauth.token.ts +158 -0
- package/src/oauth.ts +130 -0
- package/src/outbound.test.ts +10 -11
- package/src/outbound.ts +62 -44
- package/src/pending-uploads-fs.test.ts +246 -0
- package/src/pending-uploads-fs.ts +235 -0
- package/src/pending-uploads.test.ts +173 -0
- package/src/pending-uploads.ts +34 -2
- package/src/policy.test.ts +11 -5
- package/src/policy.ts +5 -5
- package/src/polls.test.ts +106 -5
- package/src/polls.ts +15 -7
- package/src/presentation.ts +68 -0
- package/src/probe.test.ts +27 -8
- package/src/probe.ts +43 -9
- package/src/reply-dispatcher.test.ts +437 -0
- package/src/reply-dispatcher.ts +259 -73
- package/src/reply-stream-controller.test.ts +235 -0
- package/src/reply-stream-controller.ts +147 -0
- package/src/resolve-allowlist.test.ts +105 -1
- package/src/resolve-allowlist.ts +112 -7
- package/src/runtime.ts +6 -3
- package/src/sdk-types.ts +43 -3
- package/src/sdk.test.ts +666 -0
- package/src/sdk.ts +867 -16
- package/src/secret-contract.ts +49 -0
- package/src/secret-input.ts +1 -1
- package/src/send-context.ts +76 -9
- package/src/send.test.ts +389 -5
- package/src/send.ts +140 -32
- package/src/sent-message-cache.ts +30 -18
- package/src/session-route.ts +40 -0
- package/src/setup-core.ts +160 -0
- package/src/setup-surface.test.ts +202 -0
- package/src/setup-surface.ts +320 -0
- package/src/sso-token-store.test.ts +72 -0
- package/src/sso-token-store.ts +166 -0
- package/src/sso.ts +300 -0
- package/src/storage.ts +1 -1
- package/src/store-fs.ts +2 -2
- package/src/streaming-message.test.ts +262 -0
- package/src/streaming-message.ts +297 -0
- package/src/test-runtime.ts +1 -1
- package/src/thread-parent-context.test.ts +224 -0
- package/src/thread-parent-context.ts +159 -0
- package/src/token.test.ts +237 -50
- package/src/token.ts +162 -7
- package/src/user-agent.test.ts +86 -0
- package/src/user-agent.ts +53 -0
- package/src/webhook-timeouts.ts +27 -0
- package/src/welcome-card.test.ts +81 -0
- package/src/welcome-card.ts +57 -0
- package/test-api.ts +1 -0
- package/tsconfig.json +16 -0
- package/CHANGELOG.md +0 -107
- package/src/file-lock.ts +0 -1
- package/src/graph-users.test.ts +0 -66
- package/src/onboarding.ts +0 -381
- package/src/polls-store.test.ts +0 -38
- package/src/revoked-context.test.ts +0 -39
- package/src/token-response.test.ts +0 -23
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { EventEmitter } from "node:events";
|
|
2
|
-
import type {
|
|
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("
|
|
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:
|
|
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:
|
|
95
|
-
MsalTokenProvider:
|
|
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
|
|
196
|
-
|
|
197
|
-
|
|
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
|
});
|
package/src/monitor.test.ts
CHANGED
|
@@ -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 "./
|
|
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 "
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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. ${
|
|
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,
|
|
251
|
-
const { ActivityHandler, MsalTokenProvider, authorizeJWT } = sdk;
|
|
230
|
+
const { sdk, app } = await loadMSTeamsSdkWithAuth(creds);
|
|
252
231
|
|
|
253
|
-
//
|
|
254
|
-
const tokenProvider =
|
|
255
|
-
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
+
};
|