import { GRAPH_ROOT } from "./attachments/shared.js"; import { loadMSTeamsSdkWithAuth } from "./sdk.js"; import { resolveMSTeamsCredentials } from "./token.js"; type GraphUser = { id?: string; displayName?: string; userPrincipalName?: string; mail?: string; }; type GraphGroup = { id?: string; displayName?: string; }; type GraphChannel = { id?: string; displayName?: string; }; type GraphResponse = { value?: T[] }; export type MSTeamsChannelResolution = { input: string; resolved: boolean; teamId?: string; teamName?: string; channelId?: string; channelName?: string; note?: string; }; export type MSTeamsUserResolution = { input: string; resolved: boolean; id?: string; name?: string; note?: string; }; function readAccessToken(value: unknown): string | null { if (typeof value === "string") return value; if (value && typeof value === "object") { const token = (value as { accessToken?: unknown }).accessToken ?? (value as { token?: unknown }).token; return typeof token === "string" ? token : null; } return null; } function stripProviderPrefix(raw: string): string { return raw.replace(/^(msteams|teams):/i, ""); } export function normalizeMSTeamsMessagingTarget(raw: string): string | undefined { let trimmed = raw.trim(); if (!trimmed) return undefined; trimmed = stripProviderPrefix(trimmed).trim(); if (/^conversation:/i.test(trimmed)) { const id = trimmed.slice("conversation:".length).trim(); return id ? `conversation:${id}` : undefined; } if (/^user:/i.test(trimmed)) { const id = trimmed.slice("user:".length).trim(); return id ? `user:${id}` : undefined; } return trimmed || undefined; } export function normalizeMSTeamsUserInput(raw: string): string { return stripProviderPrefix(raw).replace(/^(user|conversation):/i, "").trim(); } export function parseMSTeamsConversationId(raw: string): string | null { const trimmed = stripProviderPrefix(raw).trim(); if (!/^conversation:/i.test(trimmed)) return null; const id = trimmed.slice("conversation:".length).trim(); return id; } function normalizeMSTeamsTeamKey(raw: string): string | undefined { const trimmed = stripProviderPrefix(raw).replace(/^team:/i, "").trim(); return trimmed || undefined; } function normalizeMSTeamsChannelKey(raw?: string | null): string | undefined { const trimmed = raw?.trim().replace(/^#/, "").trim() ?? ""; return trimmed || undefined; } export function parseMSTeamsTeamChannelInput(raw: string): { team?: string; channel?: string } { const trimmed = stripProviderPrefix(raw).trim(); if (!trimmed) return {}; const parts = trimmed.split("/"); const team = normalizeMSTeamsTeamKey(parts[0] ?? ""); const channel = parts.length > 1 ? normalizeMSTeamsChannelKey(parts.slice(1).join("/")) : undefined; return { ...(team ? { team } : {}), ...(channel ? { channel } : {}), }; } export function parseMSTeamsTeamEntry( raw: string, ): { teamKey: string; channelKey?: string } | null { const { team, channel } = parseMSTeamsTeamChannelInput(raw); if (!team) return null; return { teamKey: team, ...(channel ? { channelKey: channel } : {}), }; } function normalizeQuery(value?: string | null): string { return value?.trim() ?? ""; } function escapeOData(value: string): string { return value.replace(/'/g, "''"); } async function fetchGraphJson(params: { token: string; path: string; headers?: Record; }): Promise { const res = await fetch(`${GRAPH_ROOT}${params.path}`, { headers: { Authorization: `Bearer ${params.token}`, ...(params.headers ?? {}), }, }); if (!res.ok) { const text = await res.text().catch(() => ""); throw new Error(`Graph ${params.path} failed (${res.status}): ${text || "unknown error"}`); } return (await res.json()) as T; } async function resolveGraphToken(cfg: unknown): Promise { const creds = resolveMSTeamsCredentials((cfg as { channels?: { msteams?: unknown } })?.channels?.msteams); if (!creds) throw new Error("MS Teams credentials missing"); const { sdk, authConfig } = await loadMSTeamsSdkWithAuth(creds); const tokenProvider = new sdk.MsalTokenProvider(authConfig); const token = await tokenProvider.getAccessToken("https://graph.microsoft.com/.default"); const accessToken = readAccessToken(token); if (!accessToken) throw new Error("MS Teams graph token unavailable"); return accessToken; } async function listTeamsByName(token: string, query: string): Promise { const escaped = escapeOData(query); const filter = `resourceProvisioningOptions/Any(x:x eq 'Team') and startsWith(displayName,'${escaped}')`; const path = `/groups?$filter=${encodeURIComponent(filter)}&$select=id,displayName`; const res = await fetchGraphJson>({ token, path }); return res.value ?? []; } async function listChannelsForTeam(token: string, teamId: string): Promise { const path = `/teams/${encodeURIComponent(teamId)}/channels?$select=id,displayName`; const res = await fetchGraphJson>({ token, path }); return res.value ?? []; } export async function resolveMSTeamsChannelAllowlist(params: { cfg: unknown; entries: string[]; }): Promise { 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({ 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; } export async function resolveMSTeamsUserAllowlist(params: { cfg: unknown; entries: string[]; }): Promise { 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; } let users: GraphUser[] = []; if (query.includes("@")) { const escaped = escapeOData(query); const filter = `(mail eq '${escaped}' or userPrincipalName eq '${escaped}')`; const path = `/users?$filter=${encodeURIComponent(filter)}&$select=id,displayName,mail,userPrincipalName`; const res = await fetchGraphJson>({ token, path }); users = res.value ?? []; } else { const path = `/users?$search=${encodeURIComponent(`"displayName:${query}"`)}&$select=id,displayName,mail,userPrincipalName&$top=10`; const res = await fetchGraphJson>({ token, path, headers: { ConsistencyLevel: "eventual" }, }); users = res.value ?? []; } 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; }