From 7e3ebb8e1063fec080e019228b43028cabf2f4f9 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 30 May 2026 11:17:13 -0400 Subject: [PATCH] fix(slack): bound external menu cache clocks --- .../monitor/external-arg-menu-store.test.ts | 44 +++++++++++++++++++ .../src/monitor/external-arg-menu-store.ts | 26 ++++++++--- 2 files changed, 64 insertions(+), 6 deletions(-) create mode 100644 extensions/slack/src/monitor/external-arg-menu-store.test.ts diff --git a/extensions/slack/src/monitor/external-arg-menu-store.test.ts b/extensions/slack/src/monitor/external-arg-menu-store.test.ts new file mode 100644 index 00000000000..4cc4678679d --- /dev/null +++ b/extensions/slack/src/monitor/external-arg-menu-store.test.ts @@ -0,0 +1,44 @@ +import { describe, expect, it } from "vitest"; +import { + createSlackExternalArgMenuStore, + SLACK_EXTERNAL_ARG_MENU_PREFIX, +} from "./external-arg-menu-store.js"; + +describe("createSlackExternalArgMenuStore", () => { + const choices = [{ label: "Daily", value: "day" }]; + + it("returns entries before their expiry", () => { + const store = createSlackExternalArgMenuStore(); + const token = store.create({ choices, userId: "U1" }, 1_700_000_000_000); + + expect(store.get(token, 1_700_000_001_000)).toEqual({ + choices, + userId: "U1", + expiresAt: 1_700_000_600_000, + }); + }); + + it("drops entries when the current clock is not a valid date timestamp", () => { + const store = createSlackExternalArgMenuStore(); + const token = store.create({ choices, userId: "U1" }, 1_700_000_000_000); + + expect(store.get(token, Number.NaN)).toBeUndefined(); + expect(store.get(token, 1_700_000_001_000)).toBeUndefined(); + }); + + it("does not retain entries when expiry would exceed the valid date range", () => { + const store = createSlackExternalArgMenuStore(); + const token = store.create({ choices, userId: "U1" }, 8_640_000_000_000_000); + + expect(store.get(token, 1_700_000_001_000)).toBeUndefined(); + }); + + it("reads only prefixed valid menu tokens", () => { + const store = createSlackExternalArgMenuStore(); + const token = store.create({ choices, userId: "U1" }, 1_700_000_000_000); + + expect(store.readToken(`${SLACK_EXTERNAL_ARG_MENU_PREFIX}${token}`)).toBe(token); + expect(store.readToken(token)).toBeUndefined(); + expect(store.readToken(`${SLACK_EXTERNAL_ARG_MENU_PREFIX}not a token`)).toBeUndefined(); + }); +}); diff --git a/extensions/slack/src/monitor/external-arg-menu-store.ts b/extensions/slack/src/monitor/external-arg-menu-store.ts index 359a82425d3..8427d0fb94a 100644 --- a/extensions/slack/src/monitor/external-arg-menu-store.ts +++ b/extensions/slack/src/monitor/external-arg-menu-store.ts @@ -1,3 +1,7 @@ +import { + asDateTimestampMs, + resolveExpiresAtMsFromDurationMs, +} from "openclaw/plugin-sdk/number-runtime"; import { generateSecureToken } from "openclaw/plugin-sdk/secure-random-runtime"; const SLACK_EXTERNAL_ARG_MENU_TOKEN_BYTES = 18; @@ -20,10 +24,15 @@ type SlackExternalArgMenuEntry = { function pruneSlackExternalArgMenuStore( store: Map, - now: number, + rawNow: number, ): void { + const now = asDateTimestampMs(rawNow); + if (now === undefined) { + store.clear(); + return; + } for (const [token, entry] of store.entries()) { - if (entry.expiresAt <= now) { + if (asDateTimestampMs(entry.expiresAt) === undefined || entry.expiresAt <= now) { store.delete(token); } } @@ -47,11 +56,16 @@ export function createSlackExternalArgMenuStore() { ): string { pruneSlackExternalArgMenuStore(store, now); const token = createSlackExternalArgMenuToken(store); - store.set(token, { - choices: params.choices, - userId: params.userId, - expiresAt: now + SLACK_EXTERNAL_ARG_MENU_TTL_MS, + const expiresAt = resolveExpiresAtMsFromDurationMs(SLACK_EXTERNAL_ARG_MENU_TTL_MS, { + nowMs: now, }); + if (expiresAt !== undefined) { + store.set(token, { + choices: params.choices, + userId: params.userId, + expiresAt, + }); + } return token; }, readToken(raw: unknown): string | undefined {