@openclaw/msteams 2026.5.2 → 2026.5.3-beta.2

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.
Files changed (197) hide show
  1. package/dist/api.js +3 -0
  2. package/dist/channel-D7hdreTh.js +984 -0
  3. package/dist/channel-config-api.js +2 -0
  4. package/dist/channel-plugin-api.js +2 -0
  5. package/dist/channel.runtime-BC1ruIfN.js +573 -0
  6. package/dist/config-schema-B8QezH6t.js +15 -0
  7. package/dist/contract-api.js +2 -0
  8. package/dist/graph-users-9uQJepqr.js +1354 -0
  9. package/dist/index.js +22 -0
  10. package/dist/oauth-BWJyilR1.js +114 -0
  11. package/dist/oauth.token-xxpoLWy5.js +115 -0
  12. package/dist/policy-DTnU2GR7.js +142 -0
  13. package/dist/probe-D_H8yFps.js +2194 -0
  14. package/dist/resolve-allowlist-D41JSziq.js +219 -0
  15. package/dist/runtime-api-DV1iVMn1.js +28 -0
  16. package/dist/runtime-api.js +2 -0
  17. package/dist/secret-contract-BuoEXmPS.js +35 -0
  18. package/dist/secret-contract-api.js +2 -0
  19. package/dist/setup-entry.js +15 -0
  20. package/dist/setup-plugin-api.js +64 -0
  21. package/dist/setup-surface-BLkFQYIQ.js +313 -0
  22. package/dist/src-CFp1QpFd.js +4064 -0
  23. package/dist/test-api.js +2 -0
  24. package/package.json +14 -6
  25. package/api.ts +0 -3
  26. package/channel-config-api.ts +0 -1
  27. package/channel-plugin-api.ts +0 -2
  28. package/config-api.ts +0 -4
  29. package/contract-api.ts +0 -4
  30. package/index.ts +0 -20
  31. package/runtime-api.ts +0 -73
  32. package/secret-contract-api.ts +0 -5
  33. package/setup-entry.ts +0 -13
  34. package/setup-plugin-api.ts +0 -3
  35. package/src/ai-entity.ts +0 -7
  36. package/src/approval-auth.ts +0 -44
  37. package/src/attachments/bot-framework.test.ts +0 -461
  38. package/src/attachments/bot-framework.ts +0 -362
  39. package/src/attachments/download.ts +0 -311
  40. package/src/attachments/graph.test.ts +0 -416
  41. package/src/attachments/graph.ts +0 -484
  42. package/src/attachments/html.ts +0 -122
  43. package/src/attachments/payload.ts +0 -14
  44. package/src/attachments/remote-media.test.ts +0 -137
  45. package/src/attachments/remote-media.ts +0 -112
  46. package/src/attachments/shared.test.ts +0 -530
  47. package/src/attachments/shared.ts +0 -626
  48. package/src/attachments/types.ts +0 -47
  49. package/src/attachments.graph.test.ts +0 -342
  50. package/src/attachments.helpers.test.ts +0 -246
  51. package/src/attachments.test-helpers.ts +0 -17
  52. package/src/attachments.test.ts +0 -687
  53. package/src/attachments.ts +0 -18
  54. package/src/block-streaming-config.test.ts +0 -61
  55. package/src/channel-api.ts +0 -1
  56. package/src/channel.actions.test.ts +0 -742
  57. package/src/channel.directory.test.ts +0 -200
  58. package/src/channel.runtime.ts +0 -56
  59. package/src/channel.setup.ts +0 -77
  60. package/src/channel.test.ts +0 -128
  61. package/src/channel.ts +0 -1136
  62. package/src/config-schema.ts +0 -6
  63. package/src/config-ui-hints.ts +0 -12
  64. package/src/conversation-store-fs.test.ts +0 -74
  65. package/src/conversation-store-fs.ts +0 -149
  66. package/src/conversation-store-helpers.test.ts +0 -202
  67. package/src/conversation-store-helpers.ts +0 -105
  68. package/src/conversation-store-memory.ts +0 -51
  69. package/src/conversation-store.shared.test.ts +0 -225
  70. package/src/conversation-store.ts +0 -71
  71. package/src/directory-live.test.ts +0 -156
  72. package/src/directory-live.ts +0 -111
  73. package/src/doctor.ts +0 -27
  74. package/src/errors.test.ts +0 -133
  75. package/src/errors.ts +0 -246
  76. package/src/feedback-reflection-prompt.ts +0 -117
  77. package/src/feedback-reflection-store.ts +0 -114
  78. package/src/feedback-reflection.test.ts +0 -237
  79. package/src/feedback-reflection.ts +0 -283
  80. package/src/file-consent-helpers.test.ts +0 -326
  81. package/src/file-consent-helpers.ts +0 -126
  82. package/src/file-consent-invoke.ts +0 -150
  83. package/src/file-consent.test.ts +0 -363
  84. package/src/file-consent.ts +0 -287
  85. package/src/graph-chat.ts +0 -55
  86. package/src/graph-group-management.test.ts +0 -318
  87. package/src/graph-group-management.ts +0 -168
  88. package/src/graph-members.test.ts +0 -89
  89. package/src/graph-members.ts +0 -48
  90. package/src/graph-messages.actions.test.ts +0 -243
  91. package/src/graph-messages.read.test.ts +0 -391
  92. package/src/graph-messages.search.test.ts +0 -213
  93. package/src/graph-messages.test-helpers.ts +0 -50
  94. package/src/graph-messages.ts +0 -534
  95. package/src/graph-teams.test.ts +0 -215
  96. package/src/graph-teams.ts +0 -114
  97. package/src/graph-thread.test.ts +0 -246
  98. package/src/graph-thread.ts +0 -146
  99. package/src/graph-upload.test.ts +0 -258
  100. package/src/graph-upload.ts +0 -531
  101. package/src/graph-users.ts +0 -29
  102. package/src/graph.test.ts +0 -516
  103. package/src/graph.ts +0 -293
  104. package/src/inbound.test.ts +0 -221
  105. package/src/inbound.ts +0 -148
  106. package/src/index.ts +0 -4
  107. package/src/media-helpers.test.ts +0 -202
  108. package/src/media-helpers.ts +0 -105
  109. package/src/mentions.test.ts +0 -244
  110. package/src/mentions.ts +0 -114
  111. package/src/messenger.test.ts +0 -865
  112. package/src/messenger.ts +0 -605
  113. package/src/monitor-handler/access.ts +0 -125
  114. package/src/monitor-handler/inbound-media.test.ts +0 -289
  115. package/src/monitor-handler/inbound-media.ts +0 -180
  116. package/src/monitor-handler/message-handler-mock-support.test-support.ts +0 -28
  117. package/src/monitor-handler/message-handler.authz.test.ts +0 -669
  118. package/src/monitor-handler/message-handler.dm-media.test.ts +0 -54
  119. package/src/monitor-handler/message-handler.test-support.ts +0 -100
  120. package/src/monitor-handler/message-handler.thread-parent.test.ts +0 -223
  121. package/src/monitor-handler/message-handler.thread-session.test.ts +0 -77
  122. package/src/monitor-handler/message-handler.ts +0 -1000
  123. package/src/monitor-handler/reaction-handler.test.ts +0 -267
  124. package/src/monitor-handler/reaction-handler.ts +0 -210
  125. package/src/monitor-handler/thread-session.ts +0 -17
  126. package/src/monitor-handler.adaptive-card.test.ts +0 -162
  127. package/src/monitor-handler.feedback-authz.test.ts +0 -314
  128. package/src/monitor-handler.file-consent.test.ts +0 -423
  129. package/src/monitor-handler.sso.test.ts +0 -563
  130. package/src/monitor-handler.test-helpers.ts +0 -180
  131. package/src/monitor-handler.ts +0 -534
  132. package/src/monitor-handler.types.ts +0 -27
  133. package/src/monitor-types.ts +0 -6
  134. package/src/monitor.lifecycle.test.ts +0 -278
  135. package/src/monitor.test.ts +0 -119
  136. package/src/monitor.ts +0 -442
  137. package/src/oauth.flow.ts +0 -77
  138. package/src/oauth.shared.ts +0 -37
  139. package/src/oauth.test.ts +0 -305
  140. package/src/oauth.token.ts +0 -158
  141. package/src/oauth.ts +0 -130
  142. package/src/outbound.test.ts +0 -130
  143. package/src/outbound.ts +0 -71
  144. package/src/pending-uploads-fs.test.ts +0 -246
  145. package/src/pending-uploads-fs.ts +0 -235
  146. package/src/pending-uploads.test.ts +0 -173
  147. package/src/pending-uploads.ts +0 -121
  148. package/src/policy.test.ts +0 -240
  149. package/src/policy.ts +0 -262
  150. package/src/polls-store-memory.ts +0 -32
  151. package/src/polls.test.ts +0 -160
  152. package/src/polls.ts +0 -323
  153. package/src/presentation.ts +0 -68
  154. package/src/probe.test.ts +0 -77
  155. package/src/probe.ts +0 -132
  156. package/src/reply-dispatcher.test.ts +0 -437
  157. package/src/reply-dispatcher.ts +0 -346
  158. package/src/reply-stream-controller.test.ts +0 -235
  159. package/src/reply-stream-controller.ts +0 -147
  160. package/src/resolve-allowlist.test.ts +0 -250
  161. package/src/resolve-allowlist.ts +0 -309
  162. package/src/revoked-context.ts +0 -17
  163. package/src/runtime.ts +0 -9
  164. package/src/sdk-types.ts +0 -59
  165. package/src/sdk.test.ts +0 -666
  166. package/src/sdk.ts +0 -884
  167. package/src/secret-contract.ts +0 -49
  168. package/src/secret-input.ts +0 -7
  169. package/src/send-context.ts +0 -231
  170. package/src/send.test.ts +0 -493
  171. package/src/send.ts +0 -637
  172. package/src/sent-message-cache.test.ts +0 -15
  173. package/src/sent-message-cache.ts +0 -56
  174. package/src/session-route.ts +0 -40
  175. package/src/setup-core.ts +0 -160
  176. package/src/setup-surface.test.ts +0 -202
  177. package/src/setup-surface.ts +0 -320
  178. package/src/sso-token-store.test.ts +0 -72
  179. package/src/sso-token-store.ts +0 -166
  180. package/src/sso.ts +0 -300
  181. package/src/storage.ts +0 -25
  182. package/src/store-fs.ts +0 -44
  183. package/src/streaming-message.test.ts +0 -262
  184. package/src/streaming-message.ts +0 -297
  185. package/src/test-runtime.ts +0 -16
  186. package/src/thread-parent-context.test.ts +0 -224
  187. package/src/thread-parent-context.ts +0 -159
  188. package/src/token-response.ts +0 -11
  189. package/src/token.test.ts +0 -259
  190. package/src/token.ts +0 -195
  191. package/src/user-agent.test.ts +0 -86
  192. package/src/user-agent.ts +0 -53
  193. package/src/webhook-timeouts.ts +0 -27
  194. package/src/welcome-card.test.ts +0 -81
  195. package/src/welcome-card.ts +0 -57
  196. package/test-api.ts +0 -1
  197. package/tsconfig.json +0 -16
package/src/graph.ts DELETED
@@ -1,293 +0,0 @@
1
- import { fetchWithSsrFGuard, type MSTeamsConfig } from "../runtime-api.js";
2
- import { GRAPH_ROOT } from "./attachments/shared.js";
3
-
4
- const GRAPH_BETA = "https://graph.microsoft.com/beta";
5
- import { createMSTeamsTokenProvider, loadMSTeamsSdkWithAuth } from "./sdk.js";
6
- import { readAccessToken } from "./token-response.js";
7
- import { resolveDelegatedAccessToken, resolveMSTeamsCredentials } from "./token.js";
8
- import { buildUserAgent } from "./user-agent.js";
9
-
10
- export type GraphUser = {
11
- id?: string;
12
- displayName?: string;
13
- userPrincipalName?: string;
14
- mail?: string;
15
- };
16
-
17
- type GraphGroup = {
18
- id?: string;
19
- displayName?: string;
20
- };
21
-
22
- type GraphChannel = {
23
- id?: string;
24
- displayName?: string;
25
- };
26
-
27
- export type GraphResponse<T> = { value?: T[] };
28
-
29
- export function normalizeQuery(value?: string | null): string {
30
- return value?.trim() ?? "";
31
- }
32
-
33
- export function escapeOData(value: string): string {
34
- return value.replace(/'/g, "''");
35
- }
36
-
37
- async function requestGraph(params: {
38
- token: string;
39
- path: string;
40
- method?: "GET" | "POST" | "PATCH" | "DELETE";
41
- root?: string;
42
- headers?: Record<string, string>;
43
- body?: unknown;
44
- errorPrefix?: string;
45
- }): Promise<Response> {
46
- const hasBody = params.body !== undefined;
47
- const res = await fetch(`${params.root ?? GRAPH_ROOT}${params.path}`, {
48
- method: params.method,
49
- headers: {
50
- "User-Agent": buildUserAgent(),
51
- Authorization: `Bearer ${params.token}`,
52
- ...(hasBody ? { "Content-Type": "application/json" } : {}),
53
- ...params.headers,
54
- },
55
- body: hasBody ? JSON.stringify(params.body) : undefined,
56
- });
57
- if (!res.ok) {
58
- const text = await res.text().catch(() => "");
59
- throw new Error(
60
- `${params.errorPrefix ?? "Graph"} ${params.path} failed (${res.status}): ${text || "unknown error"}`,
61
- );
62
- }
63
- return res;
64
- }
65
-
66
- async function readOptionalGraphJson<T>(res: Response): Promise<T> {
67
- // Use optional chaining to stay resilient to partial test mocks that do not
68
- // provide a status or Headers instance (they only shim `ok` + `json()`).
69
- if (res.status === 204 || res.headers?.get?.("content-length") === "0") {
70
- return undefined as T;
71
- }
72
- return (await res.json()) as T;
73
- }
74
-
75
- export async function fetchGraphJson<T>(params: {
76
- token: string;
77
- path: string;
78
- headers?: Record<string, string>;
79
- /** HTTP method; defaults to "GET" */
80
- method?: string;
81
- /** Request body (serialized as JSON). Only used for non-GET methods. */
82
- body?: unknown;
83
- }): Promise<T> {
84
- const res = await requestGraph({
85
- token: params.token,
86
- path: params.path,
87
- method: params.method as "GET" | "POST" | "DELETE" | undefined,
88
- body: params.body,
89
- headers: params.headers,
90
- });
91
- return await readOptionalGraphJson<T>(res);
92
- }
93
-
94
- /**
95
- * Fetch JSON from an absolute Graph API URL (for example @odata.nextLink
96
- * pagination URLs) without prepending GRAPH_ROOT.
97
- */
98
- export async function fetchGraphAbsoluteUrl<T>(params: {
99
- token: string;
100
- url: string;
101
- headers?: Record<string, string>;
102
- }): Promise<T> {
103
- const { response, release } = await fetchWithSsrFGuard({
104
- url: params.url,
105
- init: {
106
- headers: {
107
- "User-Agent": buildUserAgent(),
108
- Authorization: `Bearer ${params.token}`,
109
- ...params.headers,
110
- },
111
- },
112
- auditContext: "msteams.graph.absolute",
113
- });
114
- try {
115
- if (!response.ok) {
116
- const text = await response.text().catch(() => "");
117
- throw new Error(
118
- `Graph ${params.url} failed (${response.status}): ${text || "unknown error"}`,
119
- );
120
- }
121
- return (await response.json()) as T;
122
- } finally {
123
- await release();
124
- }
125
- }
126
-
127
- /** Graph collection response with optional pagination link. */
128
- type GraphPagedResponse<T> = {
129
- value?: T[];
130
- "@odata.nextLink"?: string;
131
- };
132
-
133
- /** Result of a paginated Graph API fetch. */
134
- type PaginatedResult<T> = {
135
- items: T[];
136
- truncated: boolean;
137
- found?: T;
138
- };
139
-
140
- /**
141
- * Fetch all pages of a Graph API collection, following @odata.nextLink.
142
- * Optionally stop early when `findOne` matches an item.
143
- */
144
- export async function fetchAllGraphPages<T>(params: {
145
- token: string;
146
- path: string;
147
- headers?: Record<string, string>;
148
- /** Max pages to fetch before stopping. Default: 50. */
149
- maxPages?: number;
150
- /** Stop pagination early when this predicate returns true. */
151
- findOne?: (item: T) => boolean;
152
- }): Promise<PaginatedResult<T>> {
153
- const maxPages = params.maxPages ?? 50;
154
- const items: T[] = [];
155
- let nextPath: string | undefined = params.path;
156
-
157
- for (let page = 0; page < maxPages && nextPath; page++) {
158
- const res: GraphPagedResponse<T> = await fetchGraphJson<GraphPagedResponse<T>>({
159
- token: params.token,
160
- path: nextPath,
161
- headers: params.headers,
162
- });
163
-
164
- const pageItems = res.value ?? [];
165
-
166
- if (params.findOne) {
167
- const match = pageItems.find(params.findOne);
168
- if (match) {
169
- items.push(...pageItems);
170
- return { items, truncated: false, found: match };
171
- }
172
- }
173
-
174
- items.push(...pageItems);
175
-
176
- // @odata.nextLink is an absolute URL; strip the Graph root to get a relative path
177
- const rawNext: string | undefined = res["@odata.nextLink"];
178
- if (rawNext) {
179
- nextPath = rawNext
180
- .replace("https://graph.microsoft.com/v1.0", "")
181
- .replace("https://graph.microsoft.com/beta", "");
182
- } else {
183
- nextPath = undefined;
184
- }
185
- }
186
-
187
- return { items, truncated: Boolean(nextPath) };
188
- }
189
-
190
- export async function resolveGraphToken(
191
- cfg: unknown,
192
- options?: { preferDelegated?: boolean },
193
- ): Promise<string> {
194
- const msteamsCfg = (cfg as { channels?: { msteams?: MSTeamsConfig } })?.channels?.msteams;
195
- const creds = resolveMSTeamsCredentials(msteamsCfg);
196
- if (!creds) {
197
- throw new Error("MS Teams credentials missing");
198
- }
199
-
200
- // Try delegated token if requested and configured
201
- if (options?.preferDelegated && msteamsCfg?.delegatedAuth?.enabled && creds.type === "secret") {
202
- const delegated = await resolveDelegatedAccessToken({
203
- tenantId: creds.tenantId,
204
- clientId: creds.appId,
205
- clientSecret: creds.appPassword,
206
- });
207
- if (delegated) {
208
- return delegated;
209
- }
210
- // Fall through to app-only token
211
- }
212
-
213
- const { app } = await loadMSTeamsSdkWithAuth(creds);
214
- const tokenProvider = createMSTeamsTokenProvider(app);
215
- const graphTokenValue = await tokenProvider.getAccessToken("https://graph.microsoft.com");
216
- const accessToken = readAccessToken(graphTokenValue);
217
- if (!accessToken) {
218
- throw new Error("MS Teams graph token unavailable");
219
- }
220
- return accessToken;
221
- }
222
-
223
- export async function listTeamsByName(token: string, query: string): Promise<GraphGroup[]> {
224
- const escaped = escapeOData(query);
225
- const filter = `resourceProvisioningOptions/Any(x:x eq 'Team') and startsWith(displayName,'${escaped}')`;
226
- const path = `/groups?$filter=${encodeURIComponent(filter)}&$select=id,displayName`;
227
- const { items } = await fetchAllGraphPages<GraphGroup>({ token, path, maxPages: 5 });
228
- return items;
229
- }
230
-
231
- export async function postGraphJson<T>(params: {
232
- token: string;
233
- path: string;
234
- body?: unknown;
235
- }): Promise<T> {
236
- const res = await requestGraph({
237
- token: params.token,
238
- path: params.path,
239
- method: "POST",
240
- body: params.body,
241
- errorPrefix: "Graph POST",
242
- });
243
- return readOptionalGraphJson<T>(res);
244
- }
245
-
246
- export async function postGraphBetaJson<T>(params: {
247
- token: string;
248
- path: string;
249
- body?: unknown;
250
- }): Promise<T> {
251
- const res = await requestGraph({
252
- token: params.token,
253
- path: params.path,
254
- method: "POST",
255
- root: GRAPH_BETA,
256
- body: params.body,
257
- errorPrefix: "Graph beta POST",
258
- });
259
- return readOptionalGraphJson<T>(res);
260
- }
261
-
262
- export async function deleteGraphRequest(params: { token: string; path: string }): Promise<void> {
263
- await requestGraph({
264
- token: params.token,
265
- path: params.path,
266
- method: "DELETE",
267
- errorPrefix: "Graph DELETE",
268
- });
269
- }
270
-
271
- export async function patchGraphJson<T>(params: {
272
- token: string;
273
- path: string;
274
- body?: unknown;
275
- }): Promise<T> {
276
- const res = await requestGraph({
277
- token: params.token,
278
- path: params.path,
279
- method: "PATCH",
280
- body: params.body,
281
- errorPrefix: "Graph PATCH",
282
- });
283
- if (res.status === 204 || res.headers.get("content-length") === "0") {
284
- return undefined as T;
285
- }
286
- return (await res.json()) as T;
287
- }
288
-
289
- export async function listChannelsForTeam(token: string, teamId: string): Promise<GraphChannel[]> {
290
- const path = `/teams/${encodeURIComponent(teamId)}/channels?$select=id,displayName`;
291
- const { items } = await fetchAllGraphPages<GraphChannel>({ token, path, maxPages: 10 });
292
- return items;
293
- }
@@ -1,221 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import {
3
- decodeHtmlEntities,
4
- extractMSTeamsQuoteInfo,
5
- htmlToPlainText,
6
- normalizeMSTeamsConversationId,
7
- parseMSTeamsActivityTimestamp,
8
- stripMSTeamsMentionTags,
9
- wasMSTeamsBotMentioned,
10
- } from "./inbound.js";
11
-
12
- describe("msteams inbound", () => {
13
- describe("stripMSTeamsMentionTags", () => {
14
- it("removes <at>...</at> tags and trims", () => {
15
- expect(stripMSTeamsMentionTags("<at>Bot</at> hi")).toBe("hi");
16
- expect(stripMSTeamsMentionTags("hi <at>Bot</at>")).toBe("hi");
17
- });
18
-
19
- it("removes <at ...> tags with attributes", () => {
20
- expect(stripMSTeamsMentionTags('<at id="1">Bot</at> hi')).toBe("hi");
21
- expect(stripMSTeamsMentionTags('hi <at itemid="2">Bot</at>')).toBe("hi");
22
- });
23
- });
24
-
25
- describe("normalizeMSTeamsConversationId", () => {
26
- it("strips the ;messageid suffix", () => {
27
- expect(normalizeMSTeamsConversationId("19:abc@thread.tacv2;messageid=deadbeef")).toBe(
28
- "19:abc@thread.tacv2",
29
- );
30
- });
31
- });
32
-
33
- describe("parseMSTeamsActivityTimestamp", () => {
34
- it("returns undefined for empty/invalid values", () => {
35
- expect(parseMSTeamsActivityTimestamp(undefined)).toBeUndefined();
36
- expect(parseMSTeamsActivityTimestamp("not-a-date")).toBeUndefined();
37
- });
38
-
39
- it("parses string timestamps", () => {
40
- const ts = parseMSTeamsActivityTimestamp("2024-01-01T00:00:00.000Z");
41
- if (!ts) {
42
- throw new Error("expected MSTeams timestamp parser to return a Date");
43
- }
44
- expect(ts.toISOString()).toBe("2024-01-01T00:00:00.000Z");
45
- });
46
-
47
- it("passes through Date instances", () => {
48
- const d = new Date("2024-01-01T00:00:00.000Z");
49
- expect(parseMSTeamsActivityTimestamp(d)).toBe(d);
50
- });
51
- });
52
-
53
- describe("wasMSTeamsBotMentioned", () => {
54
- it("returns true when a mention entity matches recipient.id", () => {
55
- expect(
56
- wasMSTeamsBotMentioned({
57
- recipient: { id: "bot" },
58
- entities: [{ type: "mention", mentioned: { id: "bot" } }],
59
- }),
60
- ).toBe(true);
61
- });
62
-
63
- it("returns false when there is no matching mention", () => {
64
- expect(
65
- wasMSTeamsBotMentioned({
66
- recipient: { id: "bot" },
67
- entities: [{ type: "mention", mentioned: { id: "other" } }],
68
- }),
69
- ).toBe(false);
70
- });
71
- });
72
-
73
- describe("decodeHtmlEntities", () => {
74
- it("decodes common entities", () => {
75
- expect(decodeHtmlEntities("&amp;&lt;&gt;&quot;&#39;&#x27;&nbsp;")).toBe("&<>\"'' ");
76
- });
77
-
78
- it("leaves plain text unchanged", () => {
79
- expect(decodeHtmlEntities("hello world")).toBe("hello world");
80
- });
81
-
82
- it("prevents double-decoding: &amp;lt; should become &lt; not <", () => {
83
- // If &amp; were decoded first, &amp;lt; → &lt; → < (wrong).
84
- // With &amp; decoded last, &amp;lt; stays as &lt; (correct).
85
- expect(decodeHtmlEntities("&amp;lt;b&amp;gt;")).toBe("&lt;b&gt;");
86
- });
87
- });
88
-
89
- describe("htmlToPlainText", () => {
90
- it("strips tags and decodes entities", () => {
91
- expect(htmlToPlainText("<strong>Hello &amp; world</strong>")).toBe("Hello & world");
92
- });
93
-
94
- it("collapses whitespace from tag removal", () => {
95
- expect(htmlToPlainText("<p>foo</p><p>bar</p>")).toBe("foo bar");
96
- });
97
-
98
- it("trims leading and trailing whitespace", () => {
99
- expect(htmlToPlainText(" <span>hi</span> ")).toBe("hi");
100
- });
101
- });
102
-
103
- describe("extractMSTeamsQuoteInfo", () => {
104
- const replyAttachment = (overrides?: { content?: string; contentType?: string }) => ({
105
- contentType: overrides?.contentType ?? "text/html",
106
- content:
107
- overrides?.content ??
108
- '<blockquote itemtype="http://schema.skype.com/Reply" itemscope>' +
109
- '<strong itemprop="mri">Alice</strong>' +
110
- '<p itemprop="copy">Hello world</p>' +
111
- "</blockquote>",
112
- });
113
-
114
- it("extracts sender and body from a Teams reply attachment", () => {
115
- const result = extractMSTeamsQuoteInfo([replyAttachment()]);
116
- expect(result).toEqual({ sender: "Alice", body: "Hello world" });
117
- });
118
-
119
- it("returns undefined for empty attachments array", () => {
120
- expect(extractMSTeamsQuoteInfo([])).toBeUndefined();
121
- });
122
-
123
- it("returns undefined when no reply blockquote is present", () => {
124
- expect(
125
- extractMSTeamsQuoteInfo([{ contentType: "text/html", content: "<p>just a message</p>" }]),
126
- ).toBeUndefined();
127
- });
128
-
129
- it("uses 'unknown' as sender when sender element is absent", () => {
130
- const result = extractMSTeamsQuoteInfo([
131
- {
132
- contentType: "text/html",
133
- content:
134
- '<blockquote itemtype="http://schema.skype.com/Reply" itemscope>' +
135
- '<p itemprop="copy">quoted text</p>' +
136
- "</blockquote>",
137
- },
138
- ]);
139
- expect(result).toEqual({ sender: "unknown", body: "quoted text" });
140
- });
141
-
142
- it("returns undefined when body element is absent", () => {
143
- const result = extractMSTeamsQuoteInfo([
144
- {
145
- contentType: "text/html",
146
- content:
147
- '<blockquote itemtype="http://schema.skype.com/Reply" itemscope>' +
148
- '<strong itemprop="mri">Alice</strong>' +
149
- "</blockquote>",
150
- },
151
- ]);
152
- expect(result).toBeUndefined();
153
- });
154
-
155
- it("decodes HTML entities in body text", () => {
156
- const result = extractMSTeamsQuoteInfo([
157
- {
158
- contentType: "text/html",
159
- content:
160
- '<blockquote itemtype="http://schema.skype.com/Reply" itemscope>' +
161
- '<strong itemprop="mri">Bob</strong>' +
162
- '<p itemprop="copy">2 &lt; 3 &amp; 4 &gt; 1</p>' +
163
- "</blockquote>",
164
- },
165
- ]);
166
- expect(result).toEqual({ sender: "Bob", body: "2 < 3 & 4 > 1" });
167
- });
168
-
169
- it("handles multiline body by collapsing whitespace", () => {
170
- const result = extractMSTeamsQuoteInfo([
171
- {
172
- contentType: "text/html",
173
- content:
174
- '<blockquote itemtype="http://schema.skype.com/Reply" itemscope>' +
175
- '<strong itemprop="mri">Carol</strong>' +
176
- '<p itemprop="copy">line one\nline two</p>' +
177
- "</blockquote>",
178
- },
179
- ]);
180
- expect(result?.body).toBe("line one line two");
181
- });
182
-
183
- it("skips non-string content values", () => {
184
- expect(
185
- extractMSTeamsQuoteInfo([{ contentType: "application/json", content: { foo: "bar" } }]),
186
- ).toBeUndefined();
187
- });
188
-
189
- it("handles object content with .text property containing the reply HTML", () => {
190
- const htmlContent =
191
- '<blockquote itemtype="http://schema.skype.com/Reply" itemscope>' +
192
- '<strong itemprop="mri">Dave</strong>' +
193
- '<p itemprop="copy">hello from object</p>' +
194
- "</blockquote>";
195
- const result = extractMSTeamsQuoteInfo([
196
- { contentType: "text/html", content: { text: htmlContent } },
197
- ]);
198
- expect(result).toEqual({ sender: "Dave", body: "hello from object" });
199
- });
200
-
201
- it("handles object content with .body property containing the reply HTML", () => {
202
- const htmlContent =
203
- '<blockquote itemtype="http://schema.skype.com/Reply" itemscope>' +
204
- '<strong itemprop="mri">Eve</strong>' +
205
- '<p itemprop="copy">hello from body field</p>' +
206
- "</blockquote>";
207
- const result = extractMSTeamsQuoteInfo([
208
- { contentType: "text/html", content: { body: htmlContent } },
209
- ]);
210
- expect(result).toEqual({ sender: "Eve", body: "hello from body field" });
211
- });
212
-
213
- it("finds quote in second attachment when first has no quote", () => {
214
- const result = extractMSTeamsQuoteInfo([
215
- { contentType: "text/plain", content: "plain text" },
216
- replyAttachment(),
217
- ]);
218
- expect(result).toEqual({ sender: "Alice", body: "Hello world" });
219
- });
220
- });
221
- });
package/src/inbound.ts DELETED
@@ -1,148 +0,0 @@
1
- type MSTeamsQuoteInfo = {
2
- sender: string;
3
- body: string;
4
- };
5
-
6
- /**
7
- * Decode common HTML entities to plain text.
8
- */
9
- export function decodeHtmlEntities(html: string): string {
10
- return html
11
- .replace(/&lt;/g, "<")
12
- .replace(/&gt;/g, ">")
13
- .replace(/&quot;/g, '"')
14
- .replace(/&#39;/g, "'")
15
- .replace(/&#x27;/g, "'")
16
- .replace(/&nbsp;/g, " ")
17
- .replace(/&amp;/g, "&"); // must be last to prevent double-decoding (e.g. &amp;lt; → &lt; not <)
18
- }
19
-
20
- /**
21
- * Strip HTML tags, preserving text content.
22
- */
23
- export function htmlToPlainText(html: string): string {
24
- return decodeHtmlEntities(
25
- html
26
- .replace(/<[^>]*>/g, " ")
27
- .replace(/\s+/g, " ")
28
- .trim(),
29
- );
30
- }
31
-
32
- /**
33
- * Extract quote info from MS Teams HTML reply attachments.
34
- * Teams wraps quoted content in a blockquote with itemtype="http://schema.skype.com/Reply".
35
- */
36
- export function extractMSTeamsQuoteInfo(
37
- attachments: Array<{ contentType?: string | null; content?: unknown }>,
38
- ): MSTeamsQuoteInfo | undefined {
39
- for (const att of attachments) {
40
- // Content may be a plain string or an object with .text/.body (e.g. Adaptive Card payloads).
41
- let content = "";
42
- if (typeof att.content === "string") {
43
- content = att.content;
44
- } else if (typeof att.content === "object" && att.content !== null) {
45
- const record = att.content as Record<string, unknown>;
46
- content =
47
- typeof record.text === "string"
48
- ? record.text
49
- : typeof record.body === "string"
50
- ? record.body
51
- : "";
52
- }
53
- if (!content) {
54
- continue;
55
- }
56
-
57
- // Look for the Skype Reply schema blockquote.
58
- if (!content.includes("http://schema.skype.com/Reply")) {
59
- continue;
60
- }
61
-
62
- // Extract sender from <strong itemprop="mri">.
63
- const senderMatch = /<strong[^>]*itemprop=["']mri["'][^>]*>(.*?)<\/strong>/i.exec(content);
64
- const sender = senderMatch?.[1] ? htmlToPlainText(senderMatch[1]) : undefined;
65
-
66
- // Extract body from <p itemprop="copy">.
67
- const bodyMatch = /<p[^>]*itemprop=["']copy["'][^>]*>(.*?)<\/p>/is.exec(content);
68
- const body = bodyMatch?.[1] ? htmlToPlainText(bodyMatch[1]) : undefined;
69
-
70
- if (body) {
71
- return { sender: sender ?? "unknown", body };
72
- }
73
- }
74
- return undefined;
75
- }
76
-
77
- type MentionableActivity = {
78
- recipient?: { id?: string } | null;
79
- entities?: Array<{
80
- type?: string;
81
- mentioned?: { id?: string };
82
- }> | null;
83
- };
84
-
85
- export function normalizeMSTeamsConversationId(raw: string): string {
86
- return raw.split(";")[0] ?? raw;
87
- }
88
-
89
- export function extractMSTeamsConversationMessageId(raw: string): string | undefined {
90
- if (!raw) {
91
- return undefined;
92
- }
93
- const match = /(?:^|;)messageid=([^;]+)/i.exec(raw);
94
- const value = match?.[1]?.trim() ?? "";
95
- return value || undefined;
96
- }
97
-
98
- export function parseMSTeamsActivityTimestamp(value: unknown): Date | undefined {
99
- if (!value) {
100
- return undefined;
101
- }
102
- if (value instanceof Date) {
103
- return value;
104
- }
105
- if (typeof value !== "string") {
106
- return undefined;
107
- }
108
- const date = new Date(value);
109
- return Number.isNaN(date.getTime()) ? undefined : date;
110
- }
111
-
112
- export function stripMSTeamsMentionTags(text: string): string {
113
- // Teams wraps mentions in <at>...</at> tags
114
- return text.replace(/<at[^>]*>.*?<\/at>/gi, "").trim();
115
- }
116
-
117
- /**
118
- * Bot Framework uses 'a:xxx' conversation IDs for personal chats, but Graph API
119
- * requires the '19:{userId}_{botAppId}@unq.gbl.spaces' format.
120
- *
121
- * This is the documented Graph API format for 1:1 chat thread IDs between a user
122
- * and a bot/app. See Microsoft docs "Get chat between user and app":
123
- * https://learn.microsoft.com/en-us/graph/api/userscopeteamsappinstallation-get-chat
124
- *
125
- * The format is only synthesized when the Bot Framework conversation ID starts with
126
- * 'a:' (the opaque format used by BF but not recognized by Graph). If the ID already
127
- * has the '19:...' Graph format, it is passed through unchanged.
128
- */
129
- export function translateMSTeamsDmConversationIdForGraph(params: {
130
- isDirectMessage: boolean;
131
- conversationId: string;
132
- aadObjectId?: string | null;
133
- appId?: string | null;
134
- }): string {
135
- const { isDirectMessage, conversationId, aadObjectId, appId } = params;
136
- return isDirectMessage && conversationId.startsWith("a:") && aadObjectId && appId
137
- ? `19:${aadObjectId}_${appId}@unq.gbl.spaces`
138
- : conversationId;
139
- }
140
-
141
- export function wasMSTeamsBotMentioned(activity: MentionableActivity): boolean {
142
- const botId = activity.recipient?.id;
143
- if (!botId) {
144
- return false;
145
- }
146
- const entities = activity.entities ?? [];
147
- return entities.some((e) => e.type === "mention" && e.mentioned?.id === botId);
148
- }
package/src/index.ts DELETED
@@ -1,4 +0,0 @@
1
- export { monitorMSTeamsProvider } from "./monitor.js";
2
- export { probeMSTeams } from "./probe.js";
3
- export { sendMessageMSTeams, sendPollMSTeams } from "./send.js";
4
- export { type MSTeamsCredentials, resolveMSTeamsCredentials } from "./token.js";