fix(session): tighten direct-session webchat routing matching (#37867)

* fix(session): require strict direct key routing shapes

* test(session): cover direct route poisoning cases
This commit is contained in:
Vincent Koc
2026-03-06 08:53:16 -05:00
committed by GitHub
parent fee91fefce
commit 9fed9f1302
2 changed files with 94 additions and 2 deletions

View File

@@ -0,0 +1,56 @@
import { describe, expect, it } from "vitest";
import { resolveLastChannelRaw, resolveLastToRaw } from "./session-delivery.js";
describe("session delivery direct-session routing overrides", () => {
it.each([
"agent:main:direct:user-1",
"agent:main:telegram:direct:123456",
"agent:main:telegram:account-a:direct:123456",
"agent:main:telegram:dm:123456",
"agent:main:telegram:direct:123456:thread:99",
"agent:main:telegram:account-a:direct:123456:topic:ops",
])("lets webchat override persisted routes for strict direct key %s", (sessionKey) => {
expect(
resolveLastChannelRaw({
originatingChannelRaw: "webchat",
persistedLastChannel: "telegram",
sessionKey,
}),
).toBe("webchat");
expect(
resolveLastToRaw({
originatingChannelRaw: "webchat",
originatingToRaw: "session:dashboard",
persistedLastChannel: "telegram",
persistedLastTo: "123456",
sessionKey,
}),
).toBe("session:dashboard");
});
it.each([
"agent:main:main:direct",
"agent:main:cron:job-1:dm",
"agent:main:subagent:worker:direct:user-1",
"agent:main:telegram:channel:direct",
"agent:main:telegram:account-a:direct",
"agent:main:telegram:direct:123456:cron:job-1",
])("keeps persisted external routes for malformed direct-like key %s", (sessionKey) => {
expect(
resolveLastChannelRaw({
originatingChannelRaw: "webchat",
persistedLastChannel: "telegram",
sessionKey,
}),
).toBe("telegram");
expect(
resolveLastToRaw({
originatingChannelRaw: "webchat",
originatingToRaw: "session:dashboard",
persistedLastChannel: "telegram",
persistedLastTo: "group:12345",
sessionKey,
}),
).toBe("group:12345");
});
});

View File

@@ -1,6 +1,6 @@
import type { SessionEntry } from "../../config/sessions.js";
import { buildAgentMainSessionKey } from "../../routing/session-key.js";
import { deriveSessionChatType, parseAgentSessionKey } from "../../sessions/session-key-utils.js";
import { parseAgentSessionKey } from "../../sessions/session-key-utils.js";
import {
deliveryContextFromSession,
deliveryContextKey,
@@ -38,8 +38,44 @@ function isMainSessionKey(sessionKey?: string): boolean {
return parsed.rest.trim().toLowerCase() === "main";
}
const DIRECT_SESSION_MARKERS = new Set(["direct", "dm"]);
const THREAD_SESSION_MARKERS = new Set(["thread", "topic"]);
function hasStrictDirectSessionTail(parts: string[], markerIndex: number): boolean {
const peerId = parts[markerIndex + 1]?.trim();
if (!peerId) {
return false;
}
const tail = parts.slice(markerIndex + 2);
if (tail.length === 0) {
return true;
}
return tail.length === 2 && THREAD_SESSION_MARKERS.has(tail[0] ?? "") && Boolean(tail[1]?.trim());
}
function isDirectSessionKey(sessionKey?: string): boolean {
return deriveSessionChatType(sessionKey) === "direct";
const raw = (sessionKey ?? "").trim().toLowerCase();
if (!raw) {
return false;
}
const scoped = parseAgentSessionKey(raw)?.rest ?? raw;
const parts = scoped.split(":").filter(Boolean);
if (parts.length < 2) {
return false;
}
if (DIRECT_SESSION_MARKERS.has(parts[0] ?? "")) {
return hasStrictDirectSessionTail(parts, 0);
}
const channel = normalizeMessageChannel(parts[0]);
if (!channel || !isDeliverableMessageChannel(channel)) {
return false;
}
if (DIRECT_SESSION_MARKERS.has(parts[1] ?? "")) {
return hasStrictDirectSessionTail(parts, 1);
}
return Boolean(parts[1]?.trim()) && DIRECT_SESSION_MARKERS.has(parts[2] ?? "")
? hasStrictDirectSessionTail(parts, 2)
: false;
}
function isExternalRoutingChannel(channel?: string): channel is string {