fix(slack): hash token cache keys

This commit is contained in:
Peter Steinberger
2026-04-25 01:17:49 +01:00
parent b7c8c53af2
commit d399ac74f7
4 changed files with 34 additions and 10 deletions

View File

@@ -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.

View File

@@ -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", () => {

View File

@@ -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;
}

View File

@@ -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<T>(key: string, task: () => Promise<T>): Promise<T> {
@@ -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 {