diff --git a/.agent-guidance/features/teams-integration.md b/.agent-guidance/features/teams-integration.md index bf3218a5..036de5b7 100644 --- a/.agent-guidance/features/teams-integration.md +++ b/.agent-guidance/features/teams-integration.md @@ -44,16 +44,15 @@ callers build providers via `/api/auth/oauth2/callback/microsoft-entra-id`, create a client secret, and save client/tenant IDs plus the secret in the comms settings card (or the `ROOMOTE_AUTH_MICROSOFT_*` env vars). The same app - serves user sign-in and the bot; in the setup wizard, Roomote requires this - single-app path and stores hidden `TEAMS_BOT_APP_ID`, - `TEAMS_BOT_APP_PASSWORD`, and `TEAMS_BOT_TENANT_ID` values copied from the - Microsoft Client ID/secret/tenant. Dedicated `TEAMS_BOT_*` env vars and the - optional `TEAMS_BOT_TOKEN_ENDPOINT` / `TEAMS_BOT_OAUTH_SCOPE` overrides are - still available from Settings or environment variables for deployments that - intentionally diverge after setup. + serves user sign-in and the bot; setup stores only the Microsoft sign-in + values. Runtime bot credentials fall back from those + (`resolveTeamsBotRuntimeCredentials`). Dedicated `TEAMS_BOT_*` env vars and + the optional `TEAMS_BOT_TOKEN_ENDPOINT` / `TEAMS_BOT_OAUTH_SCOPE` overrides + remain available from Settings or environment variables for deployments that + intentionally use a separate bot app registration. 2. **Microsoft values**: paste the Client ID, client secret, and Tenant ID in - setup. Roomote stores these for Microsoft sign-in and copies them into the - hidden setup-only bot fields for the single-app path. + setup. Roomote stores these for Microsoft sign-in; the same ownership layer + satisfies Teams bot credentials at runtime without a second write. 3. **Teams app package**: download the generated app package (manifest + icons zip, bot id and deployment URLs pre-filled, built by `apps/web/src/lib/server/teams-app-package.ts`) and upload it in Teams diff --git a/.agent-guidance/features/web-dashboard.md b/.agent-guidance/features/web-dashboard.md index 7f456770..5fa0e194 100644 --- a/.agent-guidance/features/web-dashboard.md +++ b/.agent-guidance/features/web-dashboard.md @@ -772,7 +772,7 @@ The provider-setup portion intentionally mirrors the deferred model-provider ste - fields already satisfied by saved deployment env vars stay masked until replaced - the model step's OpenRouter choice also offers a `Connect with OpenRouter` button that runs OpenRouter's PKCE OAuth flow: `/api/openrouter-oauth/initiate` stores a one-time verifier in a short-lived HTTP-only cookie and redirects to `openrouter.ai/auth`, then `/api/openrouter-oauth/callback` exchanges the returned code for a user-controlled API key and saves it through the same encrypted `OPENROUTER_API_KEY` deployment env-var path as a pasted key before redirecting back to `/setup?step=env-vars` with a success or error toast - Slack setup now includes `SLACK_SIGNING_SECRET` in addition to the shared `SLACK_CLIENT_ID` / `SLACK_CLIENT_SECRET`; `SLACK_APP_ID` is optional, and `SLACK_REDIRECT_URI` is inferred from `ROOMOTE_APP_URL` when omitted -- Microsoft Teams setup requires the Microsoft sign-in fields and every `TEAMS_BOT_*` field so operators configure Teams sign-in and Teams communications in one pass; Microsoft sign-in values alone do not satisfy the communication-provider step +- Microsoft Teams setup requires only the Microsoft sign-in fields (`ROOMOTE_AUTH_MICROSOFT_*`); the same app doubles as the Teams bot at runtime, so dedicated `TEAMS_BOT_*` values are optional dual-app overrides rather than a second setup write - the communication connect step is provider-aware for Slack and Microsoft Teams: runtime-configured Slack shows the Slack app-install CTA, while runtime-configured Teams reads `teams.integrationStatus` and either opens the Teams bot URL or shows a blocked state when the bot app ID is missing; the communication provider chooser and both connect variants also expose a subtle `Do this later` link that records a `communicationStep: 'skipped'` marker in the localStorage setup session (`setup-session.ts`) so communication setup stays skipped for the rest of the browser session instead of bouncing back on the next status refresh - source-control connect routes runtime-configured GitHub to the GitHub App installation flow and runtime-configured GitLab, Gitea, or Azure DevOps to token-backed repository sync - If the visitor is still signed out, `/setup` now exposes only this bootstrap provider-configuration segment first. After those values are saved, the route starts the selected provider sign-in directly from the page, then resumes the signed-in setup flow on the same `/setup` URL. diff --git a/apps/docs/providers/communications/microsoft-teams.mdx b/apps/docs/providers/communications/microsoft-teams.mdx index d578843f..03809adc 100644 --- a/apps/docs/providers/communications/microsoft-teams.mdx +++ b/apps/docs/providers/communications/microsoft-teams.mdx @@ -36,10 +36,10 @@ ROOMOTE_AUTH_MICROSOFT_TENANT_ID=... Roomote enables Microsoft sign-in only when all three values are present. For a single-purpose Teams deployment, use the same Entra app for Microsoft -sign-in and the Teams bot. During Roomote setup, Roomote stores hidden Teams bot -credentials copied from the Microsoft client ID, secret, and tenant values. You -can override those bot credentials later from **Settings > Comms** if you need a -separate bot identity. +sign-in and the Teams bot. Roomote setup stores only the Microsoft sign-in +values; at runtime the bot reuses those credentials automatically. You can set +dedicated bot credentials later from **Settings > Comms** if you need a separate +bot identity. After Microsoft sign-in, **Settings > Linked Accounts** shows the Microsoft Teams account. Users who signed in with another provider can link Microsoft @@ -56,13 +56,13 @@ Set the bot messaging endpoint to: /api/webhooks/teams ``` -Enable the Microsoft Teams channel in the bot resource. Roomote setup stores -these bot values automatically from the Microsoft app values. If you configure -with env vars instead, set: +Enable the Microsoft Teams channel in the bot resource. When the Microsoft +sign-in values above are present, Roomote uses them for the bot as well. To use +a separate bot app or configure with dedicated bot env vars instead, set: ```sh -TEAMS_BOT_APP_ID= -TEAMS_BOT_APP_PASSWORD= +TEAMS_BOT_APP_ID= +TEAMS_BOT_APP_PASSWORD= TEAMS_BOT_TENANT_ID= ``` diff --git a/apps/web/src/app/(onboarding)/setup/ProviderSetupExperience.tsx b/apps/web/src/app/(onboarding)/setup/ProviderSetupExperience.tsx index 04f5a7f8..3a44a052 100644 --- a/apps/web/src/app/(onboarding)/setup/ProviderSetupExperience.tsx +++ b/apps/web/src/app/(onboarding)/setup/ProviderSetupExperience.tsx @@ -27,14 +27,10 @@ type ProviderFieldStatus = ProviderStatus['fields'][number]; const MASKED_VALUE = '••••••••••••••••••••••••••••'; -const MICROSOFT_SINGLE_APP_BOT_FIELD_SOURCES: Record = { - TEAMS_BOT_APP_ID: 'ROOMOTE_AUTH_MICROSOFT_CLIENT_ID', - TEAMS_BOT_APP_PASSWORD: 'ROOMOTE_AUTH_MICROSOFT_CLIENT_SECRET', - TEAMS_BOT_TENANT_ID: 'ROOMOTE_AUTH_MICROSOFT_TENANT_ID', -}; - const MICROSOFT_SETUP_HIDDEN_ENV_VAR_NAMES = new Set([ - ...Object.keys(MICROSOFT_SINGLE_APP_BOT_FIELD_SOURCES), + 'TEAMS_BOT_APP_ID', + 'TEAMS_BOT_APP_PASSWORD', + 'TEAMS_BOT_TENANT_ID', 'TEAMS_BOT_TOKEN_ENDPOINT', 'TEAMS_BOT_OAUTH_SCOPE', ]); @@ -54,80 +50,23 @@ export function getSetupVisibleFields(provider: ProviderStatus | null) { } export function getSetupEffectiveFieldValue({ - provider, field, values, }: { - provider: ProviderStatus | null; + provider?: ProviderStatus | null; field: ProviderFieldStatus; values: Record; }) { - const ownValue = values[field.envVarName] ?? ''; - - if (ownValue.length > 0) { - return ownValue; - } - - if ( - provider?.id !== 'microsoft' || - field.runtimeSatisfied || - field.savedSatisfied - ) { - return ''; - } - - const sourceEnvVarName = - MICROSOFT_SINGLE_APP_BOT_FIELD_SOURCES[field.envVarName]; - - return sourceEnvVarName ? (values[sourceEnvVarName] ?? '') : ''; + return values[field.envVarName] ?? ''; } export function getSetupSubmitValues({ - provider, values, }: { - provider: ProviderStatus | null; + provider?: ProviderStatus | null; values: Record; }) { - if (!provider) { - return values; - } - - const nextValues = { ...values }; - - for (const field of getSetupVisibleFields(provider)) { - if (nextValues[field.envVarName]?.trim()) { - continue; - } - - const copiedValue = getSetupEffectiveFieldValue({ - provider, - field, - values, - }); - - if (copiedValue.trim()) { - nextValues[field.envVarName] = copiedValue; - } - } - - if (provider.id === 'microsoft') { - for (const [envVarName, sourceEnvVarName] of Object.entries( - MICROSOFT_SINGLE_APP_BOT_FIELD_SOURCES, - )) { - if (nextValues[envVarName]?.trim()) { - continue; - } - - const copiedValue = values[sourceEnvVarName]; - - if (copiedValue?.trim()) { - nextValues[envVarName] = copiedValue; - } - } - } - - return nextValues; + return values; } function NumberedStep({ @@ -374,8 +313,8 @@ function MicrosoftSetupExperience(props: ProviderSetupExperienceProps) {

Enter the Microsoft app values.

- Roomote stores these values for Microsoft sign-in and also saves - hidden Teams bot credentials copied from them. + Roomote stores these values for Microsoft sign-in. The same app also + powers the Teams bot at runtime.

diff --git a/apps/web/src/app/(onboarding)/setup/StepAuthEnvVars.client.test.tsx b/apps/web/src/app/(onboarding)/setup/StepAuthEnvVars.client.test.tsx index 544879e1..99bcfb70 100644 --- a/apps/web/src/app/(onboarding)/setup/StepAuthEnvVars.client.test.tsx +++ b/apps/web/src/app/(onboarding)/setup/StepAuthEnvVars.client.test.tsx @@ -245,6 +245,7 @@ function buildAuthSetup( envVarName: 'TEAMS_BOT_APP_ID', acceptedEnvVarNames: ['TEAMS_BOT_APP_ID'], label: 'Teams Bot App ID', + required: false, runtimeSatisfied: false, savedSatisfied: false, satisfiedByEnvVarName: null, @@ -254,6 +255,7 @@ function buildAuthSetup( acceptedEnvVarNames: ['TEAMS_BOT_APP_PASSWORD'], label: 'Teams Bot App Password', secret: true, + required: false, runtimeSatisfied: false, savedSatisfied: false, satisfiedByEnvVarName: null, @@ -262,6 +264,7 @@ function buildAuthSetup( envVarName: 'TEAMS_BOT_TENANT_ID', acceptedEnvVarNames: ['TEAMS_BOT_TENANT_ID'], label: 'Teams Bot Tenant ID', + required: false, runtimeSatisfied: false, savedSatisfied: false, satisfiedByEnvVarName: null, @@ -507,7 +510,7 @@ describe('StepAuthEnvVars', () => { ); }); - it('submits hidden Teams bot values for the single Microsoft app path', async () => { + it('submits only Microsoft sign-in values for the single app path', async () => { const mutateAsync = setupMutationMock(); render( @@ -543,16 +546,13 @@ describe('StepAuthEnvVars', () => { await waitFor(() => { expect(mutateAsync).toHaveBeenCalledWith({ provider: 'microsoft', - values: expect.objectContaining({ + values: { ROOMOTE_AUTH_MICROSOFT_CLIENT_ID: '11111111-2222-3333-4444-555555555555', ROOMOTE_AUTH_MICROSOFT_CLIENT_SECRET: 'client-secret', ROOMOTE_AUTH_MICROSOFT_TENANT_ID: '22222222-3333-4444-5555-666666666666', - TEAMS_BOT_APP_ID: '11111111-2222-3333-4444-555555555555', - TEAMS_BOT_APP_PASSWORD: 'client-secret', - TEAMS_BOT_TENANT_ID: '22222222-3333-4444-5555-666666666666', - }), + }, }); }); }); diff --git a/apps/web/src/app/(onboarding)/setup/StepAuthEnvVars.tsx b/apps/web/src/app/(onboarding)/setup/StepAuthEnvVars.tsx index 198d1d6f..3f88f8e9 100644 --- a/apps/web/src/app/(onboarding)/setup/StepAuthEnvVars.tsx +++ b/apps/web/src/app/(onboarding)/setup/StepAuthEnvVars.tsx @@ -181,21 +181,23 @@ export function StepAuthEnvVars({ typeof window === 'undefined' ? 'https://your-deployment-url' : window.location.origin; - const teamsBotAppIdField = selectedProvider?.fields.find( - (field) => field.envVarName === 'TEAMS_BOT_APP_ID', + const microsoftClientIdField = selectedProvider?.fields.find( + (field) => field.envVarName === 'ROOMOTE_AUTH_MICROSOFT_CLIENT_ID', ); - const enteredTeamsBotAppId = - isMicrosoftProvider && teamsBotAppIdField + const enteredMicrosoftClientId = + isMicrosoftProvider && microsoftClientIdField ? getSetupEffectiveFieldValue({ provider: selectedProvider, - field: teamsBotAppIdField, + field: microsoftClientIdField, values, }).trim() : ''; - const typedTeamsBotAppId = MICROSOFT_APP_ID_PATTERN.test(enteredTeamsBotAppId) - ? enteredTeamsBotAppId + const typedMicrosoftClientId = MICROSOFT_APP_ID_PATTERN.test( + enteredMicrosoftClientId, + ) + ? enteredMicrosoftClientId : ''; - const teamsBotAppIdStored = isMicrosoftProvider + const microsoftClientIdStored = isMicrosoftProvider ? selectedProvider.fields.some( (field) => (field.envVarName === 'TEAMS_BOT_APP_ID' || @@ -203,9 +205,9 @@ export function StepAuthEnvVars({ (field.runtimeSatisfied || field.savedSatisfied), ) : false; - const teamsAppPackageHref = typedTeamsBotAppId - ? `/api/setup/teams-app-package?botAppId=${encodeURIComponent(typedTeamsBotAppId)}` - : !bootstrapMode && teamsBotAppIdStored + const teamsAppPackageHref = typedMicrosoftClientId + ? `/api/setup/teams-app-package?botAppId=${encodeURIComponent(typedMicrosoftClientId)}` + : !bootstrapMode && microsoftClientIdStored ? '/api/teams/app-package' : null; const [showManualSlackValues, setShowManualSlackValues] = useState(false); diff --git a/apps/web/src/trpc/commands/setup-new/index.ts b/apps/web/src/trpc/commands/setup-new/index.ts index 8b1c63c8..d82f053c 100644 --- a/apps/web/src/trpc/commands/setup-new/index.ts +++ b/apps/web/src/trpc/commands/setup-new/index.ts @@ -20,6 +20,7 @@ import { inArray, isNull, sql, + invalidateTeamsBotRuntimeCredentialsCache, markTaskStartParallelCountEndedAt, resolveDeploymentEnvVar, resolveSavedWorkerImage, @@ -1583,7 +1584,7 @@ async function saveSetupAuthConfig(input: { } const provider = getSetupAuthProvider(input.provider); - return db.transaction(async (tx) => { + const result = await db.transaction(async (tx) => { const [currentState, persistedEnvVarNames] = await Promise.all([ getPersistedSetupNewState(tx), getPersistedEnvironmentVariableNames(tx), @@ -1655,6 +1656,12 @@ async function saveSetupAuthConfig(input: { setupNewState, }; }); + + if (input.provider === 'microsoft') { + invalidateTeamsBotRuntimeCredentialsCache(); + } + + return result; } export async function saveSetupNewSourceControlProviderChoiceCommand( diff --git a/packages/types/src/setup-auth-config.test.ts b/packages/types/src/setup-auth-config.test.ts index 0aaaed9e..01e8996a 100644 --- a/packages/types/src/setup-auth-config.test.ts +++ b/packages/types/src/setup-auth-config.test.ts @@ -4,9 +4,6 @@ const REQUIRED_MICROSOFT_TEAMS_ENV_VARS = [ 'ROOMOTE_AUTH_MICROSOFT_CLIENT_ID', 'ROOMOTE_AUTH_MICROSOFT_CLIENT_SECRET', 'ROOMOTE_AUTH_MICROSOFT_TENANT_ID', - 'TEAMS_BOT_APP_ID', - 'TEAMS_BOT_APP_PASSWORD', - 'TEAMS_BOT_TENANT_ID', ]; describe('buildSetupAuthStatus', () => { @@ -40,17 +37,13 @@ describe('buildSetupAuthStatus', () => { }); }); - it('does not satisfy Teams setup with only Microsoft sign-in values', () => { + it('satisfies Teams setup with only Microsoft sign-in values', () => { const status = buildSetupAuthStatus({ - persistedEnvVarNames: [ - 'ROOMOTE_AUTH_MICROSOFT_CLIENT_ID', - 'ROOMOTE_AUTH_MICROSOFT_CLIENT_SECRET', - 'ROOMOTE_AUTH_MICROSOFT_TENANT_ID', - ], + persistedEnvVarNames: REQUIRED_MICROSOFT_TEAMS_ENV_VARS, }); expect(status.setupSatisfiedByRuntimeEnv).toBe(false); - expect(status.preselectedProvider).toBe('slack'); + expect(status.preselectedProvider).toBe('microsoft'); expect(status.selectedProvider).toBeNull(); expect(status.runtimeConfiguredProvider).toBeNull(); expect(status.runtimeConfiguredProviders).toEqual([]); @@ -59,22 +52,21 @@ describe('buildSetupAuthStatus', () => { status.providers.find((provider) => provider.id === 'microsoft'), ).toMatchObject({ runtimeSatisfied: false, - savedSatisfied: false, - setupSatisfied: false, + savedSatisfied: true, + setupSatisfied: true, }); }); - it('preselects the saved Teams provider when required sign-in and bot values exist', () => { + it('still accepts optional dedicated TEAMS_BOT_* overrides when present', () => { const status = buildSetupAuthStatus({ - persistedEnvVarNames: REQUIRED_MICROSOFT_TEAMS_ENV_VARS, + persistedEnvVarNames: [ + ...REQUIRED_MICROSOFT_TEAMS_ENV_VARS, + 'TEAMS_BOT_APP_ID', + 'TEAMS_BOT_APP_PASSWORD', + 'TEAMS_BOT_TENANT_ID', + ], }); - expect(status.setupSatisfiedByRuntimeEnv).toBe(false); - expect(status.preselectedProvider).toBe('microsoft'); - expect(status.selectedProvider).toBeNull(); - expect(status.runtimeConfiguredProvider).toBeNull(); - expect(status.runtimeConfiguredProviders).toEqual([]); - expect(status.lockReason).toBeNull(); expect( status.providers.find((provider) => provider.id === 'microsoft'), ).toMatchObject({ @@ -82,6 +74,12 @@ describe('buildSetupAuthStatus', () => { savedSatisfied: true, setupSatisfied: true, }); + expect( + status.providers + .find((provider) => provider.id === 'microsoft') + ?.fields.filter((field) => field.envVarName.startsWith('TEAMS_BOT_')) + .every((field) => field.required === false), + ).toBe(true); }); it('honors Slack shared-credential fallback compatibility', () => { @@ -164,11 +162,6 @@ describe('buildSetupAuthStatus', () => { ROOMOTE_AUTH_MICROSOFT_CLIENT_ID: 'microsoft-client-id', ROOMOTE_AUTH_MICROSOFT_CLIENT_SECRET: 'microsoft-client-secret', ROOMOTE_AUTH_MICROSOFT_TENANT_ID: 'microsoft-tenant-id', - TEAMS_BOT_APP_ID: 'teams-bot-app-id', - TEAMS_BOT_APP_PASSWORD: 'teams-bot-app-password', - TEAMS_BOT_TENANT_ID: 'teams-bot-tenant-id', - TEAMS_BOT_TOKEN_ENDPOINT: 'https://login.example.test/token', - TEAMS_BOT_OAUTH_SCOPE: 'https://api.botframework.com/.default', }, }); diff --git a/packages/types/src/setup-auth-config.ts b/packages/types/src/setup-auth-config.ts index 7c4d6dc4..4fc086e9 100644 --- a/packages/types/src/setup-auth-config.ts +++ b/packages/types/src/setup-auth-config.ts @@ -108,17 +108,20 @@ export const SETUP_AUTH_PROVIDER_CATALOG = [ envVarName: 'TEAMS_BOT_APP_ID', acceptedEnvVarNames: ['TEAMS_BOT_APP_ID'], label: 'Teams Bot App ID', + required: false, }, { envVarName: 'TEAMS_BOT_APP_PASSWORD', acceptedEnvVarNames: ['TEAMS_BOT_APP_PASSWORD'], label: 'Teams Bot App Password', secret: true, + required: false, }, { envVarName: 'TEAMS_BOT_TENANT_ID', acceptedEnvVarNames: ['TEAMS_BOT_TENANT_ID'], label: 'Teams Bot Tenant ID', + required: false, }, { envVarName: 'TEAMS_BOT_TOKEN_ENDPOINT',