@openclaw/msteams 2026.3.2 → 2026.3.8-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/CHANGELOG.md +24 -0
- package/index.ts +2 -2
- package/package.json +6 -1
- package/src/attachments/graph.ts +1 -1
- package/src/attachments/payload.ts +1 -1
- package/src/attachments/remote-media.ts +1 -1
- package/src/attachments/shared.ts +2 -2
- package/src/attachments.test.ts +1 -1
- package/src/channel.directory.test.ts +1 -1
- package/src/channel.ts +95 -101
- package/src/directory-live.ts +1 -1
- package/src/file-lock.ts +1 -1
- package/src/graph.ts +1 -1
- package/src/media-helpers.ts +1 -1
- package/src/messenger.test.ts +16 -19
- package/src/messenger.ts +1 -1
- package/src/monitor-handler/message-handler.authz.test.ts +68 -12
- package/src/monitor-handler/message-handler.ts +56 -53
- package/src/monitor-handler.file-consent.test.ts +1 -1
- package/src/monitor-handler.ts +1 -1
- package/src/monitor.lifecycle.test.ts +9 -3
- package/src/monitor.ts +1 -1
- package/src/onboarding.ts +23 -47
- package/src/outbound.test.ts +131 -0
- package/src/outbound.ts +1 -1
- package/src/policy.test.ts +1 -1
- package/src/policy.ts +9 -10
- package/src/probe.test.ts +1 -1
- package/src/probe.ts +6 -2
- package/src/reply-dispatcher.ts +1 -1
- package/src/resolve-allowlist.test.ts +78 -0
- package/src/resolve-allowlist.ts +70 -79
- package/src/runtime.ts +5 -13
- package/src/secret-input.ts +1 -1
- package/src/send-context.ts +1 -1
- package/src/send.test.ts +2 -2
- package/src/send.ts +42 -43
- package/src/store-fs.ts +1 -1
- package/src/test-runtime.ts +1 -1
- package/src/token.test.ts +1 -1
- package/src/token.ts +1 -1
|
@@ -2,20 +2,24 @@ import {
|
|
|
2
2
|
DEFAULT_ACCOUNT_ID,
|
|
3
3
|
buildPendingHistoryContextFromMap,
|
|
4
4
|
clearHistoryEntriesIfEnabled,
|
|
5
|
+
dispatchReplyFromConfigWithSettledDispatcher,
|
|
5
6
|
DEFAULT_GROUP_HISTORY_LIMIT,
|
|
6
7
|
createScopedPairingAccess,
|
|
7
8
|
logInboundDrop,
|
|
9
|
+
evaluateSenderGroupAccessForPolicy,
|
|
10
|
+
resolveSenderScopedGroupPolicy,
|
|
8
11
|
recordPendingHistoryEntryIfEnabled,
|
|
9
12
|
resolveControlCommandGate,
|
|
10
13
|
resolveDefaultGroupPolicy,
|
|
11
14
|
isDangerousNameMatchingEnabled,
|
|
12
15
|
readStoreAllowFromForDmPolicy,
|
|
13
16
|
resolveMentionGating,
|
|
17
|
+
resolveInboundSessionEnvelopeContext,
|
|
14
18
|
formatAllowlistMatchMeta,
|
|
15
19
|
resolveEffectiveAllowFromLists,
|
|
16
20
|
resolveDmGroupAccessWithLists,
|
|
17
21
|
type HistoryEntry,
|
|
18
|
-
} from "openclaw/plugin-sdk";
|
|
22
|
+
} from "openclaw/plugin-sdk/msteams";
|
|
19
23
|
import {
|
|
20
24
|
buildMSTeamsAttachmentPlaceholder,
|
|
21
25
|
buildMSTeamsMediaPayload,
|
|
@@ -172,12 +176,10 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
|
|
|
172
176
|
conversationId,
|
|
173
177
|
channelName,
|
|
174
178
|
});
|
|
175
|
-
const senderGroupPolicy =
|
|
176
|
-
groupPolicy
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
? "allowlist"
|
|
180
|
-
: "open";
|
|
179
|
+
const senderGroupPolicy = resolveSenderScopedGroupPolicy({
|
|
180
|
+
groupPolicy,
|
|
181
|
+
groupAllowFrom: effectiveGroupAllowFrom,
|
|
182
|
+
});
|
|
181
183
|
const access = resolveDmGroupAccessWithLists({
|
|
182
184
|
isGroup: !isDirectMessage,
|
|
183
185
|
dmPolicy,
|
|
@@ -228,46 +230,54 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
|
|
|
228
230
|
}
|
|
229
231
|
|
|
230
232
|
if (!isDirectMessage && msteamsCfg) {
|
|
231
|
-
if (
|
|
232
|
-
log.debug?.("dropping group message (
|
|
233
|
+
if (channelGate.allowlistConfigured && !channelGate.allowed) {
|
|
234
|
+
log.debug?.("dropping group message (not in team/channel allowlist)", {
|
|
233
235
|
conversationId,
|
|
236
|
+
teamKey: channelGate.teamKey ?? "none",
|
|
237
|
+
channelKey: channelGate.channelKey ?? "none",
|
|
238
|
+
channelMatchKey: channelGate.channelMatchKey ?? "none",
|
|
239
|
+
channelMatchSource: channelGate.channelMatchSource ?? "none",
|
|
234
240
|
});
|
|
235
241
|
return;
|
|
236
242
|
}
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
channelMatchKey: channelGate.channelMatchKey ?? "none",
|
|
245
|
-
channelMatchSource: channelGate.channelMatchSource ?? "none",
|
|
246
|
-
});
|
|
247
|
-
return;
|
|
248
|
-
}
|
|
249
|
-
if (effectiveGroupAllowFrom.length === 0 && !channelGate.allowlistConfigured) {
|
|
250
|
-
log.debug?.("dropping group message (groupPolicy: allowlist, no allowlist)", {
|
|
251
|
-
conversationId,
|
|
252
|
-
});
|
|
253
|
-
return;
|
|
254
|
-
}
|
|
255
|
-
if (effectiveGroupAllowFrom.length > 0 && access.decision !== "allow") {
|
|
256
|
-
const allowMatch = resolveMSTeamsAllowlistMatch({
|
|
257
|
-
allowFrom: effectiveGroupAllowFrom,
|
|
243
|
+
const senderGroupAccess = evaluateSenderGroupAccessForPolicy({
|
|
244
|
+
groupPolicy,
|
|
245
|
+
groupAllowFrom: effectiveGroupAllowFrom,
|
|
246
|
+
senderId,
|
|
247
|
+
isSenderAllowed: (_senderId, allowFrom) =>
|
|
248
|
+
resolveMSTeamsAllowlistMatch({
|
|
249
|
+
allowFrom,
|
|
258
250
|
senderId,
|
|
259
251
|
senderName,
|
|
260
252
|
allowNameMatching: isDangerousNameMatchingEnabled(msteamsCfg),
|
|
261
|
-
})
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
253
|
+
}).allowed,
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
if (!senderGroupAccess.allowed && senderGroupAccess.reason === "disabled") {
|
|
257
|
+
log.debug?.("dropping group message (groupPolicy: disabled)", {
|
|
258
|
+
conversationId,
|
|
259
|
+
});
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
if (!senderGroupAccess.allowed && senderGroupAccess.reason === "empty_allowlist") {
|
|
263
|
+
log.debug?.("dropping group message (groupPolicy: allowlist, no allowlist)", {
|
|
264
|
+
conversationId,
|
|
265
|
+
});
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
if (!senderGroupAccess.allowed && senderGroupAccess.reason === "sender_not_allowlisted") {
|
|
269
|
+
const allowMatch = resolveMSTeamsAllowlistMatch({
|
|
270
|
+
allowFrom: effectiveGroupAllowFrom,
|
|
271
|
+
senderId,
|
|
272
|
+
senderName,
|
|
273
|
+
allowNameMatching: isDangerousNameMatchingEnabled(msteamsCfg),
|
|
274
|
+
});
|
|
275
|
+
log.debug?.("dropping group message (not in groupAllowFrom)", {
|
|
276
|
+
sender: senderId,
|
|
277
|
+
label: senderName,
|
|
278
|
+
allowlistMatch: formatAllowlistMatchMeta(allowMatch),
|
|
279
|
+
});
|
|
280
|
+
return;
|
|
271
281
|
}
|
|
272
282
|
}
|
|
273
283
|
|
|
@@ -451,12 +461,9 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
|
|
|
451
461
|
|
|
452
462
|
const mediaPayload = buildMSTeamsMediaPayload(mediaList);
|
|
453
463
|
const envelopeFrom = isDirectMessage ? senderName : conversationType;
|
|
454
|
-
const storePath =
|
|
464
|
+
const { storePath, envelopeOptions, previousTimestamp } = resolveInboundSessionEnvelopeContext({
|
|
465
|
+
cfg,
|
|
455
466
|
agentId: route.agentId,
|
|
456
|
-
});
|
|
457
|
-
const envelopeOptions = core.channel.reply.resolveEnvelopeFormatOptions(cfg);
|
|
458
|
-
const previousTimestamp = core.channel.session.readSessionUpdatedAt({
|
|
459
|
-
storePath,
|
|
460
467
|
sessionKey: route.sessionKey,
|
|
461
468
|
});
|
|
462
469
|
const body = core.channel.reply.formatAgentEnvelope({
|
|
@@ -559,18 +566,14 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
|
|
|
559
566
|
|
|
560
567
|
log.info("dispatching to agent", { sessionKey: route.sessionKey });
|
|
561
568
|
try {
|
|
562
|
-
const { queuedFinal, counts } = await
|
|
569
|
+
const { queuedFinal, counts } = await dispatchReplyFromConfigWithSettledDispatcher({
|
|
570
|
+
cfg,
|
|
571
|
+
ctxPayload,
|
|
563
572
|
dispatcher,
|
|
564
573
|
onSettled: () => {
|
|
565
574
|
markDispatchIdle();
|
|
566
575
|
},
|
|
567
|
-
|
|
568
|
-
core.channel.reply.dispatchReplyFromConfig({
|
|
569
|
-
ctx: ctxPayload,
|
|
570
|
-
cfg,
|
|
571
|
-
dispatcher,
|
|
572
|
-
replyOptions,
|
|
573
|
-
}),
|
|
576
|
+
replyOptions,
|
|
574
577
|
});
|
|
575
578
|
|
|
576
579
|
log.info("dispatch complete", { queuedFinal, counts });
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { OpenClawConfig, PluginRuntime, RuntimeEnv } from "openclaw/plugin-sdk";
|
|
1
|
+
import type { OpenClawConfig, PluginRuntime, RuntimeEnv } from "openclaw/plugin-sdk/msteams";
|
|
2
2
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
3
|
import type { MSTeamsConversationStore } from "./conversation-store.js";
|
|
4
4
|
import type { MSTeamsAdapter } from "./messenger.js";
|
package/src/monitor-handler.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { OpenClawConfig, RuntimeEnv } from "openclaw/plugin-sdk";
|
|
1
|
+
import type { OpenClawConfig, RuntimeEnv } from "openclaw/plugin-sdk/msteams";
|
|
2
2
|
import type { MSTeamsConversationStore } from "./conversation-store.js";
|
|
3
3
|
import { buildFileInfoCard, parseFileConsentInvoke, uploadToConsentUrl } from "./file-consent.js";
|
|
4
4
|
import { normalizeMSTeamsConversationId } from "./inbound.js";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { EventEmitter } from "node:events";
|
|
2
|
-
import type { OpenClawConfig, RuntimeEnv } from "openclaw/plugin-sdk";
|
|
2
|
+
import type { OpenClawConfig, RuntimeEnv } from "openclaw/plugin-sdk/msteams";
|
|
3
3
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
4
4
|
import type { MSTeamsConversationStore } from "./conversation-store.js";
|
|
5
5
|
import type { MSTeamsPollStore } from "./polls.js";
|
|
@@ -15,8 +15,14 @@ const expressControl = vi.hoisted(() => ({
|
|
|
15
15
|
mode: { value: "listening" as "listening" | "error" },
|
|
16
16
|
}));
|
|
17
17
|
|
|
18
|
-
vi.mock("openclaw/plugin-sdk", () => ({
|
|
18
|
+
vi.mock("openclaw/plugin-sdk/msteams", () => ({
|
|
19
19
|
DEFAULT_WEBHOOK_MAX_BODY_BYTES: 1024 * 1024,
|
|
20
|
+
normalizeSecretInputString: (value: unknown) =>
|
|
21
|
+
typeof value === "string" && value.trim() ? value.trim() : undefined,
|
|
22
|
+
hasConfiguredSecretInput: (value: unknown) =>
|
|
23
|
+
typeof value === "string" && value.trim().length > 0,
|
|
24
|
+
normalizeResolvedSecretInputString: (params: { value?: unknown }) =>
|
|
25
|
+
typeof params?.value === "string" && params.value.trim() ? params.value.trim() : undefined,
|
|
20
26
|
keepHttpServerTaskAlive: vi.fn(
|
|
21
27
|
async (params: { abortSignal?: AbortSignal; onAbort?: () => Promise<void> | void }) => {
|
|
22
28
|
await new Promise<void>((resolve) => {
|
|
@@ -134,7 +140,7 @@ function createConfig(port: number): OpenClawConfig {
|
|
|
134
140
|
msteams: {
|
|
135
141
|
enabled: true,
|
|
136
142
|
appId: "app-id",
|
|
137
|
-
appPassword: "app-password",
|
|
143
|
+
appPassword: "app-password", // pragma: allowlist secret
|
|
138
144
|
tenantId: "tenant-id",
|
|
139
145
|
webhook: {
|
|
140
146
|
port,
|
package/src/monitor.ts
CHANGED
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
summarizeMapping,
|
|
8
8
|
type OpenClawConfig,
|
|
9
9
|
type RuntimeEnv,
|
|
10
|
-
} from "openclaw/plugin-sdk";
|
|
10
|
+
} from "openclaw/plugin-sdk/msteams";
|
|
11
11
|
import { createMSTeamsConversationStoreFs } from "./conversation-store-fs.js";
|
|
12
12
|
import type { MSTeamsConversationStore } from "./conversation-store.js";
|
|
13
13
|
import { formatUnknownError } from "./errors.js";
|
package/src/onboarding.ts
CHANGED
|
@@ -5,14 +5,17 @@ import type {
|
|
|
5
5
|
DmPolicy,
|
|
6
6
|
WizardPrompter,
|
|
7
7
|
MSTeamsTeamConfig,
|
|
8
|
-
} from "openclaw/plugin-sdk";
|
|
8
|
+
} from "openclaw/plugin-sdk/msteams";
|
|
9
9
|
import {
|
|
10
|
-
addWildcardAllowFrom,
|
|
11
10
|
DEFAULT_ACCOUNT_ID,
|
|
12
11
|
formatDocsLink,
|
|
13
12
|
mergeAllowFromEntries,
|
|
14
13
|
promptChannelAccessConfig,
|
|
15
|
-
|
|
14
|
+
setTopLevelChannelAllowFrom,
|
|
15
|
+
setTopLevelChannelDmPolicyWithAllowFrom,
|
|
16
|
+
setTopLevelChannelGroupPolicy,
|
|
17
|
+
splitOnboardingEntries,
|
|
18
|
+
} from "openclaw/plugin-sdk/msteams";
|
|
16
19
|
import {
|
|
17
20
|
parseMSTeamsTeamEntry,
|
|
18
21
|
resolveMSTeamsChannelAllowlist,
|
|
@@ -24,41 +27,19 @@ import { hasConfiguredMSTeamsCredentials, resolveMSTeamsCredentials } from "./to
|
|
|
24
27
|
const channel = "msteams" as const;
|
|
25
28
|
|
|
26
29
|
function setMSTeamsDmPolicy(cfg: OpenClawConfig, dmPolicy: DmPolicy) {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
...cfg,
|
|
33
|
-
channels: {
|
|
34
|
-
...cfg.channels,
|
|
35
|
-
msteams: {
|
|
36
|
-
...cfg.channels?.msteams,
|
|
37
|
-
dmPolicy,
|
|
38
|
-
...(allowFrom ? { allowFrom } : {}),
|
|
39
|
-
},
|
|
40
|
-
},
|
|
41
|
-
};
|
|
30
|
+
return setTopLevelChannelDmPolicyWithAllowFrom({
|
|
31
|
+
cfg,
|
|
32
|
+
channel: "msteams",
|
|
33
|
+
dmPolicy,
|
|
34
|
+
});
|
|
42
35
|
}
|
|
43
36
|
|
|
44
37
|
function setMSTeamsAllowFrom(cfg: OpenClawConfig, allowFrom: string[]): OpenClawConfig {
|
|
45
|
-
return {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
...cfg.channels?.msteams,
|
|
51
|
-
allowFrom,
|
|
52
|
-
},
|
|
53
|
-
},
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function parseAllowFromInput(raw: string): string[] {
|
|
58
|
-
return raw
|
|
59
|
-
.split(/[\n,;]+/g)
|
|
60
|
-
.map((entry) => entry.trim())
|
|
61
|
-
.filter(Boolean);
|
|
38
|
+
return setTopLevelChannelAllowFrom({
|
|
39
|
+
cfg,
|
|
40
|
+
channel: "msteams",
|
|
41
|
+
allowFrom,
|
|
42
|
+
});
|
|
62
43
|
}
|
|
63
44
|
|
|
64
45
|
function looksLikeGuid(value: string): boolean {
|
|
@@ -115,7 +96,7 @@ async function promptMSTeamsAllowFrom(params: {
|
|
|
115
96
|
initialValue: existing[0] ? String(existing[0]) : undefined,
|
|
116
97
|
validate: (value) => (String(value ?? "").trim() ? undefined : "Required"),
|
|
117
98
|
});
|
|
118
|
-
const parts =
|
|
99
|
+
const parts = splitOnboardingEntries(String(entry));
|
|
119
100
|
if (parts.length === 0) {
|
|
120
101
|
await params.prompter.note("Enter at least one user.", "MS Teams allowlist");
|
|
121
102
|
continue;
|
|
@@ -171,17 +152,12 @@ function setMSTeamsGroupPolicy(
|
|
|
171
152
|
cfg: OpenClawConfig,
|
|
172
153
|
groupPolicy: "open" | "allowlist" | "disabled",
|
|
173
154
|
): OpenClawConfig {
|
|
174
|
-
return {
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
enabled: true,
|
|
181
|
-
groupPolicy,
|
|
182
|
-
},
|
|
183
|
-
},
|
|
184
|
-
};
|
|
155
|
+
return setTopLevelChannelGroupPolicy({
|
|
156
|
+
cfg,
|
|
157
|
+
channel: "msteams",
|
|
158
|
+
groupPolicy,
|
|
159
|
+
enabled: true,
|
|
160
|
+
});
|
|
185
161
|
}
|
|
186
162
|
|
|
187
163
|
function setMSTeamsTeamsAllowlist(
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import type { OpenClawConfig } from "openclaw/plugin-sdk/msteams";
|
|
2
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
|
|
4
|
+
const mocks = vi.hoisted(() => ({
|
|
5
|
+
sendMessageMSTeams: vi.fn(),
|
|
6
|
+
sendPollMSTeams: vi.fn(),
|
|
7
|
+
createPoll: vi.fn(),
|
|
8
|
+
}));
|
|
9
|
+
|
|
10
|
+
vi.mock("./send.js", () => ({
|
|
11
|
+
sendMessageMSTeams: mocks.sendMessageMSTeams,
|
|
12
|
+
sendPollMSTeams: mocks.sendPollMSTeams,
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
vi.mock("./polls.js", () => ({
|
|
16
|
+
createMSTeamsPollStoreFs: () => ({
|
|
17
|
+
createPoll: mocks.createPoll,
|
|
18
|
+
}),
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
vi.mock("./runtime.js", () => ({
|
|
22
|
+
getMSTeamsRuntime: () => ({
|
|
23
|
+
channel: {
|
|
24
|
+
text: {
|
|
25
|
+
chunkMarkdownText: (text: string) => [text],
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
}),
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
import { msteamsOutbound } from "./outbound.js";
|
|
32
|
+
|
|
33
|
+
describe("msteamsOutbound cfg threading", () => {
|
|
34
|
+
beforeEach(() => {
|
|
35
|
+
mocks.sendMessageMSTeams.mockReset();
|
|
36
|
+
mocks.sendPollMSTeams.mockReset();
|
|
37
|
+
mocks.createPoll.mockReset();
|
|
38
|
+
mocks.sendMessageMSTeams.mockResolvedValue({
|
|
39
|
+
messageId: "msg-1",
|
|
40
|
+
conversationId: "conv-1",
|
|
41
|
+
});
|
|
42
|
+
mocks.sendPollMSTeams.mockResolvedValue({
|
|
43
|
+
pollId: "poll-1",
|
|
44
|
+
messageId: "msg-poll-1",
|
|
45
|
+
conversationId: "conv-1",
|
|
46
|
+
});
|
|
47
|
+
mocks.createPoll.mockResolvedValue(undefined);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("passes resolved cfg to sendMessageMSTeams for text sends", async () => {
|
|
51
|
+
const cfg = {
|
|
52
|
+
channels: {
|
|
53
|
+
msteams: {
|
|
54
|
+
appId: "resolved-app-id",
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
} as OpenClawConfig;
|
|
58
|
+
|
|
59
|
+
await msteamsOutbound.sendText!({
|
|
60
|
+
cfg,
|
|
61
|
+
to: "conversation:abc",
|
|
62
|
+
text: "hello",
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
expect(mocks.sendMessageMSTeams).toHaveBeenCalledWith({
|
|
66
|
+
cfg,
|
|
67
|
+
to: "conversation:abc",
|
|
68
|
+
text: "hello",
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("passes resolved cfg and media roots for media sends", async () => {
|
|
73
|
+
const cfg = {
|
|
74
|
+
channels: {
|
|
75
|
+
msteams: {
|
|
76
|
+
appId: "resolved-app-id",
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
} as OpenClawConfig;
|
|
80
|
+
|
|
81
|
+
await msteamsOutbound.sendMedia!({
|
|
82
|
+
cfg,
|
|
83
|
+
to: "conversation:abc",
|
|
84
|
+
text: "photo",
|
|
85
|
+
mediaUrl: "file:///tmp/photo.png",
|
|
86
|
+
mediaLocalRoots: ["/tmp"],
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
expect(mocks.sendMessageMSTeams).toHaveBeenCalledWith({
|
|
90
|
+
cfg,
|
|
91
|
+
to: "conversation:abc",
|
|
92
|
+
text: "photo",
|
|
93
|
+
mediaUrl: "file:///tmp/photo.png",
|
|
94
|
+
mediaLocalRoots: ["/tmp"],
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("passes resolved cfg to sendPollMSTeams and stores poll metadata", async () => {
|
|
99
|
+
const cfg = {
|
|
100
|
+
channels: {
|
|
101
|
+
msteams: {
|
|
102
|
+
appId: "resolved-app-id",
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
} as OpenClawConfig;
|
|
106
|
+
|
|
107
|
+
await msteamsOutbound.sendPoll!({
|
|
108
|
+
cfg,
|
|
109
|
+
to: "conversation:abc",
|
|
110
|
+
poll: {
|
|
111
|
+
question: "Snack?",
|
|
112
|
+
options: ["Pizza", "Sushi"],
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
expect(mocks.sendPollMSTeams).toHaveBeenCalledWith({
|
|
117
|
+
cfg,
|
|
118
|
+
to: "conversation:abc",
|
|
119
|
+
question: "Snack?",
|
|
120
|
+
options: ["Pizza", "Sushi"],
|
|
121
|
+
maxSelections: 1,
|
|
122
|
+
});
|
|
123
|
+
expect(mocks.createPoll).toHaveBeenCalledWith(
|
|
124
|
+
expect.objectContaining({
|
|
125
|
+
id: "poll-1",
|
|
126
|
+
question: "Snack?",
|
|
127
|
+
options: ["Pizza", "Sushi"],
|
|
128
|
+
}),
|
|
129
|
+
);
|
|
130
|
+
});
|
|
131
|
+
});
|
package/src/outbound.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ChannelOutboundAdapter } from "openclaw/plugin-sdk";
|
|
1
|
+
import type { ChannelOutboundAdapter } from "openclaw/plugin-sdk/msteams";
|
|
2
2
|
import { createMSTeamsPollStoreFs } from "./polls.js";
|
|
3
3
|
import { getMSTeamsRuntime } from "./runtime.js";
|
|
4
4
|
import { sendMessageMSTeams, sendPollMSTeams } from "./send.js";
|
package/src/policy.test.ts
CHANGED
package/src/policy.ts
CHANGED
|
@@ -7,15 +7,16 @@ import type {
|
|
|
7
7
|
MSTeamsConfig,
|
|
8
8
|
MSTeamsReplyStyle,
|
|
9
9
|
MSTeamsTeamConfig,
|
|
10
|
-
} from "openclaw/plugin-sdk";
|
|
10
|
+
} from "openclaw/plugin-sdk/msteams";
|
|
11
11
|
import {
|
|
12
12
|
buildChannelKeyCandidates,
|
|
13
|
+
evaluateSenderGroupAccessForPolicy,
|
|
13
14
|
normalizeChannelSlug,
|
|
14
15
|
resolveAllowlistMatchSimple,
|
|
15
16
|
resolveToolsBySender,
|
|
16
17
|
resolveChannelEntryMatchWithFallback,
|
|
17
18
|
resolveNestedAllowlistDecision,
|
|
18
|
-
} from "openclaw/plugin-sdk";
|
|
19
|
+
} from "openclaw/plugin-sdk/msteams";
|
|
19
20
|
|
|
20
21
|
export type MSTeamsResolvedRouteConfig = {
|
|
21
22
|
teamConfig?: MSTeamsTeamConfig;
|
|
@@ -248,12 +249,10 @@ export function isMSTeamsGroupAllowed(params: {
|
|
|
248
249
|
senderName?: string | null;
|
|
249
250
|
allowNameMatching?: boolean;
|
|
250
251
|
}): boolean {
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
}
|
|
258
|
-
return resolveMSTeamsAllowlistMatch(params).allowed;
|
|
252
|
+
return evaluateSenderGroupAccessForPolicy({
|
|
253
|
+
groupPolicy: params.groupPolicy,
|
|
254
|
+
groupAllowFrom: params.allowFrom.map((entry) => String(entry)),
|
|
255
|
+
senderId: params.senderId,
|
|
256
|
+
isSenderAllowed: () => resolveMSTeamsAllowlistMatch(params).allowed,
|
|
257
|
+
}).allowed;
|
|
259
258
|
}
|
package/src/probe.test.ts
CHANGED
package/src/probe.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import
|
|
1
|
+
import {
|
|
2
|
+
normalizeStringEntries,
|
|
3
|
+
type BaseProbeResult,
|
|
4
|
+
type MSTeamsConfig,
|
|
5
|
+
} from "openclaw/plugin-sdk/msteams";
|
|
2
6
|
import { formatUnknownError } from "./errors.js";
|
|
3
7
|
import { loadMSTeamsSdkWithAuth } from "./sdk.js";
|
|
4
8
|
import { readAccessToken } from "./token-response.js";
|
|
@@ -35,7 +39,7 @@ function readStringArray(value: unknown): string[] | undefined {
|
|
|
35
39
|
if (!Array.isArray(value)) {
|
|
36
40
|
return undefined;
|
|
37
41
|
}
|
|
38
|
-
const out = value
|
|
42
|
+
const out = normalizeStringEntries(value);
|
|
39
43
|
return out.length > 0 ? out : undefined;
|
|
40
44
|
}
|
|
41
45
|
|
package/src/reply-dispatcher.ts
CHANGED
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
type OpenClawConfig,
|
|
7
7
|
type MSTeamsReplyStyle,
|
|
8
8
|
type RuntimeEnv,
|
|
9
|
-
} from "openclaw/plugin-sdk";
|
|
9
|
+
} from "openclaw/plugin-sdk/msteams";
|
|
10
10
|
import type { MSTeamsAccessTokenProvider } from "./attachments/types.js";
|
|
11
11
|
import type { StoredConversationReference } from "./conversation-store.js";
|
|
12
12
|
import {
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
listTeamsByName,
|
|
5
|
+
listChannelsForTeam,
|
|
6
|
+
normalizeQuery,
|
|
7
|
+
resolveGraphToken,
|
|
8
|
+
searchGraphUsers,
|
|
9
|
+
} = vi.hoisted(() => ({
|
|
10
|
+
listTeamsByName: vi.fn(),
|
|
11
|
+
listChannelsForTeam: vi.fn(),
|
|
12
|
+
normalizeQuery: vi.fn((value: string) => value.trim().toLowerCase()),
|
|
13
|
+
resolveGraphToken: vi.fn(async () => "graph-token"),
|
|
14
|
+
searchGraphUsers: vi.fn(),
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
vi.mock("./graph.js", () => ({
|
|
18
|
+
listTeamsByName,
|
|
19
|
+
listChannelsForTeam,
|
|
20
|
+
normalizeQuery,
|
|
21
|
+
resolveGraphToken,
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
vi.mock("./graph-users.js", () => ({
|
|
25
|
+
searchGraphUsers,
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
import {
|
|
29
|
+
resolveMSTeamsChannelAllowlist,
|
|
30
|
+
resolveMSTeamsUserAllowlist,
|
|
31
|
+
} from "./resolve-allowlist.js";
|
|
32
|
+
|
|
33
|
+
describe("resolveMSTeamsUserAllowlist", () => {
|
|
34
|
+
it("marks empty input unresolved", async () => {
|
|
35
|
+
const [result] = await resolveMSTeamsUserAllowlist({ cfg: {}, entries: [" "] });
|
|
36
|
+
expect(result).toEqual({ input: " ", resolved: false });
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("resolves first Graph user match", async () => {
|
|
40
|
+
searchGraphUsers.mockResolvedValueOnce([
|
|
41
|
+
{ id: "user-1", displayName: "Alice One" },
|
|
42
|
+
{ id: "user-2", displayName: "Alice Two" },
|
|
43
|
+
]);
|
|
44
|
+
const [result] = await resolveMSTeamsUserAllowlist({ cfg: {}, entries: ["alice"] });
|
|
45
|
+
expect(result).toEqual({
|
|
46
|
+
input: "alice",
|
|
47
|
+
resolved: true,
|
|
48
|
+
id: "user-1",
|
|
49
|
+
name: "Alice One",
|
|
50
|
+
note: "multiple matches; chose first",
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe("resolveMSTeamsChannelAllowlist", () => {
|
|
56
|
+
it("resolves team/channel by team name + channel display name", async () => {
|
|
57
|
+
listTeamsByName.mockResolvedValueOnce([{ id: "team-1", displayName: "Product Team" }]);
|
|
58
|
+
listChannelsForTeam.mockResolvedValueOnce([
|
|
59
|
+
{ id: "channel-1", displayName: "General" },
|
|
60
|
+
{ id: "channel-2", displayName: "Roadmap" },
|
|
61
|
+
]);
|
|
62
|
+
|
|
63
|
+
const [result] = await resolveMSTeamsChannelAllowlist({
|
|
64
|
+
cfg: {},
|
|
65
|
+
entries: ["Product Team/Roadmap"],
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
expect(result).toEqual({
|
|
69
|
+
input: "Product Team/Roadmap",
|
|
70
|
+
resolved: true,
|
|
71
|
+
teamId: "team-1",
|
|
72
|
+
teamName: "Product Team",
|
|
73
|
+
channelId: "channel-2",
|
|
74
|
+
channelName: "Roadmap",
|
|
75
|
+
note: "multiple channels; chose first",
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
});
|