diff --git a/extensions/mattermost/src/mattermost/slash-http.test.ts b/extensions/mattermost/src/mattermost/slash-http.test.ts index 5b45325572b..a1a79c8bc61 100644 --- a/extensions/mattermost/src/mattermost/slash-http.test.ts +++ b/extensions/mattermost/src/mattermost/slash-http.test.ts @@ -713,8 +713,9 @@ describe("slash-http", () => { it("logs when command lookup by id returns a deleted command before fallback", async () => { const registeredCommand = createRegisteredCommand(); + const commandId = `${"i".repeat(199)}😀tail`; const command = { - id: "cmd-1\r\nspoofed", + id: commandId, token: "valid-token", team_id: "t1", trigger: "oc_status", @@ -748,9 +749,9 @@ describe("slash-http", () => { expect(log).toHaveBeenCalledTimes(1); const message = firstLogMessage(log); - expect(message).not.toMatch(/[\r\n\t]/u); - expect(message).toContain("deleted command cmd-1 spoofed"); - expect(message).toContain("using team list fallback"); + expect(message).toBe( + `mattermost: slash command lookup by id returned deleted command ${"i".repeat(199)} for /oc_status; using team list fallback`, + ); }); it("rejects current commands with a mismatched method or callback URL", async () => { @@ -923,4 +924,35 @@ describe("slash-http", () => { expect(message).not.toContain("secret-query"); expect(message).not.toContain("user:pass"); }); + + it("keeps upstream lookup error previews UTF-16 safe", async () => { + const registeredCommand = createRegisteredCommand(); + const client = createCommandLookupClient({ + commandLookupError: new Error("primary failure"), + listLookupError: new Error(`${"e".repeat(299)}😀tail`), + }); + const log = vi.fn(); + + await expect( + validateMattermostSlashCommandToken({ + accountId: "default", + client, + registeredCommand, + payload: { + token: "valid-token", + team_id: "t1", + channel_id: "c1", + user_id: "u1", + command: "/oc_status", + text: "", + }, + log, + }), + ).resolves.toBe(false); + + expect(log).toHaveBeenCalledTimes(1); + expect(firstLogMessage(log)).toBe( + `mattermost: slash command registration check failed for /oc_status: ${"e".repeat(299)}; command lookup: primary failure`, + ); + }); }); diff --git a/extensions/mattermost/src/mattermost/slash-http.ts b/extensions/mattermost/src/mattermost/slash-http.ts index 4a41ddaefee..9cc7e5f2821 100644 --- a/extensions/mattermost/src/mattermost/slash-http.ts +++ b/extensions/mattermost/src/mattermost/slash-http.ts @@ -12,6 +12,7 @@ import { } from "openclaw/plugin-sdk/number-runtime"; import { safeEqualSecret } from "openclaw/plugin-sdk/security-runtime"; import { isPrivateNetworkOptInEnabled } from "openclaw/plugin-sdk/ssrf-runtime"; +import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime"; import type { ResolvedMattermostAccount } from "../mattermost/accounts.js"; import { getMattermostRuntime } from "../runtime.js"; import { @@ -142,7 +143,7 @@ function isDeletedMattermostCommand(command: { delete_at?: number }): boolean { function sanitizeCommandLookupError(error: unknown): string { const raw = error instanceof Error ? error.message : String(error); - return raw + const sanitized = raw .replace(/[\r\n\t]/gu, " ") .replace(/https?:\/\/[^\s)\]}]+/giu, (urlText) => { try { @@ -165,12 +166,12 @@ function sanitizeCommandLookupError(error: unknown): string { .replace( /\b(token|authorization|access_token|refresh_token|client_secret|botToken)\b(\s*["']?\s*(?:=|:)\s*["']?)[^"',\s;}]+/giu, "$1$2[redacted]", - ) - .slice(0, 300); + ); + return truncateUtf16Safe(sanitized, 300); } function sanitizeMattermostLogValue(value: string): string { - return value.replace(/[\r\n\t]/gu, " ").slice(0, 200); + return truncateUtf16Safe(value.replace(/[\r\n\t]/gu, " "), 200); } async function withCommandLookupTimeout(task: (signal: AbortSignal) => Promise): Promise {