@openclaw/msteams 2026.2.24 → 2026.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # Changelog
2
2
 
3
+ ## 2026.3.1
4
+
5
+ ### Changes
6
+
7
+ - Version alignment with core OpenClaw release numbers.
8
+
9
+ ## 2026.2.26
10
+
11
+ ### Changes
12
+
13
+ - Version alignment with core OpenClaw release numbers.
14
+
15
+ ## 2026.2.25
16
+
17
+ ### Changes
18
+
19
+ - Version alignment with core OpenClaw release numbers.
20
+
3
21
  ## 2026.2.24
4
22
 
5
23
  ### Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw/msteams",
3
- "version": "2026.2.24",
3
+ "version": "2026.3.1",
4
4
  "description": "OpenClaw Microsoft Teams channel plugin",
5
5
  "type": "module",
6
6
  "dependencies": {
@@ -1,3 +1,4 @@
1
+ import { fetchWithBearerAuthScopeFallback } from "openclaw/plugin-sdk";
1
2
  import { getMSTeamsRuntime } from "../runtime.js";
2
3
  import { downloadAndStoreMSTeamsRemoteMedia } from "./remote-media.js";
3
4
  import {
@@ -7,10 +8,10 @@ import {
7
8
  isRecord,
8
9
  isUrlAllowed,
9
10
  normalizeContentType,
11
+ resolveMediaSsrfPolicy,
10
12
  resolveRequestUrl,
11
13
  resolveAuthAllowedHosts,
12
14
  resolveAllowedHosts,
13
- safeFetch,
14
15
  } from "./shared.js";
15
16
  import type {
16
17
  MSTeamsAccessTokenProvider,
@@ -90,81 +91,17 @@ async function fetchWithAuthFallback(params: {
90
91
  tokenProvider?: MSTeamsAccessTokenProvider;
91
92
  fetchFn?: typeof fetch;
92
93
  requestInit?: RequestInit;
93
- allowHosts: string[];
94
94
  authAllowHosts: string[];
95
- resolveFn?: (hostname: string) => Promise<{ address: string }>;
96
95
  }): Promise<Response> {
97
- const fetchFn = params.fetchFn ?? fetch;
98
-
99
- // Use safeFetch for the initial attempt — redirect: "manual" with
100
- // allowlist + DNS/IP validation on every hop (prevents SSRF via redirect).
101
- const firstAttempt = await safeFetch({
96
+ return await fetchWithBearerAuthScopeFallback({
102
97
  url: params.url,
103
- allowHosts: params.allowHosts,
104
- fetchFn,
98
+ scopes: scopeCandidatesForUrl(params.url),
99
+ tokenProvider: params.tokenProvider,
100
+ fetchFn: params.fetchFn,
105
101
  requestInit: params.requestInit,
106
- resolveFn: params.resolveFn,
102
+ requireHttps: true,
103
+ shouldAttachAuth: (url) => isUrlAllowed(url, params.authAllowHosts),
107
104
  });
108
- if (firstAttempt.ok) {
109
- return firstAttempt;
110
- }
111
- if (!params.tokenProvider) {
112
- return firstAttempt;
113
- }
114
- if (firstAttempt.status !== 401 && firstAttempt.status !== 403) {
115
- return firstAttempt;
116
- }
117
- if (!isUrlAllowed(params.url, params.authAllowHosts)) {
118
- return firstAttempt;
119
- }
120
-
121
- const scopes = scopeCandidatesForUrl(params.url);
122
- for (const scope of scopes) {
123
- try {
124
- const token = await params.tokenProvider.getAccessToken(scope);
125
- const authHeaders = new Headers(params.requestInit?.headers);
126
- authHeaders.set("Authorization", `Bearer ${token}`);
127
- const authAttempt = await safeFetch({
128
- url: params.url,
129
- allowHosts: params.allowHosts,
130
- fetchFn,
131
- requestInit: {
132
- ...params.requestInit,
133
- headers: authHeaders,
134
- },
135
- resolveFn: params.resolveFn,
136
- });
137
- if (authAttempt.ok) {
138
- return authAttempt;
139
- }
140
- if (authAttempt.status !== 401 && authAttempt.status !== 403) {
141
- continue;
142
- }
143
-
144
- const finalUrl =
145
- typeof authAttempt.url === "string" && authAttempt.url ? authAttempt.url : "";
146
- if (!finalUrl || finalUrl === params.url || !isUrlAllowed(finalUrl, params.authAllowHosts)) {
147
- continue;
148
- }
149
- const redirectedAuthAttempt = await safeFetch({
150
- url: finalUrl,
151
- allowHosts: params.allowHosts,
152
- fetchFn,
153
- requestInit: {
154
- ...params.requestInit,
155
- headers: authHeaders,
156
- },
157
- resolveFn: params.resolveFn,
158
- });
159
- if (redirectedAuthAttempt.ok) {
160
- return redirectedAuthAttempt;
161
- }
162
- } catch {
163
- // Try the next scope.
164
- }
165
- }
166
-
167
- return firstAttempt;
168
105
  }
169
106
 
170
107
  /**
@@ -180,8 +117,6 @@ export async function downloadMSTeamsAttachments(params: {
180
117
  fetchFn?: typeof fetch;
181
118
  /** When true, embeds original filename in stored path for later extraction. */
182
119
  preserveFilenames?: boolean;
183
- /** Override DNS resolver for testing (anti-SSRF IP validation). */
184
- resolveFn?: (hostname: string) => Promise<{ address: string }>;
185
120
  }): Promise<MSTeamsInboundMedia[]> {
186
121
  const list = Array.isArray(params.attachments) ? params.attachments : [];
187
122
  if (list.length === 0) {
@@ -189,6 +124,7 @@ export async function downloadMSTeamsAttachments(params: {
189
124
  }
190
125
  const allowHosts = resolveAllowedHosts(params.allowHosts);
191
126
  const authAllowHosts = resolveAuthAllowedHosts(params.authAllowHosts);
127
+ const ssrfPolicy = resolveMediaSsrfPolicy(allowHosts);
192
128
 
193
129
  // Download ANY downloadable attachment (not just images)
194
130
  const downloadable = list.filter(isDownloadableAttachment);
@@ -257,15 +193,14 @@ export async function downloadMSTeamsAttachments(params: {
257
193
  contentTypeHint: candidate.contentTypeHint,
258
194
  placeholder: candidate.placeholder,
259
195
  preserveFilenames: params.preserveFilenames,
196
+ ssrfPolicy,
260
197
  fetchImpl: (input, init) =>
261
198
  fetchWithAuthFallback({
262
199
  url: resolveRequestUrl(input),
263
200
  tokenProvider: params.tokenProvider,
264
201
  fetchFn: params.fetchFn,
265
202
  requestInit: init,
266
- allowHosts,
267
203
  authAllowHosts,
268
- resolveFn: params.resolveFn,
269
204
  }),
270
205
  });
271
206
  out.push(media);
@@ -1,3 +1,4 @@
1
+ import { fetchWithSsrFGuard, type SsrFPolicy } from "openclaw/plugin-sdk";
1
2
  import { getMSTeamsRuntime } from "../runtime.js";
2
3
  import { downloadMSTeamsAttachments } from "./download.js";
3
4
  import { downloadAndStoreMSTeamsRemoteMedia } from "./remote-media.js";
@@ -7,9 +8,9 @@ import {
7
8
  isRecord,
8
9
  isUrlAllowed,
9
10
  normalizeContentType,
11
+ resolveMediaSsrfPolicy,
10
12
  resolveRequestUrl,
11
13
  resolveAllowedHosts,
12
- safeFetch,
13
14
  } from "./shared.js";
14
15
  import type {
15
16
  MSTeamsAccessTokenProvider,
@@ -119,20 +120,31 @@ async function fetchGraphCollection<T>(params: {
119
120
  url: string;
120
121
  accessToken: string;
121
122
  fetchFn?: typeof fetch;
123
+ ssrfPolicy?: SsrFPolicy;
122
124
  }): Promise<{ status: number; items: T[] }> {
123
125
  const fetchFn = params.fetchFn ?? fetch;
124
- const res = await fetchFn(params.url, {
125
- headers: { Authorization: `Bearer ${params.accessToken}` },
126
+ const { response, release } = await fetchWithSsrFGuard({
127
+ url: params.url,
128
+ fetchImpl: fetchFn,
129
+ init: {
130
+ headers: { Authorization: `Bearer ${params.accessToken}` },
131
+ },
132
+ policy: params.ssrfPolicy,
133
+ auditContext: "msteams.graph.collection",
126
134
  });
127
- const status = res.status;
128
- if (!res.ok) {
129
- return { status, items: [] };
130
- }
131
135
  try {
132
- const data = (await res.json()) as { value?: T[] };
133
- return { status, items: Array.isArray(data.value) ? data.value : [] };
134
- } catch {
135
- return { status, items: [] };
136
+ const status = response.status;
137
+ if (!response.ok) {
138
+ return { status, items: [] };
139
+ }
140
+ try {
141
+ const data = (await response.json()) as { value?: T[] };
142
+ return { status, items: Array.isArray(data.value) ? data.value : [] };
143
+ } catch {
144
+ return { status, items: [] };
145
+ }
146
+ } finally {
147
+ await release();
136
148
  }
137
149
  }
138
150
 
@@ -164,11 +176,13 @@ async function downloadGraphHostedContent(params: {
164
176
  maxBytes: number;
165
177
  fetchFn?: typeof fetch;
166
178
  preserveFilenames?: boolean;
179
+ ssrfPolicy?: SsrFPolicy;
167
180
  }): Promise<{ media: MSTeamsInboundMedia[]; status: number; count: number }> {
168
181
  const hosted = await fetchGraphCollection<GraphHostedContent>({
169
182
  url: `${params.messageUrl}/hostedContents`,
170
183
  accessToken: params.accessToken,
171
184
  fetchFn: params.fetchFn,
185
+ ssrfPolicy: params.ssrfPolicy,
172
186
  });
173
187
  if (hosted.items.length === 0) {
174
188
  return { media: [], status: hosted.status, count: 0 };
@@ -228,6 +242,7 @@ export async function downloadMSTeamsGraphMedia(params: {
228
242
  return { media: [] };
229
243
  }
230
244
  const allowHosts = resolveAllowedHosts(params.allowHosts);
245
+ const ssrfPolicy = resolveMediaSsrfPolicy(allowHosts);
231
246
  const messageUrl = params.messageUrl;
232
247
  let accessToken: string;
233
248
  try {
@@ -241,64 +256,67 @@ export async function downloadMSTeamsGraphMedia(params: {
241
256
  const sharePointMedia: MSTeamsInboundMedia[] = [];
242
257
  const downloadedReferenceUrls = new Set<string>();
243
258
  try {
244
- const msgRes = await fetchFn(messageUrl, {
245
- headers: { Authorization: `Bearer ${accessToken}` },
259
+ const { response: msgRes, release } = await fetchWithSsrFGuard({
260
+ url: messageUrl,
261
+ fetchImpl: fetchFn,
262
+ init: {
263
+ headers: { Authorization: `Bearer ${accessToken}` },
264
+ },
265
+ policy: ssrfPolicy,
266
+ auditContext: "msteams.graph.message",
246
267
  });
247
- if (msgRes.ok) {
248
- const msgData = (await msgRes.json()) as {
249
- body?: { content?: string; contentType?: string };
250
- attachments?: Array<{
251
- id?: string;
252
- contentUrl?: string;
253
- contentType?: string;
254
- name?: string;
255
- }>;
256
- };
268
+ try {
269
+ if (msgRes.ok) {
270
+ const msgData = (await msgRes.json()) as {
271
+ body?: { content?: string; contentType?: string };
272
+ attachments?: Array<{
273
+ id?: string;
274
+ contentUrl?: string;
275
+ contentType?: string;
276
+ name?: string;
277
+ }>;
278
+ };
257
279
 
258
- // Extract SharePoint file attachments (contentType: "reference")
259
- // Download any file type, not just images
260
- const spAttachments = (msgData.attachments ?? []).filter(
261
- (a) => a.contentType === "reference" && a.contentUrl && a.name,
262
- );
263
- for (const att of spAttachments) {
264
- const name = att.name ?? "file";
280
+ // Extract SharePoint file attachments (contentType: "reference")
281
+ // Download any file type, not just images
282
+ const spAttachments = (msgData.attachments ?? []).filter(
283
+ (a) => a.contentType === "reference" && a.contentUrl && a.name,
284
+ );
285
+ for (const att of spAttachments) {
286
+ const name = att.name ?? "file";
265
287
 
266
- try {
267
- // SharePoint URLs need to be accessed via Graph shares API
268
- const shareUrl = att.contentUrl!;
269
- if (!isUrlAllowed(shareUrl, allowHosts)) {
270
- continue;
271
- }
272
- const encodedUrl = Buffer.from(shareUrl).toString("base64url");
273
- const sharesUrl = `${GRAPH_ROOT}/shares/u!${encodedUrl}/driveItem/content`;
288
+ try {
289
+ // SharePoint URLs need to be accessed via Graph shares API
290
+ const shareUrl = att.contentUrl!;
291
+ if (!isUrlAllowed(shareUrl, allowHosts)) {
292
+ continue;
293
+ }
294
+ const encodedUrl = Buffer.from(shareUrl).toString("base64url");
295
+ const sharesUrl = `${GRAPH_ROOT}/shares/u!${encodedUrl}/driveItem/content`;
274
296
 
275
- const media = await downloadAndStoreMSTeamsRemoteMedia({
276
- url: sharesUrl,
277
- filePathHint: name,
278
- maxBytes: params.maxBytes,
279
- contentTypeHint: "application/octet-stream",
280
- preserveFilenames: params.preserveFilenames,
281
- fetchImpl: async (input, init) => {
282
- const requestUrl = resolveRequestUrl(input);
283
- const headers = new Headers(init?.headers);
284
- headers.set("Authorization", `Bearer ${accessToken}`);
285
- return await safeFetch({
286
- url: requestUrl,
287
- allowHosts,
288
- fetchFn,
289
- requestInit: {
290
- ...init,
291
- headers,
292
- },
293
- });
294
- },
295
- });
296
- sharePointMedia.push(media);
297
- downloadedReferenceUrls.add(shareUrl);
298
- } catch {
299
- // Ignore SharePoint download failures.
297
+ const media = await downloadAndStoreMSTeamsRemoteMedia({
298
+ url: sharesUrl,
299
+ filePathHint: name,
300
+ maxBytes: params.maxBytes,
301
+ contentTypeHint: "application/octet-stream",
302
+ preserveFilenames: params.preserveFilenames,
303
+ ssrfPolicy,
304
+ fetchImpl: async (input, init) => {
305
+ const requestUrl = resolveRequestUrl(input);
306
+ const headers = new Headers(init?.headers);
307
+ headers.set("Authorization", `Bearer ${accessToken}`);
308
+ return await fetchFn(requestUrl, { ...init, headers });
309
+ },
310
+ });
311
+ sharePointMedia.push(media);
312
+ downloadedReferenceUrls.add(shareUrl);
313
+ } catch {
314
+ // Ignore SharePoint download failures.
315
+ }
300
316
  }
301
317
  }
318
+ } finally {
319
+ await release();
302
320
  }
303
321
  } catch {
304
322
  // Ignore message fetch failures.
@@ -310,12 +328,14 @@ export async function downloadMSTeamsGraphMedia(params: {
310
328
  maxBytes: params.maxBytes,
311
329
  fetchFn: params.fetchFn,
312
330
  preserveFilenames: params.preserveFilenames,
331
+ ssrfPolicy,
313
332
  });
314
333
 
315
334
  const attachments = await fetchGraphCollection<GraphAttachment>({
316
335
  url: `${messageUrl}/attachments`,
317
336
  accessToken,
318
337
  fetchFn: params.fetchFn,
338
+ ssrfPolicy,
319
339
  });
320
340
 
321
341
  const normalizedAttachments = attachments.items.map(normalizeGraphAttachment);
@@ -1,3 +1,4 @@
1
+ import type { SsrFPolicy } from "openclaw/plugin-sdk";
1
2
  import { getMSTeamsRuntime } from "../runtime.js";
2
3
  import { inferPlaceholder } from "./shared.js";
3
4
  import type { MSTeamsInboundMedia } from "./types.js";
@@ -9,6 +10,7 @@ export async function downloadAndStoreMSTeamsRemoteMedia(params: {
9
10
  filePathHint: string;
10
11
  maxBytes: number;
11
12
  fetchImpl?: FetchLike;
13
+ ssrfPolicy?: SsrFPolicy;
12
14
  contentTypeHint?: string;
13
15
  placeholder?: string;
14
16
  preserveFilenames?: boolean;
@@ -18,6 +20,7 @@ export async function downloadAndStoreMSTeamsRemoteMedia(params: {
18
20
  fetchImpl: params.fetchImpl,
19
21
  filePathHint: params.filePathHint,
20
22
  maxBytes: params.maxBytes,
23
+ ssrfPolicy: params.ssrfPolicy,
21
24
  });
22
25
  const mime = await getMSTeamsRuntime().media.detectMime({
23
26
  buffer: fetched.buffer,