From f98c2de5a3ac53e8fc1b635128a37a33d3398114 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Guti=C3=A9rrez?= <35082514+alezmad@users.noreply.github.com> Date: Sat, 2 May 2026 02:01:35 +0100 Subject: [PATCH] fix(broker): topic-tagged sends bypass direct-target pre-flight MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit handleSend's pre-flight check rejected # sends because the target wasn't matched by @group / * / pubkey, so it fell into the "direct" branch and looked for a peer with that pubkey. Topic targets need their own class — delivery happens via topic_member, not by matching connected peers. Co-Authored-By: Claude Opus 4.7 (1M context) --- apps/broker/src/index.ts | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/apps/broker/src/index.ts b/apps/broker/src/index.ts index b528ce2..dec7006 100644 --- a/apps/broker/src/index.ts +++ b/apps/broker/src/index.ts @@ -1855,16 +1855,22 @@ async function handleSend( return; } - // Pre-flight: for direct sends (not @group, not *), verify at least one - // matching connected peer exists BEFORE queueing. Prevents silent drops - // when a user sends to a typo, their own pubkey with no other session, - // or a peer who has disconnected. The CLI's resolveClient already guards - // name-based targets; this catches raw-pubkey and CLI-bypassing clients. + // Pre-flight: for direct sends (not @group, not #topic, not *), verify + // at least one matching connected peer exists BEFORE queueing. Prevents + // silent drops when a user sends to a typo, their own pubkey with no + // other session, or a peer who has disconnected. The CLI's + // resolveClient already guards name-based targets; this catches + // raw-pubkey and CLI-bypassing clients. const isGroupTargetEarly = msg.targetSpec.startsWith("@"); + const isTopicTargetEarly = msg.targetSpec.startsWith("#"); const isBroadcastEarly = msg.targetSpec === "*" || (isGroupTargetEarly && msg.targetSpec === "@all"); - const isDirectEarly = !isGroupTargetEarly && !isBroadcastEarly && msg.targetSpec !== "*"; + const isDirectEarly = + !isGroupTargetEarly && + !isTopicTargetEarly && + !isBroadcastEarly && + msg.targetSpec !== "*"; if (isDirectEarly) { // Identify candidate recipient connections — anyone in the mesh whose // member or session pubkey matches the target. Then check grants to