@kodelyth/msteams 2026.5.42 → 2026.6.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.
Files changed (177) hide show
  1. package/klaw.plugin.json +726 -2
  2. package/package.json +16 -4
  3. package/api.ts +0 -3
  4. package/channel-config-api.ts +0 -1
  5. package/channel-plugin-api.ts +0 -2
  6. package/config-api.ts +0 -4
  7. package/contract-api.ts +0 -4
  8. package/index.ts +0 -20
  9. package/runtime-api.ts +0 -66
  10. package/secret-contract-api.ts +0 -5
  11. package/setup-entry.ts +0 -13
  12. package/setup-plugin-api.ts +0 -3
  13. package/src/ai-entity.ts +0 -7
  14. package/src/approval-auth.ts +0 -44
  15. package/src/attachments/bot-framework.test.ts +0 -506
  16. package/src/attachments/bot-framework.ts +0 -348
  17. package/src/attachments/download.ts +0 -328
  18. package/src/attachments/graph.test.ts +0 -441
  19. package/src/attachments/graph.ts +0 -489
  20. package/src/attachments/html.ts +0 -122
  21. package/src/attachments/payload.ts +0 -14
  22. package/src/attachments/remote-media.test.ts +0 -187
  23. package/src/attachments/remote-media.ts +0 -86
  24. package/src/attachments/shared.test.ts +0 -547
  25. package/src/attachments/shared.ts +0 -655
  26. package/src/attachments/types.ts +0 -47
  27. package/src/attachments.graph.test.ts +0 -414
  28. package/src/attachments.helpers.test.ts +0 -245
  29. package/src/attachments.test-helpers.ts +0 -17
  30. package/src/attachments.test.ts +0 -754
  31. package/src/attachments.ts +0 -18
  32. package/src/block-streaming-config.test.ts +0 -61
  33. package/src/channel-api.ts +0 -1
  34. package/src/channel.actions.test.ts +0 -797
  35. package/src/channel.directory.test.ts +0 -176
  36. package/src/channel.message-adapter.test.ts +0 -227
  37. package/src/channel.runtime.ts +0 -56
  38. package/src/channel.setup.ts +0 -77
  39. package/src/channel.test.ts +0 -136
  40. package/src/channel.ts +0 -1176
  41. package/src/config-schema.ts +0 -6
  42. package/src/config-ui-hints.ts +0 -40
  43. package/src/conversation-store-fs.test.ts +0 -81
  44. package/src/conversation-store-fs.ts +0 -149
  45. package/src/conversation-store-helpers.test.ts +0 -202
  46. package/src/conversation-store-helpers.ts +0 -105
  47. package/src/conversation-store-memory.ts +0 -51
  48. package/src/conversation-store.shared.test.ts +0 -260
  49. package/src/conversation-store.ts +0 -71
  50. package/src/directory-live.test.ts +0 -156
  51. package/src/directory-live.ts +0 -111
  52. package/src/doctor.ts +0 -27
  53. package/src/errors.test.ts +0 -154
  54. package/src/errors.ts +0 -270
  55. package/src/feedback-reflection-prompt.ts +0 -117
  56. package/src/feedback-reflection-store.ts +0 -113
  57. package/src/feedback-reflection.test.ts +0 -237
  58. package/src/feedback-reflection.ts +0 -268
  59. package/src/file-consent-helpers.test.ts +0 -328
  60. package/src/file-consent-helpers.ts +0 -115
  61. package/src/file-consent-invoke.ts +0 -150
  62. package/src/file-consent.test.ts +0 -378
  63. package/src/file-consent.ts +0 -223
  64. package/src/graph-chat.ts +0 -36
  65. package/src/graph-group-management.test.ts +0 -332
  66. package/src/graph-group-management.ts +0 -168
  67. package/src/graph-members.test.ts +0 -89
  68. package/src/graph-members.ts +0 -48
  69. package/src/graph-messages.actions.test.ts +0 -253
  70. package/src/graph-messages.read.test.ts +0 -391
  71. package/src/graph-messages.search.test.ts +0 -227
  72. package/src/graph-messages.test-helpers.ts +0 -50
  73. package/src/graph-messages.ts +0 -534
  74. package/src/graph-teams.test.ts +0 -222
  75. package/src/graph-teams.ts +0 -114
  76. package/src/graph-thread.test.ts +0 -252
  77. package/src/graph-thread.ts +0 -146
  78. package/src/graph-upload.test.ts +0 -253
  79. package/src/graph-upload.ts +0 -531
  80. package/src/graph-users.ts +0 -29
  81. package/src/graph.test.ts +0 -540
  82. package/src/graph.ts +0 -308
  83. package/src/inbound.test.ts +0 -221
  84. package/src/inbound.ts +0 -148
  85. package/src/index.ts +0 -4
  86. package/src/media-helpers.test.ts +0 -220
  87. package/src/media-helpers.ts +0 -105
  88. package/src/mentions.test.ts +0 -254
  89. package/src/mentions.ts +0 -114
  90. package/src/messenger.test.ts +0 -961
  91. package/src/messenger.ts +0 -608
  92. package/src/monitor-handler/access.ts +0 -136
  93. package/src/monitor-handler/inbound-media.test.ts +0 -314
  94. package/src/monitor-handler/inbound-media.ts +0 -180
  95. package/src/monitor-handler/message-handler-mock-support.test-support.ts +0 -28
  96. package/src/monitor-handler/message-handler.authz.test.ts +0 -739
  97. package/src/monitor-handler/message-handler.dm-media.test.ts +0 -54
  98. package/src/monitor-handler/message-handler.test-support.ts +0 -99
  99. package/src/monitor-handler/message-handler.thread-parent.test.ts +0 -225
  100. package/src/monitor-handler/message-handler.thread-session.test.ts +0 -132
  101. package/src/monitor-handler/message-handler.ts +0 -1003
  102. package/src/monitor-handler/reaction-handler.test.ts +0 -325
  103. package/src/monitor-handler/reaction-handler.ts +0 -122
  104. package/src/monitor-handler/thread-session.ts +0 -30
  105. package/src/monitor-handler.adaptive-card.test.ts +0 -158
  106. package/src/monitor-handler.feedback-authz.test.ts +0 -357
  107. package/src/monitor-handler.file-consent.test.ts +0 -443
  108. package/src/monitor-handler.sso.test.ts +0 -576
  109. package/src/monitor-handler.test-helpers.ts +0 -181
  110. package/src/monitor-handler.ts +0 -538
  111. package/src/monitor-handler.types.ts +0 -27
  112. package/src/monitor-types.ts +0 -6
  113. package/src/monitor.lifecycle.test.ts +0 -457
  114. package/src/monitor.test.ts +0 -119
  115. package/src/monitor.ts +0 -476
  116. package/src/oauth.flow.ts +0 -77
  117. package/src/oauth.shared.ts +0 -37
  118. package/src/oauth.test.ts +0 -350
  119. package/src/oauth.token.ts +0 -162
  120. package/src/oauth.ts +0 -130
  121. package/src/outbound.test.ts +0 -400
  122. package/src/outbound.ts +0 -198
  123. package/src/pending-uploads-fs.test.ts +0 -261
  124. package/src/pending-uploads-fs.ts +0 -235
  125. package/src/pending-uploads.test.ts +0 -186
  126. package/src/pending-uploads.ts +0 -121
  127. package/src/policy.test.ts +0 -156
  128. package/src/policy.ts +0 -245
  129. package/src/polls-store-memory.ts +0 -32
  130. package/src/polls.test.ts +0 -169
  131. package/src/polls.ts +0 -312
  132. package/src/presentation.ts +0 -93
  133. package/src/probe.test.ts +0 -79
  134. package/src/probe.ts +0 -132
  135. package/src/reply-dispatcher.test.ts +0 -543
  136. package/src/reply-dispatcher.ts +0 -523
  137. package/src/reply-stream-controller.test.ts +0 -424
  138. package/src/reply-stream-controller.ts +0 -334
  139. package/src/resolve-allowlist.test.ts +0 -253
  140. package/src/resolve-allowlist.ts +0 -309
  141. package/src/revoked-context.ts +0 -17
  142. package/src/runtime.ts +0 -12
  143. package/src/sdk-types.ts +0 -59
  144. package/src/sdk.test.ts +0 -727
  145. package/src/sdk.ts +0 -916
  146. package/src/secret-contract.ts +0 -49
  147. package/src/secret-input.ts +0 -7
  148. package/src/send-context.test.ts +0 -93
  149. package/src/send-context.ts +0 -269
  150. package/src/send.test.ts +0 -588
  151. package/src/send.ts +0 -697
  152. package/src/sent-message-cache.test.ts +0 -106
  153. package/src/sent-message-cache.ts +0 -174
  154. package/src/session-route.ts +0 -40
  155. package/src/setup-core.ts +0 -162
  156. package/src/setup-surface.test.ts +0 -175
  157. package/src/setup-surface.ts +0 -319
  158. package/src/sso-token-store.test.ts +0 -74
  159. package/src/sso-token-store.ts +0 -166
  160. package/src/sso.ts +0 -300
  161. package/src/storage.ts +0 -25
  162. package/src/store-fs.ts +0 -42
  163. package/src/streaming-message.test.ts +0 -323
  164. package/src/streaming-message.ts +0 -327
  165. package/src/test-runtime.ts +0 -16
  166. package/src/thread-parent-context.test.ts +0 -224
  167. package/src/thread-parent-context.ts +0 -159
  168. package/src/token-response.ts +0 -11
  169. package/src/token.test.ts +0 -268
  170. package/src/token.ts +0 -194
  171. package/src/user-agent.test.ts +0 -121
  172. package/src/user-agent.ts +0 -53
  173. package/src/webhook-timeouts.ts +0 -27
  174. package/src/welcome-card.test.ts +0 -104
  175. package/src/welcome-card.ts +0 -57
  176. package/test-api.ts +0 -1
  177. package/tsconfig.json +0 -16
package/src/graph.ts DELETED
@@ -1,308 +0,0 @@
1
- import { readProviderJsonResponse } from "klaw/plugin-sdk/provider-http";
2
- import { fetchWithSsrFGuard, type MSTeamsConfig } from "../runtime-api.js";
3
- import { GRAPH_ROOT } from "./attachments/shared.js";
4
-
5
- const GRAPH_BETA = "https://graph.microsoft.com/beta";
6
- const NULL_BODY_STATUSES = new Set([101, 204, 205, 304]);
7
- import { createMSTeamsTokenProvider, loadMSTeamsSdkWithAuth } from "./sdk.js";
8
- import { readAccessToken } from "./token-response.js";
9
- import { resolveDelegatedAccessToken, resolveMSTeamsCredentials } from "./token.js";
10
- import { buildUserAgent } from "./user-agent.js";
11
-
12
- export type GraphUser = {
13
- id?: string;
14
- displayName?: string;
15
- userPrincipalName?: string;
16
- mail?: string;
17
- };
18
-
19
- type GraphGroup = {
20
- id?: string;
21
- displayName?: string;
22
- };
23
-
24
- type GraphChannel = {
25
- id?: string;
26
- displayName?: string;
27
- };
28
-
29
- export type GraphResponse<T> = { value?: T[] };
30
-
31
- export function normalizeQuery(value?: string | null): string {
32
- return value?.trim() ?? "";
33
- }
34
-
35
- export function escapeOData(value: string): string {
36
- return value.replace(/'/g, "''");
37
- }
38
-
39
- async function requestGraph(params: {
40
- token: string;
41
- path: string;
42
- method?: "GET" | "POST" | "PATCH" | "DELETE";
43
- root?: string;
44
- headers?: Record<string, string>;
45
- body?: unknown;
46
- errorPrefix?: string;
47
- }): Promise<Response> {
48
- const hasBody = params.body !== undefined;
49
- const url = `${params.root ?? GRAPH_ROOT}${params.path}`;
50
- const currentFetch = globalThis.fetch;
51
- const { response, release } = await fetchWithSsrFGuard({
52
- url,
53
- fetchImpl: async (input, guardedInit) => await currentFetch(input, guardedInit),
54
- init: {
55
- method: params.method,
56
- headers: {
57
- "User-Agent": buildUserAgent(),
58
- Authorization: `Bearer ${params.token}`,
59
- ...(hasBody ? { "Content-Type": "application/json" } : {}),
60
- ...params.headers,
61
- },
62
- body: hasBody ? JSON.stringify(params.body) : undefined,
63
- },
64
- auditContext: "msteams.graph",
65
- });
66
- try {
67
- if (!response.ok) {
68
- const text = await response.text().catch(() => "");
69
- throw new Error(
70
- `${params.errorPrefix ?? "Graph"} ${params.path} failed (${response.status}): ${text || "unknown error"}`,
71
- );
72
- }
73
- const body = NULL_BODY_STATUSES.has(response.status) ? null : await response.arrayBuffer();
74
- return new Response(body, {
75
- status: response.status,
76
- statusText: response.statusText,
77
- headers: new Headers(response.headers),
78
- });
79
- } finally {
80
- await release();
81
- }
82
- }
83
-
84
- async function readOptionalGraphJson<T>(res: Response, label: string): Promise<T> {
85
- // Use optional chaining to stay resilient to partial test mocks that do not
86
- // provide a status or Headers instance (they only shim `ok` + `json()`).
87
- if (res.status === 204 || res.headers?.get?.("content-length") === "0") {
88
- return undefined as T;
89
- }
90
- return await readProviderJsonResponse<T>(res, label);
91
- }
92
-
93
- export async function fetchGraphJson<T>(params: {
94
- token: string;
95
- path: string;
96
- headers?: Record<string, string>;
97
- /** HTTP method; defaults to "GET" */
98
- method?: string;
99
- /** Request body (serialized as JSON). Only used for non-GET methods. */
100
- body?: unknown;
101
- }): Promise<T> {
102
- const res = await requestGraph({
103
- token: params.token,
104
- path: params.path,
105
- method: params.method as "GET" | "POST" | "DELETE" | undefined,
106
- body: params.body,
107
- headers: params.headers,
108
- });
109
- return await readOptionalGraphJson<T>(res, `Graph ${params.path} failed`);
110
- }
111
-
112
- /**
113
- * Fetch JSON from an absolute Graph API URL (for example @odata.nextLink
114
- * pagination URLs) without prepending GRAPH_ROOT.
115
- */
116
- export async function fetchGraphAbsoluteUrl<T>(params: {
117
- token: string;
118
- url: string;
119
- headers?: Record<string, string>;
120
- }): Promise<T> {
121
- const { response, release } = await fetchWithSsrFGuard({
122
- url: params.url,
123
- init: {
124
- headers: {
125
- "User-Agent": buildUserAgent(),
126
- Authorization: `Bearer ${params.token}`,
127
- ...params.headers,
128
- },
129
- },
130
- auditContext: "msteams.graph.absolute",
131
- });
132
- try {
133
- if (!response.ok) {
134
- const text = await response.text().catch(() => "");
135
- throw new Error(
136
- `Graph ${params.url} failed (${response.status}): ${text || "unknown error"}`,
137
- );
138
- }
139
- return await readProviderJsonResponse<T>(response, `Graph ${params.url} failed`);
140
- } finally {
141
- await release();
142
- }
143
- }
144
-
145
- /** Graph collection response with optional pagination link. */
146
- type GraphPagedResponse<T> = {
147
- value?: T[];
148
- "@odata.nextLink"?: string;
149
- };
150
-
151
- /** Result of a paginated Graph API fetch. */
152
- type PaginatedResult<T> = {
153
- items: T[];
154
- truncated: boolean;
155
- found?: T;
156
- };
157
-
158
- /**
159
- * Fetch all pages of a Graph API collection, following @odata.nextLink.
160
- * Optionally stop early when `findOne` matches an item.
161
- */
162
- export async function fetchAllGraphPages<T>(params: {
163
- token: string;
164
- path: string;
165
- headers?: Record<string, string>;
166
- /** Max pages to fetch before stopping. Default: 50. */
167
- maxPages?: number;
168
- /** Stop pagination early when this predicate returns true. */
169
- findOne?: (item: T) => boolean;
170
- }): Promise<PaginatedResult<T>> {
171
- const maxPages = params.maxPages ?? 50;
172
- const items: T[] = [];
173
- let nextPath: string | undefined = params.path;
174
-
175
- for (let page = 0; page < maxPages && nextPath; page++) {
176
- const res: GraphPagedResponse<T> = await fetchGraphJson<GraphPagedResponse<T>>({
177
- token: params.token,
178
- path: nextPath,
179
- headers: params.headers,
180
- });
181
-
182
- const pageItems = res.value ?? [];
183
-
184
- if (params.findOne) {
185
- const match = pageItems.find(params.findOne);
186
- if (match) {
187
- items.push(...pageItems);
188
- return { items, truncated: false, found: match };
189
- }
190
- }
191
-
192
- items.push(...pageItems);
193
-
194
- // @odata.nextLink is an absolute URL; strip the Graph root to get a relative path
195
- const rawNext: string | undefined = res["@odata.nextLink"];
196
- if (rawNext) {
197
- nextPath = rawNext
198
- .replace("https://graph.microsoft.com/v1.0", "")
199
- .replace("https://graph.microsoft.com/beta", "");
200
- } else {
201
- nextPath = undefined;
202
- }
203
- }
204
-
205
- return { items, truncated: Boolean(nextPath) };
206
- }
207
-
208
- export async function resolveGraphToken(
209
- cfg: unknown,
210
- options?: { preferDelegated?: boolean },
211
- ): Promise<string> {
212
- const msteamsCfg = (cfg as { channels?: { msteams?: MSTeamsConfig } })?.channels?.msteams;
213
- const creds = resolveMSTeamsCredentials(msteamsCfg);
214
- if (!creds) {
215
- throw new Error("MS Teams credentials missing");
216
- }
217
-
218
- // Try delegated token if requested and configured
219
- if (options?.preferDelegated && msteamsCfg?.delegatedAuth?.enabled && creds.type === "secret") {
220
- const delegated = await resolveDelegatedAccessToken({
221
- tenantId: creds.tenantId,
222
- clientId: creds.appId,
223
- clientSecret: creds.appPassword,
224
- });
225
- if (delegated) {
226
- return delegated;
227
- }
228
- // Fall through to app-only token
229
- }
230
-
231
- const { app } = await loadMSTeamsSdkWithAuth(creds);
232
- const tokenProvider = createMSTeamsTokenProvider(app);
233
- const graphTokenValue = await tokenProvider.getAccessToken("https://graph.microsoft.com");
234
- const accessToken = readAccessToken(graphTokenValue);
235
- if (!accessToken) {
236
- throw new Error("MS Teams graph token unavailable");
237
- }
238
- return accessToken;
239
- }
240
-
241
- export async function listTeamsByName(token: string, query: string): Promise<GraphGroup[]> {
242
- const escaped = escapeOData(query);
243
- const filter = `resourceProvisioningOptions/Any(x:x eq 'Team') and startsWith(displayName,'${escaped}')`;
244
- const path = `/groups?$filter=${encodeURIComponent(filter)}&$select=id,displayName`;
245
- const { items } = await fetchAllGraphPages<GraphGroup>({ token, path, maxPages: 5 });
246
- return items;
247
- }
248
-
249
- export async function postGraphJson<T>(params: {
250
- token: string;
251
- path: string;
252
- body?: unknown;
253
- }): Promise<T> {
254
- const res = await requestGraph({
255
- token: params.token,
256
- path: params.path,
257
- method: "POST",
258
- body: params.body,
259
- errorPrefix: "Graph POST",
260
- });
261
- return readOptionalGraphJson<T>(res, `Graph POST ${params.path} failed`);
262
- }
263
-
264
- export async function postGraphBetaJson<T>(params: {
265
- token: string;
266
- path: string;
267
- body?: unknown;
268
- }): Promise<T> {
269
- const res = await requestGraph({
270
- token: params.token,
271
- path: params.path,
272
- method: "POST",
273
- root: GRAPH_BETA,
274
- body: params.body,
275
- errorPrefix: "Graph beta POST",
276
- });
277
- return readOptionalGraphJson<T>(res, `Graph beta POST ${params.path} failed`);
278
- }
279
-
280
- export async function deleteGraphRequest(params: { token: string; path: string }): Promise<void> {
281
- await requestGraph({
282
- token: params.token,
283
- path: params.path,
284
- method: "DELETE",
285
- errorPrefix: "Graph DELETE",
286
- });
287
- }
288
-
289
- export async function patchGraphJson<T>(params: {
290
- token: string;
291
- path: string;
292
- body?: unknown;
293
- }): Promise<T> {
294
- const res = await requestGraph({
295
- token: params.token,
296
- path: params.path,
297
- method: "PATCH",
298
- body: params.body,
299
- errorPrefix: "Graph PATCH",
300
- });
301
- return readOptionalGraphJson<T>(res, `Graph PATCH ${params.path} failed`);
302
- }
303
-
304
- export async function listChannelsForTeam(token: string, teamId: string): Promise<GraphChannel[]> {
305
- const path = `/teams/${encodeURIComponent(teamId)}/channels?$select=id,displayName`;
306
- const { items } = await fetchAllGraphPages<GraphChannel>({ token, path, maxPages: 10 });
307
- return items;
308
- }
@@ -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";