@openclaw/msteams 2026.2.21 → 2026.2.23
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/CHANGELOG.md +6 -0
- package/package.json +1 -1
- package/src/attachments/download.ts +67 -68
- package/src/attachments/graph.ts +29 -28
- package/src/attachments/payload.ts +3 -11
- package/src/attachments/remote-media.ts +42 -0
- package/src/attachments/shared.test.ts +281 -0
- package/src/attachments/shared.ts +113 -0
- package/src/attachments.test.ts +696 -400
- package/src/channel.ts +8 -2
- package/src/directory-live.ts +2 -20
- package/src/graph-users.test.ts +66 -0
- package/src/graph-users.ts +29 -0
- package/src/graph.ts +1 -12
- package/src/messenger.test.ts +30 -48
- package/src/messenger.ts +10 -21
- package/src/monitor-handler/message-handler.ts +14 -5
- package/src/policy.test.ts +13 -1
- package/src/policy.ts +2 -0
- package/src/probe.ts +1 -12
- package/src/resolve-allowlist.ts +2 -20
- package/src/token-response.test.ts +23 -0
- package/src/token-response.ts +11 -0
package/src/channel.ts
CHANGED
|
@@ -6,6 +6,8 @@ import {
|
|
|
6
6
|
DEFAULT_ACCOUNT_ID,
|
|
7
7
|
MSTeamsConfigSchema,
|
|
8
8
|
PAIRING_APPROVED_MESSAGE,
|
|
9
|
+
resolveAllowlistProviderRuntimeGroupPolicy,
|
|
10
|
+
resolveDefaultGroupPolicy,
|
|
9
11
|
} from "openclaw/plugin-sdk";
|
|
10
12
|
import { listMSTeamsDirectoryGroupsLive, listMSTeamsDirectoryPeersLive } from "./directory-live.js";
|
|
11
13
|
import { msteamsOnboardingAdapter } from "./onboarding.js";
|
|
@@ -127,8 +129,12 @@ export const msteamsPlugin: ChannelPlugin<ResolvedMSTeamsAccount> = {
|
|
|
127
129
|
},
|
|
128
130
|
security: {
|
|
129
131
|
collectWarnings: ({ cfg }) => {
|
|
130
|
-
const defaultGroupPolicy = cfg
|
|
131
|
-
const
|
|
132
|
+
const defaultGroupPolicy = resolveDefaultGroupPolicy(cfg);
|
|
133
|
+
const { groupPolicy } = resolveAllowlistProviderRuntimeGroupPolicy({
|
|
134
|
+
providerConfigPresent: cfg.channels?.msteams !== undefined,
|
|
135
|
+
groupPolicy: cfg.channels?.msteams?.groupPolicy,
|
|
136
|
+
defaultGroupPolicy,
|
|
137
|
+
});
|
|
132
138
|
if (groupPolicy !== "open") {
|
|
133
139
|
return [];
|
|
134
140
|
}
|
package/src/directory-live.ts
CHANGED
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
import type { ChannelDirectoryEntry } from "openclaw/plugin-sdk";
|
|
2
|
+
import { searchGraphUsers } from "./graph-users.js";
|
|
2
3
|
import {
|
|
3
|
-
escapeOData,
|
|
4
|
-
fetchGraphJson,
|
|
5
4
|
type GraphChannel,
|
|
6
5
|
type GraphGroup,
|
|
7
|
-
type GraphResponse,
|
|
8
|
-
type GraphUser,
|
|
9
6
|
listChannelsForTeam,
|
|
10
7
|
listTeamsByName,
|
|
11
8
|
normalizeQuery,
|
|
@@ -24,22 +21,7 @@ export async function listMSTeamsDirectoryPeersLive(params: {
|
|
|
24
21
|
const token = await resolveGraphToken(params.cfg);
|
|
25
22
|
const limit = typeof params.limit === "number" && params.limit > 0 ? params.limit : 20;
|
|
26
23
|
|
|
27
|
-
|
|
28
|
-
if (query.includes("@")) {
|
|
29
|
-
const escaped = escapeOData(query);
|
|
30
|
-
const filter = `(mail eq '${escaped}' or userPrincipalName eq '${escaped}')`;
|
|
31
|
-
const path = `/users?$filter=${encodeURIComponent(filter)}&$select=id,displayName,mail,userPrincipalName`;
|
|
32
|
-
const res = await fetchGraphJson<GraphResponse<GraphUser>>({ token, path });
|
|
33
|
-
users = res.value ?? [];
|
|
34
|
-
} else {
|
|
35
|
-
const path = `/users?$search=${encodeURIComponent(`"displayName:${query}"`)}&$select=id,displayName,mail,userPrincipalName&$top=${limit}`;
|
|
36
|
-
const res = await fetchGraphJson<GraphResponse<GraphUser>>({
|
|
37
|
-
token,
|
|
38
|
-
path,
|
|
39
|
-
headers: { ConsistencyLevel: "eventual" },
|
|
40
|
-
});
|
|
41
|
-
users = res.value ?? [];
|
|
42
|
-
}
|
|
24
|
+
const users = await searchGraphUsers({ token, query, top: limit });
|
|
43
25
|
|
|
44
26
|
return users
|
|
45
27
|
.map((user) => {
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { searchGraphUsers } from "./graph-users.js";
|
|
3
|
+
import { fetchGraphJson } from "./graph.js";
|
|
4
|
+
|
|
5
|
+
vi.mock("./graph.js", () => ({
|
|
6
|
+
escapeOData: vi.fn((value: string) => value.replace(/'/g, "''")),
|
|
7
|
+
fetchGraphJson: vi.fn(),
|
|
8
|
+
}));
|
|
9
|
+
|
|
10
|
+
describe("searchGraphUsers", () => {
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
vi.mocked(fetchGraphJson).mockReset();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("returns empty array for blank queries", async () => {
|
|
16
|
+
await expect(searchGraphUsers({ token: "token-1", query: " " })).resolves.toEqual([]);
|
|
17
|
+
expect(fetchGraphJson).not.toHaveBeenCalled();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("uses exact mail/upn filter lookup for email-like queries", async () => {
|
|
21
|
+
vi.mocked(fetchGraphJson).mockResolvedValueOnce({
|
|
22
|
+
value: [{ id: "user-1", displayName: "User One" }],
|
|
23
|
+
} as never);
|
|
24
|
+
|
|
25
|
+
const result = await searchGraphUsers({
|
|
26
|
+
token: "token-2",
|
|
27
|
+
query: "alice.o'hara@example.com",
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
expect(fetchGraphJson).toHaveBeenCalledWith({
|
|
31
|
+
token: "token-2",
|
|
32
|
+
path: "/users?$filter=(mail%20eq%20'alice.o''hara%40example.com'%20or%20userPrincipalName%20eq%20'alice.o''hara%40example.com')&$select=id,displayName,mail,userPrincipalName",
|
|
33
|
+
});
|
|
34
|
+
expect(result).toEqual([{ id: "user-1", displayName: "User One" }]);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("uses displayName search with eventual consistency and custom top", async () => {
|
|
38
|
+
vi.mocked(fetchGraphJson).mockResolvedValueOnce({
|
|
39
|
+
value: [{ id: "user-2", displayName: "Bob" }],
|
|
40
|
+
} as never);
|
|
41
|
+
|
|
42
|
+
const result = await searchGraphUsers({
|
|
43
|
+
token: "token-3",
|
|
44
|
+
query: "bob",
|
|
45
|
+
top: 25,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
expect(fetchGraphJson).toHaveBeenCalledWith({
|
|
49
|
+
token: "token-3",
|
|
50
|
+
path: "/users?$search=%22displayName%3Abob%22&$select=id,displayName,mail,userPrincipalName&$top=25",
|
|
51
|
+
headers: { ConsistencyLevel: "eventual" },
|
|
52
|
+
});
|
|
53
|
+
expect(result).toEqual([{ id: "user-2", displayName: "Bob" }]);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("falls back to default top and empty value handling", async () => {
|
|
57
|
+
vi.mocked(fetchGraphJson).mockResolvedValueOnce({} as never);
|
|
58
|
+
|
|
59
|
+
await expect(searchGraphUsers({ token: "token-4", query: "carol" })).resolves.toEqual([]);
|
|
60
|
+
expect(fetchGraphJson).toHaveBeenCalledWith({
|
|
61
|
+
token: "token-4",
|
|
62
|
+
path: "/users?$search=%22displayName%3Acarol%22&$select=id,displayName,mail,userPrincipalName&$top=10",
|
|
63
|
+
headers: { ConsistencyLevel: "eventual" },
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { escapeOData, fetchGraphJson, type GraphResponse, type GraphUser } from "./graph.js";
|
|
2
|
+
|
|
3
|
+
export async function searchGraphUsers(params: {
|
|
4
|
+
token: string;
|
|
5
|
+
query: string;
|
|
6
|
+
top?: number;
|
|
7
|
+
}): Promise<GraphUser[]> {
|
|
8
|
+
const query = params.query.trim();
|
|
9
|
+
if (!query) {
|
|
10
|
+
return [];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
if (query.includes("@")) {
|
|
14
|
+
const escaped = escapeOData(query);
|
|
15
|
+
const filter = `(mail eq '${escaped}' or userPrincipalName eq '${escaped}')`;
|
|
16
|
+
const path = `/users?$filter=${encodeURIComponent(filter)}&$select=id,displayName,mail,userPrincipalName`;
|
|
17
|
+
const res = await fetchGraphJson<GraphResponse<GraphUser>>({ token: params.token, path });
|
|
18
|
+
return res.value ?? [];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const top = typeof params.top === "number" && params.top > 0 ? params.top : 10;
|
|
22
|
+
const path = `/users?$search=${encodeURIComponent(`"displayName:${query}"`)}&$select=id,displayName,mail,userPrincipalName&$top=${top}`;
|
|
23
|
+
const res = await fetchGraphJson<GraphResponse<GraphUser>>({
|
|
24
|
+
token: params.token,
|
|
25
|
+
path,
|
|
26
|
+
headers: { ConsistencyLevel: "eventual" },
|
|
27
|
+
});
|
|
28
|
+
return res.value ?? [];
|
|
29
|
+
}
|
package/src/graph.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { MSTeamsConfig } from "openclaw/plugin-sdk";
|
|
2
2
|
import { GRAPH_ROOT } from "./attachments/shared.js";
|
|
3
3
|
import { loadMSTeamsSdkWithAuth } from "./sdk.js";
|
|
4
|
+
import { readAccessToken } from "./token-response.js";
|
|
4
5
|
import { resolveMSTeamsCredentials } from "./token.js";
|
|
5
6
|
|
|
6
7
|
export type GraphUser = {
|
|
@@ -22,18 +23,6 @@ export type GraphChannel = {
|
|
|
22
23
|
|
|
23
24
|
export type GraphResponse<T> = { value?: T[] };
|
|
24
25
|
|
|
25
|
-
function readAccessToken(value: unknown): string | null {
|
|
26
|
-
if (typeof value === "string") {
|
|
27
|
-
return value;
|
|
28
|
-
}
|
|
29
|
-
if (value && typeof value === "object") {
|
|
30
|
-
const token =
|
|
31
|
-
(value as { accessToken?: unknown }).accessToken ?? (value as { token?: unknown }).token;
|
|
32
|
-
return typeof token === "string" ? token : null;
|
|
33
|
-
}
|
|
34
|
-
return null;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
26
|
export function normalizeQuery(value?: string | null): string {
|
|
38
27
|
return value?.trim() ?? "";
|
|
39
28
|
}
|
package/src/messenger.test.ts
CHANGED
|
@@ -49,6 +49,28 @@ const runtimeStub = {
|
|
|
49
49
|
},
|
|
50
50
|
} as unknown as PluginRuntime;
|
|
51
51
|
|
|
52
|
+
const createNoopAdapter = (): MSTeamsAdapter => ({
|
|
53
|
+
continueConversation: async () => {},
|
|
54
|
+
process: async () => {},
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const createRecordedSendActivity = (
|
|
58
|
+
sink: string[],
|
|
59
|
+
failFirstWithStatusCode?: number,
|
|
60
|
+
): ((activity: unknown) => Promise<{ id: string }>) => {
|
|
61
|
+
let attempts = 0;
|
|
62
|
+
return async (activity: unknown) => {
|
|
63
|
+
const { text } = activity as { text?: string };
|
|
64
|
+
const content = text ?? "";
|
|
65
|
+
sink.push(content);
|
|
66
|
+
attempts += 1;
|
|
67
|
+
if (failFirstWithStatusCode !== undefined && attempts === 1) {
|
|
68
|
+
throw Object.assign(new Error("send failed"), { statusCode: failFirstWithStatusCode });
|
|
69
|
+
}
|
|
70
|
+
return { id: `id:${content}` };
|
|
71
|
+
};
|
|
72
|
+
};
|
|
73
|
+
|
|
52
74
|
describe("msteams messenger", () => {
|
|
53
75
|
beforeEach(() => {
|
|
54
76
|
setMSTeamsRuntime(runtimeStub);
|
|
@@ -117,17 +139,9 @@ describe("msteams messenger", () => {
|
|
|
117
139
|
it("sends thread messages via the provided context", async () => {
|
|
118
140
|
const sent: string[] = [];
|
|
119
141
|
const ctx = {
|
|
120
|
-
sendActivity:
|
|
121
|
-
const { text } = activity as { text?: string };
|
|
122
|
-
sent.push(text ?? "");
|
|
123
|
-
return { id: `id:${text ?? ""}` };
|
|
124
|
-
},
|
|
125
|
-
};
|
|
126
|
-
|
|
127
|
-
const adapter: MSTeamsAdapter = {
|
|
128
|
-
continueConversation: async () => {},
|
|
129
|
-
process: async () => {},
|
|
142
|
+
sendActivity: createRecordedSendActivity(sent),
|
|
130
143
|
};
|
|
144
|
+
const adapter = createNoopAdapter();
|
|
131
145
|
|
|
132
146
|
const ids = await sendMSTeamsMessages({
|
|
133
147
|
replyStyle: "thread",
|
|
@@ -149,11 +163,7 @@ describe("msteams messenger", () => {
|
|
|
149
163
|
continueConversation: async (_appId, reference, logic) => {
|
|
150
164
|
seen.reference = reference;
|
|
151
165
|
await logic({
|
|
152
|
-
sendActivity:
|
|
153
|
-
const { text } = activity as { text?: string };
|
|
154
|
-
seen.texts.push(text ?? "");
|
|
155
|
-
return { id: `id:${text ?? ""}` };
|
|
156
|
-
},
|
|
166
|
+
sendActivity: createRecordedSendActivity(seen.texts),
|
|
157
167
|
});
|
|
158
168
|
},
|
|
159
169
|
process: async () => {},
|
|
@@ -192,10 +202,7 @@ describe("msteams messenger", () => {
|
|
|
192
202
|
},
|
|
193
203
|
};
|
|
194
204
|
|
|
195
|
-
const adapter
|
|
196
|
-
continueConversation: async () => {},
|
|
197
|
-
process: async () => {},
|
|
198
|
-
};
|
|
205
|
+
const adapter = createNoopAdapter();
|
|
199
206
|
|
|
200
207
|
const ids = await sendMSTeamsMessages({
|
|
201
208
|
replyStyle: "thread",
|
|
@@ -242,20 +249,9 @@ describe("msteams messenger", () => {
|
|
|
242
249
|
const retryEvents: Array<{ nextAttempt: number; delayMs: number }> = [];
|
|
243
250
|
|
|
244
251
|
const ctx = {
|
|
245
|
-
sendActivity:
|
|
246
|
-
const { text } = activity as { text?: string };
|
|
247
|
-
attempts.push(text ?? "");
|
|
248
|
-
if (attempts.length === 1) {
|
|
249
|
-
throw Object.assign(new Error("throttled"), { statusCode: 429 });
|
|
250
|
-
}
|
|
251
|
-
return { id: `id:${text ?? ""}` };
|
|
252
|
-
},
|
|
253
|
-
};
|
|
254
|
-
|
|
255
|
-
const adapter: MSTeamsAdapter = {
|
|
256
|
-
continueConversation: async () => {},
|
|
257
|
-
process: async () => {},
|
|
252
|
+
sendActivity: createRecordedSendActivity(attempts, 429),
|
|
258
253
|
};
|
|
254
|
+
const adapter = createNoopAdapter();
|
|
259
255
|
|
|
260
256
|
const ids = await sendMSTeamsMessages({
|
|
261
257
|
replyStyle: "thread",
|
|
@@ -280,10 +276,7 @@ describe("msteams messenger", () => {
|
|
|
280
276
|
},
|
|
281
277
|
};
|
|
282
278
|
|
|
283
|
-
const adapter
|
|
284
|
-
continueConversation: async () => {},
|
|
285
|
-
process: async () => {},
|
|
286
|
-
};
|
|
279
|
+
const adapter = createNoopAdapter();
|
|
287
280
|
|
|
288
281
|
await expect(
|
|
289
282
|
sendMSTeamsMessages({
|
|
@@ -303,18 +296,7 @@ describe("msteams messenger", () => {
|
|
|
303
296
|
|
|
304
297
|
const adapter: MSTeamsAdapter = {
|
|
305
298
|
continueConversation: async (_appId, _reference, logic) => {
|
|
306
|
-
await logic({
|
|
307
|
-
sendActivity: async (activity: unknown) => {
|
|
308
|
-
const { text } = activity as { text?: string };
|
|
309
|
-
attempts.push(text ?? "");
|
|
310
|
-
if (attempts.length === 1) {
|
|
311
|
-
throw Object.assign(new Error("server error"), {
|
|
312
|
-
statusCode: 503,
|
|
313
|
-
});
|
|
314
|
-
}
|
|
315
|
-
return { id: `id:${text ?? ""}` };
|
|
316
|
-
},
|
|
317
|
-
});
|
|
299
|
+
await logic({ sendActivity: createRecordedSendActivity(attempts, 503) });
|
|
318
300
|
},
|
|
319
301
|
process: async () => {},
|
|
320
302
|
};
|
package/src/messenger.ts
CHANGED
|
@@ -441,11 +441,7 @@ export async function sendMSTeamsMessages(params: {
|
|
|
441
441
|
}
|
|
442
442
|
};
|
|
443
443
|
|
|
444
|
-
|
|
445
|
-
const ctx = params.context;
|
|
446
|
-
if (!ctx) {
|
|
447
|
-
throw new Error("Missing context for replyStyle=thread");
|
|
448
|
-
}
|
|
444
|
+
const sendMessagesInContext = async (ctx: SendContext): Promise<string[]> => {
|
|
449
445
|
const messageIds: string[] = [];
|
|
450
446
|
for (const [idx, message] of messages.entries()) {
|
|
451
447
|
const response = await sendWithRetry(
|
|
@@ -464,6 +460,14 @@ export async function sendMSTeamsMessages(params: {
|
|
|
464
460
|
messageIds.push(extractMessageId(response) ?? "unknown");
|
|
465
461
|
}
|
|
466
462
|
return messageIds;
|
|
463
|
+
};
|
|
464
|
+
|
|
465
|
+
if (params.replyStyle === "thread") {
|
|
466
|
+
const ctx = params.context;
|
|
467
|
+
if (!ctx) {
|
|
468
|
+
throw new Error("Missing context for replyStyle=thread");
|
|
469
|
+
}
|
|
470
|
+
return await sendMessagesInContext(ctx);
|
|
467
471
|
}
|
|
468
472
|
|
|
469
473
|
const baseRef = buildConversationReference(params.conversationRef);
|
|
@@ -474,22 +478,7 @@ export async function sendMSTeamsMessages(params: {
|
|
|
474
478
|
|
|
475
479
|
const messageIds: string[] = [];
|
|
476
480
|
await params.adapter.continueConversation(params.appId, proactiveRef, async (ctx) => {
|
|
477
|
-
|
|
478
|
-
const response = await sendWithRetry(
|
|
479
|
-
async () =>
|
|
480
|
-
await ctx.sendActivity(
|
|
481
|
-
await buildActivity(
|
|
482
|
-
message,
|
|
483
|
-
params.conversationRef,
|
|
484
|
-
params.tokenProvider,
|
|
485
|
-
params.sharePointSiteId,
|
|
486
|
-
params.mediaMaxBytes,
|
|
487
|
-
),
|
|
488
|
-
),
|
|
489
|
-
{ messageIndex: idx, messageCount: messages.length },
|
|
490
|
-
);
|
|
491
|
-
messageIds.push(extractMessageId(response) ?? "unknown");
|
|
492
|
-
}
|
|
481
|
+
messageIds.push(...(await sendMessagesInContext(ctx)));
|
|
493
482
|
});
|
|
494
483
|
return messageIds;
|
|
495
484
|
}
|
|
@@ -5,6 +5,8 @@ import {
|
|
|
5
5
|
logInboundDrop,
|
|
6
6
|
recordPendingHistoryEntryIfEnabled,
|
|
7
7
|
resolveControlCommandGate,
|
|
8
|
+
resolveDefaultGroupPolicy,
|
|
9
|
+
isDangerousNameMatchingEnabled,
|
|
8
10
|
resolveMentionGating,
|
|
9
11
|
formatAllowlistMatchMeta,
|
|
10
12
|
type HistoryEntry,
|
|
@@ -124,16 +126,17 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
|
|
|
124
126
|
|
|
125
127
|
const senderName = from.name ?? from.id;
|
|
126
128
|
const senderId = from.aadObjectId ?? from.id;
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
|
|
129
|
+
const dmPolicy = msteamsCfg?.dmPolicy ?? "pairing";
|
|
130
|
+
const storedAllowFrom =
|
|
131
|
+
dmPolicy === "allowlist"
|
|
132
|
+
? []
|
|
133
|
+
: await core.channel.pairing.readAllowFromStore("msteams").catch(() => []);
|
|
130
134
|
const useAccessGroups = cfg.commands?.useAccessGroups !== false;
|
|
131
135
|
|
|
132
136
|
// Check DM policy for direct messages.
|
|
133
137
|
const dmAllowFrom = msteamsCfg?.allowFrom ?? [];
|
|
134
138
|
const effectiveDmAllowFrom = [...dmAllowFrom.map((v) => String(v)), ...storedAllowFrom];
|
|
135
139
|
if (isDirectMessage && msteamsCfg) {
|
|
136
|
-
const dmPolicy = msteamsCfg.dmPolicy ?? "pairing";
|
|
137
140
|
const allowFrom = dmAllowFrom;
|
|
138
141
|
|
|
139
142
|
if (dmPolicy === "disabled") {
|
|
@@ -143,10 +146,12 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
|
|
|
143
146
|
|
|
144
147
|
if (dmPolicy !== "open") {
|
|
145
148
|
const effectiveAllowFrom = [...allowFrom.map((v) => String(v)), ...storedAllowFrom];
|
|
149
|
+
const allowNameMatching = isDangerousNameMatchingEnabled(msteamsCfg);
|
|
146
150
|
const allowMatch = resolveMSTeamsAllowlistMatch({
|
|
147
151
|
allowFrom: effectiveAllowFrom,
|
|
148
152
|
senderId,
|
|
149
153
|
senderName,
|
|
154
|
+
allowNameMatching,
|
|
150
155
|
});
|
|
151
156
|
|
|
152
157
|
if (!allowMatch.allowed) {
|
|
@@ -173,7 +178,7 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
|
|
|
173
178
|
}
|
|
174
179
|
}
|
|
175
180
|
|
|
176
|
-
const defaultGroupPolicy = cfg
|
|
181
|
+
const defaultGroupPolicy = resolveDefaultGroupPolicy(cfg);
|
|
177
182
|
const groupPolicy =
|
|
178
183
|
!isDirectMessage && msteamsCfg
|
|
179
184
|
? (msteamsCfg.groupPolicy ?? defaultGroupPolicy ?? "allowlist")
|
|
@@ -224,10 +229,12 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
|
|
|
224
229
|
return;
|
|
225
230
|
}
|
|
226
231
|
if (effectiveGroupAllowFrom.length > 0) {
|
|
232
|
+
const allowNameMatching = isDangerousNameMatchingEnabled(msteamsCfg);
|
|
227
233
|
const allowMatch = resolveMSTeamsAllowlistMatch({
|
|
228
234
|
allowFrom: effectiveGroupAllowFrom,
|
|
229
235
|
senderId,
|
|
230
236
|
senderName,
|
|
237
|
+
allowNameMatching,
|
|
231
238
|
});
|
|
232
239
|
if (!allowMatch.allowed) {
|
|
233
240
|
log.debug?.("dropping group message (not in groupAllowFrom)", {
|
|
@@ -246,12 +253,14 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
|
|
|
246
253
|
allowFrom: effectiveDmAllowFrom,
|
|
247
254
|
senderId,
|
|
248
255
|
senderName,
|
|
256
|
+
allowNameMatching: isDangerousNameMatchingEnabled(msteamsCfg),
|
|
249
257
|
});
|
|
250
258
|
const groupAllowedForCommands = isMSTeamsGroupAllowed({
|
|
251
259
|
groupPolicy: "allowlist",
|
|
252
260
|
allowFrom: effectiveGroupAllowFrom,
|
|
253
261
|
senderId,
|
|
254
262
|
senderName,
|
|
263
|
+
allowNameMatching: isDangerousNameMatchingEnabled(msteamsCfg),
|
|
255
264
|
});
|
|
256
265
|
const hasControlCommandInMessage = core.channel.text.hasControlCommand(text, cfg);
|
|
257
266
|
const commandGate = resolveControlCommandGate({
|
package/src/policy.test.ts
CHANGED
|
@@ -184,7 +184,7 @@ describe("msteams policy", () => {
|
|
|
184
184
|
).toBe(true);
|
|
185
185
|
});
|
|
186
186
|
|
|
187
|
-
it("
|
|
187
|
+
it("blocks sender-name allowlist matches by default", () => {
|
|
188
188
|
expect(
|
|
189
189
|
isMSTeamsGroupAllowed({
|
|
190
190
|
groupPolicy: "allowlist",
|
|
@@ -192,6 +192,18 @@ describe("msteams policy", () => {
|
|
|
192
192
|
senderId: "other",
|
|
193
193
|
senderName: "User",
|
|
194
194
|
}),
|
|
195
|
+
).toBe(false);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it("allows sender-name allowlist matches when explicitly enabled", () => {
|
|
199
|
+
expect(
|
|
200
|
+
isMSTeamsGroupAllowed({
|
|
201
|
+
groupPolicy: "allowlist",
|
|
202
|
+
allowFrom: ["user"],
|
|
203
|
+
senderId: "other",
|
|
204
|
+
senderName: "User",
|
|
205
|
+
allowNameMatching: true,
|
|
206
|
+
}),
|
|
195
207
|
).toBe(true);
|
|
196
208
|
});
|
|
197
209
|
|
package/src/policy.ts
CHANGED
|
@@ -209,6 +209,7 @@ export function resolveMSTeamsAllowlistMatch(params: {
|
|
|
209
209
|
allowFrom: Array<string | number>;
|
|
210
210
|
senderId: string;
|
|
211
211
|
senderName?: string | null;
|
|
212
|
+
allowNameMatching?: boolean;
|
|
212
213
|
}): MSTeamsAllowlistMatch {
|
|
213
214
|
return resolveAllowlistMatchSimple(params);
|
|
214
215
|
}
|
|
@@ -245,6 +246,7 @@ export function isMSTeamsGroupAllowed(params: {
|
|
|
245
246
|
allowFrom: Array<string | number>;
|
|
246
247
|
senderId: string;
|
|
247
248
|
senderName?: string | null;
|
|
249
|
+
allowNameMatching?: boolean;
|
|
248
250
|
}): boolean {
|
|
249
251
|
const { groupPolicy } = params;
|
|
250
252
|
if (groupPolicy === "disabled") {
|
package/src/probe.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { BaseProbeResult, MSTeamsConfig } from "openclaw/plugin-sdk";
|
|
2
2
|
import { formatUnknownError } from "./errors.js";
|
|
3
3
|
import { loadMSTeamsSdkWithAuth } from "./sdk.js";
|
|
4
|
+
import { readAccessToken } from "./token-response.js";
|
|
4
5
|
import { resolveMSTeamsCredentials } from "./token.js";
|
|
5
6
|
|
|
6
7
|
export type ProbeMSTeamsResult = BaseProbeResult<string> & {
|
|
@@ -13,18 +14,6 @@ export type ProbeMSTeamsResult = BaseProbeResult<string> & {
|
|
|
13
14
|
};
|
|
14
15
|
};
|
|
15
16
|
|
|
16
|
-
function readAccessToken(value: unknown): string | null {
|
|
17
|
-
if (typeof value === "string") {
|
|
18
|
-
return value;
|
|
19
|
-
}
|
|
20
|
-
if (value && typeof value === "object") {
|
|
21
|
-
const token =
|
|
22
|
-
(value as { accessToken?: unknown }).accessToken ?? (value as { token?: unknown }).token;
|
|
23
|
-
return typeof token === "string" ? token : null;
|
|
24
|
-
}
|
|
25
|
-
return null;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
17
|
function decodeJwtPayload(token: string): Record<string, unknown> | null {
|
|
29
18
|
const parts = token.split(".");
|
|
30
19
|
if (parts.length < 2) {
|
package/src/resolve-allowlist.ts
CHANGED
|
@@ -1,8 +1,5 @@
|
|
|
1
|
+
import { searchGraphUsers } from "./graph-users.js";
|
|
1
2
|
import {
|
|
2
|
-
escapeOData,
|
|
3
|
-
fetchGraphJson,
|
|
4
|
-
type GraphResponse,
|
|
5
|
-
type GraphUser,
|
|
6
3
|
listChannelsForTeam,
|
|
7
4
|
listTeamsByName,
|
|
8
5
|
normalizeQuery,
|
|
@@ -182,22 +179,7 @@ export async function resolveMSTeamsUserAllowlist(params: {
|
|
|
182
179
|
results.push({ input, resolved: true, id: query });
|
|
183
180
|
continue;
|
|
184
181
|
}
|
|
185
|
-
|
|
186
|
-
if (query.includes("@")) {
|
|
187
|
-
const escaped = escapeOData(query);
|
|
188
|
-
const filter = `(mail eq '${escaped}' or userPrincipalName eq '${escaped}')`;
|
|
189
|
-
const path = `/users?$filter=${encodeURIComponent(filter)}&$select=id,displayName,mail,userPrincipalName`;
|
|
190
|
-
const res = await fetchGraphJson<GraphResponse<GraphUser>>({ token, path });
|
|
191
|
-
users = res.value ?? [];
|
|
192
|
-
} else {
|
|
193
|
-
const path = `/users?$search=${encodeURIComponent(`"displayName:${query}"`)}&$select=id,displayName,mail,userPrincipalName&$top=10`;
|
|
194
|
-
const res = await fetchGraphJson<GraphResponse<GraphUser>>({
|
|
195
|
-
token,
|
|
196
|
-
path,
|
|
197
|
-
headers: { ConsistencyLevel: "eventual" },
|
|
198
|
-
});
|
|
199
|
-
users = res.value ?? [];
|
|
200
|
-
}
|
|
182
|
+
const users = await searchGraphUsers({ token, query, top: 10 });
|
|
201
183
|
const match = users[0];
|
|
202
184
|
if (!match?.id) {
|
|
203
185
|
results.push({ input, resolved: false });
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { readAccessToken } from "./token-response.js";
|
|
3
|
+
|
|
4
|
+
describe("readAccessToken", () => {
|
|
5
|
+
it("returns raw string token values", () => {
|
|
6
|
+
expect(readAccessToken("abc")).toBe("abc");
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it("returns accessToken from object value", () => {
|
|
10
|
+
expect(readAccessToken({ accessToken: "access-token" })).toBe("access-token");
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("returns token fallback from object value", () => {
|
|
14
|
+
expect(readAccessToken({ token: "fallback-token" })).toBe("fallback-token");
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("returns null for unsupported values", () => {
|
|
18
|
+
expect(readAccessToken({ accessToken: 123 })).toBeNull();
|
|
19
|
+
expect(readAccessToken({ token: false })).toBeNull();
|
|
20
|
+
expect(readAccessToken(null)).toBeNull();
|
|
21
|
+
expect(readAccessToken(undefined)).toBeNull();
|
|
22
|
+
});
|
|
23
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export function readAccessToken(value: unknown): string | null {
|
|
2
|
+
if (typeof value === "string") {
|
|
3
|
+
return value;
|
|
4
|
+
}
|
|
5
|
+
if (value && typeof value === "object") {
|
|
6
|
+
const token =
|
|
7
|
+
(value as { accessToken?: unknown }).accessToken ?? (value as { token?: unknown }).token;
|
|
8
|
+
return typeof token === "string" ? token : null;
|
|
9
|
+
}
|
|
10
|
+
return null;
|
|
11
|
+
}
|