mirror of https://github.com/openclaw/openclaw.git
refactor: unify extension allowlist resolver and directory scaffolding
This commit is contained in:
parent
8e0e76697a
commit
7230b96cc7
|
|
@ -0,0 +1,40 @@
|
|||
import type { ClawdbotConfig } from "openclaw/plugin-sdk/feishu";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
vi.mock("./accounts.js", () => ({
|
||||
resolveFeishuAccount: vi.fn(() => ({
|
||||
configured: false,
|
||||
config: {
|
||||
allowFrom: ["user:alice", "user:bob"],
|
||||
dms: {
|
||||
"user:carla": {},
|
||||
},
|
||||
groups: {
|
||||
"chat-1": {},
|
||||
},
|
||||
groupAllowFrom: ["chat-2"],
|
||||
},
|
||||
})),
|
||||
}));
|
||||
|
||||
import { listFeishuDirectoryGroups, listFeishuDirectoryPeers } from "./directory.js";
|
||||
|
||||
describe("feishu directory (config-backed)", () => {
|
||||
const cfg = {} as ClawdbotConfig;
|
||||
|
||||
it("merges allowFrom + dms into peer entries", async () => {
|
||||
const peers = await listFeishuDirectoryPeers({ cfg, query: "a" });
|
||||
expect(peers).toEqual([
|
||||
{ kind: "user", id: "alice" },
|
||||
{ kind: "user", id: "carla" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("merges groups map + groupAllowFrom into group entries", async () => {
|
||||
const groups = await listFeishuDirectoryGroups({ cfg });
|
||||
expect(groups).toEqual([
|
||||
{ kind: "group", id: "chat-1" },
|
||||
{ kind: "group", id: "chat-2" },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,3 +1,7 @@
|
|||
import {
|
||||
listDirectoryGroupEntriesFromMapKeysAndAllowFrom,
|
||||
listDirectoryUserEntriesFromAllowFromAndMapKeys,
|
||||
} from "openclaw/plugin-sdk";
|
||||
import type { ClawdbotConfig } from "openclaw/plugin-sdk/feishu";
|
||||
import { resolveFeishuAccount } from "./accounts.js";
|
||||
import { createFeishuClient } from "./client.js";
|
||||
|
|
@ -22,31 +26,14 @@ export async function listFeishuDirectoryPeers(params: {
|
|||
accountId?: string;
|
||||
}): Promise<FeishuDirectoryPeer[]> {
|
||||
const account = resolveFeishuAccount({ cfg: params.cfg, accountId: params.accountId });
|
||||
const feishuCfg = account.config;
|
||||
const q = params.query?.trim().toLowerCase() || "";
|
||||
const ids = new Set<string>();
|
||||
|
||||
for (const entry of feishuCfg?.allowFrom ?? []) {
|
||||
const trimmed = String(entry).trim();
|
||||
if (trimmed && trimmed !== "*") {
|
||||
ids.add(trimmed);
|
||||
}
|
||||
}
|
||||
|
||||
for (const userId of Object.keys(feishuCfg?.dms ?? {})) {
|
||||
const trimmed = userId.trim();
|
||||
if (trimmed) {
|
||||
ids.add(trimmed);
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(ids)
|
||||
.map((raw) => raw.trim())
|
||||
.filter(Boolean)
|
||||
.map((raw) => normalizeFeishuTarget(raw) ?? raw)
|
||||
.filter((id) => (q ? id.toLowerCase().includes(q) : true))
|
||||
.slice(0, params.limit && params.limit > 0 ? params.limit : undefined)
|
||||
.map((id) => ({ kind: "user" as const, id }));
|
||||
return listDirectoryUserEntriesFromAllowFromAndMapKeys({
|
||||
allowFrom: account.config.allowFrom,
|
||||
map: account.config.dms,
|
||||
query: params.query,
|
||||
limit: params.limit,
|
||||
normalizeAllowFromId: (entry) => normalizeFeishuTarget(entry) ?? entry,
|
||||
normalizeMapKeyId: (entry) => normalizeFeishuTarget(entry) ?? entry,
|
||||
});
|
||||
}
|
||||
|
||||
export async function listFeishuDirectoryGroups(params: {
|
||||
|
|
@ -56,30 +43,12 @@ export async function listFeishuDirectoryGroups(params: {
|
|||
accountId?: string;
|
||||
}): Promise<FeishuDirectoryGroup[]> {
|
||||
const account = resolveFeishuAccount({ cfg: params.cfg, accountId: params.accountId });
|
||||
const feishuCfg = account.config;
|
||||
const q = params.query?.trim().toLowerCase() || "";
|
||||
const ids = new Set<string>();
|
||||
|
||||
for (const groupId of Object.keys(feishuCfg?.groups ?? {})) {
|
||||
const trimmed = groupId.trim();
|
||||
if (trimmed && trimmed !== "*") {
|
||||
ids.add(trimmed);
|
||||
}
|
||||
}
|
||||
|
||||
for (const entry of feishuCfg?.groupAllowFrom ?? []) {
|
||||
const trimmed = String(entry).trim();
|
||||
if (trimmed && trimmed !== "*") {
|
||||
ids.add(trimmed);
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(ids)
|
||||
.map((raw) => raw.trim())
|
||||
.filter(Boolean)
|
||||
.filter((id) => (q ? id.toLowerCase().includes(q) : true))
|
||||
.slice(0, params.limit && params.limit > 0 ? params.limit : undefined)
|
||||
.map((id) => ({ kind: "group" as const, id }));
|
||||
return listDirectoryGroupEntriesFromMapKeysAndAllowFrom({
|
||||
groups: account.config.groups,
|
||||
allowFrom: account.config.groupAllowFrom,
|
||||
query: params.query,
|
||||
limit: params.limit,
|
||||
});
|
||||
}
|
||||
|
||||
export async function listFeishuDirectoryPeersLive(params: {
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { mapAllowlistResolutionInputs } from "openclaw/plugin-sdk";
|
||||
import type {
|
||||
ChannelDirectoryEntry,
|
||||
ChannelResolveKind,
|
||||
|
|
@ -71,56 +72,54 @@ export async function resolveMatrixTargets(params: {
|
|||
kind: ChannelResolveKind;
|
||||
runtime?: RuntimeEnv;
|
||||
}): Promise<ChannelResolveResult[]> {
|
||||
const results: ChannelResolveResult[] = [];
|
||||
for (const input of params.inputs) {
|
||||
const trimmed = input.trim();
|
||||
if (!trimmed) {
|
||||
results.push({ input, resolved: false, note: "empty input" });
|
||||
continue;
|
||||
}
|
||||
if (params.kind === "user") {
|
||||
if (trimmed.startsWith("@") && trimmed.includes(":")) {
|
||||
results.push({ input, resolved: true, id: trimmed });
|
||||
continue;
|
||||
return await mapAllowlistResolutionInputs({
|
||||
inputs: params.inputs,
|
||||
mapInput: async (input): Promise<ChannelResolveResult> => {
|
||||
const trimmed = input.trim();
|
||||
if (!trimmed) {
|
||||
return { input, resolved: false, note: "empty input" };
|
||||
}
|
||||
if (params.kind === "user") {
|
||||
if (trimmed.startsWith("@") && trimmed.includes(":")) {
|
||||
return { input, resolved: true, id: trimmed };
|
||||
}
|
||||
try {
|
||||
const matches = await listMatrixDirectoryPeersLive({
|
||||
cfg: params.cfg,
|
||||
query: trimmed,
|
||||
limit: 5,
|
||||
});
|
||||
const best = pickBestUserMatch(matches, trimmed);
|
||||
return {
|
||||
input,
|
||||
resolved: Boolean(best?.id),
|
||||
id: best?.id,
|
||||
name: best?.name,
|
||||
note: best ? undefined : describeUserMatchFailure(matches, trimmed),
|
||||
};
|
||||
} catch (err) {
|
||||
params.runtime?.error?.(`matrix resolve failed: ${String(err)}`);
|
||||
return { input, resolved: false, note: "lookup failed" };
|
||||
}
|
||||
}
|
||||
try {
|
||||
const matches = await listMatrixDirectoryPeersLive({
|
||||
const matches = await listMatrixDirectoryGroupsLive({
|
||||
cfg: params.cfg,
|
||||
query: trimmed,
|
||||
limit: 5,
|
||||
});
|
||||
const best = pickBestUserMatch(matches, trimmed);
|
||||
results.push({
|
||||
const best = pickBestGroupMatch(matches, trimmed);
|
||||
return {
|
||||
input,
|
||||
resolved: Boolean(best?.id),
|
||||
id: best?.id,
|
||||
name: best?.name,
|
||||
note: best ? undefined : describeUserMatchFailure(matches, trimmed),
|
||||
});
|
||||
note: matches.length > 1 ? "multiple matches; chose first" : undefined,
|
||||
};
|
||||
} catch (err) {
|
||||
params.runtime?.error?.(`matrix resolve failed: ${String(err)}`);
|
||||
results.push({ input, resolved: false, note: "lookup failed" });
|
||||
return { input, resolved: false, note: "lookup failed" };
|
||||
}
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
const matches = await listMatrixDirectoryGroupsLive({
|
||||
cfg: params.cfg,
|
||||
query: trimmed,
|
||||
limit: 5,
|
||||
});
|
||||
const best = pickBestGroupMatch(matches, trimmed);
|
||||
results.push({
|
||||
input,
|
||||
resolved: Boolean(best?.id),
|
||||
id: best?.id,
|
||||
name: best?.name,
|
||||
note: matches.length > 1 ? "multiple matches; chose first" : undefined,
|
||||
});
|
||||
} catch (err) {
|
||||
params.runtime?.error?.(`matrix resolve failed: ${String(err)}`);
|
||||
results.push({ input, resolved: false, note: "lookup failed" });
|
||||
}
|
||||
}
|
||||
return results;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,78 @@
|
|||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
const {
|
||||
listTeamsByName,
|
||||
listChannelsForTeam,
|
||||
normalizeQuery,
|
||||
resolveGraphToken,
|
||||
searchGraphUsers,
|
||||
} = vi.hoisted(() => ({
|
||||
listTeamsByName: vi.fn(),
|
||||
listChannelsForTeam: vi.fn(),
|
||||
normalizeQuery: vi.fn((value: string) => value.trim().toLowerCase()),
|
||||
resolveGraphToken: vi.fn(async () => "graph-token"),
|
||||
searchGraphUsers: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("./graph.js", () => ({
|
||||
listTeamsByName,
|
||||
listChannelsForTeam,
|
||||
normalizeQuery,
|
||||
resolveGraphToken,
|
||||
}));
|
||||
|
||||
vi.mock("./graph-users.js", () => ({
|
||||
searchGraphUsers,
|
||||
}));
|
||||
|
||||
import {
|
||||
resolveMSTeamsChannelAllowlist,
|
||||
resolveMSTeamsUserAllowlist,
|
||||
} from "./resolve-allowlist.js";
|
||||
|
||||
describe("resolveMSTeamsUserAllowlist", () => {
|
||||
it("marks empty input unresolved", async () => {
|
||||
const [result] = await resolveMSTeamsUserAllowlist({ cfg: {}, entries: [" "] });
|
||||
expect(result).toEqual({ input: " ", resolved: false });
|
||||
});
|
||||
|
||||
it("resolves first Graph user match", async () => {
|
||||
searchGraphUsers.mockResolvedValueOnce([
|
||||
{ id: "user-1", displayName: "Alice One" },
|
||||
{ id: "user-2", displayName: "Alice Two" },
|
||||
]);
|
||||
const [result] = await resolveMSTeamsUserAllowlist({ cfg: {}, entries: ["alice"] });
|
||||
expect(result).toEqual({
|
||||
input: "alice",
|
||||
resolved: true,
|
||||
id: "user-1",
|
||||
name: "Alice One",
|
||||
note: "multiple matches; chose first",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveMSTeamsChannelAllowlist", () => {
|
||||
it("resolves team/channel by team name + channel display name", async () => {
|
||||
listTeamsByName.mockResolvedValueOnce([{ id: "team-1", displayName: "Product Team" }]);
|
||||
listChannelsForTeam.mockResolvedValueOnce([
|
||||
{ id: "channel-1", displayName: "General" },
|
||||
{ id: "channel-2", displayName: "Roadmap" },
|
||||
]);
|
||||
|
||||
const [result] = await resolveMSTeamsChannelAllowlist({
|
||||
cfg: {},
|
||||
entries: ["Product Team/Roadmap"],
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
input: "Product Team/Roadmap",
|
||||
resolved: true,
|
||||
teamId: "team-1",
|
||||
teamName: "Product Team",
|
||||
channelId: "channel-2",
|
||||
channelName: "Roadmap",
|
||||
note: "multiple channels; chose first",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
import { mapAllowlistResolutionInputs } from "openclaw/plugin-sdk";
|
||||
import { searchGraphUsers } from "./graph-users.js";
|
||||
import {
|
||||
listChannelsForTeam,
|
||||
|
|
@ -105,61 +106,55 @@ export async function resolveMSTeamsChannelAllowlist(params: {
|
|||
entries: string[];
|
||||
}): Promise<MSTeamsChannelResolution[]> {
|
||||
const token = await resolveGraphToken(params.cfg);
|
||||
const results: MSTeamsChannelResolution[] = [];
|
||||
|
||||
for (const input of params.entries) {
|
||||
const { team, channel } = parseMSTeamsTeamChannelInput(input);
|
||||
if (!team) {
|
||||
results.push({ input, resolved: false });
|
||||
continue;
|
||||
}
|
||||
const teams = /^[0-9a-fA-F-]{16,}$/.test(team)
|
||||
? [{ id: team, displayName: team }]
|
||||
: await listTeamsByName(token, team);
|
||||
if (teams.length === 0) {
|
||||
results.push({ input, resolved: false, note: "team not found" });
|
||||
continue;
|
||||
}
|
||||
const teamMatch = teams[0];
|
||||
const teamId = teamMatch.id?.trim();
|
||||
const teamName = teamMatch.displayName?.trim() || team;
|
||||
if (!teamId) {
|
||||
results.push({ input, resolved: false, note: "team id missing" });
|
||||
continue;
|
||||
}
|
||||
if (!channel) {
|
||||
results.push({
|
||||
return await mapAllowlistResolutionInputs({
|
||||
inputs: params.entries,
|
||||
mapInput: async (input): Promise<MSTeamsChannelResolution> => {
|
||||
const { team, channel } = parseMSTeamsTeamChannelInput(input);
|
||||
if (!team) {
|
||||
return { input, resolved: false };
|
||||
}
|
||||
const teams = /^[0-9a-fA-F-]{16,}$/.test(team)
|
||||
? [{ id: team, displayName: team }]
|
||||
: await listTeamsByName(token, team);
|
||||
if (teams.length === 0) {
|
||||
return { input, resolved: false, note: "team not found" };
|
||||
}
|
||||
const teamMatch = teams[0];
|
||||
const teamId = teamMatch.id?.trim();
|
||||
const teamName = teamMatch.displayName?.trim() || team;
|
||||
if (!teamId) {
|
||||
return { input, resolved: false, note: "team id missing" };
|
||||
}
|
||||
if (!channel) {
|
||||
return {
|
||||
input,
|
||||
resolved: true,
|
||||
teamId,
|
||||
teamName,
|
||||
note: teams.length > 1 ? "multiple teams; chose first" : undefined,
|
||||
};
|
||||
}
|
||||
const channels = await listChannelsForTeam(token, teamId);
|
||||
const channelMatch =
|
||||
channels.find((item) => item.id === channel) ??
|
||||
channels.find((item) => item.displayName?.toLowerCase() === channel.toLowerCase()) ??
|
||||
channels.find((item) =>
|
||||
item.displayName?.toLowerCase().includes(channel.toLowerCase() ?? ""),
|
||||
);
|
||||
if (!channelMatch?.id) {
|
||||
return { input, resolved: false, note: "channel not found" };
|
||||
}
|
||||
return {
|
||||
input,
|
||||
resolved: true,
|
||||
teamId,
|
||||
teamName,
|
||||
note: teams.length > 1 ? "multiple teams; chose first" : undefined,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
const channels = await listChannelsForTeam(token, teamId);
|
||||
const channelMatch =
|
||||
channels.find((item) => item.id === channel) ??
|
||||
channels.find((item) => item.displayName?.toLowerCase() === channel.toLowerCase()) ??
|
||||
channels.find((item) =>
|
||||
item.displayName?.toLowerCase().includes(channel.toLowerCase() ?? ""),
|
||||
);
|
||||
if (!channelMatch?.id) {
|
||||
results.push({ input, resolved: false, note: "channel not found" });
|
||||
continue;
|
||||
}
|
||||
results.push({
|
||||
input,
|
||||
resolved: true,
|
||||
teamId,
|
||||
teamName,
|
||||
channelId: channelMatch.id,
|
||||
channelName: channelMatch.displayName ?? channel,
|
||||
note: channels.length > 1 ? "multiple channels; chose first" : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
return results;
|
||||
channelId: channelMatch.id,
|
||||
channelName: channelMatch.displayName ?? channel,
|
||||
note: channels.length > 1 ? "multiple channels; chose first" : undefined,
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function resolveMSTeamsUserAllowlist(params: {
|
||||
|
|
@ -167,32 +162,28 @@ export async function resolveMSTeamsUserAllowlist(params: {
|
|||
entries: string[];
|
||||
}): Promise<MSTeamsUserResolution[]> {
|
||||
const token = await resolveGraphToken(params.cfg);
|
||||
const results: MSTeamsUserResolution[] = [];
|
||||
|
||||
for (const input of params.entries) {
|
||||
const query = normalizeQuery(normalizeMSTeamsUserInput(input));
|
||||
if (!query) {
|
||||
results.push({ input, resolved: false });
|
||||
continue;
|
||||
}
|
||||
if (/^[0-9a-fA-F-]{16,}$/.test(query)) {
|
||||
results.push({ input, resolved: true, id: query });
|
||||
continue;
|
||||
}
|
||||
const users = await searchGraphUsers({ token, query, top: 10 });
|
||||
const match = users[0];
|
||||
if (!match?.id) {
|
||||
results.push({ input, resolved: false });
|
||||
continue;
|
||||
}
|
||||
results.push({
|
||||
input,
|
||||
resolved: true,
|
||||
id: match.id,
|
||||
name: match.displayName ?? undefined,
|
||||
note: users.length > 1 ? "multiple matches; chose first" : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
return results;
|
||||
return await mapAllowlistResolutionInputs({
|
||||
inputs: params.entries,
|
||||
mapInput: async (input): Promise<MSTeamsUserResolution> => {
|
||||
const query = normalizeQuery(normalizeMSTeamsUserInput(input));
|
||||
if (!query) {
|
||||
return { input, resolved: false };
|
||||
}
|
||||
if (/^[0-9a-fA-F-]{16,}$/.test(query)) {
|
||||
return { input, resolved: true, id: query };
|
||||
}
|
||||
const users = await searchGraphUsers({ token, query, top: 10 });
|
||||
const match = users[0];
|
||||
if (!match?.id) {
|
||||
return { input, resolved: false };
|
||||
}
|
||||
return {
|
||||
input,
|
||||
resolved: true,
|
||||
id: match.id,
|
||||
name: match.displayName ?? undefined,
|
||||
note: users.length > 1 ? "multiple matches; chose first" : undefined,
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
listDirectoryGroupEntriesFromMapKeysAndAllowFrom,
|
||||
listDirectoryGroupEntriesFromMapKeys,
|
||||
listDirectoryUserEntriesFromAllowFromAndMapKeys,
|
||||
listDirectoryUserEntriesFromAllowFrom,
|
||||
} from "./directory-config-helpers.js";
|
||||
|
||||
|
|
@ -37,3 +39,41 @@ describe("listDirectoryGroupEntriesFromMapKeys", () => {
|
|||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("listDirectoryUserEntriesFromAllowFromAndMapKeys", () => {
|
||||
it("merges allowFrom and map keys with dedupe/query/limit", () => {
|
||||
const entries = listDirectoryUserEntriesFromAllowFromAndMapKeys({
|
||||
allowFrom: ["user:alice", "user:bob"],
|
||||
map: {
|
||||
"user:carla": {},
|
||||
"user:alice": {},
|
||||
},
|
||||
normalizeAllowFromId: (entry) => entry.replace(/^user:/i, ""),
|
||||
normalizeMapKeyId: (entry) => entry.replace(/^user:/i, ""),
|
||||
query: "a",
|
||||
limit: 2,
|
||||
});
|
||||
|
||||
expect(entries).toEqual([
|
||||
{ kind: "user", id: "alice" },
|
||||
{ kind: "user", id: "carla" },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("listDirectoryGroupEntriesFromMapKeysAndAllowFrom", () => {
|
||||
it("merges groups keys and group allowFrom entries", () => {
|
||||
const entries = listDirectoryGroupEntriesFromMapKeysAndAllowFrom({
|
||||
groups: {
|
||||
"team/a": {},
|
||||
},
|
||||
allowFrom: ["team/b", "team/a"],
|
||||
query: "team/",
|
||||
});
|
||||
|
||||
expect(entries).toEqual([
|
||||
{ kind: "group", id: "team/a" },
|
||||
{ kind: "group", id: "team/b" },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -22,44 +22,106 @@ function toDirectoryEntries(kind: "user" | "group", ids: string[]): ChannelDirec
|
|||
return ids.map((id) => ({ kind, id }) as const);
|
||||
}
|
||||
|
||||
function collectDirectoryIdsFromEntries(params: {
|
||||
entries?: readonly unknown[];
|
||||
normalizeId?: (entry: string) => string | null | undefined;
|
||||
}): string[] {
|
||||
return (params.entries ?? [])
|
||||
.map((entry) => String(entry).trim())
|
||||
.filter((entry) => Boolean(entry) && entry !== "*")
|
||||
.map((entry) => {
|
||||
const normalized = params.normalizeId ? params.normalizeId(entry) : entry;
|
||||
return typeof normalized === "string" ? normalized.trim() : "";
|
||||
})
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function collectDirectoryIdsFromMapKeys(params: {
|
||||
groups?: Record<string, unknown>;
|
||||
normalizeId?: (entry: string) => string | null | undefined;
|
||||
}): string[] {
|
||||
return Object.keys(params.groups ?? {})
|
||||
.map((entry) => entry.trim())
|
||||
.filter((entry) => Boolean(entry) && entry !== "*")
|
||||
.map((entry) => {
|
||||
const normalized = params.normalizeId ? params.normalizeId(entry) : entry;
|
||||
return typeof normalized === "string" ? normalized.trim() : "";
|
||||
})
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function dedupeDirectoryIds(ids: string[]): string[] {
|
||||
return Array.from(new Set(ids));
|
||||
}
|
||||
|
||||
export function listDirectoryUserEntriesFromAllowFrom(params: {
|
||||
allowFrom?: readonly unknown[];
|
||||
query?: string | null;
|
||||
limit?: number | null;
|
||||
normalizeId?: (entry: string) => string | null | undefined;
|
||||
}): ChannelDirectoryEntry[] {
|
||||
const ids = Array.from(
|
||||
new Set(
|
||||
(params.allowFrom ?? [])
|
||||
.map((entry) => String(entry).trim())
|
||||
.filter((entry) => Boolean(entry) && entry !== "*")
|
||||
.map((entry) => {
|
||||
const normalized = params.normalizeId ? params.normalizeId(entry) : entry;
|
||||
return typeof normalized === "string" ? normalized.trim() : "";
|
||||
})
|
||||
.filter(Boolean),
|
||||
),
|
||||
const ids = dedupeDirectoryIds(
|
||||
collectDirectoryIdsFromEntries({
|
||||
entries: params.allowFrom,
|
||||
normalizeId: params.normalizeId,
|
||||
}),
|
||||
);
|
||||
return toDirectoryEntries("user", applyDirectoryQueryAndLimit(ids, params));
|
||||
}
|
||||
|
||||
export function listDirectoryUserEntriesFromAllowFromAndMapKeys(params: {
|
||||
allowFrom?: readonly unknown[];
|
||||
map?: Record<string, unknown>;
|
||||
query?: string | null;
|
||||
limit?: number | null;
|
||||
normalizeAllowFromId?: (entry: string) => string | null | undefined;
|
||||
normalizeMapKeyId?: (entry: string) => string | null | undefined;
|
||||
}): ChannelDirectoryEntry[] {
|
||||
const ids = dedupeDirectoryIds([
|
||||
...collectDirectoryIdsFromEntries({
|
||||
entries: params.allowFrom,
|
||||
normalizeId: params.normalizeAllowFromId,
|
||||
}),
|
||||
...collectDirectoryIdsFromMapKeys({
|
||||
groups: params.map,
|
||||
normalizeId: params.normalizeMapKeyId,
|
||||
}),
|
||||
]);
|
||||
return toDirectoryEntries("user", applyDirectoryQueryAndLimit(ids, params));
|
||||
}
|
||||
|
||||
export function listDirectoryGroupEntriesFromMapKeys(params: {
|
||||
groups?: Record<string, unknown>;
|
||||
query?: string | null;
|
||||
limit?: number | null;
|
||||
normalizeId?: (entry: string) => string | null | undefined;
|
||||
}): ChannelDirectoryEntry[] {
|
||||
const ids = Array.from(
|
||||
new Set(
|
||||
Object.keys(params.groups ?? {})
|
||||
.map((entry) => entry.trim())
|
||||
.filter((entry) => Boolean(entry) && entry !== "*")
|
||||
.map((entry) => {
|
||||
const normalized = params.normalizeId ? params.normalizeId(entry) : entry;
|
||||
return typeof normalized === "string" ? normalized.trim() : "";
|
||||
})
|
||||
.filter(Boolean),
|
||||
),
|
||||
const ids = dedupeDirectoryIds(
|
||||
collectDirectoryIdsFromMapKeys({
|
||||
groups: params.groups,
|
||||
normalizeId: params.normalizeId,
|
||||
}),
|
||||
);
|
||||
return toDirectoryEntries("group", applyDirectoryQueryAndLimit(ids, params));
|
||||
}
|
||||
|
||||
export function listDirectoryGroupEntriesFromMapKeysAndAllowFrom(params: {
|
||||
groups?: Record<string, unknown>;
|
||||
allowFrom?: readonly unknown[];
|
||||
query?: string | null;
|
||||
limit?: number | null;
|
||||
normalizeMapKeyId?: (entry: string) => string | null | undefined;
|
||||
normalizeAllowFromId?: (entry: string) => string | null | undefined;
|
||||
}): ChannelDirectoryEntry[] {
|
||||
const ids = dedupeDirectoryIds([
|
||||
...collectDirectoryIdsFromMapKeys({
|
||||
groups: params.groups,
|
||||
normalizeId: params.normalizeMapKeyId,
|
||||
}),
|
||||
...collectDirectoryIdsFromEntries({
|
||||
entries: params.allowFrom,
|
||||
normalizeId: params.normalizeAllowFromId,
|
||||
}),
|
||||
]);
|
||||
return toDirectoryEntries("group", applyDirectoryQueryAndLimit(ids, params));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,40 +1,18 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
mapBasicAllowlistResolutionEntries,
|
||||
type BasicAllowlistResolutionEntry,
|
||||
} from "./allowlist-resolution.js";
|
||||
import { mapAllowlistResolutionInputs } from "./allowlist-resolution.js";
|
||||
|
||||
describe("mapBasicAllowlistResolutionEntries", () => {
|
||||
it("maps entries to normalized allowlist resolver output", () => {
|
||||
const entries: BasicAllowlistResolutionEntry[] = [
|
||||
{
|
||||
input: "alice",
|
||||
resolved: true,
|
||||
id: "U123",
|
||||
name: "Alice",
|
||||
note: "ok",
|
||||
describe("mapAllowlistResolutionInputs", () => {
|
||||
it("maps inputs sequentially and preserves order", async () => {
|
||||
const visited: string[] = [];
|
||||
const result = await mapAllowlistResolutionInputs({
|
||||
inputs: ["one", "two", "three"],
|
||||
mapInput: async (input) => {
|
||||
visited.push(input);
|
||||
return input.toUpperCase();
|
||||
},
|
||||
{
|
||||
input: "bob",
|
||||
resolved: false,
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
expect(mapBasicAllowlistResolutionEntries(entries)).toEqual([
|
||||
{
|
||||
input: "alice",
|
||||
resolved: true,
|
||||
id: "U123",
|
||||
name: "Alice",
|
||||
note: "ok",
|
||||
},
|
||||
{
|
||||
input: "bob",
|
||||
resolved: false,
|
||||
id: undefined,
|
||||
name: undefined,
|
||||
note: undefined,
|
||||
},
|
||||
]);
|
||||
expect(visited).toEqual(["one", "two", "three"]);
|
||||
expect(result).toEqual(["ONE", "TWO", "THREE"]);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -17,3 +17,14 @@ export function mapBasicAllowlistResolutionEntries(
|
|||
note: entry.note,
|
||||
}));
|
||||
}
|
||||
|
||||
export async function mapAllowlistResolutionInputs<T>(params: {
|
||||
inputs: string[];
|
||||
mapInput: (input: string) => Promise<T> | T;
|
||||
}): Promise<T[]> {
|
||||
const results: T[] = [];
|
||||
for (const input of params.inputs) {
|
||||
results.push(await params.mapInput(input));
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -133,6 +133,7 @@ export { isDangerousNameMatchingEnabled } from "../config/dangerous-name-matchin
|
|||
export type { FileLockHandle, FileLockOptions } from "./file-lock.js";
|
||||
export { acquireFileLock, withFileLock } from "./file-lock.js";
|
||||
export {
|
||||
mapAllowlistResolutionInputs,
|
||||
mapBasicAllowlistResolutionEntries,
|
||||
type BasicAllowlistResolutionEntry,
|
||||
} from "./allowlist-resolution.js";
|
||||
|
|
@ -515,6 +516,12 @@ export { optionalStringEnum, stringEnum } from "../agents/schema/typebox.js";
|
|||
export type { PollInput } from "../polls.js";
|
||||
|
||||
export { buildChannelConfigSchema } from "../channels/plugins/config-schema.js";
|
||||
export {
|
||||
listDirectoryGroupEntriesFromMapKeys,
|
||||
listDirectoryGroupEntriesFromMapKeysAndAllowFrom,
|
||||
listDirectoryUserEntriesFromAllowFrom,
|
||||
listDirectoryUserEntriesFromAllowFromAndMapKeys,
|
||||
} from "../channels/plugins/directory-config-helpers.js";
|
||||
export {
|
||||
clearAccountEntryFields,
|
||||
deleteAccountFromConfigSection,
|
||||
|
|
|
|||
Loading…
Reference in New Issue