feat(channels): add microsoft teams bot framework adapter
This commit is contained in:
@@ -17,4 +17,5 @@ export { SlackAdapter, type SlackAdapterConfig } from './slack/index.js';
|
||||
export { WhatsAppAdapter, type WhatsAppAdapterConfig } from './whatsapp/index.js';
|
||||
export { MatrixAdapter, type MatrixAdapterConfig } from './matrix/index.js';
|
||||
export { SignalAdapter, type SignalAdapterConfig } from './signal/index.js';
|
||||
export { TeamsAdapter, type TeamsAdapterConfig } from './teams/index.js';
|
||||
export { PairingManager, type PairingConfig, type PairingStore, type ApprovedSender } from './pairing.js';
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
|
||||
import { TeamsAdapter } from './adapter.js';
|
||||
import type { InboundMessage } from '../types.js';
|
||||
|
||||
const mockFetch = vi.fn();
|
||||
|
||||
function jsonResponse(body: unknown, status = 200): Response {
|
||||
return {
|
||||
ok: status >= 200 && status < 300,
|
||||
status,
|
||||
json: async () => body,
|
||||
text: async () => JSON.stringify(body),
|
||||
} as Response;
|
||||
}
|
||||
|
||||
describe('TeamsAdapter', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.stubGlobal('fetch', mockFetch);
|
||||
});
|
||||
|
||||
it('has name teams and starts disconnected', () => {
|
||||
const adapter = new TeamsAdapter({ appId: 'id', appPassword: 'secret' });
|
||||
expect(adapter.name).toBe('teams');
|
||||
expect(adapter.status).toBe('disconnected');
|
||||
});
|
||||
|
||||
it('send fetches bot token and posts activity', async () => {
|
||||
const adapter = new TeamsAdapter({ appId: 'id', appPassword: 'secret' });
|
||||
await adapter.connect();
|
||||
|
||||
mockFetch.mockImplementation(async (url: string) => {
|
||||
if (url.includes('/oauth2/v2.0/token')) {
|
||||
return jsonResponse({ access_token: 'token123', expires_in: 3600 });
|
||||
}
|
||||
if (url.includes('/v3/conversations/conv1/activities')) {
|
||||
return jsonResponse({ id: 'activity1' });
|
||||
}
|
||||
throw new Error(`Unexpected fetch URL: ${url}`);
|
||||
});
|
||||
|
||||
const peer = `teams:${Buffer.from(JSON.stringify({ conversationId: 'conv1', serviceUrl: 'https://smba.trafficmanager.net/amer' }), 'utf8').toString('base64url')}`;
|
||||
await adapter.send(peer, { text: 'Hello Teams' });
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledTimes(2);
|
||||
expect(mockFetch.mock.calls[1]?.[0]).toContain('/v3/conversations/conv1/activities');
|
||||
});
|
||||
|
||||
it('handleActivity emits inbound message with replyPeerId', async () => {
|
||||
const adapter = new TeamsAdapter({ appId: 'id', appPassword: 'secret', requireMention: true });
|
||||
const inbound: InboundMessage[] = [];
|
||||
adapter.onMessage((msg) => inbound.push(msg));
|
||||
|
||||
await adapter.handleActivity({
|
||||
type: 'message',
|
||||
id: 'm1',
|
||||
text: '<at>Flynn</at> run diagnostics',
|
||||
serviceUrl: 'https://smba.trafficmanager.net/amer',
|
||||
conversation: { id: 'conv1', conversationType: 'channel' },
|
||||
from: { id: 'user1', name: 'Alice' },
|
||||
recipient: { id: 'bot1', name: 'Flynn' },
|
||||
entities: [{ type: 'mention', mentioned: { id: 'bot1', name: 'Flynn' } }],
|
||||
timestamp: '2026-02-16T00:00:00.000Z',
|
||||
});
|
||||
|
||||
expect(inbound).toHaveLength(1);
|
||||
expect(inbound[0]?.senderId).toBe('user1');
|
||||
expect(inbound[0]?.text).toBe('run diagnostics');
|
||||
const metadata = inbound[0]?.metadata as Record<string, unknown>;
|
||||
expect(typeof metadata?.replyPeerId).toBe('string');
|
||||
});
|
||||
|
||||
it('drops group messages without mention when require_mention=true', async () => {
|
||||
const adapter = new TeamsAdapter({ appId: 'id', appPassword: 'secret', requireMention: true });
|
||||
const handler = vi.fn();
|
||||
adapter.onMessage(handler);
|
||||
|
||||
await adapter.handleActivity({
|
||||
type: 'message',
|
||||
id: 'm2',
|
||||
text: 'hello bot',
|
||||
serviceUrl: 'https://smba.trafficmanager.net/amer',
|
||||
conversation: { id: 'conv1', conversationType: 'channel' },
|
||||
from: { id: 'user1', name: 'Alice' },
|
||||
recipient: { id: 'bot1', name: 'Flynn' },
|
||||
entities: [],
|
||||
});
|
||||
|
||||
expect(handler).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,263 @@
|
||||
import type { IncomingMessage, ServerResponse } from 'http';
|
||||
|
||||
import type {
|
||||
InboundMessage,
|
||||
OutboundMessage,
|
||||
ChannelAdapter,
|
||||
ChannelStatus,
|
||||
} from '../types.js';
|
||||
import { shouldIgnoreForMissingMention, splitMessage } from '../utils.js';
|
||||
import { readRequestBody } from '../../utils/httpBody.js';
|
||||
|
||||
export interface TeamsAdapterConfig {
|
||||
appId: string;
|
||||
appPassword: string;
|
||||
allowedConversationIds?: string[];
|
||||
requireMention?: boolean;
|
||||
}
|
||||
|
||||
interface BotTokenCache {
|
||||
token: string;
|
||||
expiresAt: number;
|
||||
}
|
||||
|
||||
interface TeamsActivity {
|
||||
type?: string;
|
||||
id?: string;
|
||||
text?: string;
|
||||
serviceUrl?: string;
|
||||
conversation?: { id?: string; conversationType?: string };
|
||||
from?: { id?: string; name?: string };
|
||||
recipient?: { id?: string; name?: string };
|
||||
entities?: Array<{ type?: string; mentioned?: { id?: string; name?: string } }>;
|
||||
timestamp?: string;
|
||||
}
|
||||
|
||||
const MAX_MESSAGE_LENGTH = 2500;
|
||||
|
||||
export class TeamsAdapter implements ChannelAdapter {
|
||||
readonly name = 'teams';
|
||||
|
||||
private _status: ChannelStatus = 'disconnected';
|
||||
private messageHandler?: (msg: InboundMessage) => void;
|
||||
private tokenCache: BotTokenCache | null = null;
|
||||
|
||||
constructor(private readonly config: TeamsAdapterConfig) {}
|
||||
|
||||
get status(): ChannelStatus {
|
||||
return this._status;
|
||||
}
|
||||
|
||||
onMessage(handler: (msg: InboundMessage) => void): void {
|
||||
this.messageHandler = handler;
|
||||
}
|
||||
|
||||
async connect(): Promise<void> {
|
||||
this._status = 'connected';
|
||||
}
|
||||
|
||||
async disconnect(): Promise<void> {
|
||||
this._status = 'disconnected';
|
||||
}
|
||||
|
||||
async send(peerId: string, message: OutboundMessage): Promise<void> {
|
||||
if (this._status !== 'connected') {
|
||||
throw new Error('Teams adapter not connected');
|
||||
}
|
||||
|
||||
const ref = this.decodePeerRef(peerId);
|
||||
const text = message.text.trim();
|
||||
if (!text) {
|
||||
return;
|
||||
}
|
||||
|
||||
const chunks = text.length > MAX_MESSAGE_LENGTH ? splitMessage(text, MAX_MESSAGE_LENGTH) : [text];
|
||||
for (const chunk of chunks) {
|
||||
await this.sendActivity(ref.serviceUrl, ref.conversationId, chunk);
|
||||
}
|
||||
}
|
||||
|
||||
async handleRequest(req: IncomingMessage, res: ServerResponse): Promise<void> {
|
||||
let body = '';
|
||||
try {
|
||||
body = await readRequestBody(req, { maxBytes: 1_048_576 });
|
||||
} catch {
|
||||
res.writeHead(400, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: 'Invalid request body' }));
|
||||
return;
|
||||
}
|
||||
|
||||
let activity: TeamsActivity;
|
||||
try {
|
||||
activity = JSON.parse(body) as TeamsActivity;
|
||||
} catch {
|
||||
res.writeHead(400, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: 'Invalid JSON' }));
|
||||
return;
|
||||
}
|
||||
|
||||
await this.handleActivity(activity);
|
||||
res.writeHead(202, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ accepted: true }));
|
||||
}
|
||||
|
||||
async handleActivity(activity: TeamsActivity): Promise<void> {
|
||||
if (!this.messageHandler) {
|
||||
return;
|
||||
}
|
||||
if (activity.type !== 'message') {
|
||||
return;
|
||||
}
|
||||
|
||||
const text = (activity.text ?? '').trim();
|
||||
const conversationId = activity.conversation?.id?.trim();
|
||||
const serviceUrl = activity.serviceUrl?.trim();
|
||||
const fromId = activity.from?.id?.trim();
|
||||
if (!text || !conversationId || !serviceUrl || !fromId) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.config.allowedConversationIds && this.config.allowedConversationIds.length > 0) {
|
||||
if (!this.config.allowedConversationIds.includes(conversationId)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const conversationType = (activity.conversation?.conversationType ?? '').toLowerCase();
|
||||
const isPersonal = conversationType === 'personal';
|
||||
const mentionsBot = this.activityMentionsBot(activity);
|
||||
if (shouldIgnoreForMissingMention({
|
||||
requireMention: this.config.requireMention,
|
||||
defaultRequireMention: true,
|
||||
mentionsBot: isPersonal || mentionsBot,
|
||||
})) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cleaned = this.stripLeadingMention(text).trim();
|
||||
if (!cleaned) {
|
||||
return;
|
||||
}
|
||||
|
||||
const peerRef = this.encodePeerRef({ conversationId, serviceUrl });
|
||||
this.messageHandler({
|
||||
id: activity.id ?? `teams-${Date.now()}`,
|
||||
channel: 'teams',
|
||||
senderId: fromId,
|
||||
senderName: activity.from?.name,
|
||||
text: cleaned,
|
||||
timestamp: activity.timestamp ? Date.parse(activity.timestamp) || Date.now() : Date.now(),
|
||||
metadata: {
|
||||
conversationId,
|
||||
serviceUrl,
|
||||
replyPeerId: peerRef,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private stripLeadingMention(text: string): string {
|
||||
return text
|
||||
.replace(/^\s*<at>[^<]+<\/at>\s*/i, '')
|
||||
.replace(/\s+/g, ' ');
|
||||
}
|
||||
|
||||
private activityMentionsBot(activity: TeamsActivity): boolean {
|
||||
const recipientId = activity.recipient?.id;
|
||||
if (!recipientId) {
|
||||
return false;
|
||||
}
|
||||
const mentionEntity = activity.entities?.find((entity) =>
|
||||
entity.type?.toLowerCase() === 'mention' && entity.mentioned?.id === recipientId,
|
||||
);
|
||||
return Boolean(mentionEntity);
|
||||
}
|
||||
|
||||
private async sendActivity(serviceUrl: string, conversationId: string, text: string): Promise<void> {
|
||||
const token = await this.getBotToken();
|
||||
const base = serviceUrl.replace(/\/+$/, '');
|
||||
const endpoint = `${base}/v3/conversations/${encodeURIComponent(conversationId)}/activities`;
|
||||
|
||||
const response = await fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
type: 'message',
|
||||
text,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const body = await response.text().catch(() => '');
|
||||
throw new Error(`Teams send failed (${response.status}): ${body}`);
|
||||
}
|
||||
}
|
||||
|
||||
private async getBotToken(): Promise<string> {
|
||||
const now = Date.now();
|
||||
if (this.tokenCache && this.tokenCache.expiresAt > now + 30_000) {
|
||||
return this.tokenCache.token;
|
||||
}
|
||||
|
||||
const form = new URLSearchParams();
|
||||
form.set('grant_type', 'client_credentials');
|
||||
form.set('client_id', this.config.appId);
|
||||
form.set('client_secret', this.config.appPassword);
|
||||
form.set('scope', 'https://api.botframework.com/.default');
|
||||
|
||||
const response = await fetch('https://login.microsoftonline.com/botframework.com/oauth2/v2.0/token', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: form.toString(),
|
||||
});
|
||||
if (!response.ok) {
|
||||
const body = await response.text().catch(() => '');
|
||||
throw new Error(`Teams token fetch failed (${response.status}): ${body}`);
|
||||
}
|
||||
|
||||
const payload = await response.json() as { access_token?: string; expires_in?: number };
|
||||
if (!payload.access_token) {
|
||||
throw new Error('Teams token fetch failed: missing access_token');
|
||||
}
|
||||
const expiresIn = typeof payload.expires_in === 'number' ? payload.expires_in : 3600;
|
||||
this.tokenCache = {
|
||||
token: payload.access_token,
|
||||
expiresAt: now + expiresIn * 1000,
|
||||
};
|
||||
return payload.access_token;
|
||||
}
|
||||
|
||||
private encodePeerRef(ref: { conversationId: string; serviceUrl: string }): string {
|
||||
const payload = Buffer.from(JSON.stringify(ref), 'utf8').toString('base64url');
|
||||
return `teams:${payload}`;
|
||||
}
|
||||
|
||||
private decodePeerRef(peerId: string): { conversationId: string; serviceUrl: string } {
|
||||
if (!peerId.startsWith('teams:')) {
|
||||
throw new Error('Teams peerId must use teams:<base64url-json> format');
|
||||
}
|
||||
const encoded = peerId.slice('teams:'.length);
|
||||
let decoded = '';
|
||||
try {
|
||||
decoded = Buffer.from(encoded, 'base64url').toString('utf8');
|
||||
} catch {
|
||||
throw new Error('Invalid Teams peer reference encoding');
|
||||
}
|
||||
|
||||
let parsed: { conversationId?: string; serviceUrl?: string } = {};
|
||||
try {
|
||||
parsed = JSON.parse(decoded) as { conversationId?: string; serviceUrl?: string };
|
||||
} catch {
|
||||
throw new Error('Invalid Teams peer reference payload');
|
||||
}
|
||||
if (!parsed.conversationId || !parsed.serviceUrl) {
|
||||
throw new Error('Teams peer reference missing conversationId/serviceUrl');
|
||||
}
|
||||
return {
|
||||
conversationId: parsed.conversationId,
|
||||
serviceUrl: parsed.serviceUrl,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { TeamsAdapter, type TeamsAdapterConfig } from './adapter.js';
|
||||
@@ -149,6 +149,16 @@ models:
|
||||
const matrix = redacted.matrix as Record<string, unknown>;
|
||||
expect(matrix.access_token).toBe('***');
|
||||
});
|
||||
|
||||
it('redacts app_password (teams)', () => {
|
||||
const config = {
|
||||
teams: { app_id: 'id', app_password: 'teams-secret' },
|
||||
models: { default: { provider: 'anthropic', model: 'claude' } },
|
||||
};
|
||||
const redacted = redactSecrets(config);
|
||||
const teams = redacted.teams as Record<string, unknown>;
|
||||
expect(teams.app_password).toBe('***');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDataDir', () => {
|
||||
|
||||
+1
-1
@@ -74,7 +74,7 @@ export function loadConfigSafe(configPath?: string): { config?: Config; error?:
|
||||
|
||||
/** Deep-clone config and replace sensitive fields with '***'. */
|
||||
export function redactSecrets(config: Record<string, unknown>): Record<string, unknown> {
|
||||
const sensitiveKeys = ['bot_token', 'api_key', 'auth_token', 'access_token'];
|
||||
const sensitiveKeys = ['bot_token', 'api_key', 'auth_token', 'access_token', 'app_password'];
|
||||
|
||||
function redact(obj: unknown): unknown {
|
||||
if (obj === null || obj === undefined) {return obj;}
|
||||
|
||||
@@ -350,6 +350,31 @@ describe('configSchema — signal', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('configSchema — teams', () => {
|
||||
const minimalConfig = {
|
||||
telegram: { bot_token: 'test', allowed_chat_ids: [1] },
|
||||
models: { default: { provider: 'anthropic', model: 'claude-3' } },
|
||||
};
|
||||
|
||||
it('accepts teams config and defaults allowlist/mention fields', () => {
|
||||
const result = configSchema.parse({
|
||||
...minimalConfig,
|
||||
teams: {
|
||||
app_id: 'app-id',
|
||||
app_password: 'app-password',
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.teams).toBeDefined();
|
||||
if (!result.teams) {
|
||||
throw new Error('Expected teams config');
|
||||
}
|
||||
expect(result.teams.app_id).toBe('app-id');
|
||||
expect(result.teams.allowed_conversation_ids).toEqual([]);
|
||||
expect(result.teams.require_mention).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('configSchema — whatsapp', () => {
|
||||
const minimalConfig = {
|
||||
telegram: { bot_token: 'test', allowed_chat_ids: [1] },
|
||||
|
||||
@@ -411,6 +411,13 @@ const signalSchema = z.object({
|
||||
send_timeout_ms: z.number().min(1000).max(60000).default(15000),
|
||||
}).optional();
|
||||
|
||||
const teamsSchema = z.object({
|
||||
app_id: z.string().min(1, 'Teams app_id is required'),
|
||||
app_password: z.string().min(1, 'Teams app_password is required'),
|
||||
allowed_conversation_ids: z.array(z.string()).default([]),
|
||||
require_mention: z.boolean().default(true),
|
||||
}).optional();
|
||||
|
||||
const browserSchema = z.object({
|
||||
enabled: z.boolean().default(false),
|
||||
executable_path: z.string().optional(),
|
||||
@@ -578,6 +585,7 @@ export const configSchema = z.object({
|
||||
whatsapp: whatsappSchema,
|
||||
matrix: matrixSchema,
|
||||
signal: signalSchema,
|
||||
teams: teamsSchema,
|
||||
server: serverSchema.default({}),
|
||||
models: modelsSchema,
|
||||
backends: backendsSchema.default({}),
|
||||
@@ -623,6 +631,7 @@ export type SlackConfig = z.infer<typeof slackSchema>;
|
||||
export type WhatsAppConfig = z.infer<typeof whatsappSchema>;
|
||||
export type MatrixConfig = z.infer<typeof matrixSchema>;
|
||||
export type SignalConfig = z.infer<typeof signalSchema>;
|
||||
export type TeamsConfig = z.infer<typeof teamsSchema>;
|
||||
export type RetryPolicyConfig = z.infer<typeof retrySchema>;
|
||||
export type ContextLevel = z.infer<typeof contextLevelSchema>;
|
||||
export type PromptConfig = z.infer<typeof promptSchema>;
|
||||
|
||||
+13
-1
@@ -1,6 +1,6 @@
|
||||
import type { Config } from '../config/index.js';
|
||||
import type { HookEngine } from '../hooks/index.js';
|
||||
import { ChannelRegistry, TelegramAdapter, WebChatAdapter, DiscordAdapter, SlackAdapter, WhatsAppAdapter, MatrixAdapter, SignalAdapter, PairingManager } from '../channels/index.js';
|
||||
import { ChannelRegistry, TelegramAdapter, WebChatAdapter, DiscordAdapter, SlackAdapter, WhatsAppAdapter, MatrixAdapter, SignalAdapter, TeamsAdapter, PairingManager } from '../channels/index.js';
|
||||
import { CronScheduler, WebhookHandler, GmailWatcher } from '../automation/index.js';
|
||||
import type { GatewayServer } from '../gateway/index.js';
|
||||
|
||||
@@ -101,6 +101,18 @@ export function registerChannels(deps: ChannelsDeps): ChannelsResult {
|
||||
channelRegistry.register(signalAdapter);
|
||||
}
|
||||
|
||||
// Register Microsoft Teams adapter (if configured)
|
||||
if (config.teams) {
|
||||
const teamsAdapter = new TeamsAdapter({
|
||||
appId: config.teams.app_id,
|
||||
appPassword: config.teams.app_password,
|
||||
allowedConversationIds: config.teams.allowed_conversation_ids.length > 0 ? config.teams.allowed_conversation_ids : undefined,
|
||||
requireMention: config.teams.require_mention,
|
||||
});
|
||||
channelRegistry.register(teamsAdapter);
|
||||
gateway.setTeamsHandler(teamsAdapter);
|
||||
}
|
||||
|
||||
// Register WebChat adapter (wraps the gateway)
|
||||
const webChatAdapter = new WebChatAdapter({ gateway });
|
||||
channelRegistry.register(webChatAdapter);
|
||||
|
||||
@@ -31,6 +31,7 @@ function makeBaseConfig(): Config {
|
||||
whatsapp: undefined,
|
||||
matrix: undefined,
|
||||
signal: undefined,
|
||||
teams: undefined,
|
||||
} as unknown as Config;
|
||||
}
|
||||
|
||||
@@ -45,6 +46,7 @@ describe('discoverServices', () => {
|
||||
expect.objectContaining({ name: 'telegram', status: 'not_configured' }),
|
||||
expect.objectContaining({ name: 'matrix', status: 'not_configured' }),
|
||||
expect.objectContaining({ name: 'signal', status: 'not_configured' }),
|
||||
expect.objectContaining({ name: 'teams', status: 'not_configured' }),
|
||||
expect.objectContaining({ name: 'cron', status: 'not_configured' }),
|
||||
expect.objectContaining({ name: 'mcp', status: 'not_configured' }),
|
||||
expect.objectContaining({ name: 'web_search', status: 'configured' }),
|
||||
|
||||
@@ -54,6 +54,7 @@ export function discoverServices(
|
||||
{ key: 'whatsapp', name: 'whatsapp', description: 'WhatsApp gateway' },
|
||||
{ key: 'matrix', name: 'matrix', description: 'Matrix bot' },
|
||||
{ key: 'signal', name: 'signal', description: 'Signal bot (signal-cli)' },
|
||||
{ key: 'teams', name: 'teams', description: 'Microsoft Teams bot' },
|
||||
];
|
||||
|
||||
for (const { key, name, description } of channelConfigs) {
|
||||
|
||||
@@ -43,6 +43,7 @@ import type { ComponentRegistry } from '../intents/index.js';
|
||||
import type { RoutingPolicy } from '../routing/index.js';
|
||||
import type { ChannelRegistry } from '../channels/index.js';
|
||||
import { RequestBodyTooLargeError, readRequestBody } from '../utils/httpBody.js';
|
||||
import type { TeamsAdapter } from '../channels/teams/adapter.js';
|
||||
|
||||
export interface GatewayServerConfig {
|
||||
port: number;
|
||||
@@ -93,6 +94,8 @@ export interface GatewayServerConfig {
|
||||
serviceType: string;
|
||||
txtRecord?: Record<string, string>;
|
||||
};
|
||||
/** Optional Teams adapter for inbound Bot Framework activity webhooks. */
|
||||
teamsHandler?: Pick<TeamsAdapter, 'handleRequest'>;
|
||||
}
|
||||
|
||||
export class GatewayServer {
|
||||
@@ -476,6 +479,12 @@ export class GatewayServer {
|
||||
return;
|
||||
}
|
||||
|
||||
// Teams Bot Framework events route — bypass gateway auth (Bot Framework posts directly)
|
||||
if (this.config.teamsHandler && req.method === 'POST' && req.url?.startsWith('/teams/events')) {
|
||||
await this.config.teamsHandler.handleRequest(req, res);
|
||||
return;
|
||||
}
|
||||
|
||||
// Apply auth to HTTP requests when configured
|
||||
const authConfig = this.config.auth ?? {};
|
||||
if (this.config.authHttp !== false && authConfig.token) {
|
||||
@@ -563,6 +572,11 @@ export class GatewayServer {
|
||||
this.config.gmailHandler = handler;
|
||||
}
|
||||
|
||||
/** Set the Teams handler for inbound Bot Framework activity HTTP routes (late binding). */
|
||||
setTeamsHandler(handler: Pick<TeamsAdapter, 'handleRequest'>): void {
|
||||
this.config.teamsHandler = handler;
|
||||
}
|
||||
|
||||
private async startDiscovery(host: string, port: number): Promise<void> {
|
||||
const discovery = this.config.discovery;
|
||||
if (!discovery?.enabled) {
|
||||
|
||||
Reference in New Issue
Block a user