feat: add per-provider scope probes to channels capabilities

This commit is contained in:
Peter Steinberger
2026-01-17 19:28:46 +00:00
parent 53218b91c6
commit a7c0887f94
7 changed files with 266 additions and 6 deletions

View File

@@ -7,6 +7,7 @@ import { DEFAULT_ACCOUNT_ID } from "../../../src/routing/session-key.js";
import { msteamsOnboardingAdapter } from "./onboarding.js";
import { msteamsOutbound } from "./outbound.js";
import { probeMSTeams } from "./probe.js";
import { sendMessageMSTeams } from "./send.js";
import { resolveMSTeamsCredentials } from "./token.js";
@@ -218,7 +219,8 @@ export const msteamsPlugin: ChannelPlugin<ResolvedMSTeamsAccount> = {
probe: snapshot.probe,
lastProbeAt: snapshot.lastProbeAt ?? null,
}),
buildAccountSnapshot: ({ account, runtime }) => ({
probeAccount: async ({ cfg }) => await probeMSTeams(cfg.channels?.msteams),
buildAccountSnapshot: ({ account, runtime, probe }) => ({
accountId: account.accountId,
enabled: account.enabled,
configured: account.configured,
@@ -227,6 +229,7 @@ export const msteamsPlugin: ChannelPlugin<ResolvedMSTeamsAccount> = {
lastStopAt: runtime?.lastStopAt ?? null,
lastError: runtime?.lastError ?? null,
port: runtime?.port ?? null,
probe,
}),
},
gateway: {

View File

@@ -7,8 +7,52 @@ export type ProbeMSTeamsResult = {
ok: boolean;
error?: string;
appId?: string;
graph?: {
ok: boolean;
error?: string;
roles?: string[];
scopes?: 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 decodeJwtPayload(token: string): Record<string, unknown> | null {
const parts = token.split(".");
if (parts.length < 2) return null;
const payload = parts[1] ?? "";
const padded = payload.padEnd(payload.length + ((4 - (payload.length % 4)) % 4), "=");
const normalized = padded.replace(/-/g, "+").replace(/_/g, "/");
try {
const decoded = Buffer.from(normalized, "base64").toString("utf8");
const parsed = JSON.parse(decoded) as Record<string, unknown>;
return parsed && typeof parsed === "object" ? parsed : null;
} catch {
return null;
}
}
function readStringArray(value: unknown): string[] | undefined {
if (!Array.isArray(value)) return undefined;
const out = value.map((entry) => String(entry).trim()).filter(Boolean);
return out.length > 0 ? out : undefined;
}
function readScopes(value: unknown): string[] | undefined {
if (typeof value !== "string") return undefined;
const out = value.split(/\s+/).map((entry) => entry.trim()).filter(Boolean);
return out.length > 0 ? out : undefined;
}
export async function probeMSTeams(cfg?: MSTeamsConfig): Promise<ProbeMSTeamsResult> {
const creds = resolveMSTeamsCredentials(cfg);
if (!creds) {
@@ -22,7 +66,29 @@ export async function probeMSTeams(cfg?: MSTeamsConfig): Promise<ProbeMSTeamsRes
const { sdk, authConfig } = await loadMSTeamsSdkWithAuth(creds);
const tokenProvider = new sdk.MsalTokenProvider(authConfig);
await tokenProvider.getAccessToken("https://api.botframework.com/.default");
return { ok: true, appId: creds.appId };
let graph:
| {
ok: boolean;
error?: string;
roles?: string[];
scopes?: string[];
}
| undefined;
try {
const graphToken = await tokenProvider.getAccessToken(
"https://graph.microsoft.com/.default",
);
const accessToken = readAccessToken(graphToken);
const payload = accessToken ? decodeJwtPayload(accessToken) : null;
graph = {
ok: true,
roles: readStringArray(payload?.roles),
scopes: readScopes(payload?.scp),
};
} catch (err) {
graph = { ok: false, error: formatUnknownError(err) };
}
return { ok: true, appId: creds.appId, ...(graph ? { graph } : {}) };
} catch (err) {
return {
ok: false,