fix(routing): exclude peer-specific bindings from guild-wide matching (#15274)

* fix(routing): exclude peer-specific bindings from guild-wide matching (#14752)

* fix(routing): enforce binding scope AND semantics + regressions

* fix(routing): document strict binding-scope behavior (#15274) (thanks @lailoo)

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
大猫子
2026-02-14 10:05:09 +08:00
committed by GitHub
parent 1b95220a99
commit dbe026214f
6 changed files with 326 additions and 38 deletions

View File

@@ -152,25 +152,6 @@ function matchesPeer(
return kind === peer.kind && id === peer.id;
}
function matchesGuild(
match: { guildId?: string | undefined } | undefined,
guildId: string,
): boolean {
const id = normalizeId(match?.guildId);
if (!id) {
return false;
}
return id === guildId;
}
function matchesTeam(match: { teamId?: string | undefined } | undefined, teamId: string): boolean {
const id = normalizeId(match?.teamId);
if (!id) {
return false;
}
return id === teamId;
}
function matchesRoles(
match: { roles?: string[] | undefined } | undefined,
memberRoleIds: string[],
@@ -182,6 +163,91 @@ function matchesRoles(
return roles.some((role) => memberRoleIds.includes(role));
}
function hasGuildConstraint(match: { guildId?: string | undefined } | undefined): boolean {
return Boolean(normalizeId(match?.guildId));
}
function hasTeamConstraint(match: { teamId?: string | undefined } | undefined): boolean {
return Boolean(normalizeId(match?.teamId));
}
function hasRolesConstraint(match: { roles?: string[] | undefined } | undefined): boolean {
return Array.isArray(match?.roles) && match.roles.length > 0;
}
function matchesOptionalPeer(
match: { peer?: { kind?: string; id?: string } | undefined } | undefined,
peer: RoutePeer | null,
): boolean {
if (!match?.peer) {
return true;
}
if (!peer) {
return false;
}
return matchesPeer(match, peer);
}
function matchesOptionalGuild(
match: { guildId?: string | undefined } | undefined,
guildId: string,
): boolean {
const requiredGuildId = normalizeId(match?.guildId);
if (!requiredGuildId) {
return true;
}
if (!guildId) {
return false;
}
return requiredGuildId === guildId;
}
function matchesOptionalTeam(
match: { teamId?: string | undefined } | undefined,
teamId: string,
): boolean {
const requiredTeamId = normalizeId(match?.teamId);
if (!requiredTeamId) {
return true;
}
if (!teamId) {
return false;
}
return requiredTeamId === teamId;
}
function matchesOptionalRoles(
match: { roles?: string[] | undefined } | undefined,
memberRoleIds: string[],
): boolean {
if (!hasRolesConstraint(match)) {
return true;
}
return matchesRoles(match, memberRoleIds);
}
function matchesBindingScope(params: {
match:
| {
peer?: { kind?: string; id?: string } | undefined;
guildId?: string | undefined;
teamId?: string | undefined;
roles?: string[] | undefined;
}
| undefined;
peer: RoutePeer | null;
guildId: string;
teamId: string;
memberRoleIds: string[];
}): boolean {
return (
matchesOptionalPeer(params.match, params.peer) &&
matchesOptionalGuild(params.match, params.guildId) &&
matchesOptionalTeam(params.match, params.teamId) &&
matchesOptionalRoles(params.match, params.memberRoleIds)
);
}
export function resolveAgentRoute(input: ResolveAgentRouteInput): ResolvedAgentRoute {
const channel = normalizeToken(input.channel);
const accountId = normalizeAccountId(input.accountId);
@@ -228,7 +294,17 @@ export function resolveAgentRoute(input: ResolveAgentRouteInput): ResolvedAgentR
};
if (peer) {
const peerMatch = bindings.find((b) => matchesPeer(b.match, peer));
const peerMatch = bindings.find(
(b) =>
Boolean(b.match?.peer) &&
matchesBindingScope({
match: b.match,
peer,
guildId,
teamId,
memberRoleIds,
}),
);
if (peerMatch) {
return choose(peerMatch.agentId, "binding.peer");
}
@@ -239,7 +315,17 @@ export function resolveAgentRoute(input: ResolveAgentRouteInput): ResolvedAgentR
? { kind: input.parentPeer.kind, id: normalizeId(input.parentPeer.id) }
: null;
if (parentPeer && parentPeer.id) {
const parentPeerMatch = bindings.find((b) => matchesPeer(b.match, parentPeer));
const parentPeerMatch = bindings.find(
(b) =>
Boolean(b.match?.peer) &&
matchesBindingScope({
match: b.match,
peer: parentPeer,
guildId,
teamId,
memberRoleIds,
}),
);
if (parentPeerMatch) {
return choose(parentPeerMatch.agentId, "binding.peer.parent");
}
@@ -247,7 +333,16 @@ export function resolveAgentRoute(input: ResolveAgentRouteInput): ResolvedAgentR
if (guildId && memberRoleIds.length > 0) {
const guildRolesMatch = bindings.find(
(b) => matchesGuild(b.match, guildId) && matchesRoles(b.match, memberRoleIds),
(b) =>
hasGuildConstraint(b.match) &&
hasRolesConstraint(b.match) &&
matchesBindingScope({
match: b.match,
peer,
guildId,
teamId,
memberRoleIds,
}),
);
if (guildRolesMatch) {
return choose(guildRolesMatch.agentId, "binding.guild+roles");
@@ -257,8 +352,15 @@ export function resolveAgentRoute(input: ResolveAgentRouteInput): ResolvedAgentR
if (guildId) {
const guildMatch = bindings.find(
(b) =>
matchesGuild(b.match, guildId) &&
(!Array.isArray(b.match?.roles) || b.match.roles.length === 0),
hasGuildConstraint(b.match) &&
!hasRolesConstraint(b.match) &&
matchesBindingScope({
match: b.match,
peer,
guildId,
teamId,
memberRoleIds,
}),
);
if (guildMatch) {
return choose(guildMatch.agentId, "binding.guild");
@@ -266,7 +368,17 @@ export function resolveAgentRoute(input: ResolveAgentRouteInput): ResolvedAgentR
}
if (teamId) {
const teamMatch = bindings.find((b) => matchesTeam(b.match, teamId));
const teamMatch = bindings.find(
(b) =>
hasTeamConstraint(b.match) &&
matchesBindingScope({
match: b.match,
peer,
guildId,
teamId,
memberRoleIds,
}),
);
if (teamMatch) {
return choose(teamMatch.agentId, "binding.team");
}
@@ -274,7 +386,14 @@ export function resolveAgentRoute(input: ResolveAgentRouteInput): ResolvedAgentR
const accountMatch = bindings.find(
(b) =>
b.match?.accountId?.trim() !== "*" && !b.match?.peer && !b.match?.guildId && !b.match?.teamId,
b.match?.accountId?.trim() !== "*" &&
matchesBindingScope({
match: b.match,
peer,
guildId,
teamId,
memberRoleIds,
}),
);
if (accountMatch) {
return choose(accountMatch.agentId, "binding.account");
@@ -282,7 +401,14 @@ export function resolveAgentRoute(input: ResolveAgentRouteInput): ResolvedAgentR
const anyAccountMatch = bindings.find(
(b) =>
b.match?.accountId?.trim() === "*" && !b.match?.peer && !b.match?.guildId && !b.match?.teamId,
b.match?.accountId?.trim() === "*" &&
matchesBindingScope({
match: b.match,
peer,
guildId,
teamId,
memberRoleIds,
}),
);
if (anyAccountMatch) {
return choose(anyAccountMatch.agentId, "binding.channel");