Skip to content

Commit 7e5acc4

Browse files
committed
Auto-merge upstream openclaw/openclaw
2 parents b896ea0 + 935c555 commit 7e5acc4

12 files changed

Lines changed: 470 additions & 69 deletions

extensions/slack/src/action-runtime.test.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -729,7 +729,10 @@ describe("handleSlackAction", () => {
729729
expect(requireRecordArg(downloadSlackFile, "downloadSlackFile", 0, 1).maxBytes).toBe(
730730
20 * 1024 * 1024,
731731
);
732-
expect(requireDetails(result).ok).toBe(false);
732+
expect(requireDetails(result)).toMatchObject({
733+
ok: false,
734+
error: expect.stringMatching(/requested Slack channel or explicit thread/i),
735+
});
733736
});
734737

735738
it("fails closed for downloadFile when no channel target can be authorized", async () => {

extensions/slack/src/action-runtime.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -925,7 +925,8 @@ export async function handleSlackAction(
925925
if (!downloaded) {
926926
return jsonResult({
927927
ok: false,
928-
error: "File could not be downloaded (not found, too large, or inaccessible).",
928+
error:
929+
"File could not be downloaded. Confirm the fileId came from the requested Slack channel or explicit thread and that the file is accessible and within the size limit.",
929930
});
930931
}
931932
if (!isImageContentType(downloaded.contentType)) {

extensions/slack/src/actions.download-file.test.ts

Lines changed: 169 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import type { WebClient } from "@slack/web-api";
33
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
44
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
55

6-
const resolveSlackMedia = vi.fn();
6+
const resolveSlackMedia = vi.fn<typeof import("./monitor/media.js").resolveSlackMedia>();
77
const createSlackLookupClientMock = vi.hoisted(() => vi.fn());
88

99
vi.mock("./monitor/media.js", () => ({
@@ -35,6 +35,7 @@ function makeSlackFileInfo(overrides?: Record<string, unknown>) {
3535
name: "image.png",
3636
mimetype: "image/png",
3737
url_private_download: "https://files.slack.com/files-pri/T1-F123/image.png",
38+
channels: ["C123"],
3839
...overrides,
3940
};
4041
}
@@ -53,6 +54,14 @@ function expectNoMediaDownload(result: Awaited<ReturnType<typeof downloadSlackFi
5354
expect(resolveSlackMedia).not.toHaveBeenCalled();
5455
}
5556

57+
function requireRefreshedFileAdmission() {
58+
const admission = resolveSlackMedia.mock.calls[0]?.[0].isRefreshedFileAllowed;
59+
if (!admission) {
60+
throw new Error("Expected refreshed Slack file admission");
61+
}
62+
return admission;
63+
}
64+
5665
function expectResolveSlackMediaCalledWithDefaults(client: ReturnType<typeof createClient>) {
5766
expect(resolveSlackMedia).toHaveBeenCalledWith({
5867
files: [
@@ -65,6 +74,7 @@ function expectResolveSlackMediaCalledWithDefaults(client: ReturnType<typeof cre
6574
},
6675
],
6776
client,
77+
isRefreshedFileAllowed: expect.any(Function),
6878
token: "xoxb-test",
6979
maxBytes: 1024,
7080
});
@@ -100,6 +110,7 @@ describe("downloadSlackFile", () => {
100110
client,
101111
token: "xoxb-test",
102112
maxBytes: 1024,
113+
channelId: "C123",
103114
});
104115

105116
expect(result).toBeNull();
@@ -114,6 +125,7 @@ describe("downloadSlackFile", () => {
114125
client,
115126
token: "xoxb-test",
116127
maxBytes: 1024,
128+
channelId: "C123",
117129
});
118130

119131
expect(client.files.info).toHaveBeenCalledWith({ file: "F123" });
@@ -134,6 +146,7 @@ describe("downloadSlackFile", () => {
134146
client,
135147
token: "xoxb-test",
136148
maxBytes: 1024,
149+
channelId: "C123",
137150
});
138151

139152
expect(resolveSlackMedia).toHaveBeenCalledWith(expect.objectContaining({ client }));
@@ -160,6 +173,7 @@ describe("downloadSlackFile", () => {
160173
client,
161174
token: "xoxb-test",
162175
maxBytes: 1024,
176+
channelId: "C123",
163177
});
164178

165179
expect(resolveSlackMedia).toHaveBeenCalledWith({
@@ -173,6 +187,7 @@ describe("downloadSlackFile", () => {
173187
},
174188
],
175189
client,
190+
isRefreshedFileAllowed: expect.any(Function),
176191
token: "xoxb-test",
177192
maxBytes: 1024,
178193
});
@@ -201,6 +216,55 @@ describe("downloadSlackFile", () => {
201216
expectNoMediaDownload(result);
202217
});
203218

219+
it.each([
220+
{ name: "public channel metadata", file: { channels: ["C123"] } },
221+
{ name: "private channel metadata", file: { groups: ["C123"] } },
222+
{ name: "DM metadata", file: { ims: ["C123"] } },
223+
{
224+
name: "share metadata",
225+
file: {
226+
channels: undefined,
227+
shares: { private: { C123: [{ ts: "111.111" }] } },
228+
},
229+
},
230+
])("downloads when $name proves the requested channel", async ({ file }) => {
231+
const client = createClient();
232+
client.files.info.mockResolvedValueOnce({
233+
file: makeSlackFileInfo(file),
234+
});
235+
resolveSlackMedia.mockResolvedValueOnce([makeResolvedSlackMedia()]);
236+
237+
const result = await downloadSlackFile("F123", {
238+
client,
239+
token: "xoxb-test",
240+
maxBytes: 1024,
241+
channelId: "C123",
242+
});
243+
244+
expect(result).toEqual(makeResolvedSlackMedia());
245+
});
246+
247+
it("accepts positive channel proof even when Slack reports additional shares", async () => {
248+
const client = createClient();
249+
client.files.info.mockResolvedValueOnce({
250+
file: makeSlackFileInfo({
251+
channels: ["C123"],
252+
has_more_shares: true,
253+
skipped_shares: true,
254+
}),
255+
});
256+
resolveSlackMedia.mockResolvedValueOnce([makeResolvedSlackMedia()]);
257+
258+
const result = await downloadSlackFile("F123", {
259+
client,
260+
token: "xoxb-test",
261+
maxBytes: 1024,
262+
channelId: "C123",
263+
});
264+
265+
expect(result).toEqual(makeResolvedSlackMedia());
266+
});
267+
204268
it("returns null when thread scope definitely mismatches file share thread", async () => {
205269
const client = createClient();
206270
client.files.info.mockResolvedValueOnce({
@@ -224,9 +288,11 @@ describe("downloadSlackFile", () => {
224288
expectNoMediaDownload(result);
225289
});
226290

227-
it("keeps legacy behavior when file metadata does not expose channel/thread shares", async () => {
291+
it("returns null when file metadata proves the channel but not the requested thread", async () => {
228292
const client = createClient();
229-
mockSuccessfulMediaDownload(client);
293+
client.files.info.mockResolvedValueOnce({
294+
file: makeSlackFileInfo({ channels: ["C123"] }),
295+
});
230296

231297
const result = await downloadSlackFile("F123", {
232298
client,
@@ -236,9 +302,105 @@ describe("downloadSlackFile", () => {
236302
threadId: "222.222",
237303
});
238304

305+
expectNoMediaDownload(result);
306+
});
307+
308+
it.each([
309+
{ name: "share message timestamp", share: { ts: "111.111" } },
310+
{ name: "thread timestamp", share: { ts: "222.222", thread_ts: "111.111" } },
311+
])("downloads when $name proves the requested thread", async ({ share }) => {
312+
const client = createClient();
313+
client.files.info.mockResolvedValueOnce({
314+
file: makeSlackFileInfo({
315+
shares: { private: { C123: [share] } },
316+
}),
317+
});
318+
resolveSlackMedia.mockResolvedValueOnce([makeResolvedSlackMedia()]);
319+
320+
const result = await downloadSlackFile("F123", {
321+
client,
322+
token: "xoxb-test",
323+
maxBytes: 1024,
324+
channelId: "C123",
325+
threadId: "111.111",
326+
});
327+
239328
expect(result).toEqual(makeResolvedSlackMedia());
240-
expect(resolveSlackMedia).toHaveBeenCalledTimes(1);
241-
expectResolveSlackMediaCalledWithDefaults(client);
329+
});
330+
331+
it("reapplies the requested channel and thread scope to refreshed metadata", async () => {
332+
const client = createClient();
333+
client.files.info.mockResolvedValueOnce({
334+
file: makeSlackFileInfo({
335+
shares: { private: { C123: [{ ts: "111.111" }] } },
336+
}),
337+
});
338+
resolveSlackMedia.mockResolvedValueOnce([makeResolvedSlackMedia()]);
339+
340+
await downloadSlackFile("F123", {
341+
client,
342+
token: "xoxb-test",
343+
maxBytes: 1024,
344+
channelId: "C123",
345+
threadId: "111.111",
346+
});
347+
348+
const isAllowed = requireRefreshedFileAdmission();
349+
expect(
350+
isAllowed(makeSlackFileInfo({ shares: { private: { C123: [{ ts: "111.111" }] } } })),
351+
).toBe(true);
352+
expect(
353+
isAllowed(makeSlackFileInfo({ shares: { private: { C999: [{ ts: "111.111" }] } } })),
354+
).toBe(false);
355+
expect(
356+
isAllowed(makeSlackFileInfo({ shares: { private: { C123: [{ ts: "222.222" }] } } })),
357+
).toBe(false);
358+
});
359+
360+
it.each([
361+
{ name: "absent channel/share evidence", file: { channels: undefined } },
362+
{
363+
name: "malformed shares container",
364+
file: { channels: undefined, shares: "invalid" },
365+
},
366+
{
367+
name: "requested channel with a non-array share value",
368+
file: { channels: undefined, shares: { private: { C123: {} } } },
369+
},
370+
{
371+
name: "requested channel with an empty share array",
372+
file: { channels: undefined, shares: { private: { C123: [] } } },
373+
},
374+
{
375+
name: "requested channel with a share entry lacking timestamps",
376+
file: { channels: undefined, shares: { private: { C123: [{}] } } },
377+
},
378+
])("returns null for $name", async ({ file }) => {
379+
const client = createClient();
380+
client.files.info.mockResolvedValueOnce({ file: makeSlackFileInfo(file) });
381+
382+
const result = await downloadSlackFile("F123", {
383+
client,
384+
token: "xoxb-test",
385+
maxBytes: 1024,
386+
channelId: "C123",
387+
});
388+
389+
expectNoMediaDownload(result);
390+
});
391+
392+
it("returns null when the requested channel is empty after normalization", async () => {
393+
const client = createClient();
394+
mockSuccessfulMediaDownload(client);
395+
396+
const result = await downloadSlackFile("F123", {
397+
client,
398+
token: "xoxb-test",
399+
maxBytes: 1024,
400+
channelId: " ",
401+
});
402+
403+
expectNoMediaDownload(result);
242404
});
243405

244406
it("resolves the bot token from cfg when no explicit token or client is provided", async () => {
@@ -265,6 +427,7 @@ describe("downloadSlackFile", () => {
265427
cfg,
266428
accountId: "default",
267429
maxBytes: 1024,
430+
channelId: "C123",
268431
});
269432

270433
expect(createSlackLookupClientMock).toHaveBeenCalledWith("xoxb-from-cfg", {
@@ -281,6 +444,7 @@ describe("downloadSlackFile", () => {
281444
},
282445
],
283446
client,
447+
isRefreshedFileAllowed: expect.any(Function),
284448
token: "xoxb-from-cfg",
285449
maxBytes: 1024,
286450
});

0 commit comments

Comments
 (0)