fix(discord): canonicalize resolved allowlists to ids

This commit is contained in:
Peter Steinberger
2026-02-21 19:53:00 +01:00
parent 3ed71d6f76
commit 747bb581b3
5 changed files with 168 additions and 30 deletions

View File

@@ -2,6 +2,8 @@ import { describe, expect, it } from "vitest";
import {
addAllowlistUserEntriesFromConfigEntry,
buildAllowlistResolutionSummary,
canonicalizeAllowlistWithResolvedIds,
patchAllowlistUsersInConfigEntries,
} from "./resolve-utils.js";
describe("buildAllowlistResolutionSummary", () => {
@@ -40,3 +42,46 @@ describe("addAllowlistUserEntriesFromConfigEntry", () => {
expect(Array.from(target)).toEqual(["a"]);
});
});
describe("canonicalizeAllowlistWithResolvedIds", () => {
it("replaces resolved names with ids and keeps unresolved entries", () => {
const resolvedMap = new Map([
["Alice#1234", { input: "Alice#1234", resolved: true, id: "111" }],
["bob", { input: "bob", resolved: false }],
]);
const result = canonicalizeAllowlistWithResolvedIds({
existing: ["Alice#1234", "bob", "222", "*"],
resolvedMap,
});
expect(result).toEqual(["111", "bob", "222", "*"]);
});
it("deduplicates ids after canonicalization", () => {
const resolvedMap = new Map([["alice", { input: "alice", resolved: true, id: "111" }]]);
const result = canonicalizeAllowlistWithResolvedIds({
existing: ["alice", "111", "alice"],
resolvedMap,
});
expect(result).toEqual(["111"]);
});
});
describe("patchAllowlistUsersInConfigEntries", () => {
it("supports canonicalization strategy for nested users", () => {
const entries = {
alpha: { users: ["Alice", "111", "Bob"] },
beta: { users: ["*"] },
};
const resolvedMap = new Map([
["Alice", { input: "Alice", resolved: true, id: "111" }],
["Bob", { input: "Bob", resolved: false }],
]);
const patched = patchAllowlistUsersInConfigEntries({
entries,
resolvedMap,
strategy: "canonicalize",
});
expect((patched.alpha as { users: string[] }).users).toEqual(["111", "Bob"]);
expect((patched.beta as { users: string[] }).users).toEqual(["*"]);
});
});

View File

@@ -6,31 +6,32 @@ export type AllowlistUserResolutionLike = {
id?: string;
};
function dedupeAllowlistEntries(entries: string[]): string[] {
const seen = new Set<string>();
const deduped: string[] = [];
for (const entry of entries) {
const normalized = entry.trim();
if (!normalized) {
continue;
}
const key = normalized.toLowerCase();
if (seen.has(key)) {
continue;
}
seen.add(key);
deduped.push(normalized);
}
return deduped;
}
export function mergeAllowlist(params: {
existing?: Array<string | number>;
additions: string[];
}): string[] {
const seen = new Set<string>();
const merged: string[] = [];
const push = (value: string) => {
const normalized = value.trim();
if (!normalized) {
return;
}
const key = normalized.toLowerCase();
if (seen.has(key)) {
return;
}
seen.add(key);
merged.push(normalized);
};
for (const entry of params.existing ?? []) {
push(String(entry));
}
for (const entry of params.additions) {
push(entry);
}
return merged;
return dedupeAllowlistEntries([
...(params.existing ?? []).map((entry) => String(entry)),
...params.additions,
]);
}
export function buildAllowlistResolutionSummary<T extends AllowlistUserResolutionLike>(
@@ -71,10 +72,33 @@ export function resolveAllowlistIdAdditions<T extends AllowlistUserResolutionLik
return additions;
}
export function canonicalizeAllowlistWithResolvedIds<
T extends AllowlistUserResolutionLike,
>(params: { existing?: Array<string | number>; resolvedMap: Map<string, T> }): string[] {
const canonicalized: string[] = [];
for (const entry of params.existing ?? []) {
const trimmed = String(entry).trim();
if (!trimmed) {
continue;
}
if (trimmed === "*") {
canonicalized.push(trimmed);
continue;
}
const resolved = params.resolvedMap.get(trimmed);
canonicalized.push(resolved?.resolved && resolved.id ? resolved.id : trimmed);
}
return dedupeAllowlistEntries(canonicalized);
}
export function patchAllowlistUsersInConfigEntries<
T extends AllowlistUserResolutionLike,
TEntries extends Record<string, unknown>,
>(params: { entries: TEntries; resolvedMap: Map<string, T> }): TEntries {
>(params: {
entries: TEntries;
resolvedMap: Map<string, T>;
strategy?: "merge" | "canonicalize";
}): TEntries {
const nextEntries: Record<string, unknown> = { ...params.entries };
for (const [entryKey, entryConfig] of Object.entries(params.entries)) {
if (!entryConfig || typeof entryConfig !== "object") {
@@ -88,9 +112,16 @@ export function patchAllowlistUsersInConfigEntries<
existing: users,
resolvedMap: params.resolvedMap,
});
const resolvedUsers =
params.strategy === "canonicalize"
? canonicalizeAllowlistWithResolvedIds({
existing: users,
resolvedMap: params.resolvedMap,
})
: mergeAllowlist({ existing: users, additions });
nextEntries[entryKey] = {
...entryConfig,
users: mergeAllowlist({ existing: users, additions }),
users: resolvedUsers,
};
}
return nextEntries as TEntries;