From 283238fd77eece1c59e8d05a283e92b06efbfa32 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 30 May 2026 12:56:54 -0400 Subject: [PATCH] fix(matrix): bound allowlist store cache expiry --- .../matrix/src/matrix/monitor/handler.test.ts | 53 +++++++++++++++++++ .../matrix/src/matrix/monitor/handler.ts | 18 +++++-- 2 files changed, 66 insertions(+), 5 deletions(-) diff --git a/extensions/matrix/src/matrix/monitor/handler.test.ts b/extensions/matrix/src/matrix/monitor/handler.test.ts index 5235f2d0878..e79d3abc148 100644 --- a/extensions/matrix/src/matrix/monitor/handler.test.ts +++ b/extensions/matrix/src/matrix/monitor/handler.test.ts @@ -1,6 +1,7 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; +import { MAX_DATE_TIMESTAMP_MS } from "openclaw/plugin-sdk/number-runtime"; import { testing as sessionBindingTesting, registerSessionBindingAdapter, @@ -321,6 +322,58 @@ describe("matrix monitor handler pairing account scope", () => { } }); + it("does not reuse account-scoped allowFrom cache while the process clock is invalid", async () => { + const readAllowFromStore = vi.fn(async () => [] as string[]); + const nowSpy = vi.spyOn(Date, "now"); + const { handler } = createMatrixHandlerTestHarness({ + readAllowFromStore, + dmPolicy: "pairing", + buildPairingReply: () => "pairing", + }); + const makeEvent = (id: string): MatrixRawEvent => + createMatrixTextMessageEvent({ + eventId: id, + body: "@room hello", + mentions: { room: true }, + }); + + try { + nowSpy.mockReturnValue(Number.NaN); + await handler("!room:example.org", makeEvent("$event1")); + await handler("!room:example.org", makeEvent("$event2")); + + expect(readAllowFromStore).toHaveBeenCalledTimes(2); + } finally { + nowSpy.mockRestore(); + } + }); + + it("does not cache account-scoped allowFrom reads when cache expiry overflows", async () => { + const readAllowFromStore = vi.fn(async () => [] as string[]); + const nowSpy = vi.spyOn(Date, "now"); + const { handler } = createMatrixHandlerTestHarness({ + readAllowFromStore, + dmPolicy: "pairing", + buildPairingReply: () => "pairing", + }); + const makeEvent = (id: string): MatrixRawEvent => + createMatrixTextMessageEvent({ + eventId: id, + body: "@room hello", + mentions: { room: true }, + }); + + try { + nowSpy.mockReturnValue(MAX_DATE_TIMESTAMP_MS); + await handler("!room:example.org", makeEvent("$event1")); + await handler("!room:example.org", makeEvent("$event2")); + + expect(readAllowFromStore).toHaveBeenCalledTimes(2); + } finally { + nowSpy.mockRestore(); + } + }); + it("pins direct-message main route updates to the configured owner", async () => { const { handler, recordInboundSession } = createMatrixHandlerTestHarness({ cfg: { diff --git a/extensions/matrix/src/matrix/monitor/handler.ts b/extensions/matrix/src/matrix/monitor/handler.ts index 952aa9891aa..29a3b4113c0 100644 --- a/extensions/matrix/src/matrix/monitor/handler.ts +++ b/extensions/matrix/src/matrix/monitor/handler.ts @@ -27,6 +27,10 @@ import { resolveChannelContextVisibilityMode, } from "openclaw/plugin-sdk/context-visibility-runtime"; import { isDangerousNameMatchingEnabled } from "openclaw/plugin-sdk/dangerous-name-runtime"; +import { + isFutureDateTimestampMs, + resolveExpiresAtMsFromDurationMs, +} from "openclaw/plugin-sdk/number-runtime"; import { mergePairLoopGuardConfig } from "openclaw/plugin-sdk/pair-loop-guard-runtime"; import { buildInboundHistoryFromEntries } from "openclaw/plugin-sdk/reply-history"; import { @@ -510,9 +514,13 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam const readStoreAllowFrom = async (): Promise => { const now = Date.now(); - if (cachedStoreAllowFrom && now < cachedStoreAllowFrom.expiresAtMs) { + if ( + cachedStoreAllowFrom && + isFutureDateTimestampMs(cachedStoreAllowFrom.expiresAtMs, { nowMs: now }) + ) { return cachedStoreAllowFrom.value; } + cachedStoreAllowFrom = null; const value = await core.channel.pairing .readAllowFromStore({ channel: "matrix", @@ -520,10 +528,10 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam accountId, }) .catch(() => []); - cachedStoreAllowFrom = { - value, - expiresAtMs: now + ALLOW_FROM_STORE_CACHE_TTL_MS, - }; + const expiresAtMs = resolveExpiresAtMsFromDurationMs(ALLOW_FROM_STORE_CACHE_TTL_MS, { + nowMs: now, + }); + cachedStoreAllowFrom = expiresAtMs === undefined ? null : { value, expiresAtMs }; return value; };