From fe662c54a6c5a0c2ff2f39d8a0e2c25f7a15ab7e Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Sun, 15 Mar 2026 15:54:02 -0700 Subject: [PATCH] Plugins: expire stale binding approvals --- src/plugins/conversation-binding.ts | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/plugins/conversation-binding.ts b/src/plugins/conversation-binding.ts index 3de655abbe1..750050be7de 100644 --- a/src/plugins/conversation-binding.ts +++ b/src/plugins/conversation-binding.ts @@ -100,6 +100,8 @@ type PluginBindingResolveResult = }; const pendingRequests = new Map(); +const PENDING_REQUEST_TTL_MS = 30 * 60_000; +const MAX_PENDING_REQUESTS = 512; type PluginBindingGlobalState = { fallbackNoticeBindingIds: Set; @@ -119,6 +121,21 @@ function getPluginBindingGlobalState(): PluginBindingGlobalState { }); } +function prunePendingRequests(now = Date.now()): void { + for (const [id, request] of pendingRequests.entries()) { + if (now - request.requestedAt >= PENDING_REQUEST_TTL_MS) { + pendingRequests.delete(id); + } + } + while (pendingRequests.size > MAX_PENDING_REQUESTS) { + const oldest = pendingRequests.entries().next().value; + if (!oldest) { + break; + } + pendingRequests.delete(oldest[0]); + } +} + class PluginBindingApprovalButton extends Button { customId: string; label: string; @@ -622,6 +639,16 @@ export async function requestPluginConversationBinding(params: { }): Promise { const conversation = normalizeConversation(params.conversation); const ref = toConversationRef(conversation); + const capabilities = getSessionBindingService().getCapabilities({ + channel: ref.channel, + accountId: ref.accountId, + }); + if (!capabilities.bindSupported) { + return { + status: "error", + message: "This channel does not support plugin conversation binding.", + }; + } const existing = getSessionBindingService().resolveByConversation(ref); const existingPluginBinding = toPluginConversationBinding(existing); const existingLegacyPluginBinding = isLegacyPluginBindingRecord({ @@ -687,6 +714,7 @@ export async function requestPluginConversationBinding(params: { return { status: "bound", binding: bound }; } + prunePendingRequests(); const request: PendingPluginBindingRequest = { id: createApprovalRequestId(), pluginId: params.pluginId, @@ -752,6 +780,7 @@ export async function resolvePluginConversationBindingApproval(params: { decision: PluginBindingApprovalDecision; senderId?: string; }): Promise { + prunePendingRequests(); const request = pendingRequests.get(params.approvalId); if (!request) { return { status: "expired" };