@openclaw/msteams 2026.3.7 → 2026.3.10
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
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2026.3.10
|
|
4
|
+
|
|
5
|
+
### Changes
|
|
6
|
+
|
|
7
|
+
- Version alignment with core OpenClaw release numbers.
|
|
8
|
+
|
|
9
|
+
## 2026.3.9
|
|
10
|
+
|
|
11
|
+
### Changes
|
|
12
|
+
|
|
13
|
+
- Version alignment with core OpenClaw release numbers.
|
|
14
|
+
|
|
15
|
+
## 2026.3.8-beta.1
|
|
16
|
+
|
|
17
|
+
### Changes
|
|
18
|
+
|
|
19
|
+
- Version alignment with core OpenClaw release numbers.
|
|
20
|
+
|
|
21
|
+
## 2026.3.8
|
|
22
|
+
|
|
23
|
+
### Changes
|
|
24
|
+
|
|
25
|
+
- Version alignment with core OpenClaw release numbers.
|
|
26
|
+
|
|
3
27
|
## 2026.3.7
|
|
4
28
|
|
|
5
29
|
### Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openclaw/msteams",
|
|
3
|
-
"version": "2026.3.
|
|
3
|
+
"version": "2026.3.10",
|
|
4
4
|
"description": "OpenClaw Microsoft Teams channel plugin",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"dependencies": {
|
|
@@ -27,6 +27,11 @@
|
|
|
27
27
|
"npmSpec": "@openclaw/msteams",
|
|
28
28
|
"localPath": "extensions/msteams",
|
|
29
29
|
"defaultChoice": "npm"
|
|
30
|
+
},
|
|
31
|
+
"releaseChecks": {
|
|
32
|
+
"rootDependencyMirrorAllowlist": [
|
|
33
|
+
"@microsoft/agents-hosting"
|
|
34
|
+
]
|
|
30
35
|
}
|
|
31
36
|
}
|
|
32
37
|
}
|
|
@@ -5,7 +5,7 @@ import { setMSTeamsRuntime } from "../runtime.js";
|
|
|
5
5
|
import { createMSTeamsMessageHandler } from "./message-handler.js";
|
|
6
6
|
|
|
7
7
|
describe("msteams monitor handler authz", () => {
|
|
8
|
-
|
|
8
|
+
function createDeps(cfg: OpenClawConfig) {
|
|
9
9
|
const readAllowFromStore = vi.fn(async () => ["attacker-aad"]);
|
|
10
10
|
setMSTeamsRuntime({
|
|
11
11
|
logging: { shouldLogVerbose: () => false },
|
|
@@ -35,16 +35,7 @@ describe("msteams monitor handler authz", () => {
|
|
|
35
35
|
};
|
|
36
36
|
|
|
37
37
|
const deps: MSTeamsMessageHandlerDeps = {
|
|
38
|
-
cfg
|
|
39
|
-
channels: {
|
|
40
|
-
msteams: {
|
|
41
|
-
dmPolicy: "pairing",
|
|
42
|
-
allowFrom: [],
|
|
43
|
-
groupPolicy: "allowlist",
|
|
44
|
-
groupAllowFrom: [],
|
|
45
|
-
},
|
|
46
|
-
},
|
|
47
|
-
} as OpenClawConfig,
|
|
38
|
+
cfg,
|
|
48
39
|
runtime: { error: vi.fn() } as unknown as RuntimeEnv,
|
|
49
40
|
appId: "test-app",
|
|
50
41
|
adapter: {} as MSTeamsMessageHandlerDeps["adapter"],
|
|
@@ -65,6 +56,21 @@ describe("msteams monitor handler authz", () => {
|
|
|
65
56
|
} as unknown as MSTeamsMessageHandlerDeps["log"],
|
|
66
57
|
};
|
|
67
58
|
|
|
59
|
+
return { conversationStore, deps, readAllowFromStore };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
it("does not treat DM pairing-store entries as group allowlist entries", async () => {
|
|
63
|
+
const { conversationStore, deps, readAllowFromStore } = createDeps({
|
|
64
|
+
channels: {
|
|
65
|
+
msteams: {
|
|
66
|
+
dmPolicy: "pairing",
|
|
67
|
+
allowFrom: [],
|
|
68
|
+
groupPolicy: "allowlist",
|
|
69
|
+
groupAllowFrom: [],
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
} as OpenClawConfig);
|
|
73
|
+
|
|
68
74
|
const handler = createMSTeamsMessageHandler(deps);
|
|
69
75
|
await handler({
|
|
70
76
|
activity: {
|
|
@@ -96,4 +102,54 @@ describe("msteams monitor handler authz", () => {
|
|
|
96
102
|
});
|
|
97
103
|
expect(conversationStore.upsert).not.toHaveBeenCalled();
|
|
98
104
|
});
|
|
105
|
+
|
|
106
|
+
it("does not widen sender auth when only a teams route allowlist is configured", async () => {
|
|
107
|
+
const { conversationStore, deps } = createDeps({
|
|
108
|
+
channels: {
|
|
109
|
+
msteams: {
|
|
110
|
+
dmPolicy: "pairing",
|
|
111
|
+
allowFrom: [],
|
|
112
|
+
groupPolicy: "allowlist",
|
|
113
|
+
groupAllowFrom: [],
|
|
114
|
+
teams: {
|
|
115
|
+
team123: {
|
|
116
|
+
channels: {
|
|
117
|
+
"19:group@thread.tacv2": { requireMention: false },
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
} as OpenClawConfig);
|
|
124
|
+
|
|
125
|
+
const handler = createMSTeamsMessageHandler(deps);
|
|
126
|
+
await handler({
|
|
127
|
+
activity: {
|
|
128
|
+
id: "msg-1",
|
|
129
|
+
type: "message",
|
|
130
|
+
text: "hello",
|
|
131
|
+
from: {
|
|
132
|
+
id: "attacker-id",
|
|
133
|
+
aadObjectId: "attacker-aad",
|
|
134
|
+
name: "Attacker",
|
|
135
|
+
},
|
|
136
|
+
recipient: {
|
|
137
|
+
id: "bot-id",
|
|
138
|
+
name: "Bot",
|
|
139
|
+
},
|
|
140
|
+
conversation: {
|
|
141
|
+
id: "19:group@thread.tacv2",
|
|
142
|
+
conversationType: "groupChat",
|
|
143
|
+
},
|
|
144
|
+
channelData: {
|
|
145
|
+
team: { id: "team123", name: "Team 123" },
|
|
146
|
+
channel: { name: "General" },
|
|
147
|
+
},
|
|
148
|
+
attachments: [],
|
|
149
|
+
},
|
|
150
|
+
sendActivity: vi.fn(async () => undefined),
|
|
151
|
+
} as unknown as Parameters<typeof handler>[0]);
|
|
152
|
+
|
|
153
|
+
expect(conversationStore.upsert).not.toHaveBeenCalled();
|
|
154
|
+
});
|
|
99
155
|
});
|
|
@@ -242,10 +242,7 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
|
|
|
242
242
|
}
|
|
243
243
|
const senderGroupAccess = evaluateSenderGroupAccessForPolicy({
|
|
244
244
|
groupPolicy,
|
|
245
|
-
groupAllowFrom:
|
|
246
|
-
effectiveGroupAllowFrom.length > 0 || !channelGate.allowlistConfigured
|
|
247
|
-
? effectiveGroupAllowFrom
|
|
248
|
-
: ["*"],
|
|
245
|
+
groupAllowFrom: effectiveGroupAllowFrom,
|
|
249
246
|
senderId,
|
|
250
247
|
isSenderAllowed: (_senderId, allowFrom) =>
|
|
251
248
|
resolveMSTeamsAllowlistMatch({
|
|
@@ -54,10 +54,12 @@ describe("resolveMSTeamsUserAllowlist", () => {
|
|
|
54
54
|
|
|
55
55
|
describe("resolveMSTeamsChannelAllowlist", () => {
|
|
56
56
|
it("resolves team/channel by team name + channel display name", async () => {
|
|
57
|
-
|
|
57
|
+
// After the fix, listChannelsForTeam is called once and reused for both
|
|
58
|
+
// General channel resolution and channel matching.
|
|
59
|
+
listTeamsByName.mockResolvedValueOnce([{ id: "team-guid-1", displayName: "Product Team" }]);
|
|
58
60
|
listChannelsForTeam.mockResolvedValueOnce([
|
|
59
|
-
{ id: "
|
|
60
|
-
{ id: "
|
|
61
|
+
{ id: "19:general-conv-id@thread.tacv2", displayName: "General" },
|
|
62
|
+
{ id: "19:roadmap-conv-id@thread.tacv2", displayName: "Roadmap" },
|
|
61
63
|
]);
|
|
62
64
|
|
|
63
65
|
const [result] = await resolveMSTeamsChannelAllowlist({
|
|
@@ -65,14 +67,80 @@ describe("resolveMSTeamsChannelAllowlist", () => {
|
|
|
65
67
|
entries: ["Product Team/Roadmap"],
|
|
66
68
|
});
|
|
67
69
|
|
|
70
|
+
// teamId is now the General channel's conversation ID — not the Graph GUID —
|
|
71
|
+
// because that's what Bot Framework sends as channelData.team.id at runtime.
|
|
68
72
|
expect(result).toEqual({
|
|
69
73
|
input: "Product Team/Roadmap",
|
|
70
74
|
resolved: true,
|
|
71
|
-
teamId: "
|
|
75
|
+
teamId: "19:general-conv-id@thread.tacv2",
|
|
72
76
|
teamName: "Product Team",
|
|
73
|
-
channelId: "
|
|
77
|
+
channelId: "19:roadmap-conv-id@thread.tacv2",
|
|
74
78
|
channelName: "Roadmap",
|
|
75
79
|
note: "multiple channels; chose first",
|
|
76
80
|
});
|
|
77
81
|
});
|
|
82
|
+
|
|
83
|
+
it("uses General channel conversation ID as team key for team-only entry", async () => {
|
|
84
|
+
// When no channel is specified we still resolve the General channel so the
|
|
85
|
+
// stored key matches what Bot Framework sends as channelData.team.id.
|
|
86
|
+
listTeamsByName.mockResolvedValueOnce([{ id: "guid-engineering", displayName: "Engineering" }]);
|
|
87
|
+
listChannelsForTeam.mockResolvedValueOnce([
|
|
88
|
+
{ id: "19:eng-general@thread.tacv2", displayName: "General" },
|
|
89
|
+
{ id: "19:eng-standups@thread.tacv2", displayName: "Standups" },
|
|
90
|
+
]);
|
|
91
|
+
|
|
92
|
+
const [result] = await resolveMSTeamsChannelAllowlist({
|
|
93
|
+
cfg: {},
|
|
94
|
+
entries: ["Engineering"],
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
expect(result).toEqual({
|
|
98
|
+
input: "Engineering",
|
|
99
|
+
resolved: true,
|
|
100
|
+
teamId: "19:eng-general@thread.tacv2",
|
|
101
|
+
teamName: "Engineering",
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("falls back to Graph GUID when listChannelsForTeam throws", async () => {
|
|
106
|
+
// Edge case: API call fails (rate limit, network error). We fall back to
|
|
107
|
+
// the Graph GUID as the team key — the pre-fix behavior — so resolution
|
|
108
|
+
// still succeeds instead of propagating the error.
|
|
109
|
+
listTeamsByName.mockResolvedValueOnce([{ id: "guid-flaky", displayName: "Flaky Team" }]);
|
|
110
|
+
listChannelsForTeam.mockRejectedValueOnce(new Error("429 Too Many Requests"));
|
|
111
|
+
|
|
112
|
+
const [result] = await resolveMSTeamsChannelAllowlist({
|
|
113
|
+
cfg: {},
|
|
114
|
+
entries: ["Flaky Team"],
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
expect(result).toEqual({
|
|
118
|
+
input: "Flaky Team",
|
|
119
|
+
resolved: true,
|
|
120
|
+
teamId: "guid-flaky",
|
|
121
|
+
teamName: "Flaky Team",
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("falls back to Graph GUID when General channel is not found", async () => {
|
|
126
|
+
// Edge case: General channel was renamed or deleted. We fall back to the
|
|
127
|
+
// Graph GUID so resolution still succeeds rather than silently breaking.
|
|
128
|
+
listTeamsByName.mockResolvedValueOnce([{ id: "guid-ops", displayName: "Operations" }]);
|
|
129
|
+
listChannelsForTeam.mockResolvedValueOnce([
|
|
130
|
+
{ id: "19:ops-announce@thread.tacv2", displayName: "Announcements" },
|
|
131
|
+
{ id: "19:ops-random@thread.tacv2", displayName: "Random" },
|
|
132
|
+
]);
|
|
133
|
+
|
|
134
|
+
const [result] = await resolveMSTeamsChannelAllowlist({
|
|
135
|
+
cfg: {},
|
|
136
|
+
entries: ["Operations"],
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
expect(result).toEqual({
|
|
140
|
+
input: "Operations",
|
|
141
|
+
resolved: true,
|
|
142
|
+
teamId: "guid-ops",
|
|
143
|
+
teamName: "Operations",
|
|
144
|
+
});
|
|
145
|
+
});
|
|
78
146
|
});
|
package/src/resolve-allowlist.ts
CHANGED
|
@@ -120,11 +120,26 @@ export async function resolveMSTeamsChannelAllowlist(params: {
|
|
|
120
120
|
return { input, resolved: false, note: "team not found" };
|
|
121
121
|
}
|
|
122
122
|
const teamMatch = teams[0];
|
|
123
|
-
const
|
|
123
|
+
const graphTeamId = teamMatch.id?.trim();
|
|
124
124
|
const teamName = teamMatch.displayName?.trim() || team;
|
|
125
|
-
if (!
|
|
125
|
+
if (!graphTeamId) {
|
|
126
126
|
return { input, resolved: false, note: "team id missing" };
|
|
127
127
|
}
|
|
128
|
+
// Bot Framework sends the General channel's conversation ID as
|
|
129
|
+
// channelData.team.id at runtime, NOT the Graph API group GUID.
|
|
130
|
+
// Fetch channels upfront so we can resolve the correct key format for
|
|
131
|
+
// runtime matching and reuse the list for channel lookups.
|
|
132
|
+
let teamChannels: Awaited<ReturnType<typeof listChannelsForTeam>> = [];
|
|
133
|
+
try {
|
|
134
|
+
teamChannels = await listChannelsForTeam(token, graphTeamId);
|
|
135
|
+
} catch {
|
|
136
|
+
// API failure (rate limit, network error) — fall back to Graph GUID as team key
|
|
137
|
+
}
|
|
138
|
+
const generalChannel = teamChannels.find((ch) => ch.displayName?.toLowerCase() === "general");
|
|
139
|
+
// Use the General channel's conversation ID as the team key — this
|
|
140
|
+
// matches what Bot Framework sends at runtime. Fall back to the Graph
|
|
141
|
+
// GUID if the General channel isn't found (renamed or deleted).
|
|
142
|
+
const teamId = generalChannel?.id?.trim() || graphTeamId;
|
|
128
143
|
if (!channel) {
|
|
129
144
|
return {
|
|
130
145
|
input,
|
|
@@ -134,11 +149,11 @@ export async function resolveMSTeamsChannelAllowlist(params: {
|
|
|
134
149
|
note: teams.length > 1 ? "multiple teams; chose first" : undefined,
|
|
135
150
|
};
|
|
136
151
|
}
|
|
137
|
-
|
|
152
|
+
// Reuse teamChannels — already fetched above
|
|
138
153
|
const channelMatch =
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
154
|
+
teamChannels.find((item) => item.id === channel) ??
|
|
155
|
+
teamChannels.find((item) => item.displayName?.toLowerCase() === channel.toLowerCase()) ??
|
|
156
|
+
teamChannels.find((item) =>
|
|
142
157
|
item.displayName?.toLowerCase().includes(channel.toLowerCase() ?? ""),
|
|
143
158
|
);
|
|
144
159
|
if (!channelMatch?.id) {
|
|
@@ -151,7 +166,7 @@ export async function resolveMSTeamsChannelAllowlist(params: {
|
|
|
151
166
|
teamName,
|
|
152
167
|
channelId: channelMatch.id,
|
|
153
168
|
channelName: channelMatch.displayName ?? channel,
|
|
154
|
-
note:
|
|
169
|
+
note: teamChannels.length > 1 ? "multiple channels; chose first" : undefined,
|
|
155
170
|
};
|
|
156
171
|
},
|
|
157
172
|
});
|
package/src/runtime.ts
CHANGED
|
@@ -1,14 +1,6 @@
|
|
|
1
|
+
import { createPluginRuntimeStore } from "openclaw/plugin-sdk/compat";
|
|
1
2
|
import type { PluginRuntime } from "openclaw/plugin-sdk/msteams";
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
export
|
|
6
|
-
runtime = next;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export function getMSTeamsRuntime(): PluginRuntime {
|
|
10
|
-
if (!runtime) {
|
|
11
|
-
throw new Error("MSTeams runtime not initialized");
|
|
12
|
-
}
|
|
13
|
-
return runtime;
|
|
14
|
-
}
|
|
4
|
+
const { setRuntime: setMSTeamsRuntime, getRuntime: getMSTeamsRuntime } =
|
|
5
|
+
createPluginRuntimeStore<PluginRuntime>("MSTeams runtime not initialized");
|
|
6
|
+
export { getMSTeamsRuntime, setMSTeamsRuntime };
|