@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
package/src/token.ts
CHANGED
|
@@ -1,17 +1,75 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
import type { MSTeamsConfig } from "../runtime-api.js";
|
|
4
|
+
import type { MSTeamsDelegatedTokens } from "./oauth.shared.js";
|
|
5
|
+
import { refreshMSTeamsDelegatedTokens } from "./oauth.token.js";
|
|
2
6
|
import {
|
|
3
7
|
hasConfiguredSecretInput,
|
|
4
8
|
normalizeResolvedSecretInputString,
|
|
5
9
|
normalizeSecretInputString,
|
|
6
10
|
} from "./secret-input.js";
|
|
11
|
+
import { resolveMSTeamsStorePath } from "./storage.js";
|
|
7
12
|
|
|
8
|
-
|
|
13
|
+
// ── Credential types ───────────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
export type MSTeamsSecretCredentials = {
|
|
16
|
+
type: "secret";
|
|
9
17
|
appId: string;
|
|
10
18
|
appPassword: string;
|
|
11
19
|
tenantId: string;
|
|
12
20
|
};
|
|
13
21
|
|
|
22
|
+
export type MSTeamsFederatedCredentials = {
|
|
23
|
+
type: "federated";
|
|
24
|
+
appId: string;
|
|
25
|
+
tenantId: string;
|
|
26
|
+
certificatePath?: string;
|
|
27
|
+
certificateThumbprint?: string;
|
|
28
|
+
useManagedIdentity?: boolean;
|
|
29
|
+
managedIdentityClientId?: string;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export type MSTeamsCredentials = MSTeamsSecretCredentials | MSTeamsFederatedCredentials;
|
|
33
|
+
|
|
34
|
+
// ── Helpers ────────────────────────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
function resolveAuthType(cfg?: MSTeamsConfig): "secret" | "federated" {
|
|
37
|
+
const fromCfg = cfg?.authType;
|
|
38
|
+
if (fromCfg === "secret" || fromCfg === "federated") {
|
|
39
|
+
return fromCfg;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const fromEnv = process.env.MSTEAMS_AUTH_TYPE;
|
|
43
|
+
if (fromEnv === "federated") {
|
|
44
|
+
return "federated";
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return "secret";
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ── hasConfiguredMSTeamsCredentials ────────────────────────────────────────
|
|
51
|
+
|
|
14
52
|
export function hasConfiguredMSTeamsCredentials(cfg?: MSTeamsConfig): boolean {
|
|
53
|
+
const authType = resolveAuthType(cfg);
|
|
54
|
+
|
|
55
|
+
const hasAppId = Boolean(
|
|
56
|
+
normalizeSecretInputString(cfg?.appId) ||
|
|
57
|
+
normalizeSecretInputString(process.env.MSTEAMS_APP_ID),
|
|
58
|
+
);
|
|
59
|
+
const hasTenantId = Boolean(
|
|
60
|
+
normalizeSecretInputString(cfg?.tenantId) ||
|
|
61
|
+
normalizeSecretInputString(process.env.MSTEAMS_TENANT_ID),
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
if (authType === "federated") {
|
|
65
|
+
const hasCert = Boolean(cfg?.certificatePath || process.env.MSTEAMS_CERTIFICATE_PATH);
|
|
66
|
+
const hasManagedIdentity =
|
|
67
|
+
cfg?.useManagedIdentity ?? process.env.MSTEAMS_USE_MANAGED_IDENTITY === "true";
|
|
68
|
+
|
|
69
|
+
return hasAppId && hasTenantId && (hasCert || hasManagedIdentity);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// "secret" (default) — original logic
|
|
15
73
|
return Boolean(
|
|
16
74
|
normalizeSecretInputString(cfg?.appId) &&
|
|
17
75
|
hasConfiguredSecretInput(cfg?.appPassword) &&
|
|
@@ -19,22 +77,119 @@ export function hasConfiguredMSTeamsCredentials(cfg?: MSTeamsConfig): boolean {
|
|
|
19
77
|
);
|
|
20
78
|
}
|
|
21
79
|
|
|
80
|
+
// ── resolveMSTeamsCredentials ─────────────────────────────────────────────
|
|
81
|
+
|
|
22
82
|
export function resolveMSTeamsCredentials(cfg?: MSTeamsConfig): MSTeamsCredentials | undefined {
|
|
83
|
+
const authType = resolveAuthType(cfg);
|
|
84
|
+
|
|
23
85
|
const appId =
|
|
24
86
|
normalizeSecretInputString(cfg?.appId) ||
|
|
25
87
|
normalizeSecretInputString(process.env.MSTEAMS_APP_ID);
|
|
88
|
+
|
|
89
|
+
const tenantId =
|
|
90
|
+
normalizeSecretInputString(cfg?.tenantId) ||
|
|
91
|
+
normalizeSecretInputString(process.env.MSTEAMS_TENANT_ID);
|
|
92
|
+
|
|
93
|
+
if (!appId || !tenantId) {
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (authType === "federated") {
|
|
98
|
+
const certificatePath =
|
|
99
|
+
cfg?.certificatePath || process.env.MSTEAMS_CERTIFICATE_PATH || undefined;
|
|
100
|
+
|
|
101
|
+
const certificateThumbprint =
|
|
102
|
+
cfg?.certificateThumbprint || process.env.MSTEAMS_CERTIFICATE_THUMBPRINT || undefined;
|
|
103
|
+
|
|
104
|
+
const useManagedIdentity =
|
|
105
|
+
cfg?.useManagedIdentity ?? process.env.MSTEAMS_USE_MANAGED_IDENTITY === "true";
|
|
106
|
+
|
|
107
|
+
const managedIdentityClientId =
|
|
108
|
+
cfg?.managedIdentityClientId || process.env.MSTEAMS_MANAGED_IDENTITY_CLIENT_ID || undefined;
|
|
109
|
+
|
|
110
|
+
// At least one federated mechanism must be configured.
|
|
111
|
+
if (!certificatePath && !useManagedIdentity) {
|
|
112
|
+
return undefined;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
type: "federated",
|
|
117
|
+
appId,
|
|
118
|
+
tenantId,
|
|
119
|
+
certificatePath,
|
|
120
|
+
certificateThumbprint,
|
|
121
|
+
useManagedIdentity: useManagedIdentity || undefined,
|
|
122
|
+
managedIdentityClientId,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// "secret" (default) — original logic
|
|
26
127
|
const appPassword =
|
|
27
128
|
normalizeResolvedSecretInputString({
|
|
28
129
|
value: cfg?.appPassword,
|
|
29
130
|
path: "channels.msteams.appPassword",
|
|
30
131
|
}) || normalizeSecretInputString(process.env.MSTEAMS_APP_PASSWORD);
|
|
31
|
-
const tenantId =
|
|
32
|
-
normalizeSecretInputString(cfg?.tenantId) ||
|
|
33
|
-
normalizeSecretInputString(process.env.MSTEAMS_TENANT_ID);
|
|
34
132
|
|
|
35
|
-
if (!
|
|
133
|
+
if (!appPassword) {
|
|
36
134
|
return undefined;
|
|
37
135
|
}
|
|
38
136
|
|
|
39
|
-
return { appId, appPassword, tenantId };
|
|
137
|
+
return { type: "secret", appId, appPassword, tenantId };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ---------------------------------------------------------------------------
|
|
141
|
+
// Delegated token storage / resolution
|
|
142
|
+
// ---------------------------------------------------------------------------
|
|
143
|
+
|
|
144
|
+
const DELEGATED_TOKEN_FILENAME = "msteams-delegated.json";
|
|
145
|
+
|
|
146
|
+
function resolveDelegatedTokenPath(): string {
|
|
147
|
+
return resolveMSTeamsStorePath({ filename: DELEGATED_TOKEN_FILENAME });
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function loadDelegatedTokens(): MSTeamsDelegatedTokens | undefined {
|
|
151
|
+
try {
|
|
152
|
+
const content = readFileSync(resolveDelegatedTokenPath(), "utf8");
|
|
153
|
+
return JSON.parse(content) as MSTeamsDelegatedTokens;
|
|
154
|
+
} catch {
|
|
155
|
+
return undefined;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function saveDelegatedTokens(tokens: MSTeamsDelegatedTokens): void {
|
|
160
|
+
const tokenPath = resolveDelegatedTokenPath();
|
|
161
|
+
const dir = dirname(tokenPath);
|
|
162
|
+
mkdirSync(dir, { recursive: true });
|
|
163
|
+
writeFileSync(tokenPath, JSON.stringify(tokens, null, 2), "utf8");
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export async function resolveDelegatedAccessToken(params: {
|
|
167
|
+
tenantId: string;
|
|
168
|
+
clientId: string;
|
|
169
|
+
clientSecret: string;
|
|
170
|
+
}): Promise<string | undefined> {
|
|
171
|
+
const tokens = loadDelegatedTokens();
|
|
172
|
+
if (!tokens) {
|
|
173
|
+
return undefined;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Token still valid (5-min buffer already baked into expiresAt)
|
|
177
|
+
if (tokens.expiresAt > Date.now()) {
|
|
178
|
+
return tokens.accessToken;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Attempt refresh
|
|
182
|
+
try {
|
|
183
|
+
const refreshed = await refreshMSTeamsDelegatedTokens({
|
|
184
|
+
tenantId: params.tenantId,
|
|
185
|
+
clientId: params.clientId,
|
|
186
|
+
clientSecret: params.clientSecret,
|
|
187
|
+
refreshToken: tokens.refreshToken,
|
|
188
|
+
scopes: tokens.scopes,
|
|
189
|
+
});
|
|
190
|
+
saveDelegatedTokens(refreshed);
|
|
191
|
+
return refreshed.accessToken;
|
|
192
|
+
} catch {
|
|
193
|
+
return undefined;
|
|
194
|
+
}
|
|
40
195
|
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
// Mock the runtime before importing buildUserAgent
|
|
4
|
+
const mockRuntime = {
|
|
5
|
+
version: "2026.3.19",
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
vi.mock("./runtime.js", () => ({
|
|
9
|
+
getMSTeamsRuntime: vi.fn(() => mockRuntime),
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
import { fetchGraphJson } from "./graph.js";
|
|
13
|
+
import { getMSTeamsRuntime } from "./runtime.js";
|
|
14
|
+
import { buildUserAgent, ensureUserAgentHeader, resetUserAgentCache } from "./user-agent.js";
|
|
15
|
+
|
|
16
|
+
describe("buildUserAgent", () => {
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
resetUserAgentCache();
|
|
19
|
+
vi.mocked(getMSTeamsRuntime).mockReturnValue(mockRuntime as never);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
afterEach(() => {
|
|
23
|
+
vi.restoreAllMocks();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("returns teams.ts[apps]/<sdk> OpenClaw/<version> format", () => {
|
|
27
|
+
const ua = buildUserAgent();
|
|
28
|
+
expect(ua).toMatch(/^teams\.ts\[apps\]\/.+ OpenClaw\/2026\.3\.19$/);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("reflects the runtime version", () => {
|
|
32
|
+
vi.mocked(getMSTeamsRuntime).mockReturnValue({ version: "1.2.3" } as never);
|
|
33
|
+
const ua = buildUserAgent();
|
|
34
|
+
expect(ua).toMatch(/OpenClaw\/1\.2\.3$/);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("returns OpenClaw/unknown when runtime is not initialized", () => {
|
|
38
|
+
vi.mocked(getMSTeamsRuntime).mockImplementation(() => {
|
|
39
|
+
throw new Error("MSTeams runtime not initialized");
|
|
40
|
+
});
|
|
41
|
+
const ua = buildUserAgent();
|
|
42
|
+
expect(ua).toMatch(/OpenClaw\/unknown$/);
|
|
43
|
+
// SDK version should still be present
|
|
44
|
+
expect(ua).toMatch(/^teams\.ts\[apps\]\//);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("sends the generated User-Agent in Graph requests by default", async () => {
|
|
48
|
+
const mockFetch = vi.fn().mockResolvedValueOnce({
|
|
49
|
+
ok: true,
|
|
50
|
+
json: async () => ({ value: [] }),
|
|
51
|
+
});
|
|
52
|
+
vi.stubGlobal("fetch", mockFetch);
|
|
53
|
+
|
|
54
|
+
await fetchGraphJson({ token: "test-token", path: "/groups" });
|
|
55
|
+
|
|
56
|
+
expect(mockFetch).toHaveBeenCalledOnce();
|
|
57
|
+
const [, init] = mockFetch.mock.calls[0];
|
|
58
|
+
expect(init.headers["User-Agent"]).toMatch(/^teams\.ts\[apps\]\/.+ OpenClaw\/2026\.3\.19$/);
|
|
59
|
+
expect(init.headers).toHaveProperty("Authorization", "Bearer test-token");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("lets caller headers override the default Graph User-Agent", async () => {
|
|
63
|
+
const mockFetch = vi.fn().mockResolvedValueOnce({
|
|
64
|
+
ok: true,
|
|
65
|
+
json: async () => ({ value: [] }),
|
|
66
|
+
});
|
|
67
|
+
vi.stubGlobal("fetch", mockFetch);
|
|
68
|
+
|
|
69
|
+
await fetchGraphJson({
|
|
70
|
+
token: "test-token",
|
|
71
|
+
path: "/groups",
|
|
72
|
+
headers: { "User-Agent": "custom-agent/1.0" },
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const [, init] = mockFetch.mock.calls[0];
|
|
76
|
+
expect(init.headers["User-Agent"]).toBe("custom-agent/1.0");
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("adds the generated User-Agent to Headers instances without overwriting callers", () => {
|
|
80
|
+
const generated = ensureUserAgentHeader();
|
|
81
|
+
expect(generated.get("User-Agent")).toMatch(/^teams\.ts\[apps\]\/.+ OpenClaw\/2026\.3\.19$/);
|
|
82
|
+
|
|
83
|
+
const custom = ensureUserAgentHeader({ "User-Agent": "custom-agent/2.0" });
|
|
84
|
+
expect(custom.get("User-Agent")).toBe("custom-agent/2.0");
|
|
85
|
+
});
|
|
86
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { getMSTeamsRuntime } from "./runtime.js";
|
|
3
|
+
|
|
4
|
+
let cachedUserAgent: string | undefined;
|
|
5
|
+
|
|
6
|
+
function resolveTeamsSdkVersion(): string {
|
|
7
|
+
try {
|
|
8
|
+
const require = createRequire(import.meta.url);
|
|
9
|
+
const pkg = require("@microsoft/teams.apps/package.json") as { version?: string };
|
|
10
|
+
return pkg.version ?? "unknown";
|
|
11
|
+
} catch {
|
|
12
|
+
return "unknown";
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function resolveOpenClawVersion(): string {
|
|
17
|
+
try {
|
|
18
|
+
return getMSTeamsRuntime().version;
|
|
19
|
+
} catch {
|
|
20
|
+
return "unknown";
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Build a combined User-Agent string that preserves the Teams SDK identity
|
|
26
|
+
* and appends the OpenClaw version.
|
|
27
|
+
*
|
|
28
|
+
* Format: "teams.ts[apps]/<sdk-version> OpenClaw/<openclaw-version>"
|
|
29
|
+
* Example: "teams.ts[apps]/2.0.5 OpenClaw/2026.3.22"
|
|
30
|
+
*
|
|
31
|
+
* This lets the Teams backend track SDK usage while also identifying the
|
|
32
|
+
* host application.
|
|
33
|
+
*/
|
|
34
|
+
/** Reset the cached User-Agent (for testing). */
|
|
35
|
+
export function resetUserAgentCache(): void {
|
|
36
|
+
cachedUserAgent = undefined;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function buildUserAgent(): string {
|
|
40
|
+
if (cachedUserAgent) {
|
|
41
|
+
return cachedUserAgent;
|
|
42
|
+
}
|
|
43
|
+
cachedUserAgent = `teams.ts[apps]/${resolveTeamsSdkVersion()} OpenClaw/${resolveOpenClawVersion()}`;
|
|
44
|
+
return cachedUserAgent;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function ensureUserAgentHeader(headers?: HeadersInit): Headers {
|
|
48
|
+
const nextHeaders = new Headers(headers);
|
|
49
|
+
if (!nextHeaders.has("User-Agent")) {
|
|
50
|
+
nextHeaders.set("User-Agent", buildUserAgent());
|
|
51
|
+
}
|
|
52
|
+
return nextHeaders;
|
|
53
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Server } from "node:http";
|
|
2
|
+
|
|
3
|
+
const MSTEAMS_WEBHOOK_INACTIVITY_TIMEOUT_MS = 30_000;
|
|
4
|
+
const MSTEAMS_WEBHOOK_REQUEST_TIMEOUT_MS = 30_000;
|
|
5
|
+
const MSTEAMS_WEBHOOK_HEADERS_TIMEOUT_MS = 15_000;
|
|
6
|
+
|
|
7
|
+
type ApplyMSTeamsWebhookTimeoutsOpts = {
|
|
8
|
+
inactivityTimeoutMs?: number;
|
|
9
|
+
requestTimeoutMs?: number;
|
|
10
|
+
headersTimeoutMs?: number;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export function applyMSTeamsWebhookTimeouts(
|
|
14
|
+
httpServer: Server,
|
|
15
|
+
opts?: ApplyMSTeamsWebhookTimeoutsOpts,
|
|
16
|
+
): void {
|
|
17
|
+
const inactivityTimeoutMs = opts?.inactivityTimeoutMs ?? MSTEAMS_WEBHOOK_INACTIVITY_TIMEOUT_MS;
|
|
18
|
+
const requestTimeoutMs = opts?.requestTimeoutMs ?? MSTEAMS_WEBHOOK_REQUEST_TIMEOUT_MS;
|
|
19
|
+
const headersTimeoutMs = Math.min(
|
|
20
|
+
opts?.headersTimeoutMs ?? MSTEAMS_WEBHOOK_HEADERS_TIMEOUT_MS,
|
|
21
|
+
requestTimeoutMs,
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
httpServer.setTimeout(inactivityTimeoutMs);
|
|
25
|
+
httpServer.requestTimeout = requestTimeoutMs;
|
|
26
|
+
httpServer.headersTimeout = headersTimeoutMs;
|
|
27
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { buildMSTeamsPresentationCard } from "./presentation.js";
|
|
3
|
+
import { buildGroupWelcomeText, buildWelcomeCard } from "./welcome-card.js";
|
|
4
|
+
|
|
5
|
+
describe("buildMSTeamsPresentationCard", () => {
|
|
6
|
+
it("preserves message text when rendering presentation controls", () => {
|
|
7
|
+
expect(
|
|
8
|
+
buildMSTeamsPresentationCard({
|
|
9
|
+
text: "Deploy finished",
|
|
10
|
+
presentation: {
|
|
11
|
+
blocks: [
|
|
12
|
+
{
|
|
13
|
+
type: "buttons",
|
|
14
|
+
buttons: [{ label: "Open", value: "open" }],
|
|
15
|
+
},
|
|
16
|
+
],
|
|
17
|
+
},
|
|
18
|
+
}),
|
|
19
|
+
).toEqual({
|
|
20
|
+
type: "AdaptiveCard",
|
|
21
|
+
version: "1.4",
|
|
22
|
+
body: [{ type: "TextBlock", text: "Deploy finished", wrap: true }],
|
|
23
|
+
actions: [{ type: "Action.Submit", title: "Open", data: { value: "open", label: "Open" } }],
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe("buildWelcomeCard", () => {
|
|
29
|
+
it("builds card with default prompt starters", () => {
|
|
30
|
+
const card = buildWelcomeCard();
|
|
31
|
+
expect(card.type).toBe("AdaptiveCard");
|
|
32
|
+
expect(card.version).toBe("1.5");
|
|
33
|
+
|
|
34
|
+
const body = card.body as Array<{ text: string }>;
|
|
35
|
+
expect(body[0]?.text).toContain("OpenClaw");
|
|
36
|
+
|
|
37
|
+
const actions = card.actions as Array<{ title: string; data: unknown }>;
|
|
38
|
+
expect(actions.length).toBe(3);
|
|
39
|
+
expect(actions[0]?.title).toBe("What can you do?");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("uses custom bot name", () => {
|
|
43
|
+
const card = buildWelcomeCard({ botName: "TestBot" });
|
|
44
|
+
const body = card.body as Array<{ text: string }>;
|
|
45
|
+
expect(body[0]?.text).toContain("TestBot");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("uses custom prompt starters", () => {
|
|
49
|
+
const card = buildWelcomeCard({
|
|
50
|
+
promptStarters: ["Do X", "Do Y"],
|
|
51
|
+
});
|
|
52
|
+
const actions = card.actions as Array<{ title: string; data: unknown }>;
|
|
53
|
+
expect(actions.length).toBe(2);
|
|
54
|
+
expect(actions[0]?.title).toBe("Do X");
|
|
55
|
+
expect(actions[1]?.title).toBe("Do Y");
|
|
56
|
+
|
|
57
|
+
// Verify imBack data
|
|
58
|
+
const data = actions[0]?.data as { msteams: { type: string; value: string } };
|
|
59
|
+
expect(data.msteams.type).toBe("imBack");
|
|
60
|
+
expect(data.msteams.value).toBe("Do X");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("falls back to defaults when promptStarters is empty", () => {
|
|
64
|
+
const card = buildWelcomeCard({ promptStarters: [] });
|
|
65
|
+
const actions = card.actions as Array<{ title: string }>;
|
|
66
|
+
expect(actions.length).toBe(3);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe("buildGroupWelcomeText", () => {
|
|
71
|
+
it("includes bot name", () => {
|
|
72
|
+
const text = buildGroupWelcomeText("MyBot");
|
|
73
|
+
expect(text).toContain("MyBot");
|
|
74
|
+
expect(text).toContain("@MyBot");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("defaults to OpenClaw", () => {
|
|
78
|
+
const text = buildGroupWelcomeText();
|
|
79
|
+
expect(text).toContain("OpenClaw");
|
|
80
|
+
});
|
|
81
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builds an Adaptive Card for welcoming users when the bot is added to a conversation.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const DEFAULT_PROMPT_STARTERS = [
|
|
6
|
+
"What can you do?",
|
|
7
|
+
"Summarize my last meeting",
|
|
8
|
+
"Help me draft an email",
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
type WelcomeCardOptions = {
|
|
12
|
+
/** Bot display name. Falls back to "OpenClaw". */
|
|
13
|
+
botName?: string;
|
|
14
|
+
/** Custom prompt starters. Falls back to defaults. */
|
|
15
|
+
promptStarters?: string[];
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Build a welcome Adaptive Card for 1:1 personal chats.
|
|
20
|
+
*/
|
|
21
|
+
export function buildWelcomeCard(options?: WelcomeCardOptions): Record<string, unknown> {
|
|
22
|
+
const botName = options?.botName || "OpenClaw";
|
|
23
|
+
const starters = options?.promptStarters?.length
|
|
24
|
+
? options.promptStarters
|
|
25
|
+
: DEFAULT_PROMPT_STARTERS;
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
type: "AdaptiveCard",
|
|
29
|
+
version: "1.5",
|
|
30
|
+
body: [
|
|
31
|
+
{
|
|
32
|
+
type: "TextBlock",
|
|
33
|
+
text: `Hi! I'm ${botName}.`,
|
|
34
|
+
weight: "bolder",
|
|
35
|
+
size: "medium",
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
type: "TextBlock",
|
|
39
|
+
text: "I can help you with questions, tasks, and more. Here are some things to try:",
|
|
40
|
+
wrap: true,
|
|
41
|
+
},
|
|
42
|
+
],
|
|
43
|
+
actions: starters.map((label) => ({
|
|
44
|
+
type: "Action.Submit",
|
|
45
|
+
title: label,
|
|
46
|
+
data: { msteams: { type: "imBack", value: label } },
|
|
47
|
+
})),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Build a brief welcome message for group chats (when the bot is @mentioned).
|
|
53
|
+
*/
|
|
54
|
+
export function buildGroupWelcomeText(botName?: string): string {
|
|
55
|
+
const name = botName || "OpenClaw";
|
|
56
|
+
return `Hi! I'm ${name}. Mention me with @${name} to get started.`;
|
|
57
|
+
}
|
package/test-api.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { msteamsPlugin } from "./src/channel.js";
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../tsconfig.package-boundary.base.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"rootDir": "."
|
|
5
|
+
},
|
|
6
|
+
"include": ["./*.ts", "./src/**/*.ts"],
|
|
7
|
+
"exclude": [
|
|
8
|
+
"./**/*.test.ts",
|
|
9
|
+
"./dist/**",
|
|
10
|
+
"./node_modules/**",
|
|
11
|
+
"./src/test-support/**",
|
|
12
|
+
"./src/**/*test-helpers.ts",
|
|
13
|
+
"./src/**/*test-harness.ts",
|
|
14
|
+
"./src/**/*test-support.ts"
|
|
15
|
+
]
|
|
16
|
+
}
|
package/CHANGELOG.md
DELETED
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
# Changelog
|
|
2
|
-
|
|
3
|
-
## 2026.3.13
|
|
4
|
-
|
|
5
|
-
### Changes
|
|
6
|
-
|
|
7
|
-
- Version alignment with core OpenClaw release numbers.
|
|
8
|
-
|
|
9
|
-
## 2026.3.12
|
|
10
|
-
|
|
11
|
-
### Changes
|
|
12
|
-
|
|
13
|
-
- Version alignment with core OpenClaw release numbers.
|
|
14
|
-
|
|
15
|
-
## 2026.3.11
|
|
16
|
-
|
|
17
|
-
### Changes
|
|
18
|
-
|
|
19
|
-
- Version alignment with core OpenClaw release numbers.
|
|
20
|
-
|
|
21
|
-
## 2026.3.10
|
|
22
|
-
|
|
23
|
-
### Changes
|
|
24
|
-
|
|
25
|
-
- Version alignment with core OpenClaw release numbers.
|
|
26
|
-
|
|
27
|
-
## 2026.3.9
|
|
28
|
-
|
|
29
|
-
### Changes
|
|
30
|
-
|
|
31
|
-
- Version alignment with core OpenClaw release numbers.
|
|
32
|
-
|
|
33
|
-
## 2026.3.8-beta.1
|
|
34
|
-
|
|
35
|
-
### Changes
|
|
36
|
-
|
|
37
|
-
- Version alignment with core OpenClaw release numbers.
|
|
38
|
-
|
|
39
|
-
## 2026.3.8
|
|
40
|
-
|
|
41
|
-
### Changes
|
|
42
|
-
|
|
43
|
-
- Version alignment with core OpenClaw release numbers.
|
|
44
|
-
|
|
45
|
-
## 2026.3.7
|
|
46
|
-
|
|
47
|
-
### Changes
|
|
48
|
-
|
|
49
|
-
- Version alignment with core OpenClaw release numbers.
|
|
50
|
-
|
|
51
|
-
## 2026.3.3
|
|
52
|
-
|
|
53
|
-
### Changes
|
|
54
|
-
|
|
55
|
-
- Version alignment with core OpenClaw release numbers.
|
|
56
|
-
|
|
57
|
-
## 2026.3.2
|
|
58
|
-
|
|
59
|
-
### Changes
|
|
60
|
-
|
|
61
|
-
- Version alignment with core OpenClaw release numbers.
|
|
62
|
-
|
|
63
|
-
## 2026.3.1
|
|
64
|
-
|
|
65
|
-
### Changes
|
|
66
|
-
|
|
67
|
-
- Version alignment with core OpenClaw release numbers.
|
|
68
|
-
|
|
69
|
-
## 2026.2.26
|
|
70
|
-
|
|
71
|
-
### Changes
|
|
72
|
-
|
|
73
|
-
- Version alignment with core OpenClaw release numbers.
|
|
74
|
-
|
|
75
|
-
## 2026.2.25
|
|
76
|
-
|
|
77
|
-
### Changes
|
|
78
|
-
|
|
79
|
-
- Version alignment with core OpenClaw release numbers.
|
|
80
|
-
|
|
81
|
-
## 2026.2.24
|
|
82
|
-
|
|
83
|
-
### Changes
|
|
84
|
-
|
|
85
|
-
- Version alignment with core OpenClaw release numbers.
|
|
86
|
-
|
|
87
|
-
## 2026.2.22
|
|
88
|
-
|
|
89
|
-
### Changes
|
|
90
|
-
|
|
91
|
-
- Version alignment with core OpenClaw release numbers.
|
|
92
|
-
|
|
93
|
-
## 2026.1.15
|
|
94
|
-
|
|
95
|
-
### Features
|
|
96
|
-
|
|
97
|
-
- Bot Framework gateway monitor (Express + JWT auth) with configurable webhook path/port and `/api/messages` fallback.
|
|
98
|
-
- Onboarding flow for Azure Bot credentials (config + env var detection) and DM policy setup.
|
|
99
|
-
- Channel capabilities: DMs, group chats, channels, threads, media, polls, and `teams` alias.
|
|
100
|
-
- DM pairing/allowlist enforcement plus group policies with per-team/channel overrides and mention gating.
|
|
101
|
-
- Inbound debounce + history context for room/group chats; mention tag stripping and timestamp parsing.
|
|
102
|
-
- Proactive messaging via stored conversation references (file store with TTL/size pruning).
|
|
103
|
-
- Outbound text/media send with markdown chunking, 4k limit, split/inline media handling.
|
|
104
|
-
- Adaptive Card polls: build cards, parse votes, and persist poll state with vote tracking.
|
|
105
|
-
- Attachment processing: placeholders + HTML summaries, inline image extraction (including data: URLs).
|
|
106
|
-
- Media downloads with host allowlist, auth scope fallback, and Graph hostedContents/attachments fallback.
|
|
107
|
-
- Retry/backoff on transient/throttled sends with classified errors + helpful hints.
|
package/src/file-lock.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { withFileLock } from "openclaw/plugin-sdk/msteams";
|