diff --git a/.gitignore b/.gitignore
index cf87964f..b2cef02b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -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/
diff --git a/bot/README.md b/bot/README.md
index 0a197f98..deb29f94 100644
--- a/bot/README.md
+++ b/bot/README.md
@@ -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 |
diff --git a/bot/src/bridge.ts b/bot/src/bridge.ts
index 6de7cc4a..9323be9a 100644
--- a/bot/src/bridge.ts
+++ b/bot/src/bridge.ts
@@ -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 || "";
diff --git a/bot/teams-app/build-package.mjs b/bot/teams-app/build-package.mjs
index bb0a07d3..380e8e23 100644
--- a/bot/teams-app/build-package.mjs
+++ b/bot/teams-app/build-package.mjs
@@ -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";
@@ -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",
);
diff --git a/bot/teams-app/manifest.json b/bot/teams-app/manifest.json
index 70d3a48d..537caebc 100644
--- a/bot/teams-app/manifest.json
+++ b/bot/teams-app/manifest.json
@@ -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",
@@ -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"
}
diff --git a/docs/content/getting-started/teams-setup.mdx b/docs/content/getting-started/teams-setup.mdx
index 28616452..300f8b65 100644
--- a/docs/content/getting-started/teams-setup.mdx
+++ b/docs/content/getting-started/teams-setup.mdx
@@ -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.
+
+**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.
+
+
**Beta Status**: Teams integration is in beta. Features may change as we improve the integration. Requires Azure AD admin access.
@@ -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.
-**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`.
### 2.2 Grant Admin Consent
@@ -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
```
+
+**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.
+
+
**Alternative**: Add credentials through the web UI (Settings → Connections) for encrypted storage.
diff --git a/web/src/components/settings/ConnectionWizard.tsx b/web/src/components/settings/ConnectionWizard.tsx
index e1893f4c..69ff8945 100644
--- a/web/src/components/settings/ConnectionWizard.tsx
+++ b/web/src/components/settings/ConnectionWizard.tsx
@@ -1,4 +1,4 @@
-import { useState } from "react";
+import { useState, useEffect } from "react";
import { X, ArrowLeft, ArrowRight, CheckCircle2, Loader2, AlertCircle, ExternalLink, Zap } from "lucide-react";
import { cn } from "@/lib/utils";
import { ChannelSelector } from "./ChannelSelector";
@@ -47,13 +47,27 @@ const DISCORD_INSTRUCTIONS = [
];
const TEAMS_INSTRUCTIONS = [
- { text: "Go to", link: "https://portal.azure.com/#create/Microsoft.AzureBot", linkText: "Azure Portal" },
- { text: "Create a new Azure Bot resource — choose SingleTenant or MultiTenant" },
- { text: "Under Configuration, copy the Microsoft App ID" },
- { text: "Click Manage Password → New client secret and copy the value" },
- { text: "Note your Azure AD Tenant ID from the Azure Active Directory overview (required for SingleTenant)" },
- { text: "Under Channels, add the Microsoft Teams channel and save" },
- { text: "Set the messaging endpoint to your bot's webhook URL" },
+ {
+ text: "Create an Azure Bot resource",
+ link: "https://portal.azure.com/#create/Microsoft.AzureBot",
+ linkText: "in Azure Portal",
+ details: ["App type: SingleTenant (recommended) or MultiTenant"],
+ },
+ {
+ text: "Expose this bridge over HTTPS, then set the Bot's Messaging endpoint to your URL + /api/teams",
+ details: [
+ "Local dev: ngrok http 3001",
+ "https:///api/teams",
+ ],
+ },
+ { text: "Copy the Microsoft App ID from Bot → Configuration" },
+ { text: "On the linked App Registration → Manage Password, create a client secret and copy the VALUE (shown once)" },
+ { text: "Copy the Tenant ID from Azure Active Directory → Overview" },
+ { text: "On the Bot resource → Channels, add the Microsoft Teams channel" },
+ {
+ text: "On API permissions, add Microsoft Graph application permission, then Grant admin consent — a Global Admin must do this",
+ details: ["Channel.ReadBasic.All"],
+ },
];
const TELEGRAM_INSTRUCTIONS = [
@@ -70,7 +84,43 @@ const MATTERMOST_INSTRUCTIONS = [
{ text: "Add the bot user to any channels where it should read from. The bot will only receive events from channels it is a member of" },
];
-const CREDENTIAL_FIELDS: Record = {
+interface CredentialField {
+ key: string;
+ label: string;
+ placeholder: string;
+ type?: string;
+ optional?: boolean;
+ /** When present, render as a `