Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ docker-compose.override.yml
# Beever Atlas specific
bot/node_modules/
bot/dist/
bot/teams-app/beever-atlas-bot.zip
bot/teams-app/beever-atlas-teams.zip
web/node_modules/
web/dist/

Expand Down
2 changes: 1 addition & 1 deletion bot/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ See [`.env.example`](../.env.example) for the canonical list and descriptions. K
|---|---|
| `SLACK_BOT_TOKEN`, `SLACK_SIGNING_SECRET` | Slack Events API adapter |
| `DISCORD_BOT_TOKEN`, `DISCORD_PUBLIC_KEY`, `DISCORD_APPLICATION_ID` | Discord interactions adapter |
| `TEAMS_APP_ID`, `TEAMS_APP_PASSWORD` | Microsoft Teams / Azure Bot adapter |
| `TEAMS_APP_ID`, `TEAMS_APP_PASSWORD`, `TEAMS_APP_TENANT_ID` | Microsoft Teams / Azure Bot adapter. The bot detects SingleTenant vs MultiTenant from the presence of `TEAMS_APP_TENANT_ID` (see `registerTeamsFromEnvIfPresent` in `src/index.ts`) — SingleTenant is the supported path; MultiTenant requires extra MSAL configuration. Tenant id is also required for any call into `fetchMessages`. |
| `TELEGRAM_BOT_TOKEN` | Telegram Bot API adapter |
| `MATTERMOST_BASE_URL`, `MATTERMOST_BOT_TOKEN` | Mattermost outgoing-webhook adapter |

Expand Down
38 changes: 36 additions & 2 deletions bot/src/bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2913,8 +2913,42 @@ async function handleValidateAdapter(
appTenantId: credentials.appTenantId,
appType: credentials.appType || "MultiTenant",
});
// Teams adapter creation validates credentials format; no simple ping API
jsonResponse(res, 200, { valid: true, message: "Adapter created successfully. Verify messaging endpoint is configured in Azure." });
// Real validation: actually mint a Graph token via MSAL. The previous
// construct-only check let users typo the App ID (e.g. paste a display
// name like "Teams" instead of the GUID) and only catch it later when
// channel enumeration silently returned []. We exercise the token mint
// by hitting a minimal Graph endpoint:
// • 2xx → credentials valid
// • AADSTS / unauthorized_client → credentials wrong (400)
// • other (403/404/network) → creds look valid; soft accept
try {
const graph = (tempAdapter as any).app?.graph;
if (!graph) {
jsonResponse(res, 200, {
valid: true,
message: "Adapter constructed. Token mint could not be exercised; verify the messaging endpoint and Graph admin consent in Azure.",
});
return;
}
await graph.http.get("/applications?$top=1");
jsonResponse(res, 200, { valid: true, message: "Credentials valid (Graph token minted successfully)." });
} catch (err) {
const msg = safeErrorMessage(err);
const lower = msg.toLowerCase();
const isAuthFailure =
/aadsts\d{4,6}/i.test(msg) ||
lower.includes("unauthorized_client") ||
lower.includes("invalid_client") ||
lower.includes("was not found in the directory");
if (isAuthFailure) {
jsonResponse(res, 200, { valid: false, error: `Microsoft rejected the credentials: ${msg}` });
} else {
jsonResponse(res, 200, {
valid: true,
message: `Credentials look valid but a Graph probe failed: ${msg}. Check Channel.ReadBasic.All admin consent if channel listing fails later.`,
});
}
}
} else if (platform === "mattermost") {
const baseUrl = (credentials.baseUrl || credentials.server_url || "").replace(/\/+$/, "");
const botToken = credentials.botToken || credentials.bot_token || "";
Expand Down
6 changes: 3 additions & 3 deletions bot/teams-app/build-package.mjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#!/usr/bin/env node
// Builds beever-atlas-bot.zip from manifest.json + generated placeholder icons.
// Builds beever-atlas-teams.zip from manifest.json + generated placeholder icons.
// Replace outline.png / color.png with real artwork before production release.

import { writeFileSync } from "node:fs";
Expand Down Expand Up @@ -67,10 +67,10 @@ writeFileSync(join(here, "outline.png"), solidPng(32, 32, 0xff, 0xff, 0xff));
console.log("✓ generated color.png (192x192) and outline.png (32x32)");

execSync(
`cd "${here}" && rm -f beever-atlas-bot.zip && zip -j beever-atlas-bot.zip manifest.json color.png outline.png`,
`cd "${here}" && rm -f beever-atlas-teams.zip && zip -j beever-atlas-teams.zip manifest.json color.png outline.png`,
{ stdio: "inherit" },
);
console.log(`\n✓ built ${join(here, "beever-atlas-bot.zip")}`);
console.log(`\n✓ built ${join(here, "beever-atlas-teams.zip")}`);
console.log(
"\nSideload: Teams → Apps → Manage your apps → Upload a custom app → pick this zip",
);
45 changes: 38 additions & 7 deletions bot/teams-app/manifest.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
{
"$schema": "https://developer.microsoft.com/en-us/json-schemas/teams/v1.16/MicrosoftTeams.schema.json",
"manifestVersion": "1.16",
"version": "1.0.1",
"id": "eefc03cb-3132-46a1-ac50-e200792849b6",
"$schema": "https://developer.microsoft.com/en-us/json-schemas/teams/v1.25/MicrosoftTeams.schema.json",
"manifestVersion": "1.25",
"version": "1.0.3",
"id": "fb24e83f-52e6-40a4-bafb-764160e4ca7a",
"packageName": "ai.beever.atlas",
"developer": {
"name": "Beever AI",
Expand All @@ -23,12 +23,43 @@
"color": "color.png"
},
"accentColor": "#5B4ECC",
"staticTabs": [
{
"entityId": "conversations",
"scopes": ["personal"]
},
{
"entityId": "about",
"scopes": ["personal"]
}
],
"bots": [
{
"botId": "eefc03cb-3132-46a1-ac50-e200792849b6",
"scopes": ["personal", "team", "groupchat"],
"botId": "fb24e83f-52e6-40a4-bafb-764160e4ca7a",
"scopes": ["personal", "team", "groupChat"],
"supportsFiles": false,
"supportsCalling": false,
"supportsVideo": false,
"isNotificationOnly": false
}
]
],
"validDomains": ["*.botframework.com"],
"webApplicationInfo": {
"id": "fb24e83f-52e6-40a4-bafb-764160e4ca7a"
},
"authorization": {
"permissions": {
"resourceSpecific": [
{
"name": "ChannelMessage.Read.Group",
"type": "Application"
},
{
"name": "ChatMessage.Read.Chat",
"type": "Application"
}
]
}
},
"supportsChannelFeatures": "tier1"
}
33 changes: 23 additions & 10 deletions docs/content/getting-started/teams-setup.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ description: Connect Beever Atlas to Microsoft Teams

Connect Beever Atlas to Microsoft Teams to automatically ingest conversations and build a searchable knowledge base.

<Callout type="info">
**Preferred path: the UI wizard.** Beever Atlas Settings → Integrations → Connect Microsoft Teams walks through every step below with inline guidance, the right App Type default (SingleTenant), and a runnable ngrok / endpoint snippet. Use this doc for context and as a reference for `.env`-based provisioning.
</Callout>

<Callout type="warning">
**Beta Status**: Teams integration is in beta. Features may change as we improve the integration. Requires Azure AD admin access.
</Callout>
Expand Down Expand Up @@ -57,17 +61,18 @@ In the application overview:
3. Select **Microsoft Graph**
4. Select **Application permissions** (for background sync):

Add the following permissions:
Add at minimum:

| Permission | Purpose |
|------------|---------|
| `ChannelMessage.Read.All` | Read all channel messages |
| `Chat.Read` | Read chat messages |
| `User.Read.All` | Read user information |
| `Files.Read.All` | Access file attachments |
| `Channel.ReadBasic.All` | **Required.** Channel enumeration via `GET /teams/{id}/channels` so workspaces and channels appear in the sidebar without an @mention. Beever Atlas falls back to a Redis-cached identity scan if this is missing, but loses the workspace whenever the cache is wiped. |
| `User.Read.All` | Optional. Look up author display names + emails when receiving messages. |
| `Chat.Read.All` | Optional. Fetch DM message history. |

The historical permissions table (`ChannelMessage.Read.All`, `Chat.Read`, `Files.Read.All`) is **superseded** — the live bot uses RSC (Resource Specific Consent) declared in the Teams app manifest at `bot/teams-app/manifest.json` for per-message reads, so those tenant-wide Application permissions are no longer required.

<Callout type="warning">
**Application vs Delegated**: We use Application permissions for background sync (no user interaction required). This requires admin consent.
**Application vs Delegated**: We use Application permissions for background sync (no user interaction required). This requires admin consent — **only a Global Administrator can grant consent for Microsoft Graph application permissions**. Application Administrator and Cloud Application Administrator both return `Authorization_RequestDenied`.
</Callout>

### 2.2 Grant Admin Consent
Expand Down Expand Up @@ -120,12 +125,20 @@ ADAPTER_MOCK=false
Add the following to your `.env`:

```bash
# Microsoft Teams / Graph API
TEAMS_TENANT_ID=your_tenant_id_here
TEAMS_CLIENT_ID=your_client_id_here
TEAMS_CLIENT_SECRET=your_client_secret_here
# Microsoft Teams / Azure Bot adapter (env vars used by the bot's
# `registerTeamsFromEnvIfPresent` startup path — see bot/src/index.ts).
# These match the keys the UI wizard writes into the encrypted
# `platform_connections` document, with snake_case here vs camelCase
# inside the Chat SDK adapter.
TEAMS_APP_ID=your_microsoft_app_id # Azure AD Application (client) ID
TEAMS_APP_PASSWORD=your_azure_client_secret # Client secret value (NOT the secret id)
TEAMS_APP_TENANT_ID=your_azure_tenant_id # Directory (tenant) ID — required
```

<Callout type="warning">
**Historical names removed.** Earlier revisions of this doc referenced `TEAMS_TENANT_ID`, `TEAMS_CLIENT_ID`, `TEAMS_CLIENT_SECRET`. Those variables are **not read by the current bot** — the code uses the `TEAMS_APP_*` names above. If your `.env` still has the old names, rename them or you'll get a "no connections found" startup log even with correct values.
</Callout>

<Callout type="info">
**Alternative**: Add credentials through the web UI (Settings → Connections) for encrypted storage.
</Callout>
Expand Down
Loading
Loading