refactor: share teams allowlist matching helpers

Co-authored-by: thewilloftheshadow <thewilloftheshadow@users.noreply.github.com>
This commit is contained in:
Peter Steinberger
2026-01-18 01:37:18 +00:00
parent 0674f1fa3c
commit c1da78a271
5 changed files with 174 additions and 45 deletions

View File

@@ -2,8 +2,10 @@ import { describe, expect, it } from "vitest";
import {
buildChannelKeyCandidates,
normalizeChannelSlug,
resolveChannelEntryMatch,
resolveChannelEntryMatchWithFallback,
resolveNestedAllowlistDecision,
} from "./channel-config.js";
describe("buildChannelKeyCandidates", () => {
@@ -12,6 +14,14 @@ describe("buildChannelKeyCandidates", () => {
});
});
describe("normalizeChannelSlug", () => {
it("normalizes names into slugs", () => {
expect(normalizeChannelSlug("My Team")).toBe("my-team");
expect(normalizeChannelSlug("#General Chat")).toBe("general-chat");
expect(normalizeChannelSlug(" Dev__Chat ")).toBe("dev-chat");
});
});
describe("resolveChannelEntryMatch", () => {
it("returns matched entry and wildcard metadata", () => {
const entries = { a: { allow: true }, "*": { allow: false } };
@@ -66,4 +76,59 @@ describe("resolveChannelEntryMatchWithFallback", () => {
expect(match.matchSource).toBe("wildcard");
expect(match.matchKey).toBe("*");
});
it("matches normalized keys when normalizeKey is provided", () => {
const entries = { "My Team": { allow: true } };
const match = resolveChannelEntryMatchWithFallback({
entries,
keys: ["my-team"],
normalizeKey: normalizeChannelSlug,
});
expect(match.entry).toBe(entries["My Team"]);
expect(match.matchSource).toBe("direct");
expect(match.matchKey).toBe("My Team");
});
});
describe("resolveNestedAllowlistDecision", () => {
it("allows when outer allowlist is disabled", () => {
expect(
resolveNestedAllowlistDecision({
outerConfigured: false,
outerMatched: false,
innerConfigured: false,
innerMatched: false,
}),
).toBe(true);
});
it("blocks when outer allowlist is configured but missing match", () => {
expect(
resolveNestedAllowlistDecision({
outerConfigured: true,
outerMatched: false,
innerConfigured: false,
innerMatched: false,
}),
).toBe(false);
});
it("requires inner match when inner allowlist is configured", () => {
expect(
resolveNestedAllowlistDecision({
outerConfigured: true,
outerMatched: true,
innerConfigured: true,
innerMatched: false,
}),
).toBe(false);
expect(
resolveNestedAllowlistDecision({
outerConfigured: true,
outerMatched: true,
innerConfigured: true,
innerMatched: true,
}),
).toBe(true);
});
});

View File

@@ -1,8 +1,5 @@
export type ChannelMatchSource = "direct" | "parent" | "wildcard";
export function buildChannelKeyCandidates(
...keys: Array<string | undefined | null>
): string[] {
export type ChannelEntryMatch<T> = {
entry?: T;
key?: string;
@@ -14,6 +11,15 @@ export type ChannelEntryMatch<T> = {
matchSource?: ChannelMatchSource;
};
export function normalizeChannelSlug(value: string): string {
return value
.trim()
.toLowerCase()
.replace(/^#/, "")
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-+|-+$/g, "");
}
export function buildChannelKeyCandidates(
...keys: Array<string | undefined | null>
): string[] {
@@ -54,6 +60,7 @@ export function resolveChannelEntryMatchWithFallback<T>(params: {
keys: string[];
parentKeys?: string[];
wildcardKey?: string;
normalizeKey?: (value: string) => string;
}): ChannelEntryMatch<T> {
const direct = resolveChannelEntryMatch({
entries: params.entries,
@@ -65,6 +72,25 @@ export function resolveChannelEntryMatchWithFallback<T>(params: {
return { ...direct, matchKey: direct.key, matchSource: "direct" };
}
const normalizeKey = params.normalizeKey;
if (normalizeKey) {
const normalizedKeys = params.keys.map((key) => normalizeKey(key)).filter(Boolean);
if (normalizedKeys.length > 0) {
for (const [entryKey, entry] of Object.entries(params.entries ?? {})) {
const normalizedEntry = normalizeKey(entryKey);
if (normalizedEntry && normalizedKeys.includes(normalizedEntry)) {
return {
...direct,
entry,
key: entryKey,
matchKey: entryKey,
matchSource: "direct",
};
}
}
}
}
const parentKeys = params.parentKeys ?? [];
if (parentKeys.length > 0) {
const parent = resolveChannelEntryMatch({ entries: params.entries, keys: parentKeys });
@@ -79,6 +105,25 @@ export function resolveChannelEntryMatchWithFallback<T>(params: {
matchSource: "parent",
};
}
if (normalizeKey) {
const normalizedParentKeys = parentKeys.map((key) => normalizeKey(key)).filter(Boolean);
if (normalizedParentKeys.length > 0) {
for (const [entryKey, entry] of Object.entries(params.entries ?? {})) {
const normalizedEntry = normalizeKey(entryKey);
if (normalizedEntry && normalizedParentKeys.includes(normalizedEntry)) {
return {
...direct,
entry,
key: entryKey,
parentEntry: entry,
parentKey: entryKey,
matchKey: entryKey,
matchSource: "parent",
};
}
}
}
}
}
if (direct.wildcardEntry && direct.wildcardKey) {
@@ -93,3 +138,15 @@ export function resolveChannelEntryMatchWithFallback<T>(params: {
return direct;
}
export function resolveNestedAllowlistDecision(params: {
outerConfigured: boolean;
outerMatched: boolean;
innerConfigured: boolean;
innerMatched: boolean;
}): boolean {
if (!params.outerConfigured) return true;
if (!params.outerMatched) return false;
if (!params.innerConfigured) return true;
return params.innerMatched;
}

View File

@@ -1,6 +1,8 @@
export type { ChannelEntryMatch, ChannelMatchSource } from "../channel-config.js";
export {
buildChannelKeyCandidates,
normalizeChannelSlug,
resolveChannelEntryMatch,
resolveChannelEntryMatchWithFallback,
resolveNestedAllowlistDecision,
} from "../channel-config.js";

View File

@@ -86,8 +86,10 @@ export {
} from "./directory-config.js";
export {
buildChannelKeyCandidates,
normalizeChannelSlug,
resolveChannelEntryMatch,
resolveChannelEntryMatchWithFallback,
resolveNestedAllowlistDecision,
type ChannelEntryMatch,
type ChannelMatchSource,
} from "./channel-config.js";