diff --git a/CHANGELOG.md b/CHANGELOG.md index a42bf1b7e96..d0e557cd2bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ Docs: https://docs.openclaw.ai ### Fixes - Slack/messages: serialize write-client requests and whole outbound sends per target so rapid multi-message Slack replies preserve send order. Fixes #69101. (#69105) Thanks @nightq and @ztexydt-cqh. +- Slack/messages: keep Slack bot tokens out of internal message-ordering and DM cache keys. - Slack/exec approvals: resolve native approval button clicks over the Gateway instead of delivering `/approve ...` as plain agent text, preserving retry buttons if Gateway resolution fails. Fixes #71023. (#71025) Thanks @marusan03. - Browser/tool: expose browser doctor diagnostics to agents and extend `openclaw doctor` browser readiness notes for managed Chromium launch prerequisites. (#62948, #62936) Thanks @seanc-dev. - Slack/files: return non-image `download-file` results as local file paths instead of image payloads, and include Slack file IDs in inbound file placeholders so agents can call `download-file`. Fixes #71212. Thanks @teamrazo. diff --git a/extensions/slack/src/client.test.ts b/extensions/slack/src/client.test.ts index e4ada517b43..ae5d8aa47cc 100644 --- a/extensions/slack/src/client.test.ts +++ b/extensions/slack/src/client.test.ts @@ -14,6 +14,7 @@ vi.mock("@slack/web-api", () => { let createSlackWebClient: typeof import("./client.js").createSlackWebClient; let createSlackWriteClient: typeof import("./client.js").createSlackWriteClient; +let createSlackTokenCacheKey: typeof import("./client.js").createSlackTokenCacheKey; let getSlackWriteClient: typeof import("./client.js").getSlackWriteClient; let clearSlackWriteClientCacheForTest: typeof import("./client.js").clearSlackWriteClientCacheForTest; let resolveSlackWebClientOptions: typeof import("./client.js").resolveSlackWebClientOptions; @@ -27,6 +28,7 @@ beforeAll(async () => { ({ createSlackWebClient, createSlackWriteClient, + createSlackTokenCacheKey, getSlackWriteClient, clearSlackWriteClientCacheForTest, resolveSlackWebClientOptions, @@ -121,6 +123,17 @@ describe("slack web client config", () => { expect(second).not.toBe(first); expect(WebClient).toHaveBeenCalledTimes(2); }); + + it("builds stable non-secret token cache keys", () => { + const token = "xoxb-sensitive-token"; + const first = createSlackTokenCacheKey(token); + const second = createSlackTokenCacheKey(token); + + expect(first).toBe(second); + expect(first).toMatch(/^sha256:/); + expect(first).not.toContain(token); + expect(createSlackTokenCacheKey("xoxb-other-token")).not.toBe(first); + }); }); describe("slack proxy agent", () => { diff --git a/extensions/slack/src/client.ts b/extensions/slack/src/client.ts index 006b9a3e65a..9b90ea26e15 100644 --- a/extensions/slack/src/client.ts +++ b/extensions/slack/src/client.ts @@ -1,3 +1,4 @@ +import { createHash } from "node:crypto"; import { type WebClientOptions, WebClient } from "@slack/web-api"; import { resolveSlackWebClientOptions, resolveSlackWriteClientOptions } from "./client-options.js"; @@ -19,21 +20,26 @@ export function createSlackWriteClient(token: string, options: WebClientOptions return new WebClient(token, resolveSlackWriteClientOptions(options)); } +export function createSlackTokenCacheKey(token: string): string { + return `sha256:${createHash("sha256").update(token).digest("base64url")}`; +} + export function getSlackWriteClient(token: string): WebClient { - const cached = slackWriteClientCache.get(token); + const tokenKey = createSlackTokenCacheKey(token); + const cached = slackWriteClientCache.get(tokenKey); if (cached) { - slackWriteClientCache.delete(token); - slackWriteClientCache.set(token, cached); + slackWriteClientCache.delete(tokenKey); + slackWriteClientCache.set(tokenKey, cached); return cached; } const client = createSlackWriteClient(token); if (slackWriteClientCache.size >= SLACK_WRITE_CLIENT_CACHE_MAX) { - const oldestToken = slackWriteClientCache.keys().next().value; - if (oldestToken) { - slackWriteClientCache.delete(oldestToken); + const oldestTokenKey = slackWriteClientCache.keys().next().value; + if (oldestTokenKey) { + slackWriteClientCache.delete(oldestTokenKey); } } - slackWriteClientCache.set(token, client); + slackWriteClientCache.set(tokenKey, client); return client; } diff --git a/extensions/slack/src/send.ts b/extensions/slack/src/send.ts index 34cfc44a8e4..5901dc367dc 100644 --- a/extensions/slack/src/send.ts +++ b/extensions/slack/src/send.ts @@ -19,7 +19,7 @@ import type { SlackTokenSource } from "./accounts.js"; import { resolveSlackAccount } from "./accounts.js"; import { buildSlackBlocksFallbackText } from "./blocks-fallback.js"; import { validateSlackBlocksArray } from "./blocks-input.js"; -import { getSlackWriteClient } from "./client.js"; +import { createSlackTokenCacheKey, getSlackWriteClient } from "./client.js"; import { markdownToSlackMrkdwnChunks } from "./format.js"; import { SLACK_TEXT_LIMIT } from "./limits.js"; import { loadOutboundMediaFromUrl } from "./runtime-api.js"; @@ -188,7 +188,9 @@ function createSlackSendQueueKey(params: { }): string { const isUserId = params.recipient.kind === "user" || /^U[A-Z0-9]+$/i.test(params.recipient.id); const recipientKey = `${isUserId ? "user" : params.recipient.kind}:${params.recipient.id}`; - return `${params.accountId}:${params.token}:${recipientKey}:${params.threadTs ?? ""}`; + return `${params.accountId}:${createSlackTokenCacheKey(params.token)}:${recipientKey}:${ + params.threadTs ?? "" + }`; } async function runQueuedSlackSend(key: string, task: () => Promise): Promise { @@ -215,7 +217,9 @@ function createSlackDmCacheKey(params: { token: string; recipientId: string; }): string { - return `${params.accountId ?? "default"}:${params.token}:${params.recipientId}`; + return `${params.accountId ?? "default"}:${createSlackTokenCacheKey(params.token)}:${ + params.recipientId + }`; } function setSlackDmChannelCache(key: string, channelId: string): void {