Skip to content

feat(js,react,api-service): implement MS Teams connect and link-user components#10870

Merged
djabarovgeorge merged 17 commits intonextfrom
implement-msteams-components
Apr 27, 2026
Merged

feat(js,react,api-service): implement MS Teams connect and link-user components#10870
djabarovgeorge merged 17 commits intonextfrom
implement-msteams-components

Conversation

@djabarovgeorge
Copy link
Copy Markdown
Contributor

@djabarovgeorge djabarovgeorge commented Apr 26, 2026

Summary

  • Add <MsTeamsConnectButton> and <MsTeamsLinkUser> components to @novu/js and @novu/react
  • Add two new REST API endpoints: POST /integrations/channel-connections/oauth (connect) and POST /integrations/channel-endpoints/oauth (link user), deprecating the unified POST /integrations/chat/oauth

New Components

<MsTeamsConnectButton>

Creates a workspace/tenant-level MS Teams connection via admin consent OAuth.

import { MsTeamsConnectButton } from '@novu/react';

<MsTeamsConnectButton
  integrationIdentifier="msteams"
  connectionIdentifier="my-connection"
  // subscriberId="subscriber-123"
  // context={{ tenantId: 'acme' }}
  // scope={['ChatMessage.Send']}
  // connectionMode="subscriber"
  // autoLinkUser={true}
  // connectLabel="Connect MS Teams"
  // connectedLabel="Connected"
  // onConnectSuccess={(connectionIdentifier) => {}}
  // onConnectError={(error) => {}}
  // onDisconnectSuccess={() => {}}
  // onDisconnectError={(error) => {}}
/>

<MsTeamsLinkUser>

Links a subscriber to their MS Teams user identity via delegated OAuth.

import { MsTeamsLinkUser } from '@novu/react';

<MsTeamsLinkUser
  integrationIdentifier="msteams"
  connectionIdentifier="my-connection"
  // context={{ tenantId: 'acme' }}
  // onLinkSuccess={(endpoint) => {}}
  // onLinkError={(error) => {}}
  // onUnlinkSuccess={() => {}}
  // onUnlinkError={(error) => {}}
  // linkLabel="Link Teams User"
  // unlinkLabel="Unlink"
/>

New REST API Endpoints

POST /integrations/channel-connections/oauth

Generate an OAuth URL for a workspace/tenant connection (Slack install or MS Teams admin consent).

Property Type Required Description
integrationIdentifier string yes Integration identifier
subscriberId string no Subscriber to associate with the connection
connectionIdentifier string no Connection identifier (auto-generated if omitted)
context object no Context payload (max 5 keys)
scope string[] no OAuth scopes (Slack only; ignored for MS Teams)
connectionMode "subscriber" | "shared" no How the connection is scoped (default: "subscriber")
autoLinkUser boolean no Auto-link the clicking subscriber after consent (default: true for subscriber mode)

POST /integrations/channel-endpoints/oauth

Generate an OAuth URL to link a subscriber to their chat user identity.

Property Type Required Description
subscriberId string yes Subscriber ID to link
integrationIdentifier string yes Integration identifier
connectionIdentifier string no Existing connection to associate with
context object no Context payload (max 5 keys)
userScope string[] no User-level scopes (Slack only; ignored for MS Teams)

POST /integrations/chat/oauth is now deprecated in favor of these two endpoints.

…ing and OAuth improvements

- Updated MS Teams OAuth callback to handle user linking and admin consent modes.
- Added optional provider code to the MS Teams OAuth command.
- Introduced new components for linking individual users in the dashboard.
- Enhanced UI to support MS Teams user linking functionality.
- Updated environment variables for MS Teams integration in the playground.
…s bot management

- Added MsTeamsTokenService to handle token management for MS Teams integration.
- Updated various use cases to utilize the new service for fetching bot tokens and managing app installations.
- Enhanced error handling in the MS Teams OAuth callback to provide clearer feedback on installation issues.
- Updated Teams setup guide in the dashboard to include necessary Azure API permissions for seamless integration.
- Eliminated agent logging functions and related debug logging from the SendMessageChat use case to streamline the code.
- Simplified the fetchChannelEndpoints method in ResolveChannelEndpoints by removing unnecessary logging and query parameters.
…tion method filter

- Updated the query to include a filter for distributionMethod to ensure only org-published Teams apps are considered, preventing conflicts with sideloaded versions.
- Added logging to warn when multiple org-published apps are found, ensuring clarity in app selection.
…ance setup instructions

- Refactored the OAuth callback URL generation to use a dynamic base URL instead of a hardcoded value.
- Added new components for displaying the OAuth callback URL and improved the setup guide with detailed instructions on Microsoft Graph API permissions.
- Updated the steps in the Teams setup guide to reflect changes in required permissions and added a section for the redirect URI configuration.
@netlify
Copy link
Copy Markdown

netlify Bot commented Apr 26, 2026

Deploy Preview for dashboard-v2-novu-staging canceled.

Name Link
🔨 Latest commit d5571cf
🔍 Latest deploy log https://app.netlify.com/projects/dashboard-v2-novu-staging/deploys/69efa36c5dfc000008ac2b15

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 26, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds per-user MS Teams "link_user" OAuth flow and mode/autoLinkUser state, introduces a cached MsTeamsTokenService used by API and worker, adds Solid/React connect & link UI components and plumbing, updates OAuth DTOs/controllers/helpers, playground/dashboard pages, and tightens many icon prop typings.

Changes

Cohort / File(s) Summary
MsTeams Token Service
libs/application-generic/src/services/ms-teams-token.service.ts, libs/application-generic/src/services/ms-teams-token.service.spec.ts, libs/application-generic/src/services/index.ts
New Nest service providing cached AAD tokens for Graph and Bot Framework; Graph errors propagate, Bot Framework failures return empty string; tests and barrel export added.
API: MsTeams OAuth callback (connect & link_user)
apps/api/src/app/integrations/usecases/chat-oauth-callback/.../msteams-oauth-callback.usecase.ts, .../msteams-oauth-callback.command.ts, .../msteams-oauth-callback.usecase.spec.ts, apps/api/src/app/integrations/integrations.module.ts
Implements link_user flow (code exchange → extract oid → app-catalog lookup → per-user install → create MS_TEAMS_USER endpoint); refactors admin-consent path; makes tenant optional; adds providerCode; registers MsTeamsTokenService.
API: OAuth URL generation & commands
apps/api/src/app/integrations/usecases/generate-chat-oath-url/.../generate-msteams-oauth-url.usecase.ts, .../generate-msteams-oauth-url.command.ts, .../generate-msteams-oauth-url.usecase.spec.ts, apps/api/src/app/integrations/usecases/generate-chat-oath-url/generate-chat-oauth-url.usecase.ts
Introduces OAuthMode (connect
API: New connect/link user endpoints & DTOs
apps/api/src/app/integrations/integrations.controller.ts, apps/api/src/app/integrations/dtos/*, apps/api/src/app/integrations/usecases/*/generate-connect-oauth-url.*, generate-link-user-oauth-url.*, usecases/index.ts
Adds GenerateConnectOauthUrl and GenerateLinkUserOauthUrl commands/usecases and new controller routes for channel-connections/oauth and channel-endpoints/oauth; adds corresponding DTOs/commands and wires into USE_CASES; deprecates old single POST route.
Worker: token usage & logging
apps/worker/src/app/workflow/usecases/send-message/.../resolve-channel-endpoints.usecase.ts, apps/worker/src/app/workflow/usecases/send-message/send-message-chat.usecase.ts, apps/worker/src/app/workflow/workflow.module.ts
Worker now injects MsTeamsTokenService for Bot Framework token retrieval (removes local cached fetch); adds warning logs when integrations are missing; registers service in workflow providers/exports.
Slack OAuth auto-linking
apps/api/src/app/integrations/usecases/chat-oauth-callback/slack-oauth-callback/..., apps/api/src/app/integrations/usecases/generate-chat-oath-url/.../generate-slack-oauth-url.usecase.ts, related DTOs/commands
Threads autoLinkUser into Slack OAuth state and generation; Slack callback now creates SLACK_USER endpoint only when stateData.autoLinkUser === true.
Dashboard & Playground
apps/dashboard/src/components/agents/teams-setup-guide.tsx, playground/nextjs/src/pages/connect-msteams/index.tsx, playground/nextjs/src/pages/api/msteams-dm-endpoint.ts, playground/nextjs/src/lib/msteams-dm-endpoint-connect.ts, playground/nextjs/.env.example, playground/nextjs/src/components/SideNav.tsx
Dashboard adds redirect-URI step and embeds connect UI; playground adds Connect MS Teams page, server API to create idempotent MS Teams DM endpoint, env vars and nav entry.
UI Components: Solid & React + exports
packages/js/src/ui/components/msteams-connect-button/..., packages/js/src/ui/components/msteams-link-user/..., packages/react/src/components/.../MsTeams*, packages/js/src/ui/components/Renderer.tsx, packages/js/src/ui/components/msteams-constants.ts, packages/js/src/ui/components/index.ts, packages/nextjs/*, packages/react/src/components/index.ts
Adds Solid components MsTeamsConnectButton and MsTeamsLinkUser (connect/link/polling/timeouts) plus React wrappers/defaults, constants, adds exports across package barrels and Next.js/React entry points.
UI: appearance, icons, types, small fixes
packages/js/src/ui/config/appearanceKeys.ts, packages/js/src/ui/types.ts, packages/js/src/ui/icons/MsTeamsColored.tsx, many packages/js/src/ui/icons/*
Adds MS Teams appearance keys and types, new colored Teams icon, and tightens many icon prop typings from HTML to SVG-specific types.
Provider error handling
packages/providers/src/lib/chat/msTeams/msTeams.provider.ts
Expands Bot Framework error string matching to detect additional "not installed" patterns.
Client libs: channel-connections/endpoints & hooks
packages/js/src/channel-connections/*, packages/js/src/channel-endpoints/*, packages/js/src/ui/api/hooks/*, packages/js/src/api/inbox-service.ts
Adds generateConnectOAuthUrl and generateLinkUserOAuthUrl helpers/methods, deprecates older generateOAuthUrl, updates hooks to expose new methods and threads autoLinkUser.
Misc / tooling
package.json, packages/js/scripts/size-limit.mjs
Adds cache:clean script; bumps UMD minified size threshold slightly.

Sequence Diagram(s)

sequenceDiagram
    participant User as User/Client
    participant UI as Novu UI
    participant API as Novu API
    participant MSID as Microsoft Identity
    participant Graph as Microsoft Graph

    rect rgba(0,128,0,0.5)
    User->>UI: Click "Link Account"
    UI->>API: POST /channel-endpoints/oauth (mode:'link_user', subscriberId, autoLinkUser)
    API->>API: sign state (mode/autoLinkUser)
    API-->>UI: return tenant-scoped authorize URL
    end

    rect rgba(0,0,255,0.5)
    UI->>MSID: Redirect user to tenant authorize URL
    MSID-->>User: User authenticates & consents (code)
    User->>API: Callback with code + state
    end

    rect rgba(128,0,128,0.5)
    API->>MSID: Exchange code -> id_token + token
    MSID-->>API: Return tokens (id_token contains oid)
    API->>Graph: Query Teams app catalog by externalId
    Graph-->>API: Return app catalog id
    API->>Graph: Install app for user
    Graph-->>API: Installation result (2xx / 409 / 4xx)
    API->>API: Create MS_TEAMS_USER endpoint on success
    API-->>User: Return HTML success or error page
    end
Loading
sequenceDiagram
    participant Admin as Admin
    participant UI as Novu UI
    participant API as Novu API
    participant MSID as Microsoft Identity

    rect rgba(0,128,0,0.5)
    Admin->>UI: Click "Connect to Teams"
    UI->>API: POST /channel-connections/oauth (mode:'connect', autoLinkUser?)
    API-->>UI: Return admin-consent URL
    end

    rect rgba(0,0,255,0.5)
    UI->>MSID: Redirect to admin-consent URL
    MSID-->>Admin: Admin consents (adminConsent=true)
    Admin->>API: Callback(adminConsent, state)
    end

    rect rgba(128,0,128,0.5)
    API->>API: validate adminConsent == "True"
    API->>API: Create channel connection (app-only token)
    API-->>Admin: Return HTML success (optionally chain to link_user if autoLinkUser && subscriberId)
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • scopsy
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 14.81% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title follows Conventional Commits format with valid type (feat), valid scopes (js, react, api-service in comma-separated list), and provides a clear, lowercase, imperative description of the main changes.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 26, 2026

Hey there and thank you for opening this pull request! 👋

We require pull request titles to follow specific formatting rules and it looks like your proposed title needs to be adjusted.

Your PR title is: feat(js,react,api-service): implement MS Teams connect and link-user components

Requirements:

  1. Follow the Conventional Commits specification
  2. As a team member, include Linear ticket ID at the end: fixes TICKET-ID or include it in your branch name

Expected format: feat(scope): Add fancy new feature fixes NOV-123

Details:

PR title must end with 'fixes TICKET-ID' (e.g., 'fixes NOV-123') or include ticket ID in branch name

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 12

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/react/src/index.ts (1)

40-55: ⚠️ Potential issue | 🟡 Minor

Public API addition — bump @novu/react and @novu/nextjs minor versions.

MsTeamsConnectButton, MsTeamsLinkUser, and their *Props types are new public exports. Bump @novu/react to a new minor version. Since @novu/nextjs depends on @novu/react, it should also be bumped to a new minor. @novu/js does not depend on @novu/react and does not require a version bump for this change.

Per coding guidelines: "follow semver conventions with breaking changes requiring major bumps, new exports as minor versions, and fixes as patches".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/react/src/index.ts` around lines 40 - 55, You've added new public
exports MsTeamsConnectButton, MsTeamsLinkUser and their props types
(MsTeamsConnectButtonProps, MsTeamsLinkUserProps) in packages/react (see the
exports in src/index.ts), so bump the `@novu/react` package minor version and also
bump `@novu/nextjs` minor (because it depends on `@novu/react`); update both
package.json version fields and any changelog/release notes to reflect the new
minor versions and the new public API exports, leaving `@novu/js` unchanged.
🧹 Nitpick comments (12)
packages/providers/src/lib/chat/msTeams/msTeams.provider.ts (1)

187-194: Remove redundant condition and tighten the "not installed" matcher to avoid false positives.

Two issues with the error handling at lines 187–194:

  1. Line 190 (errorMessage.includes('Bot is not installed in user')) is dead code—any message matching this line would also match line 191's case-insensitive 'not installed' substring check.

  2. The broad errorMessage.toLowerCase().includes('not installed') does not match the actual Bot Framework error message returned for this scenario. The framework returns "The bot is not part of the conversation roster." (from error code BotNotInConversationRoster), which is already correctly handled by lines 188–189. The generic 'not installed' check can incorrectly match unrelated errors (e.g., "App is not installed", "Manifest is not installed") and mislead operators into troubleshooting bot installation when the underlying issue is different.

Consider removing line 190 and either removing line 191 entirely (since lines 188–189 cover the actual Bot Framework behavior) or, if broader matching is intentional, replacing it with a more specific pattern tied to known Bot Framework error messages.

Example refinement
     if (
       errorCode === 'BotNotInConversationRoster' ||
-      errorMessage.includes('BotNotInConversationRoster') ||
-      errorMessage.includes('Bot is not installed in user') ||
-      errorMessage.toLowerCase().includes('not installed')
+      errorMessage.includes('BotNotInConversationRoster')
     ) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/providers/src/lib/chat/msTeams/msTeams.provider.ts` around lines 187
- 194, In the if block that checks errorCode === 'BotNotInConversationRoster'
and inspects errorMessage, remove the redundant condition
errorMessage.includes('Bot is not installed in user') and replace the broad
errorMessage.toLowerCase().includes('not installed') check with a tighter, Bot
Framework–specific matcher (for example check for 'bot is not part of the
conversation roster' or 'part of the conversation roster') so only true
BotNotInConversationRoster cases trigger the MSTEAMS_BOT_NOT_INSTALLED error;
update the if to only include errorCode === 'BotNotInConversationRoster' ||
errorMessage.includes('BotNotInConversationRoster') ||
errorMessage.includes('part of the conversation roster') (or delete the last
clause if you prefer relying solely on the errorCode/message exact match).
packages/js/src/ui/components/msteams-connect-button/MsTeamsConnectButton.tsx (2)

81-112: Polling iterations can overlap and the timeout edge isn't perfectly safe.

setInterval fires every POLL_INTERVAL_MS regardless of whether the previous async iteration is still in flight. If channelConnections.get runs slowly, multiple requests will pile up in parallel and the success-path mutate(...)/onConnectSuccess could fire more than once for the same connection (the interval is cleared inside the callback, but two callbacks may already be queued). Using setTimeout chained from the previous resolution avoids the overlap and makes the timeout check exact:

♻️ Proposed refactor
-    const startedAt = Date.now();
-
-    intervalIdRef.current = setInterval(async () => {
-      try {
-        const response = await novuAccessor().channelConnections.get({
-          identifier: connectionIdentifier(),
-        });
-
-        if (response.data) {
-          if (intervalIdRef.current !== null) {
-            clearInterval(intervalIdRef.current);
-            intervalIdRef.current = null;
-          }
-
-          setActionLoading(false);
-          mutate(response.data);
-          props.onConnectSuccess?.(connectionIdentifier());
-
-          return;
-        }
-      } catch {
-        // ignore transient errors during polling
-      }
-
-      if (Date.now() - startedAt >= POLL_TIMEOUT_MS) {
-        if (intervalIdRef.current !== null) {
-          clearInterval(intervalIdRef.current);
-          intervalIdRef.current = null;
-        }
-
-        setActionLoading(false);
-        props.onConnectError?.(new Error('MS Teams OAuth timed out. Please try again.'));
-      }
-    }, POLL_INTERVAL_MS);
+    const startedAt = Date.now();
+
+    const tick = async () => {
+      try {
+        const response = await novuAccessor().channelConnections.get({ identifier: connectionIdentifier() });
+        if (response.data) {
+          intervalIdRef.current = null;
+          setActionLoading(false);
+          mutate(response.data);
+          props.onConnectSuccess?.(connectionIdentifier());
+
+          return;
+        }
+      } catch {
+        // ignore transient errors during polling
+      }
+
+      if (Date.now() - startedAt >= POLL_TIMEOUT_MS) {
+        intervalIdRef.current = null;
+        setActionLoading(false);
+        props.onConnectError?.(new Error('MS Teams OAuth timed out. Please try again.'));
+
+        return;
+      }
+
+      intervalIdRef.current = setTimeout(tick, POLL_INTERVAL_MS);
+    };
+
+    intervalIdRef.current = setTimeout(tick, POLL_INTERVAL_MS);

(Update the ref's type and onCleanup to use clearTimeout accordingly.)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/js/src/ui/components/msteams-connect-button/MsTeamsConnectButton.tsx`
around lines 81 - 112, The polling uses setInterval (intervalIdRef,
POLL_INTERVAL_MS) causing overlapping async channelConnections.get calls and
possible duplicate mutate/onConnectSuccess calls; replace the setInterval
pattern with a chained setTimeout loop: change intervalIdRef to hold a timeout
id (update its type), schedule the next poll with setTimeout only after the
previous async call completes, keep the same timeout check using startedAt and
POLL_TIMEOUT_MS, clear the timeout in the cleanup via clearTimeout, and ensure
mutate(response.data) and props.onConnectSuccess are only called once when a
response is received.

64-64: Use a plain mutable variable instead of a current ref object.

In SolidJS there is no Strict Mode/render-double-invocation, so a closure-captured let intervalId is idiomatic and slightly leaner than the React-style { current } shape used here.

♻️ Proposed refactor
-  const intervalIdRef: { current: ReturnType<typeof setInterval> | null } = { current: null };
-
-  onCleanup(() => {
-    if (intervalIdRef.current !== null) {
-      clearInterval(intervalIdRef.current);
-      intervalIdRef.current = null;
-    }
-  });
+  let intervalId: ReturnType<typeof setInterval> | null = null;
+
+  onCleanup(() => {
+    if (intervalId !== null) {
+      clearInterval(intervalId);
+      intervalId = null;
+    }
+  });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/js/src/ui/components/msteams-connect-button/MsTeamsConnectButton.tsx`
at line 64, The code uses a React-style ref object intervalIdRef: { current: ...
} inside the MsTeamsConnectButton component; replace it with a plain mutable
variable (e.g., let intervalId: ReturnType<typeof setInterval> | null = null)
and update all places that read/write intervalIdRef.current to read/write
intervalId instead (including where you call clearInterval and setInterval).
Ensure the variable has the same union type (or undefined/null) and is in the
component scope so existing closures (start/stop/polling handlers) continue to
access the same mutable value.
libs/application-generic/src/services/ms-teams-token.service.spec.ts (2)

91-102: Stubbing axios.isAxiosError is brittle and can leak across tests.

Forcing axios.isAxiosError to always return true couples the test to an implementation detail and, although sinon.restore() runs in afterEach, any new test added in the same describe block before this one's stub is set up could still receive the stubbed value if test ordering changes. Prefer constructing a real Axios-shaped error so the genuine axios.isAxiosError returns true:

♻️ Proposed refactor
-      const axiosError = Object.assign(new Error('Unauthorized'), {
-        isAxiosError: true,
-        response: { status: 401, data: { error: 'unauthorized' } },
-      });
-      sinon.stub(axios, 'isAxiosError').returns(true);
-      axiosPost.rejects(axiosError);
+      const axiosError = new axios.AxiosError(
+        'Unauthorized',
+        '401',
+        undefined,
+        undefined,
+        { status: 401, data: { error: 'unauthorized' }, statusText: 'Unauthorized', headers: {}, config: {} as never }
+      );
+      axiosPost.rejects(axiosError);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/application-generic/src/services/ms-teams-token.service.spec.ts` around
lines 91 - 102, Remove the brittle stub of axios.isAxiosError and instead create
a real Axios-shaped error object so the real axios.isAxiosError returns true:
delete the sinon.stub(axios, 'isAxiosError').returns(true) line in the test for
getBotFrameworkToken, construct axiosError with the usual Axios shape (e.g., an
Error instance extended with isAxiosError: true and response: { status: 401,
data: { error: 'unauthorized' } }), and continue to have
axiosPost.rejects(axiosError); keep using the existing axiosPost stub and ensure
test cleanup (sinon.restore()) still runs in afterEach.

83-102: Test names mention logging but don't assert it.

The two failure tests are titled "...and log on...", but neither asserts that logger.error was invoked. Either drop "and log" from the names or assert against the error stub captured in buildService (you'll need to expose it from buildService or stub at the prototype level) so logging regressions are actually caught.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/application-generic/src/services/ms-teams-token.service.spec.ts` around
lines 83 - 102, The test names claim they "and log" but never assert logging;
update the two specs around getBotFrameworkToken to either remove "and log" from
their descriptions or add assertions that logger.error was called: modify
buildService to return the error logger stub (or alternatively stub the
service's logger at the prototype level) and in the tests assert the returned
stub's error method was called with the expected message when axiosPost rejects
(both for network error and axios HTTP error), keeping the existing assertions
that the token equals '' and referencing getBotFrameworkToken, buildService, and
logger.error in your changes.
apps/api/src/app/integrations/usecases/generate-chat-oath-url/generate-msteams-oath-url/generate-msteams-oauth-url.command.ts (1)

4-26: Avoid duplicating OAuth mode literals between @IsIn and the OAuthMode type.

The literal array ['connect', 'link_user'] duplicates the source-of-truth for OAuthMode (defined in the usecase file). If OAuthMode ever gains/loses a value, this validator will silently drift. Also, importing a type from a usecase into its command inverts the typical dependency direction.

Consider extracting the modes into a shared constants module (e.g., a const MSTEAMS_OAUTH_MODES = ['connect', 'link_user'] as const) and deriving both the type and the validator from it.

♻️ Sketch of the refactor
-import { IsIn, IsOptional, IsString } from 'class-validator';
 import { EnvironmentCommand } from '../../../../shared/commands/project.command';
-import { OAuthMode } from './generate-msteams-oauth-url.usecase';
+import { IsIn, IsOptional, IsString } from 'class-validator';
+import { MSTEAMS_OAUTH_MODES, OAuthMode } from './msteams-oauth-modes';
@@
-  `@IsOptional`()
-  `@IsString`()
-  `@IsIn`(['connect', 'link_user'])
-  readonly mode?: OAuthMode;
+  `@IsOptional`()
+  `@IsString`()
+  `@IsIn`(MSTEAMS_OAUTH_MODES)
+  readonly mode?: OAuthMode;
// msteams-oauth-modes.ts
export const MSTEAMS_OAUTH_MODES = ['connect', 'link_user'] as const;
export type OAuthMode = (typeof MSTEAMS_OAUTH_MODES)[number];
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/api/src/app/integrations/usecases/generate-chat-oath-url/generate-msteams-oath-url/generate-msteams-oauth-url.command.ts`
around lines 4 - 26, The command duplicates OAuth mode literals; extract the
mode literals into a shared constant (e.g., export const MSTEAMS_OAUTH_MODES =
['connect','link_user'] as const) and derive the OAuthMode type from it (export
type OAuthMode = (typeof MSTEAMS_OAUTH_MODES)[number]); then update
GenerateMsTeamsOauthUrlCommand to import MSTEAMS_OAUTH_MODES and replace the
`@IsIn`(['connect','link_user']) usage with `@IsIn`(MSTEAMS_OAUTH_MODES cast to a
string[] as required by class-validator), and update the usecase to import the
OAuthMode type from that shared module instead of inverting dependencies.
apps/dashboard/src/components/agents/teams-setup-guide.tsx (2)

50-52: Optional: share the OAuth callback path with the backend.

/v1/integrations/chat/oauth/callback is duplicated here and in the API's CHAT_OAUTH_CALLBACK_PATH constant. If the backend path ever changes, the redirect URI shown to users (and the one accepted by Azure) will silently drift. Consider exposing the constant via a shared package (e.g., @novu/shared) so both ends stay in lockstep.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/dashboard/src/components/agents/teams-setup-guide.tsx` around lines 50 -
52, The hardcoded callback path in buildOAuthCallbackUrl() duplicates the
backend constant CHAT_OAUTH_CALLBACK_PATH; replace the literal path with an
imported/shared constant so frontend and backend stay in sync. Import
CHAT_OAUTH_CALLBACK_PATH from the shared package (e.g., `@novu/shared`) and use it
in buildOAuthCallbackUrl() to construct the redirect URI, ensuring you update
exports on the backend/shared package if needed so the constant is available to
the frontend.

415-421: Remove the commented-out connectionIdentifier prop.

Either restore it with a comment explaining why it's needed, or delete it — leaving commented code in production paths invites confusion, especially since the same ${user.externalId}:agent-quickstart:${agent._id} value is already used as subscriberId on Line 406.

♻️ Cleanup
               <MsTeamsConnectButton
                 integrationIdentifier={integrationIdentifier}
-                // connectionIdentifier={`${user.externalId}:agent-quickstart:${agent._id}`}
                 connectLabel={`Connect ${agent.name} ↗`}
                 connectedLabel="Connected to MS Teams"
                 onConnectSuccess={handleConnected}
               />
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/dashboard/src/components/agents/teams-setup-guide.tsx` around lines 415
- 421, Remove the leftover commented-out connectionIdentifier prop on the
MsTeamsConnectButton: either uncomment and document why it’s required or delete
the commented line entirely to avoid confusion with the existing subscriberId
usage; specifically update the MsTeamsConnectButton usage (the
connectionIdentifier prop and its value
`${user.externalId}:agent-quickstart:${agent._id}`) to match the already-used
subscriberId pattern (or add a short inline comment if the prop must be
preserved) and ensure no stale commented code remains.
apps/api/src/app/integrations/usecases/chat-oauth-callback/msteams-oauth-callback/msteams-oauth-callback.usecase.ts (2)

285-313: Reuse MS_TEAMS_LINK_USER_OAUTH_SCOPES instead of hardcoding the scope string.

The token-exchange scope (Line 291) is the literal string 'openid profile User.Read', which must match what getLinkUserOAuthUrl requested at the authorize step. That URL is built from MS_TEAMS_LINK_USER_OAUTH_SCOPES exported from generate-msteams-oauth-url.usecase.ts. Keeping two parallel literals invites silent drift if scopes are ever expanded.

♻️ Suggested fix
+import {
+  GenerateMsTeamsOauthUrl,
+  MS_TEAMS_LINK_USER_OAUTH_SCOPES,
+  StateData,
+} from '../../generate-chat-oath-url/generate-msteams-oath-url/generate-msteams-oauth-url.usecase';
...
-      scope: 'openid profile User.Read',
+      scope: MS_TEAMS_LINK_USER_OAUTH_SCOPES.join(' '),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/api/src/app/integrations/usecases/chat-oauth-callback/msteams-oauth-callback/msteams-oauth-callback.usecase.ts`
around lines 285 - 313, Replace the hardcoded scope string in the token exchange
with the shared constant to avoid drift: import and use
MS_TEAMS_LINK_USER_OAUTH_SCOPES (the same constant used by
GenerateMsTeamsOauthUrl/getLinkUserOAuthUrl) instead of the literal 'openid
profile User.Read' when building tokenParams in the method that posts to
MS_TEAMS_TOKEN_URL; ensure the URLSearchParams uses
MS_TEAMS_LINK_USER_OAUTH_SCOPES.toString() (or the correct shape) so the
authorize and token-exchange scopes match and preserve existing calls to
GenerateMsTeamsOauthUrl.buildRedirectUri() and extractOidFromIdToken.

315-323: Add id_token signature and claims validation before extracting oid.

The extractOidFromIdToken method decodes the JWT payload without validating the signature, issuer (iss), audience (aud), expiration, or tenant ID (tid). Although the token is fetched directly from login.microsoftonline.com over TLS via a confidential-client request, validating against Microsoft's JWKS endpoint is an OIDC best practice required by Microsoft's specification for confidential clients. The extracted oid drives downstream Graph API calls and creates user channel endpoints, making cryptographic validation a sound defense-in-depth measure.

Implement validation using jsonwebtoken (already a dependency) with the following checks:

  • Fetch and verify signature using Microsoft's JWKS endpoint
  • Validate iss claim matches https://login.microsoftonline.com/{tenant_id}/v2.0
  • Validate aud claim matches your Azure app's client ID
  • Validate timestamps (exp, nbf, iat)
  • Optionally validate tid claim matches expected tenant

This aligns with Microsoft's OIDC id_token validation requirements for the authorization code flow.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/api/src/app/integrations/usecases/chat-oauth-callback/msteams-oauth-callback/msteams-oauth-callback.usecase.ts`
around lines 315 - 323, The extractOidFromIdToken function decodes the JWT
without validating signature or claims; replace the raw decode with proper OIDC
validation: fetch Microsoft's JWKS
(https://login.microsoftonline.com/{tenant}/discovery/v2.0/keys), use
jsonwebtoken (and jwks retrieval utility) to verify the id_token signature and
validate claims (iss should be
https://login.microsoftonline.com/{tenant_id}/v2.0, aud must equal your Azure
client ID, exp/nbf/iat timestamps must be valid, and optionally tid matches
expected tenant); after successful verification return the decoded.oid; on
verification failure throw BadRequestException('Failed to decode MS Teams
id_token') or a clearer validation error.
apps/api/src/app/integrations/usecases/chat-oauth-callback/msteams-oauth-callback/msteams-oauth-callback.usecase.spec.ts (2)

70-99: Prefer a Nest Test.createTestingModule over hand-wiring stubs.

The use case is instantiated by mocking every collaborator (createStubInstance + as any) and passing them positionally. This couples the spec to constructor argument order and bypasses Nest's DI, which makes refactors (adding/removing dependencies) silently break the spec instead of failing at module-resolve time. Consider a Nest testing module with .overrideProvider(...).useValue(stub) to reduce churn and align with other API specs.

As per coding guidelines: Bootstrap a NestJS testing module for integration-style unit tests rather than mocking the entire DI container.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/api/src/app/integrations/usecases/chat-oauth-callback/msteams-oauth-callback/msteams-oauth-callback.usecase.spec.ts`
around lines 70 - 99, The spec hand-wires stubs into the MsTeamsOauthCallback
constructor (integrationRepository, environmentRepository,
CreateChannelConnection, CreateChannelEndpoint, MsTeamsTokenService) which
couples the test to constructor ordering; replace this with a Nest
Test.createTestingModule that provides the MsTeamsOauthCallback class and use
.overrideProvider(<ProviderTokenOrClass>).useValue(stub) for each collaborator
(integrationRepository, environmentRepository, CreateChannelConnection,
CreateChannelEndpoint, MsTeamsTokenService, logger) so the usecase is resolved
via Nest DI and the test no longer breaks when constructor args change.

226-226: Duplicated sinon.stub(axios, 'isAxiosError').returns(true) across cases.

This same stub is repeated at lines 226, 247, 268, and 304. Hoist it into the link_user mode beforeEach (or a small helper) to keep the failure-mode tests focused on the specific axios rejection being exercised.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/api/src/app/integrations/usecases/chat-oauth-callback/msteams-oauth-callback/msteams-oauth-callback.usecase.spec.ts`
at line 226, The duplicated stub of axios.isAxiosError (sinon.stub(axios,
'isAxiosError').returns(true)) should be hoisted out of individual tests into
the link_user mode setup; add it to the link_user mode beforeEach (or a small
shared helper invoked from that beforeEach) so all failure-mode tests reuse the
same stub and remove the repeated lines at 226, 247, 268, and 304; ensure you
restore/restoreSinonState in afterEach if you use global stubbing so tests
remain isolated.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@apps/api/src/app/integrations/usecases/chat-oauth-callback/msteams-oauth-callback/msteams-oauth-callback.usecase.spec.ts`:
- Around line 319-346: The two specs named "should throw if id_token is missing
from token response" and "should throw if oid claim is absent from id_token" are
misleading because they await usecase.execute(command) and assert on
result.result (HTML) instead of expecting a thrown/rejected error; update the
tests for MsTeamsOauthCallbackCommand/usecase.execute to either (A) assert a
rejection by using chai-as-promised (e.g.,
expect(usecase.execute(command)).to.be.rejectedWith(/MS Teams Bot Installation
Failed/)) and remove the result.result checks, or (B) change the test
descriptions to something like "should return error HTML when id_token is
missing" and keep the existing assertions (including ensure
createChannelEndpoint.execute remains asserted false and result.result includes
the error message); reference the test cases by their existing names and the
usecase.execute invocation when making the change.

In
`@apps/api/src/app/integrations/usecases/chat-oauth-callback/msteams-oauth-callback/msteams-oauth-callback.usecase.ts`:
- Around line 252-274: The HTML-escaping in buildErrorHtml is incomplete and
ordered wrongly: ensure ampersands are escaped first, then escape `<`, `>`, and
`"` (and consider `'` if needed) so existing entities are not reinterpreted or
double-encoded; update the buildErrorHtml function to perform replace(/&/g,
'&amp;') before other replacements (while keeping the rest of the template
intact) to properly sanitize message input.

In
`@apps/api/src/app/integrations/usecases/generate-chat-oath-url/generate-msteams-oath-url/generate-msteams-oauth-url.usecase.ts`:
- Around line 63-71: When command.mode === 'link_user' ensure the caller
provides a subscriberId upfront instead of allowing context-only; update the
check in generate-msteams-oauth-url.usecase.ts (where getLinkUserOAuthUrl is
returned) to validate that validateSubscriberIdOrContext resolved to a concrete
subscriberId (or explicitly check credentials/subscriberId) and throw a
BadRequestException/NotFoundException immediately if missing, so the flow
matches the expectation in msteams-oauth-callback.usecase.ts where
linkUserEndpoint requires stateData.subscriberId; this prevents issuing an OAuth
URL for callers who only supplied context and avoids the downstream HTML error
on callback.

In `@libs/application-generic/src/services/ms-teams-token.service.ts`:
- Around line 77-79: The axios.post call in getBotFrameworkToken that posts to
tokenUrl currently lacks a timeout and can hang; update the request options
passed to axios.post in ms-teams-token.service.ts (the call that expects {
access_token: string; expires_in: number }) to include a sensible timeout (e.g.,
timeout: 10000) alongside the existing headers, so timeouts throw and are
handled by the existing getBotFrameworkToken error path for graceful
degradation.
- Around line 22-29: The cache key builder for the CachedResponse decorator
currently uses only clientId and appTenantId (in the builder passed to
CachedResponse) so rotating secretKey won't change keys; update the builder to
incorporate a stable fingerprint of the secretKey (e.g., a short hash of the
secretKey parameter) into the key string used for msteams:graph-token to ensure
keys change after secret rotation, and apply the same fix to the other
CachedResponse usage in this file (the second decorator around the same token
flow); alternatively, add logic to invalidate/evict cached entries when
integration credentials are updated, and keep the TTL (TOKEN_TTL_SECONDS) as-is.

In
`@packages/js/src/ui/components/msteams-connect-button/MsTeamsConnectButton.tsx`:
- Line 32: POLL_TIMEOUT_MS (currently 120_000) is too short for
MFA/conditional-access flows; increase it to 300_000–600_000 (5–10 minutes) and
update any polling logic that reads POLL_TIMEOUT_MS (e.g., the MS Teams connect
polling routine in MsTeamsConnectButton) to respect the longer timeout;
additionally implement an increasing/backoff poll interval (exponential or
linear) in the polling loop so requests are less frequent over time rather than
a fixed short interval, ensuring the UI stays in a waiting state until the
extended timeout expires or success/failure is confirmed.
- Around line 127-154: handleClick can leave actionLoading true and lose the
user-gesture for window.open; to fix, open the popup synchronously at the start
of the user click (e.g., call window.open('about:blank', '_blank',
'noopener,noreferrer') and keep a reference), then await connect(...) and if
result.data?.url assign popup.location.href = result.data.url; ensure you check
the popup reference (it may be null if blocked) and call props.onConnectError
with an appropriate error and setActionLoading(false) if the popup was blocked;
also add a clear fallback path that sets setActionLoading(false) when connect
resolves without result.data?.url (and call onConnectError) so no code path
leaves actionLoading true; keep startPolling usage unchanged but only call it
when you successfully navigated the popup.

In `@packages/js/src/ui/components/msteams-link-user/MsTeamsLinkUser.tsx`:
- Around line 117-155: The handleClick function can leave actionLoading true if
generateOAuthUrl returns no error but also no data.url; update handleClick
(inside MsTeamsLinkUser.tsx) so after calling
novuAccessor().channelConnections.generateOAuthUrl you always clear loading: if
result.error -> setActionLoading(false) and call props.onLinkError; else if
result.data?.url -> open window and startPolling; else ->
setActionLoading(false) and call props.onLinkError with a descriptive fallback
error (e.g. "Empty OAuth response"). Ensure you reference the existing symbols:
handleClick, setActionLoading,
novuAccessor().channelConnections.generateOAuthUrl, result.data?.url,
props.onLinkError, and startPolling.
- Around line 135-153: The OAuth popup is opened after an await which can
trigger popup blockers; modify the click handler (handleClick) to open a blank
popup synchronously before calling
novuAccessor().channelConnections.generateOAuthUrl(...), store it (e.g., popup =
window.open('about:blank', '_blank', 'noopener,noreferrer')), then await the
generateOAuthUrl call; if result.error close the popup and call
setActionLoading(false) and props.onLinkError(result.error), if result.data?.url
set popup.location.href = result.data.url (or fallback to window.location.href
if popup is null/blocked) and then call startPolling(); ensure you reference
integrationIdentifier(), connectionIdentifier(), props.subscriberId/provider
context as before and handle popup null case to avoid leaving a stray window.

In `@packages/js/src/ui/icons/MsTeamsColored.tsx`:
- Line 1: Rename the file MsTeamsColored.tsx to lowercase-with-dashes
(msteams-colored.tsx) to follow the repository naming convention; after
renaming, update all import paths that reference "MsTeamsColored" (e.g., imports
that point to ./MsTeamsColored or similar) to the new filename
./msteams-colored.tsx and ensure any tooling or exports that reference the file
name are adjusted accordingly so builds and imports resolve correctly.
- Around line 3-4: The function MsTeamsColored has its return statement
immediately following the function signature; insert a single blank line before
the return statement in the MsTeamsColored component so the return is separated
by an empty line (follow the project's rule of adding a blank line before every
return).
- Line 3: The MsTeamsColored icon component currently types its props as
JSX.HTMLAttributes<SVGSVGElement>; change the prop type to
JSX.SvgSVGAttributes<SVGSVGElement> in the MsTeamsColored declaration to enable
SVG-specific attributes (and apply the same change to all other icon components
like ArrowDown, ArrowRight, etc., in packages/js/src/ui/icons/), ensuring the
component signature uses JSX.SvgSVGAttributes<SVGSVGElement> instead of
JSX.HTMLAttributes<SVGSVGElement>.

---

Outside diff comments:
In `@packages/react/src/index.ts`:
- Around line 40-55: You've added new public exports MsTeamsConnectButton,
MsTeamsLinkUser and their props types (MsTeamsConnectButtonProps,
MsTeamsLinkUserProps) in packages/react (see the exports in src/index.ts), so
bump the `@novu/react` package minor version and also bump `@novu/nextjs` minor
(because it depends on `@novu/react`); update both package.json version fields and
any changelog/release notes to reflect the new minor versions and the new public
API exports, leaving `@novu/js` unchanged.

---

Nitpick comments:
In
`@apps/api/src/app/integrations/usecases/chat-oauth-callback/msteams-oauth-callback/msteams-oauth-callback.usecase.spec.ts`:
- Around line 70-99: The spec hand-wires stubs into the MsTeamsOauthCallback
constructor (integrationRepository, environmentRepository,
CreateChannelConnection, CreateChannelEndpoint, MsTeamsTokenService) which
couples the test to constructor ordering; replace this with a Nest
Test.createTestingModule that provides the MsTeamsOauthCallback class and use
.overrideProvider(<ProviderTokenOrClass>).useValue(stub) for each collaborator
(integrationRepository, environmentRepository, CreateChannelConnection,
CreateChannelEndpoint, MsTeamsTokenService, logger) so the usecase is resolved
via Nest DI and the test no longer breaks when constructor args change.
- Line 226: The duplicated stub of axios.isAxiosError (sinon.stub(axios,
'isAxiosError').returns(true)) should be hoisted out of individual tests into
the link_user mode setup; add it to the link_user mode beforeEach (or a small
shared helper invoked from that beforeEach) so all failure-mode tests reuse the
same stub and remove the repeated lines at 226, 247, 268, and 304; ensure you
restore/restoreSinonState in afterEach if you use global stubbing so tests
remain isolated.

In
`@apps/api/src/app/integrations/usecases/chat-oauth-callback/msteams-oauth-callback/msteams-oauth-callback.usecase.ts`:
- Around line 285-313: Replace the hardcoded scope string in the token exchange
with the shared constant to avoid drift: import and use
MS_TEAMS_LINK_USER_OAUTH_SCOPES (the same constant used by
GenerateMsTeamsOauthUrl/getLinkUserOAuthUrl) instead of the literal 'openid
profile User.Read' when building tokenParams in the method that posts to
MS_TEAMS_TOKEN_URL; ensure the URLSearchParams uses
MS_TEAMS_LINK_USER_OAUTH_SCOPES.toString() (or the correct shape) so the
authorize and token-exchange scopes match and preserve existing calls to
GenerateMsTeamsOauthUrl.buildRedirectUri() and extractOidFromIdToken.
- Around line 315-323: The extractOidFromIdToken function decodes the JWT
without validating signature or claims; replace the raw decode with proper OIDC
validation: fetch Microsoft's JWKS
(https://login.microsoftonline.com/{tenant}/discovery/v2.0/keys), use
jsonwebtoken (and jwks retrieval utility) to verify the id_token signature and
validate claims (iss should be
https://login.microsoftonline.com/{tenant_id}/v2.0, aud must equal your Azure
client ID, exp/nbf/iat timestamps must be valid, and optionally tid matches
expected tenant); after successful verification return the decoded.oid; on
verification failure throw BadRequestException('Failed to decode MS Teams
id_token') or a clearer validation error.

In
`@apps/api/src/app/integrations/usecases/generate-chat-oath-url/generate-msteams-oath-url/generate-msteams-oauth-url.command.ts`:
- Around line 4-26: The command duplicates OAuth mode literals; extract the mode
literals into a shared constant (e.g., export const MSTEAMS_OAUTH_MODES =
['connect','link_user'] as const) and derive the OAuthMode type from it (export
type OAuthMode = (typeof MSTEAMS_OAUTH_MODES)[number]); then update
GenerateMsTeamsOauthUrlCommand to import MSTEAMS_OAUTH_MODES and replace the
`@IsIn`(['connect','link_user']) usage with `@IsIn`(MSTEAMS_OAUTH_MODES cast to a
string[] as required by class-validator), and update the usecase to import the
OAuthMode type from that shared module instead of inverting dependencies.

In `@apps/dashboard/src/components/agents/teams-setup-guide.tsx`:
- Around line 50-52: The hardcoded callback path in buildOAuthCallbackUrl()
duplicates the backend constant CHAT_OAUTH_CALLBACK_PATH; replace the literal
path with an imported/shared constant so frontend and backend stay in sync.
Import CHAT_OAUTH_CALLBACK_PATH from the shared package (e.g., `@novu/shared`) and
use it in buildOAuthCallbackUrl() to construct the redirect URI, ensuring you
update exports on the backend/shared package if needed so the constant is
available to the frontend.
- Around line 415-421: Remove the leftover commented-out connectionIdentifier
prop on the MsTeamsConnectButton: either uncomment and document why it’s
required or delete the commented line entirely to avoid confusion with the
existing subscriberId usage; specifically update the MsTeamsConnectButton usage
(the connectionIdentifier prop and its value
`${user.externalId}:agent-quickstart:${agent._id}`) to match the already-used
subscriberId pattern (or add a short inline comment if the prop must be
preserved) and ensure no stale commented code remains.

In `@libs/application-generic/src/services/ms-teams-token.service.spec.ts`:
- Around line 91-102: Remove the brittle stub of axios.isAxiosError and instead
create a real Axios-shaped error object so the real axios.isAxiosError returns
true: delete the sinon.stub(axios, 'isAxiosError').returns(true) line in the
test for getBotFrameworkToken, construct axiosError with the usual Axios shape
(e.g., an Error instance extended with isAxiosError: true and response: {
status: 401, data: { error: 'unauthorized' } }), and continue to have
axiosPost.rejects(axiosError); keep using the existing axiosPost stub and ensure
test cleanup (sinon.restore()) still runs in afterEach.
- Around line 83-102: The test names claim they "and log" but never assert
logging; update the two specs around getBotFrameworkToken to either remove "and
log" from their descriptions or add assertions that logger.error was called:
modify buildService to return the error logger stub (or alternatively stub the
service's logger at the prototype level) and in the tests assert the returned
stub's error method was called with the expected message when axiosPost rejects
(both for network error and axios HTTP error), keeping the existing assertions
that the token equals '' and referencing getBotFrameworkToken, buildService, and
logger.error in your changes.

In
`@packages/js/src/ui/components/msteams-connect-button/MsTeamsConnectButton.tsx`:
- Around line 81-112: The polling uses setInterval (intervalIdRef,
POLL_INTERVAL_MS) causing overlapping async channelConnections.get calls and
possible duplicate mutate/onConnectSuccess calls; replace the setInterval
pattern with a chained setTimeout loop: change intervalIdRef to hold a timeout
id (update its type), schedule the next poll with setTimeout only after the
previous async call completes, keep the same timeout check using startedAt and
POLL_TIMEOUT_MS, clear the timeout in the cleanup via clearTimeout, and ensure
mutate(response.data) and props.onConnectSuccess are only called once when a
response is received.
- Line 64: The code uses a React-style ref object intervalIdRef: { current: ...
} inside the MsTeamsConnectButton component; replace it with a plain mutable
variable (e.g., let intervalId: ReturnType<typeof setInterval> | null = null)
and update all places that read/write intervalIdRef.current to read/write
intervalId instead (including where you call clearInterval and setInterval).
Ensure the variable has the same union type (or undefined/null) and is in the
component scope so existing closures (start/stop/polling handlers) continue to
access the same mutable value.

In `@packages/providers/src/lib/chat/msTeams/msTeams.provider.ts`:
- Around line 187-194: In the if block that checks errorCode ===
'BotNotInConversationRoster' and inspects errorMessage, remove the redundant
condition errorMessage.includes('Bot is not installed in user') and replace the
broad errorMessage.toLowerCase().includes('not installed') check with a tighter,
Bot Framework–specific matcher (for example check for 'bot is not part of the
conversation roster' or 'part of the conversation roster') so only true
BotNotInConversationRoster cases trigger the MSTEAMS_BOT_NOT_INSTALLED error;
update the if to only include errorCode === 'BotNotInConversationRoster' ||
errorMessage.includes('BotNotInConversationRoster') ||
errorMessage.includes('part of the conversation roster') (or delete the last
clause if you prefer relying solely on the errorCode/message exact match).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: a5cc214f-56f5-4e94-86d5-55b2f2c038b1

📥 Commits

Reviewing files that changed from the base of the PR and between 54c82c9 and 5f749df.

📒 Files selected for processing (40)
  • apps/api/src/app/integrations/integrations.module.ts
  • apps/api/src/app/integrations/usecases/chat-oauth-callback/chat-oauth-callback.usecase.ts
  • apps/api/src/app/integrations/usecases/chat-oauth-callback/msteams-oauth-callback/msteams-oauth-callback.command.ts
  • apps/api/src/app/integrations/usecases/chat-oauth-callback/msteams-oauth-callback/msteams-oauth-callback.usecase.spec.ts
  • apps/api/src/app/integrations/usecases/chat-oauth-callback/msteams-oauth-callback/msteams-oauth-callback.usecase.ts
  • apps/api/src/app/integrations/usecases/generate-chat-oath-url/generate-chat-oauth-url.usecase.ts
  • apps/api/src/app/integrations/usecases/generate-chat-oath-url/generate-msteams-oath-url/generate-msteams-oauth-url.command.ts
  • apps/api/src/app/integrations/usecases/generate-chat-oath-url/generate-msteams-oath-url/generate-msteams-oauth-url.usecase.spec.ts
  • apps/api/src/app/integrations/usecases/generate-chat-oath-url/generate-msteams-oath-url/generate-msteams-oauth-url.usecase.ts
  • apps/dashboard/src/components/agents/teams-setup-guide.tsx
  • apps/worker/src/app/workflow/usecases/send-message/channel-endpoint-resolution/resolve-channel-endpoints.usecase.ts
  • apps/worker/src/app/workflow/usecases/send-message/send-message-chat.usecase.ts
  • apps/worker/src/app/workflow/workflow.module.ts
  • libs/application-generic/src/services/index.ts
  • libs/application-generic/src/services/ms-teams-token.service.spec.ts
  • libs/application-generic/src/services/ms-teams-token.service.ts
  • package.json
  • packages/js/src/ui/components/Renderer.tsx
  • packages/js/src/ui/components/index.ts
  • packages/js/src/ui/components/msteams-connect-button/MsTeamsConnectButton.tsx
  • packages/js/src/ui/components/msteams-constants.ts
  • packages/js/src/ui/components/msteams-link-user/MsTeamsLinkUser.tsx
  • packages/js/src/ui/config/appearanceKeys.ts
  • packages/js/src/ui/icons/MsTeamsColored.tsx
  • packages/js/src/ui/index.ts
  • packages/js/src/ui/types.ts
  • packages/nextjs/src/app-router/index.ts
  • packages/nextjs/src/pages-router/index.ts
  • packages/providers/src/lib/chat/msTeams/msTeams.provider.ts
  • packages/react/src/components/index.ts
  • packages/react/src/components/msteams-connect-button/DefaultMsTeamsConnectButton.tsx
  • packages/react/src/components/msteams-connect-button/MsTeamsConnectButton.tsx
  • packages/react/src/components/msteams-link-user/DefaultMsTeamsLinkUser.tsx
  • packages/react/src/components/msteams-link-user/MsTeamsLinkUser.tsx
  • packages/react/src/index.ts
  • playground/nextjs/.env.example
  • playground/nextjs/src/components/SideNav.tsx
  • playground/nextjs/src/lib/msteams-dm-endpoint-connect.ts
  • playground/nextjs/src/pages/api/msteams-dm-endpoint.ts
  • playground/nextjs/src/pages/connect-msteams/index.tsx

Comment thread libs/application-generic/src/services/ms-teams-token.service.ts
Comment thread libs/application-generic/src/services/ms-teams-token.service.ts
Comment thread packages/js/src/ui/components/msteams-link-user/MsTeamsLinkUser.tsx
Comment thread packages/js/src/ui/components/msteams-link-user/MsTeamsLinkUser.tsx Outdated
Comment thread packages/js/src/ui/icons/MsTeamsColored.tsx
Comment thread packages/js/src/ui/icons/MsTeamsColored.tsx Outdated
Comment thread packages/js/src/ui/icons/MsTeamsColored.tsx Outdated
…ng token claims

- Updated test descriptions to clarify expected behavior when id_token or oid claim is missing from the token response.
- Enhanced error HTML generation to escape additional characters for better security.
- Added validation to ensure subscriberId is required for 'link_user' mode in GenerateMsTeamsOauthUrl use case.
- Improved polling mechanism in MsTeamsConnectButton and related components to handle OAuth URL retrieval errors more gracefully.
- Added logic to open a new popup for OAuth URL retrieval and handle its closure on errors.
- Updated the URL redirection to use the popup if available, enhancing user experience during the linking process.
- Ensured proper error handling for cases where the OAuth URL is not returned, closing the popup as needed.
@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented Apr 27, 2026

Open in StackBlitz

@novu/js

npm i https://pkg.pr.new/@novu/js@10870

@novu/nextjs

npm i https://pkg.pr.new/@novu/nextjs@10870

novu

npm i https://pkg.pr.new/novu@10870

@novu/providers

npm i https://pkg.pr.new/@novu/providers@10870

@novu/react

npm i https://pkg.pr.new/@novu/react@10870

@novu/react-native

npm i https://pkg.pr.new/@novu/react-native@10870

commit: d5571cf

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

♻️ Duplicate comments (1)
packages/js/src/ui/components/msteams-connect-button/MsTeamsConnectButton.tsx (1)

130-159: ⚠️ Potential issue | 🟠 Major

Popup still opened after await connect(...) — gesture token likely lost.

The stuck actionLoading issue (lines 156–159) is now handled, but the popup-blocker concern remains: window.open at line 154 runs after await connect(...), which strips the user-gesture token in Safari/Firefox-strict and triggers blockers in Chrome under enhanced policies.

The sibling component MsTeamsLinkUser.tsx already adopts the recommended pattern (open about:blank synchronously, then assign popup.location.href once the URL resolves). Aligning this component would keep behavior consistent.

🛡️ Suggested fix mirroring MsTeamsLinkUser.tsx
     } else {
       setActionLoading(true);
+      const popup = window.open('about:blank', '_blank', 'noopener,noreferrer');

       const mode = connectionMode();
       const ctx = resolvedContext();
       const resolvedSubscriberId =
         mode === 'subscriber' ? (props.subscriberId ?? novuAccessor().subscriberId) : undefined;

       const result = await connect({
         integrationIdentifier: integrationIdentifier(),
         connectionIdentifier: connectionIdentifier(),
         subscriberId: resolvedSubscriberId,
         context: ctx,
         scope: props.scope,
         connectionMode: mode,
       });

       if (result.error) {
+        popup?.close();
         setActionLoading(false);
         props.onConnectError?.(result.error);

         return;
       }

       if (result.data?.url) {
-        window.open(result.data.url, '_blank', 'noopener,noreferrer');
+        if (popup) {
+          popup.location.href = result.data.url;
+        }
         startPolling();
       } else {
+        popup?.close();
         setActionLoading(false);
         props.onConnectError?.(new Error('OAuth URL was not returned. Please try again.'));
       }
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/js/src/ui/components/msteams-connect-button/MsTeamsConnectButton.tsx`
around lines 130 - 159, The popup is opened after await connect(...) which can
lose the user-gesture and trigger popup blockers; change MsTeamsConnectButton to
open a blank popup synchronously (e.g. window.open('about:blank', ...) and keep
the popup reference) before calling connect(), then await connect() and if
result.data?.url assign popup.location.href = result.data.url and call
startPolling(); on error or missing URL close the popup (popup.close()), call
setActionLoading(false) and invoke props.onConnectError with the error; ensure
all branches use the same popup reference and that startPolling is only started
after assigning the popup location.
🧹 Nitpick comments (2)
packages/js/src/ui/components/slack-link-user/SlackLinkUser.tsx (1)

151-157: LGTM — closes a real UX dead-end.

Previously, a successful response with no url left actionLoading stuck at true and never fired onLinkError. The new else branch resolves both. Optional nit: the timeout error message at line 112 says "Slack OAuth timed out…" (provider-prefixed), while the new error says "OAuth URL was not returned…" (generic). Consider aligning naming for parity, e.g. "Slack OAuth URL was not returned. Please try again.". The same wording inconsistency exists in SlackConnectButton.tsx line 155.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/js/src/ui/components/slack-link-user/SlackLinkUser.tsx` around lines
151 - 157, The else branch in SlackLinkUser.tsx now clears loading and calls
props.onLinkError but uses a generic error message; update that message to match
the provider-prefixed style (e.g. "Slack OAuth URL was not returned. Please try
again.") so it aligns with the timeout message, and make the same wording change
in SlackConnectButton.tsx (the error thrown where Slack OAuth URL is missing);
locate the callers around setActionLoading, startPolling, and props.onLinkError
to adjust the Error text consistently.
packages/js/src/ui/components/msteams-link-user/MsTeamsLinkUser.tsx (1)

86-115: Use chained setTimeout to avoid overlapping in-flight requests.

If channelEndpoints.list ever takes longer than POLL_INTERVAL_MS (2.5s) under load or slow networks, setInterval will fire a new request before the previous response returns, stacking concurrent requests against the user's environment. The sibling MsTeamsConnectButton.tsx already uses chained setTimeout with exponential backoff (schedulePoll helper) — aligning here would also give parity in retry behavior. While here, consider matching its POLL_TIMEOUT_MS of 5 minutes (line 27 here is 2 minutes), as MFA-gated tenant sign-in commonly exceeds 2 minutes.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/js/src/ui/components/msteams-link-user/MsTeamsLinkUser.tsx` around
lines 86 - 115, The startPolling function uses setInterval (pollingIntervalId)
which can start overlapping requests when channelEndpoints.list takes longer
than POLL_INTERVAL_MS; replace the setInterval loop with a chained setTimeout
retry (use the same schedulePoll/backoff approach as in
MsTeamsConnectButton.tsx) so each poll awaits the previous request before
scheduling the next, ensure you still clear any scheduled timeout on success or
timeout, and increase POLL_TIMEOUT_MS from 2 minutes to match the 5-minute value
used by MsTeamsConnectButton.tsx to handle MFA delays.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@apps/api/src/app/integrations/usecases/chat-oauth-callback/msteams-oauth-callback/msteams-oauth-callback.usecase.ts`:
- Line 167: The interpolated values azureClientId (used in the OData $filter)
and userOid (used in the URL path) must be encoded before insertion to prevent
malformed Graph URLs; update the code in msteams-oauth-callback.usecase.ts to
escape these values (e.g., use a safe encoder for path segments like
encodeURIComponent for userOid and properly escape/encode single quotes and
special chars in the $filter value for azureClientId) wherever they are used
(the line constructing url with MS_GRAPH_BASE_URL/appCatalogs/teamsApps and the
lines building the URL with userOid around 209–213), ensuring the filter string
remains valid OData (wrap or double single-quotes as necessary) and remove raw
interpolation of these variables.
- Around line 47-71: In the catch block around linkUserEndpoint in the msteams
oauth callback use case, log the caught error via this.logger.error (including
the error object and contextual info such as stateData.mode and integration
identifier) before returning the HTML response; ensure you call
this.logger.error(error, { context: 'msteams-oauth-callback', stateData,
integration }) or similar so the full stack/metadata is preserved for production
debugging, then continue to build and return the HTML as currently done.

In `@packages/js/src/ui/components/msteams-link-user/MsTeamsLinkUser.tsx`:
- Around line 153-160: The current fallback that sets window.location.href when
popup is null breaks the polling flow; change MsTeamsLinkUser.tsx to only call
startPolling() when a popup was successfully opened (popup !== null) and instead
of navigating the host window on popup block, surface a popup-blocked error to
the caller (e.g., invoke an onError/onPopupBlocked callback or set an error
state) so the app can show a user-facing message; locate the block around the
code that checks result.data?.url, the popup variable, and the startPolling()
invocation and remove the window.location.href fallback behavior.

---

Duplicate comments:
In
`@packages/js/src/ui/components/msteams-connect-button/MsTeamsConnectButton.tsx`:
- Around line 130-159: The popup is opened after await connect(...) which can
lose the user-gesture and trigger popup blockers; change MsTeamsConnectButton to
open a blank popup synchronously (e.g. window.open('about:blank', ...) and keep
the popup reference) before calling connect(), then await connect() and if
result.data?.url assign popup.location.href = result.data.url and call
startPolling(); on error or missing URL close the popup (popup.close()), call
setActionLoading(false) and invoke props.onConnectError with the error; ensure
all branches use the same popup reference and that startPolling is only started
after assigning the popup location.

---

Nitpick comments:
In `@packages/js/src/ui/components/msteams-link-user/MsTeamsLinkUser.tsx`:
- Around line 86-115: The startPolling function uses setInterval
(pollingIntervalId) which can start overlapping requests when
channelEndpoints.list takes longer than POLL_INTERVAL_MS; replace the
setInterval loop with a chained setTimeout retry (use the same
schedulePoll/backoff approach as in MsTeamsConnectButton.tsx) so each poll
awaits the previous request before scheduling the next, ensure you still clear
any scheduled timeout on success or timeout, and increase POLL_TIMEOUT_MS from 2
minutes to match the 5-minute value used by MsTeamsConnectButton.tsx to handle
MFA delays.

In `@packages/js/src/ui/components/slack-link-user/SlackLinkUser.tsx`:
- Around line 151-157: The else branch in SlackLinkUser.tsx now clears loading
and calls props.onLinkError but uses a generic error message; update that
message to match the provider-prefixed style (e.g. "Slack OAuth URL was not
returned. Please try again.") so it aligns with the timeout message, and make
the same wording change in SlackConnectButton.tsx (the error thrown where Slack
OAuth URL is missing); locate the callers around setActionLoading, startPolling,
and props.onLinkError to adjust the Error text consistently.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: bf77eda9-8195-4d03-afe9-1858ae624154

📥 Commits

Reviewing files that changed from the base of the PR and between 5f749df and ed1fd53.

📒 Files selected for processing (44)
  • apps/api/src/app/integrations/usecases/chat-oauth-callback/msteams-oauth-callback/msteams-oauth-callback.usecase.spec.ts
  • apps/api/src/app/integrations/usecases/chat-oauth-callback/msteams-oauth-callback/msteams-oauth-callback.usecase.ts
  • apps/api/src/app/integrations/usecases/generate-chat-oath-url/generate-msteams-oath-url/generate-msteams-oauth-url.usecase.spec.ts
  • apps/api/src/app/integrations/usecases/generate-chat-oath-url/generate-msteams-oath-url/generate-msteams-oauth-url.usecase.ts
  • libs/application-generic/src/services/ms-teams-token.service.ts
  • packages/js/src/ui/components/msteams-connect-button/MsTeamsConnectButton.tsx
  • packages/js/src/ui/components/msteams-link-user/MsTeamsLinkUser.tsx
  • packages/js/src/ui/components/slack-connect-button/SlackConnectButton.tsx
  • packages/js/src/ui/components/slack-link-user/SlackLinkUser.tsx
  • packages/js/src/ui/icons/ArrowDown.tsx
  • packages/js/src/ui/icons/ArrowDropDown.tsx
  • packages/js/src/ui/icons/ArrowLeft.tsx
  • packages/js/src/ui/icons/ArrowRight.tsx
  • packages/js/src/ui/icons/ArrowUpRight.tsx
  • packages/js/src/ui/icons/Bell.tsx
  • packages/js/src/ui/icons/BellCross.tsx
  • packages/js/src/ui/icons/BellPlus.tsx
  • packages/js/src/ui/icons/CalendarSchedule.tsx
  • packages/js/src/ui/icons/Chat.tsx
  • packages/js/src/ui/icons/Check.tsx
  • packages/js/src/ui/icons/CheckCircleFill.tsx
  • packages/js/src/ui/icons/Clock.tsx
  • packages/js/src/ui/icons/Cogs.tsx
  • packages/js/src/ui/icons/Copy.tsx
  • packages/js/src/ui/icons/Dots.tsx
  • packages/js/src/ui/icons/Email.tsx
  • packages/js/src/ui/icons/InApp.tsx
  • packages/js/src/ui/icons/Info.tsx
  • packages/js/src/ui/icons/Key.tsx
  • packages/js/src/ui/icons/Loader.tsx
  • packages/js/src/ui/icons/MarkAsArchived.tsx
  • packages/js/src/ui/icons/MarkAsArchivedRead.tsx
  • packages/js/src/ui/icons/MarkAsRead.tsx
  • packages/js/src/ui/icons/MarkAsUnarchived.tsx
  • packages/js/src/ui/icons/MarkAsUnread.tsx
  • packages/js/src/ui/icons/MsTeamsColored.tsx
  • packages/js/src/ui/icons/NodeTree.tsx
  • packages/js/src/ui/icons/Novu.tsx
  • packages/js/src/ui/icons/Push.tsx
  • packages/js/src/ui/icons/RouteFill.tsx
  • packages/js/src/ui/icons/SlackColored.tsx
  • packages/js/src/ui/icons/Sms.tsx
  • packages/js/src/ui/icons/Unread.tsx
  • packages/js/src/ui/icons/Unsnooze.tsx
✅ Files skipped from review due to trivial changes (34)
  • packages/js/src/ui/icons/Cogs.tsx
  • packages/js/src/ui/icons/Copy.tsx
  • packages/js/src/ui/icons/ArrowRight.tsx
  • packages/js/src/ui/icons/Check.tsx
  • packages/js/src/ui/icons/CheckCircleFill.tsx
  • packages/js/src/ui/icons/Chat.tsx
  • packages/js/src/ui/icons/NodeTree.tsx
  • packages/js/src/ui/icons/BellPlus.tsx
  • packages/js/src/ui/icons/Novu.tsx
  • packages/js/src/ui/icons/RouteFill.tsx
  • packages/js/src/ui/icons/MarkAsRead.tsx
  • packages/js/src/ui/icons/ArrowDropDown.tsx
  • packages/js/src/ui/icons/CalendarSchedule.tsx
  • packages/js/src/ui/icons/ArrowDown.tsx
  • packages/js/src/ui/icons/BellCross.tsx
  • packages/js/src/ui/icons/ArrowUpRight.tsx
  • packages/js/src/ui/icons/Unread.tsx
  • packages/js/src/ui/icons/InApp.tsx
  • packages/js/src/ui/icons/Dots.tsx
  • packages/js/src/ui/icons/Sms.tsx
  • packages/js/src/ui/icons/Push.tsx
  • packages/js/src/ui/icons/ArrowLeft.tsx
  • packages/js/src/ui/icons/MarkAsArchived.tsx
  • packages/js/src/ui/icons/Key.tsx
  • packages/js/src/ui/icons/MarkAsArchivedRead.tsx
  • packages/js/src/ui/icons/MarkAsUnarchived.tsx
  • packages/js/src/ui/icons/MarkAsUnread.tsx
  • packages/js/src/ui/icons/Clock.tsx
  • packages/js/src/ui/icons/Bell.tsx
  • packages/js/src/ui/icons/Info.tsx
  • packages/js/src/ui/icons/Email.tsx
  • packages/js/src/ui/icons/Unsnooze.tsx
  • packages/js/src/ui/icons/Loader.tsx
  • packages/js/src/ui/icons/MsTeamsColored.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/api/src/app/integrations/usecases/generate-chat-oath-url/generate-msteams-oath-url/generate-msteams-oauth-url.usecase.spec.ts

Comment on lines +47 to +71
if (stateData.mode === 'link_user') {
try {
await this.linkUserEndpoint(command, stateData, integration, credentials);
} catch (error) {
const message =
error instanceof Error ? error.message : 'An unexpected error occurred during bot installation.';

return {
type: ResponseTypeEnum.HTML,
result: this.buildErrorHtml(message),
};
}
} else {
await this.createAdminConsentConnection(command, stateData, integration);
}

if (credentials.redirectUrl) {
return { type: ResponseTypeEnum.URL, result: credentials.redirectUrl };
}

return {
type: ResponseTypeEnum.HTML,
result: this.SCRIPT_CLOSE_TAB,
};
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Log the error before rendering it as HTML.

The catch swallows the underlying error into a user-facing HTML page without invoking this.logger. For production debugging of bot-installation failures (Graph API errors, token issues, network), the only signal will be a redacted message in a closed popup — operators will be unable to correlate failures to root cause.

🪵 Suggested fix
     if (stateData.mode === 'link_user') {
       try {
         await this.linkUserEndpoint(command, stateData, integration, credentials);
       } catch (error) {
+        this.logger.error(
+          { err: error, environmentId: stateData.environmentId, organizationId: stateData.organizationId, integrationIdentifier: stateData.integrationIdentifier },
+          'MS Teams link_user flow failed'
+        );
         const message =
           error instanceof Error ? error.message : 'An unexpected error occurred during bot installation.';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/api/src/app/integrations/usecases/chat-oauth-callback/msteams-oauth-callback/msteams-oauth-callback.usecase.ts`
around lines 47 - 71, In the catch block around linkUserEndpoint in the msteams
oauth callback use case, log the caught error via this.logger.error (including
the error object and contextual info such as stateData.mode and integration
identifier) before returning the HTML response; ensure you call
this.logger.error(error, { context: 'msteams-oauth-callback', stateData,
integration }) or similar so the full stack/metadata is preserved for production
debugging, then continue to build and return the HTML as currently done.

* apps use a different identity model. If store-app support is ever needed, expand the filter
* to: distributionMethod eq 'organization' or distributionMethod eq 'store'.
*/
const url = `${MS_GRAPH_BASE_URL}/appCatalogs/teamsApps?$filter=externalId eq '${azureClientId}' and distributionMethod eq 'organization'`;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Encode interpolated URL components defensively.

azureClientId (line 167) is interpolated into an OData $filter value, and userOid (line 210) into a URL path segment. Both originate from admin-controlled credentials and Microsoft-issued JWTs respectively — so practical risk today is low — but neither is encoded. A future credential-shape change or a malformed oid claim would silently produce a malformed Graph URL or a broken $filter.

🛡️ Suggested fix
-    const url = `${MS_GRAPH_BASE_URL}/appCatalogs/teamsApps?$filter=externalId eq '${azureClientId}' and distributionMethod eq 'organization'`;
+    // Escape single quotes per OData literal escaping rules
+    const safeClientId = azureClientId.replace(/'/g, "''");
+    const url = `${MS_GRAPH_BASE_URL}/appCatalogs/teamsApps?$filter=externalId eq '${safeClientId}' and distributionMethod eq 'organization'`;
-    const url = `${MS_GRAPH_BASE_URL}/users/${userOid}/teamwork/installedApps`;
+    const url = `${MS_GRAPH_BASE_URL}/users/${encodeURIComponent(userOid)}/teamwork/installedApps`;

Also applies to: 209-213

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/api/src/app/integrations/usecases/chat-oauth-callback/msteams-oauth-callback/msteams-oauth-callback.usecase.ts`
at line 167, The interpolated values azureClientId (used in the OData $filter)
and userOid (used in the URL path) must be encoded before insertion to prevent
malformed Graph URLs; update the code in msteams-oauth-callback.usecase.ts to
escape these values (e.g., use a safe encoder for path segments like
encodeURIComponent for userOid and properly escape/encode single quotes and
special chars in the $filter value for azureClientId) wherever they are used
(the line constructing url with MS_GRAPH_BASE_URL/appCatalogs/teamsApps and the
lines building the URL with userOid around 209–213), ensuring the filter string
remains valid OData (wrap or double single-quotes as necessary) and remove raw
interpolation of these variables.

Comment thread packages/js/src/ui/components/msteams-link-user/MsTeamsLinkUser.tsx
… management

- Removed unnecessary popup creation and closure logic during OAuth URL retrieval.
- Updated the URL redirection to directly open the OAuth URL in a new tab, simplifying the user experience.
- Enhanced error handling for cases where the OAuth URL is not returned, ensuring proper feedback to the user.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
packages/js/src/ui/components/msteams-link-user/MsTeamsLinkUser.tsx (2)

135-141: Consider failing fast when no subscriberId is resolvable.

subscriberId: props.subscriberId ?? novuAccessor().subscriberId will silently send undefined when neither is set. Per the PR objectives, the server now requires subscriberId for link_user mode, so this will round-trip to the API and surface as a generic server error via onLinkError. A short-circuit with a clear client-side error gives SDK consumers a much better signal:

♻️ Suggested fail-fast guard
     } else {
       setActionLoading(true);
 
+      const resolvedSubscriberId = props.subscriberId ?? novuAccessor().subscriberId;
+      if (!resolvedSubscriberId) {
+        setActionLoading(false);
+        props.onLinkError?.(new Error('subscriberId is required to link an MS Teams user.'));
+
+        return;
+      }
+
       const result = await novuAccessor().channelConnections.generateOAuthUrl({
         integrationIdentifier: integrationIdentifier(),
         connectionIdentifier: connectionIdentifier(),
-        subscriberId: props.subscriberId ?? novuAccessor().subscriberId,
+        subscriberId: resolvedSubscriberId,
         context: props.context,
         mode: 'link_user',
       });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/js/src/ui/components/msteams-link-user/MsTeamsLinkUser.tsx` around
lines 135 - 141, The call to novuAccessor().channelConnections.generateOAuthUrl
in MsTeamsLinkUser.tsx can send undefined for subscriberId because it uses
props.subscriberId ?? novuAccessor().subscriberId; add a fail-fast guard that
resolves subscriberId into a local variable and, if it is undefined/null,
immediately call the existing onLinkError handler (e.g., props.onLinkError?.(new
Error('subscriberId is required for link_user mode'))) and return without
calling generateOAuthUrl; otherwise proceed to call generateOAuthUrl with the
validated subscriberId.

86-115: Defensive: clear any existing interval before starting a new one.

The disabled-button guard makes re-entry unlikely today, but pollingIntervalId = setInterval(...) overwrites the handle without clearing a previous one if startPolling is ever invoked twice (e.g., future refactor that re-triggers linking before unmount). Cheap to add and prevents a silent leak:

♻️ Defensive cleanup
   const startPolling = () => {
+    if (pollingIntervalId) {
+      clearInterval(pollingIntervalId);
+    }
     const startedAt = Date.now();
 
     pollingIntervalId = setInterval(async () => {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/js/src/ui/components/msteams-link-user/MsTeamsLinkUser.tsx` around
lines 86 - 115, The startPolling function may overwrite an existing
pollingIntervalId and leak intervals if invoked twice; before calling
setInterval in startPolling, check if pollingIntervalId is set and call
clearInterval(pollingIntervalId) to dispose the previous timer, then assign the
new interval to pollingIntervalId as you do now (also ensure pollingIntervalId
is the same identifier used in clearInterval elsewhere and is declared in the
enclosing scope so clearInterval can access it).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/js/src/ui/components/msteams-link-user/MsTeamsLinkUser.tsx`:
- Around line 135-141: The call to
novuAccessor().channelConnections.generateOAuthUrl in MsTeamsLinkUser.tsx can
send undefined for subscriberId because it uses props.subscriberId ??
novuAccessor().subscriberId; add a fail-fast guard that resolves subscriberId
into a local variable and, if it is undefined/null, immediately call the
existing onLinkError handler (e.g., props.onLinkError?.(new Error('subscriberId
is required for link_user mode'))) and return without calling generateOAuthUrl;
otherwise proceed to call generateOAuthUrl with the validated subscriberId.
- Around line 86-115: The startPolling function may overwrite an existing
pollingIntervalId and leak intervals if invoked twice; before calling
setInterval in startPolling, check if pollingIntervalId is set and call
clearInterval(pollingIntervalId) to dispose the previous timer, then assign the
new interval to pollingIntervalId as you do now (also ensure pollingIntervalId
is the same identifier used in clearInterval elsewhere and is declared in the
enclosing scope so clearInterval can access it).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: aa660557-ff7a-4ef7-b11c-44b9b7782a0d

📥 Commits

Reviewing files that changed from the base of the PR and between ed1fd53 and 2b4ea79.

📒 Files selected for processing (1)
  • packages/js/src/ui/components/msteams-link-user/MsTeamsLinkUser.tsx

…king

- Introduced the autoLinkUser option in the OAuth flow to automatically link the subscriber's identity after admin consent.
- Updated relevant DTOs and commands to include autoLinkUser as an optional boolean parameter.
- Enhanced the MsTeamsOAuth callback logic to handle user linking based on the autoLinkUser setting.
- Improved tests to validate the new autoLinkUser functionality and its impact on the OAuth flow.
- Added autoLinkUser parameter to various DTOs and commands to control user linking behavior during OAuth flows for Slack and MS Teams.
- Updated the MsTeams and Slack connect buttons to conditionally handle autoLinkUser based on the connection mode.
- Refined the OAuth callback logic to ensure proper handling of autoLinkUser settings, improving user experience during the linking process.
- Enhanced tests to validate the new autoLinkUser feature and its integration with existing functionality.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
apps/api/src/app/integrations/usecases/chat-oauth-callback/slack-oauth-callback/slack-oauth-callback.usecase.ts (1)

84-97: ⚠️ Potential issue | 🟠 Major

Behavioral change: Existing in-flight Slack OAuth flows will lose auto-created SLACK_USER endpoint.

The strict stateData.autoLinkUser === true check will fail for old OAuth states lacking this field (since validateAndDecodeState applies no defaults). Any user mid-OAuth at deploy time silently loses the auto-linked endpoint.

SlackConnectButton default is set, so component users are protected: autoLinkUser: mode === 'subscriber' ? (props.autoLinkUser ?? true) : false, defaults to true. However, raw API callers who don't explicitly pass autoLinkUser: true will also see the behavior change.

Recommend either:

  1. Treat missing autoLinkUser as true in validateAndDecodeState for one release as a transition, or
  2. Call this out clearly in the changelog so API consumers can opt in
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/api/src/app/integrations/usecases/chat-oauth-callback/slack-oauth-callback/slack-oauth-callback.usecase.ts`
around lines 84 - 97, Old OAuth states missing autoLinkUser cause silent loss of
auto-created SLACK_USER endpoint; fix by treating a missing autoLinkUser as true
during decoding: update validateAndDecodeState to set a default autoLinkUser =
true when the field is undefined (so stateData.autoLinkUser is true for legacy
states), or alternatively change the check before calling
this.createChannelEndpoint.execute to treat undefined as true (e.g., proceed
when stateData.autoLinkUser !== false); reference stateData.autoLinkUser,
validateAndDecodeState, and the SLACK_USER creation logic around
this.createChannelEndpoint.execute to locate where to apply the
default/conditional change.
apps/api/src/app/integrations/usecases/generate-chat-oath-url/generate-slack-oath-url/generate-slack-oauth-url.usecase.ts (1)

225-233: ⚠️ Potential issue | 🔴 Critical

Critical: hardcoded ngrok redirect URI must be reverted before merge.

buildRedirectUri ignores the computed baseUrl and returns a hardcoded developer ngrok tunnel. This will break Slack OAuth in every non-local environment (the registered redirect_uri won't match the Slack app config) and is also flagged by your own // TODO Revert this. baseUrl is now dead.

🛡️ Proposed fix
   static buildRedirectUri(): string {
     if (!process.env.API_ROOT_URL) {
       throw new Error('API_ROOT_URL environment variable is required');
     }

-    const baseUrl = process.env.API_ROOT_URL.replace(/\/$/, ''); // Remove trailing slash
-    // TODO Revert this
-    return `https://49c2-79-177-157-205.ngrok-free.app${CHAT_OAUTH_CALLBACK_PATH}`;
+    const baseUrl = process.env.API_ROOT_URL.replace(/\/$/, ''); // Remove trailing slash
+
+    return `${baseUrl}${CHAT_OAUTH_CALLBACK_PATH}`;
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/api/src/app/integrations/usecases/generate-chat-oath-url/generate-slack-oath-url/generate-slack-oauth-url.usecase.ts`
around lines 225 - 233, The buildRedirectUri function currently returns a
hardcoded ngrok URL and ignores the computed baseUrl; revert this by returning
the computed baseUrl concatenated with CHAT_OAUTH_CALLBACK_PATH (e.g.,
`${baseUrl}${CHAT_OAUTH_CALLBACK_PATH}`) after stripping any trailing slash from
process.env.API_ROOT_URL, remove the temporary hardcoded value, and keep the
existing environment variable validation so non-local environments use the
correct redirect_uri.
apps/api/src/app/integrations/usecases/generate-chat-oath-url/generate-msteams-oath-url/generate-msteams-oauth-url.usecase.ts (1)

190-198: ⚠️ Potential issue | 🔴 Critical

Critical: hardcoded ngrok redirect URI must be reverted before merge (same issue as Slack file).

buildRedirectUri discards the computed baseUrl and returns a developer ngrok tunnel. This will break the entire MS Teams admin-consent + link_user flow in any non-local environment because (a) the AAD app registration will reject the unmatched redirect_uri, and (b) the chained link_user redirect built in msteams-oauth-callback.usecase.ts:75-85 reuses this method.

🛡️ Proposed fix
   static buildRedirectUri(): string {
     if (!process.env.API_ROOT_URL) {
       throw new Error('API_ROOT_URL environment variable is required');
     }

-    const baseUrl = process.env.API_ROOT_URL.replace(/\/$/, ''); // Remove trailing slash
-    // TODO Revert this
-    return `https://49c2-79-177-157-205.ngrok-free.app${CHAT_OAUTH_CALLBACK_PATH}`;
+    const baseUrl = process.env.API_ROOT_URL.replace(/\/$/, ''); // Remove trailing slash
+
+    return `${baseUrl}${CHAT_OAUTH_CALLBACK_PATH}`;
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/api/src/app/integrations/usecases/generate-chat-oath-url/generate-msteams-oath-url/generate-msteams-oauth-url.usecase.ts`
around lines 190 - 198, The buildRedirectUri function currently returns a
hardcoded ngrok URL instead of using the computed baseUrl, which breaks MS Teams
OAuth flows; change the return to combine the computed baseUrl with
CHAT_OAUTH_CALLBACK_PATH (e.g., return `${baseUrl}${CHAT_OAUTH_CALLBACK_PATH}`),
ensuring you remove any duplicate slashes (baseUrl has trailing slash removed)
and that CHAT_OAUTH_CALLBACK_PATH starts with a leading slash; verify callers
(e.g., the logic in msteams-oauth-callback.usecase.ts that chains link_user
redirects) continue to use buildRedirectUri so the AAD app redirect_uri matches
the API_ROOT_URL-derived value.
🧹 Nitpick comments (1)
apps/api/src/app/integrations/usecases/chat-oauth-callback/slack-oauth-callback/slack-oauth-callback.usecase.spec.ts (1)

71-102: Consider using libs/testing and a NestJS test module instead of stubbed repositories.

Repositories are stubbed via sinon.createStubInstance and the SUT is wired manually. This bypasses real DB state and DI. The repository-history of this codebase already has shared harnesses (libs/testing) that manage a real Mongo state for integration-style usecase tests, and the project guideline is to bootstrap a NestJS testing module rather than hand-wire dependencies. Migrating these tests would let you assert on persisted artifacts (the actual ChannelEndpoint doc, its keys, etc.) rather than on stub call args.

As per coding guidelines: "Never mock MongoDB models directly; use the test harness in libs/testing which manages real database state" and "Bootstrap a NestJS testing module for integration-style unit tests rather than mocking the entire DI container".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/api/src/app/integrations/usecases/chat-oauth-callback/slack-oauth-callback/slack-oauth-callback.usecase.spec.ts`
around lines 71 - 102, Replace the manual sinon stubs and hand-wired
SlackOauthCallback construction with the shared test harness and a NestJS
TestingModule from libs/testing: bootstrap a TestingModule that imports the app
module or the specific module under test, inject real IntegrationRepository and
EnvironmentRepository (instead of sinon.createStubInstance), call the
libs/testing utilities to provision/reset a real Mongo state, and use the actual
CreateChannelConnection and CreateChannelEndpoint providers (or their real
implementations) from the module; then perform the usecase invocation
(SlackOauthCallback.execute) via the injected instance and assert against
persisted documents (ChannelEndpoint, connections, apiKeys) in the DB rather
than asserting stub call args. Ensure any external HTTP calls (axios.post) are
either routed through a test HTTP mock provided by the harness or replaced by a
controlled test double within the DI container so the test remains
deterministic.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@apps/api/src/app/integrations/usecases/chat-oauth-callback/msteams-oauth-callback/msteams-oauth-callback.usecase.ts`:
- Around line 314-361: The current extractOidFromIdToken decodes the id_token
without verifying its signature; update exchangeCodeForAadObjectId and
extractOidFromIdToken to perform full JWT verification: fetch the tenant JWKS
from https://login.microsoftonline.com/{tenantId}/discovery/v2.0/keys (use
jwks-rsa or similar), obtain the signing key for the id_token's kid, and verify
the token signature and claims (iss matches Microsoft issuer for the tenant, aud
equals credentials.clientId, and exp/iat validity) using jsonwebtoken.verify
before extracting oid; pass tenantId and clientId into the verification function
(or perform verification inside exchangeCodeForAadObjectId) and throw
BadRequestException on any validation failure.

In `@packages/js/src/ui/components/slack-connect-button/SlackConnectButton.tsx`:
- Around line 23-31: The public prop default for autoLinkUser currently flips
behavior for consumers; change the default to false to preserve backwards
compatibility: update the SlackConnectButton component (the autoLinkUser prop,
any defaultProps or destructured default in the function/component) to default
to false, and update the JSDoc comment to reflect the new default; also add a
short changelog note indicating this behavioral choice if you prefer to keep the
current versioning, but implement the code default change in SlackConnectButton
so existing integrations are not opt-into creating SLACK_USER endpoints.

In `@playground/nextjs/src/pages/connect-chat/index.tsx`:
- Around line 110-115: The change touches a read-only demo under playground (you
modified the connect-chat component by adding autoLinkUser and onConnectError),
so either revert the edits to restore the original file or move the demo to a
writable location and apply the update there; specifically, revert/remove the
autoLinkUser={false} and onConnectError={(error) => console.error(error)}
changes in the ConnectChat component (connect-chat/index.tsx) and, if the new
autoLinkUser API must be demonstrated, create a new example outside the
playground directory and apply the changes there after getting confirmation that
the demo should be relocated.

---

Outside diff comments:
In
`@apps/api/src/app/integrations/usecases/chat-oauth-callback/slack-oauth-callback/slack-oauth-callback.usecase.ts`:
- Around line 84-97: Old OAuth states missing autoLinkUser cause silent loss of
auto-created SLACK_USER endpoint; fix by treating a missing autoLinkUser as true
during decoding: update validateAndDecodeState to set a default autoLinkUser =
true when the field is undefined (so stateData.autoLinkUser is true for legacy
states), or alternatively change the check before calling
this.createChannelEndpoint.execute to treat undefined as true (e.g., proceed
when stateData.autoLinkUser !== false); reference stateData.autoLinkUser,
validateAndDecodeState, and the SLACK_USER creation logic around
this.createChannelEndpoint.execute to locate where to apply the
default/conditional change.

In
`@apps/api/src/app/integrations/usecases/generate-chat-oath-url/generate-msteams-oath-url/generate-msteams-oauth-url.usecase.ts`:
- Around line 190-198: The buildRedirectUri function currently returns a
hardcoded ngrok URL instead of using the computed baseUrl, which breaks MS Teams
OAuth flows; change the return to combine the computed baseUrl with
CHAT_OAUTH_CALLBACK_PATH (e.g., return `${baseUrl}${CHAT_OAUTH_CALLBACK_PATH}`),
ensuring you remove any duplicate slashes (baseUrl has trailing slash removed)
and that CHAT_OAUTH_CALLBACK_PATH starts with a leading slash; verify callers
(e.g., the logic in msteams-oauth-callback.usecase.ts that chains link_user
redirects) continue to use buildRedirectUri so the AAD app redirect_uri matches
the API_ROOT_URL-derived value.

In
`@apps/api/src/app/integrations/usecases/generate-chat-oath-url/generate-slack-oath-url/generate-slack-oauth-url.usecase.ts`:
- Around line 225-233: The buildRedirectUri function currently returns a
hardcoded ngrok URL and ignores the computed baseUrl; revert this by returning
the computed baseUrl concatenated with CHAT_OAUTH_CALLBACK_PATH (e.g.,
`${baseUrl}${CHAT_OAUTH_CALLBACK_PATH}`) after stripping any trailing slash from
process.env.API_ROOT_URL, remove the temporary hardcoded value, and keep the
existing environment variable validation so non-local environments use the
correct redirect_uri.

---

Nitpick comments:
In
`@apps/api/src/app/integrations/usecases/chat-oauth-callback/slack-oauth-callback/slack-oauth-callback.usecase.spec.ts`:
- Around line 71-102: Replace the manual sinon stubs and hand-wired
SlackOauthCallback construction with the shared test harness and a NestJS
TestingModule from libs/testing: bootstrap a TestingModule that imports the app
module or the specific module under test, inject real IntegrationRepository and
EnvironmentRepository (instead of sinon.createStubInstance), call the
libs/testing utilities to provision/reset a real Mongo state, and use the actual
CreateChannelConnection and CreateChannelEndpoint providers (or their real
implementations) from the module; then perform the usecase invocation
(SlackOauthCallback.execute) via the injected instance and assert against
persisted documents (ChannelEndpoint, connections, apiKeys) in the DB rather
than asserting stub call args. Ensure any external HTTP calls (axios.post) are
either routed through a test HTTP mock provided by the harness or replaced by a
controlled test double within the DI container so the test remains
deterministic.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 291dd689-524a-4205-93c8-b40f7e84d490

📥 Commits

Reviewing files that changed from the base of the PR and between 2b4ea79 and 408dd08.

📒 Files selected for processing (21)
  • apps/api/src/app/inbox/inbox.controller.ts
  • apps/api/src/app/integrations/dtos/generate-chat-oauth-url.dto.ts
  • apps/api/src/app/integrations/integrations.controller.ts
  • apps/api/src/app/integrations/usecases/chat-oauth-callback/msteams-oauth-callback/msteams-oauth-callback.usecase.spec.ts
  • apps/api/src/app/integrations/usecases/chat-oauth-callback/msteams-oauth-callback/msteams-oauth-callback.usecase.ts
  • apps/api/src/app/integrations/usecases/chat-oauth-callback/slack-oauth-callback/slack-oauth-callback.usecase.spec.ts
  • apps/api/src/app/integrations/usecases/chat-oauth-callback/slack-oauth-callback/slack-oauth-callback.usecase.ts
  • apps/api/src/app/integrations/usecases/generate-chat-oath-url/generate-chat-oauth-url.command.ts
  • apps/api/src/app/integrations/usecases/generate-chat-oath-url/generate-chat-oauth-url.usecase.ts
  • apps/api/src/app/integrations/usecases/generate-chat-oath-url/generate-msteams-oath-url/generate-msteams-oauth-url.command.ts
  • apps/api/src/app/integrations/usecases/generate-chat-oath-url/generate-msteams-oath-url/generate-msteams-oauth-url.usecase.ts
  • apps/api/src/app/integrations/usecases/generate-chat-oath-url/generate-slack-oath-url/generate-slack-oauth-url.command.ts
  • apps/api/src/app/integrations/usecases/generate-chat-oath-url/generate-slack-oath-url/generate-slack-oauth-url.usecase.ts
  • packages/js/src/api/inbox-service.ts
  • packages/js/src/channel-connections/types.ts
  • packages/js/src/ui/components/msteams-connect-button/MsTeamsConnectButton.tsx
  • packages/js/src/ui/components/slack-connect-button/SlackConnectButton.tsx
  • packages/react/src/components/msteams-connect-button/DefaultMsTeamsConnectButton.tsx
  • packages/react/src/components/slack-connect-button/DefaultSlackConnectButton.tsx
  • playground/nextjs/src/pages/connect-chat/index.tsx
  • playground/nextjs/src/pages/connect-msteams/index.tsx
✅ Files skipped from review due to trivial changes (2)
  • packages/js/src/channel-connections/types.ts
  • packages/react/src/components/slack-connect-button/DefaultSlackConnectButton.tsx
🚧 Files skipped from review as they are similar to previous changes (4)
  • playground/nextjs/src/pages/connect-msteams/index.tsx
  • apps/api/src/app/integrations/usecases/generate-chat-oath-url/generate-msteams-oath-url/generate-msteams-oauth-url.command.ts
  • packages/js/src/ui/components/msteams-connect-button/MsTeamsConnectButton.tsx
  • apps/api/src/app/integrations/usecases/chat-oauth-callback/msteams-oauth-callback/msteams-oauth-callback.usecase.spec.ts

Comment on lines +314 to +361
private async exchangeCodeForAadObjectId(code: string, credentials: ICredentialsEntity): Promise<string> {
const { clientId, secretKey, tenantId } = credentials;

if (!clientId || !secretKey || !tenantId) {
throw new BadRequestException(
'MS Teams integration missing required credentials (clientId, secretKey, tenantId)'
);
}

const tokenParams = new URLSearchParams({
grant_type: 'authorization_code',
client_id: clientId,
client_secret: secretKey,
code,
redirect_uri: GenerateMsTeamsOauthUrl.buildRedirectUri(),
scope: 'openid profile User.Read',
});

const response = await axios.post(
`${this.MS_TEAMS_TOKEN_URL}/${tenantId}/oauth2/v2.0/token`,
tokenParams.toString(),
{ headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }
);

const { id_token: idToken } = response.data;

if (!idToken) {
throw new BadRequestException('MS Teams OAuth response missing id_token');
}

const oid = this.extractOidFromIdToken(idToken);

if (!oid) {
throw new BadRequestException('MS Teams id_token missing oid claim — ensure the Azure app is single-tenant');
}

return oid;
}

private extractOidFromIdToken(idToken: string): string | undefined {
try {
const payload = idToken.split('.')[1];
const decoded = JSON.parse(Buffer.from(payload, 'base64url').toString('utf-8'));

return decoded.oid as string | undefined;
} catch {
throw new BadRequestException('Failed to decode MS Teams id_token');
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

What is the recommended way to verify a Microsoft Entra ID (AAD) id_token signature server-side in Node.js, and is signature verification required when the token is obtained directly from the /oauth2/v2.0/token endpoint over HTTPS?

💡 Result:

The recommended way to verify a Microsoft Entra ID (formerly Azure AD) id_token signature server-side in Node.js is to use the jsonwebtoken and jwks-rsa npm packages. Fetch the public signing key from Entra ID's JWKS endpoint (e.g., https://login.microsoftonline.com/{tenant_id}/discovery/v2.0/keys) matching the token's kid header claim, then verify the signature using jwt.verify with RS256 algorithm, followed by validating claims like iss, aud, exp, iat, and nonce if applicable. Example code: const jwt = require('jsonwebtoken'); const jwksClient = require('jwks-rsa'); const client = jwksClient({ jwksUri: 'https://login.microsoftonline.com/{tenant_id}/discovery/v2.0/keys' }); function getKey(header, callback) { client.getSigningKey(header.kid, (err, key) => { const signingKey = key.getPublicKey; callback(null, signingKey); }); } jwt.verify(idToken, getKey, { algorithms: ['RS256'], issuer: 'https://login.microsoftonline.com/{tenant_id}/v2.0', audience: '{client_id}' }, (err, decoded) => { if (err) { // Invalid token } else { // Valid token, use decoded } }); Replace {tenant_id} and {client_id} with your values. Use the OpenID configuration endpoint https://login.microsoftonline.com/{tenant_id}/v2.0/.well-known/openid-configuration to get the exact jwks_uri and issuer. Signature verification is required even when the id_token is obtained directly from the /oauth2/v2.0/token endpoint over HTTPS. Official Microsoft documentation states that applications must validate the id_token signature using public keys from the JWKS endpoint, along with claims validation (iss, aud, exp, etc.), regardless of the endpoint. HTTPS protects transit but does not replace server-side cryptographic verification to ensure the token's authenticity and integrity against tampering or replay attacks. This applies to confidential clients like server-side apps; public clients may skip it but server-side validation is mandatory for security.

Citations:


Required: Implement JWT signature verification for MS Teams id_token.

The id_token is decoded without validating its signature. Microsoft's official documentation mandates that confidential clients (server-side applications) verify id_tokens against the Entra ID JWKS endpoint, regardless of HTTPS transport. HTTPS protects the transmission but does not replace cryptographic verification needed to ensure token authenticity and prevent tampering or replay attacks.

Verify the token signature using packages like jsonwebtoken and jwks-rsa by fetching the public signing key from https://login.microsoftonline.com/{tenantId}/discovery/v2.0/keys and validating claims (iss, aud, exp, iat) accordingly.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/api/src/app/integrations/usecases/chat-oauth-callback/msteams-oauth-callback/msteams-oauth-callback.usecase.ts`
around lines 314 - 361, The current extractOidFromIdToken decodes the id_token
without verifying its signature; update exchangeCodeForAadObjectId and
extractOidFromIdToken to perform full JWT verification: fetch the tenant JWKS
from https://login.microsoftonline.com/{tenantId}/discovery/v2.0/keys (use
jwks-rsa or similar), obtain the signing key for the id_token's kid, and verify
the token signature and claims (iss matches Microsoft issuer for the tenant, aud
equals credentials.clientId, and exp/iat validity) using jsonwebtoken.verify
before extracting oid; pass tenantId and clientId into the verification function
(or perform verification inside exchangeCodeForAadObjectId) and throw
BadRequestException on any validation failure.

Comment on lines +23 to +31
/**
* When true (default), after the workspace connection is created the OAuth
* flow also links the subscriber who clicked "Connect" as a personal Slack
* endpoint using the authed_user.id already returned by oauth.v2.access.
* Set to false to skip the user-linking step and only create the workspace
* connection. Raw API callers must pass true explicitly; this component
* defaults to true.
*/
autoLinkUser?: boolean;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

❓ Verification inconclusive

Script executed:

#!/bin/bash
# Confirm raw-API default and SDK default are intentionally divergent, and find migration notes if any.
fd -e md -e mdx CHANGELOG | xargs -I{} grep -l -i "autoLinkUser\|slack" {} 2>/dev/null
rg -nP --type=ts 'autoLinkUser' -g '!**/*.spec.ts' -g '!**/*.test.ts' -C2

Repository: novuhq/novu


Repository: novuhq/novu
Exit code: 0

stdout:

packages/framework/CHANGELOG.md
packages/react/src/components/slack-connect-button/DefaultSlackConnectButton.tsx-12-  | 'scope'
packages/react/src/components/slack-connect-button/DefaultSlackConnectButton.tsx-13-  | 'connectionMode'
packages/react/src/components/slack-connect-button/DefaultSlackConnectButton.tsx:14:  | 'autoLinkUser'
packages/react/src/components/slack-connect-button/DefaultSlackConnectButton.tsx-15-  | 'onConnectSuccess'
packages/react/src/components/slack-connect-button/DefaultSlackConnectButton.tsx-16-  | 'onConnectError'
--
packages/react/src/components/slack-connect-button/DefaultSlackConnectButton.tsx-29-    scope,
packages/react/src/components/slack-connect-button/DefaultSlackConnectButton.tsx-30-    connectionMode,
packages/react/src/components/slack-connect-button/DefaultSlackConnectButton.tsx:31:    autoLinkUser,
packages/react/src/components/slack-connect-button/DefaultSlackConnectButton.tsx-32-    onConnectSuccess,
packages/react/src/components/slack-connect-button/DefaultSlackConnectButton.tsx-33-    onConnectError,
--
packages/react/src/components/slack-connect-button/DefaultSlackConnectButton.tsx-50-          scope,
packages/react/src/components/slack-connect-button/DefaultSlackConnectButton.tsx-51-          connectionMode,
packages/react/src/components/slack-connect-button/DefaultSlackConnectButton.tsx:52:          autoLinkUser,
packages/react/src/components/slack-connect-button/DefaultSlackConnectButton.tsx-53-          onConnectSuccess,
packages/react/src/components/slack-connect-button/DefaultSlackConnectButton.tsx-54-          onConnectError,
--
packages/react/src/components/slack-connect-button/DefaultSlackConnectButton.tsx-69-      scope,
packages/react/src/components/slack-connect-button/DefaultSlackConnectButton.tsx-70-      connectionMode,
packages/react/src/components/slack-connect-button/DefaultSlackConnectButton.tsx:71:      autoLinkUser,
packages/react/src/components/slack-connect-button/DefaultSlackConnectButton.tsx-72-      onConnectSuccess,
packages/react/src/components/slack-connect-button/DefaultSlackConnectButton.tsx-73-      onConnectError,
--
packages/react/src/components/msteams-connect-button/DefaultMsTeamsConnectButton.tsx-12-  | 'scope'
packages/react/src/components/msteams-connect-button/DefaultMsTeamsConnectButton.tsx-13-  | 'connectionMode'
packages/react/src/components/msteams-connect-button/DefaultMsTeamsConnectButton.tsx:14:  | 'autoLinkUser'
packages/react/src/components/msteams-connect-button/DefaultMsTeamsConnectButton.tsx-15-  | 'onConnectSuccess'
packages/react/src/components/msteams-connect-button/DefaultMsTeamsConnectButton.tsx-16-  | 'onConnectError'
--
packages/react/src/components/msteams-connect-button/DefaultMsTeamsConnectButton.tsx-29-    scope,
packages/react/src/components/msteams-connect-button/DefaultMsTeamsConnectButton.tsx-30-    connectionMode,
packages/react/src/components/msteams-connect-button/DefaultMsTeamsConnectButton.tsx:31:    autoLinkUser,
packages/react/src/components/msteams-connect-button/DefaultMsTeamsConnectButton.tsx-32-    onConnectSuccess,
packages/react/src/components/msteams-connect-button/DefaultMsTeamsConnectButton.tsx-33-    onConnectError,
--
packages/react/src/components/msteams-connect-button/DefaultMsTeamsConnectButton.tsx-50-          scope,
packages/react/src/components/msteams-connect-button/DefaultMsTeamsConnectButton.tsx-51-          connectionMode,
packages/react/src/components/msteams-connect-button/DefaultMsTeamsConnectButton.tsx:52:          autoLinkUser,
packages/react/src/components/msteams-connect-button/DefaultMsTeamsConnectButton.tsx-53-          onConnectSuccess,
packages/react/src/components/msteams-connect-button/DefaultMsTeamsConnectButton.tsx-54-          onConnectError,
--
packages/react/src/components/msteams-connect-button/DefaultMsTeamsConnectButton.tsx-69-      scope,
packages/react/src/components/msteams-connect-button/DefaultMsTeamsConnectButton.tsx-70-      connectionMode,
packages/react/src/components/msteams-connect-button/DefaultMsTeamsConnectButton.tsx:71:      autoLinkUser,
packages/react/src/components/msteams-connect-button/DefaultMsTeamsConnectButton.tsx-72-      onConnectSuccess,
packages/react/src/components/msteams-connect-button/DefaultMsTeamsConnectButton.tsx-73-      onConnectError,
--
playground/nextjs/src/pages/connect-msteams/index.tsx-114-              // connectionMode="shared"
playground/nextjs/src/pages/connect-msteams/index.tsx-115-              onConnectError={(error) => console.error(error)}
playground/nextjs/src/pages/connect-msteams/index.tsx:116:              autoLinkUser={false}
playground/nextjs/src/pages/connect-msteams/index.tsx-117-            />
playground/nextjs/src/pages/connect-msteams/index.tsx-118-          </NovuProvider>
--
playground/nextjs/src/pages/connect-chat/index.tsx-113-              // ...(context && { context: context }),
playground/nextjs/src/pages/connect-chat/index.tsx-114-              onConnectError={(error) => console.error(error)}
playground/nextjs/src/pages/connect-chat/index.tsx:115:              autoLinkUser={false}
playground/nextjs/src/pages/connect-chat/index.tsx-116-            />
playground/nextjs/src/pages/connect-chat/index.tsx-117-          </NovuProvider>
--
packages/js/src/ui/components/slack-connect-button/SlackConnectButton.tsx-29-   * defaults to true.
packages/js/src/ui/components/slack-connect-button/SlackConnectButton.tsx-30-   */
packages/js/src/ui/components/slack-connect-button/SlackConnectButton.tsx:31:  autoLinkUser?: boolean;
packages/js/src/ui/components/slack-connect-button/SlackConnectButton.tsx-32-  onConnectSuccess?: (connectionIdentifier: string) => void;
packages/js/src/ui/components/slack-connect-button/SlackConnectButton.tsx-33-  onConnectError?: (error: unknown) => void;
--
packages/js/src/ui/components/slack-connect-button/SlackConnectButton.tsx-148-        scope: props.scope,
packages/js/src/ui/components/slack-connect-button/SlackConnectButton.tsx-149-        connectionMode: mode,
packages/js/src/ui/components/slack-connect-button/SlackConnectButton.tsx:150:        autoLinkUser: mode === 'subscriber' ? (props.autoLinkUser ?? true) : false,
packages/js/src/ui/components/slack-connect-button/SlackConnectButton.tsx-151-      });
packages/js/src/ui/components/slack-connect-button/SlackConnectButton.tsx-152-
--
packages/js/src/ui/components/msteams-connect-button/MsTeamsConnectButton.tsx-27-   * Set to false to perform only the tenant-level admin consent without linking the user.
packages/js/src/ui/components/msteams-connect-button/MsTeamsConnectButton.tsx-28-   */
packages/js/src/ui/components/msteams-connect-button/MsTeamsConnectButton.tsx:29:  autoLinkUser?: boolean;
packages/js/src/ui/components/msteams-connect-button/MsTeamsConnectButton.tsx-30-  onConnectSuccess?: (connectionIdentifier: string) => void;
packages/js/src/ui/components/msteams-connect-button/MsTeamsConnectButton.tsx-31-  onConnectError?: (error: unknown) => void;
--
packages/js/src/ui/components/msteams-connect-button/MsTeamsConnectButton.tsx-149-        scope: props.scope,
packages/js/src/ui/components/msteams-connect-button/MsTeamsConnectButton.tsx-150-        connectionMode: mode,
packages/js/src/ui/components/msteams-connect-button/MsTeamsConnectButton.tsx:151:        autoLinkUser: mode === 'subscriber' ? (props.autoLinkUser ?? true) : false,
packages/js/src/ui/components/msteams-connect-button/MsTeamsConnectButton.tsx-152-      });
packages/js/src/ui/components/msteams-connect-button/MsTeamsConnectButton.tsx-153-
--
packages/js/src/api/inbox-service.ts-552-    mode,
packages/js/src/api/inbox-service.ts-553-    connectionMode,
packages/js/src/api/inbox-service.ts:554:    autoLinkUser,
packages/js/src/api/inbox-service.ts-555-  }: GenerateChatOAuthUrlArgs): Promise<{ url: string }> {
packages/js/src/api/inbox-service.ts-556-    return this.#httpClient.post(CHAT_OAUTH_ROUTE, {
--
packages/js/src/api/inbox-service.ts-563-      mode,
packages/js/src/api/inbox-service.ts-564-      connectionMode,
packages/js/src/api/inbox-service.ts:565:      autoLinkUser,
packages/js/src/api/inbox-service.ts-566-    });
packages/js/src/api/inbox-service.ts-567-  }
--
packages/js/src/channel-connections/types.ts-23-  mode?: OAuthMode;
packages/js/src/channel-connections/types.ts-24-  connectionMode?: ConnectionMode;
packages/js/src/channel-connections/types.ts:25:  autoLinkUser?: boolean;
packages/js/src/channel-connections/types.ts-26-};
packages/js/src/channel-connections/types.ts-27-
--
apps/api/src/app/integrations/integrations.controller.ts-436-        mode: body.mode,
apps/api/src/app/integrations/integrations.controller.ts-437-        connectionMode: body.connectionMode,
apps/api/src/app/integrations/integrations.controller.ts:438:        autoLinkUser: body.autoLinkUser,
apps/api/src/app/integrations/integrations.controller.ts-439-      })
apps/api/src/app/integrations/integrations.controller.ts-440-    );
--
apps/api/src/app/integrations/usecases/chat-oauth-callback/msteams-oauth-callback/msteams-oauth-callback.usecase.ts-63-
apps/api/src/app/integrations/usecases/chat-oauth-callback/msteams-oauth-callback/msteams-oauth-callback.usecase.ts-64-      /*
apps/api/src/app/integrations/usecases/chat-oauth-callback/msteams-oauth-callback/msteams-oauth-callback.usecase.ts:65:       * After admin consent, if autoLinkUser is explicitly true and a subscriberId is
apps/api/src/app/integrations/usecases/chat-oauth-callback/msteams-oauth-callback/msteams-oauth-callback.usecase.ts-66-       * present, chain into the link_user OAuth flow so the subscriber who clicked
apps/api/src/app/integrations/usecases/chat-oauth-callback/msteams-oauth-callback/msteams-oauth-callback.usecase.ts-67-       * "Connect" also gets their personal Teams identity linked in one go.
apps/api/src/app/integrations/usecases/chat-oauth-callback/msteams-oauth-callback/msteams-oauth-callback.usecase.ts-68-       *
apps/api/src/app/integrations/usecases/chat-oauth-callback/msteams-oauth-callback/msteams-oauth-callback.usecase.ts:69:       * autoLinkUser must be explicitly true — absent or false skips the chain.
apps/api/src/app/integrations/usecases/chat-oauth-callback/msteams-oauth-callback/msteams-oauth-callback.usecase.ts:70:       * The MsTeamsConnectButton SDK component defaults autoLinkUser to true so SDK
apps/api/src/app/integrations/usecases/chat-oauth-callback/msteams-oauth-callback/msteams-oauth-callback.usecase.ts-71-       * users get this behaviour by default; raw API callers must opt in explicitly.
apps/api/src/app/integrations/usecases/chat-oauth-callback/msteams-oauth-callback/msteams-oauth-callback.usecase.ts-72-       */
apps/api/src/app/integrations/usecases/chat-oauth-callback/msteams-oauth-callback/msteams-oauth-callback.usecase.ts:73:      if (stateData.autoLinkUser === true && stateData.subscriberId) {
apps/api/src/app/integrations/usecases/chat-oauth-callback/msteams-oauth-callback/msteams-oauth-callback.usecase.ts-74-        try {
apps/api/src/app/integrations/usecases/chat-oauth-callback/msteams-oauth-callback/msteams-oauth-callback.usecase.ts-75-          const linkUserUrl = await this.generateMsTeamsOauthUrl.execute(
--
apps/api/src/app/integrations/usecases/chat-oauth-callback/slack-oauth-callback/slack-oauth-callback.usecase.ts-82-        })
apps/api/src/app/integrations/usecases/chat-oauth-callback/slack-oauth-callback/slack-oauth-callback.usecase.ts-83-      );
apps/api/src/app/integrations/usecases/chat-oauth-callback/slack-oauth-callback/slack-oauth-callback.usecase.ts:84:      if (stateData.autoLinkUser === true && stateData.subscriberId && authData.authed_user?.id) {
apps/api/src/app/integrations/usecases/chat-oauth-callback/slack-oauth-callback/slack-oauth-callback.usecase.ts-85-        await this.createChannelEndpoint.execute(
apps/api/src/app/integrations/usecases/chat-oauth-callback/slack-oauth-callback/slack-oauth-callback.usecase.ts-86-          CreateChannelEndpointCommand.create({
--
apps/api/src/app/integrations/usecases/generate-chat-oath-url/generate-chat-oauth-url.usecase.ts-34-            mode: command.mode,
apps/api/src/app/integrations/usecases/generate-chat-oath-url/generate-chat-oauth-url.usecase.ts-35-            connectionMode: command.connectionMode,
apps/api/src/app/integrations/usecases/generate-chat-oath-url/generate-chat-oauth-url.usecase.ts:36:            autoLinkUser: command.autoLinkUser,
apps/api/src/app/integrations/usecases/generate-chat-oath-url/generate-chat-oauth-url.usecase.ts-37-          })
apps/api/src/app/integrations/usecases/generate-chat-oath-url/generate-chat-oauth-url.usecase.ts-38-        );
--
apps/api/src/app/integrations/usecases/generate-chat-oath-url/generate-chat-oauth-url.usecase.ts-48-            context: command.context,
apps/api/src/app/integrations/usecases/generate-chat-oath-url/generate-chat-oauth-url.usecase.ts-49-            mode: command.mode,
apps/api/src/app/integrations/usecases/generate-chat-oath-url/generate-chat-oauth-url.usecase.ts:50:            autoLinkUser: command.autoLinkUser,
apps/api/src/app/integrations/usecases/generate-chat-oath-url/generate-chat-oauth-url.usecase.ts-51-          })
apps/api/src/app/integrations/usecases/generate-chat-oath-url/generate-chat-oauth-url.usecase.ts-52-        );
--
apps/api/src/app/integrations/usecases/generate-chat-oath-url/generate-slack-oath-url/generate-slack-oauth-url.command.ts-43-  `@IsOptional`()
apps/api/src/app/integrations/usecases/generate-chat-oath-url/generate-slack-oath-url/generate-slack-oauth-url.command.ts-44-  `@IsBoolean`()
apps/api/src/app/integrations/usecases/generate-chat-oath-url/generate-slack-oath-url/generate-slack-oauth-url.command.ts:45:  readonly autoLinkUser?: boolean;
apps/api/src/app/integrations/usecases/generate-chat-oath-url/generate-slack-oath-url/generate-slack-oauth-url.command.ts-46-}
--
apps/api/src/app/integrations/usecases/generate-chat-oath-url/generate-msteams-oath-url/generate-msteams-oauth-url.command.ts-28-  `@IsOptional`()
apps/api/src/app/integrations/usecases/generate-chat-oath-url/generate-msteams-oath-url/generate-msteams-oauth-url.command.ts-29-  `@IsBoolean`()
apps/api/src/app/integrations/usecases/generate-chat-oath-url/generate-msteams-oath-url/generate-msteams-oauth-url.command.ts:30:  readonly autoLinkUser?: boolean;
apps/api/src/app/integrations/usecases/generate-chat-oath-url/generate-msteams-oath-url/generate-msteams-oauth-url.command.ts-31-}
--
apps/api/src/app/integrations/usecases/generate-chat-oath-url/generate-msteams-oath-url/generate-msteams-oauth-url.usecase.ts-21-  timestamp: number;
apps/api/src/app/integrations/usecases/generate-chat-oath-url/generate-msteams-oath-url/generate-msteams-oauth-url.usecase.ts-22-  mode?: OAuthMode;
apps/api/src/app/integrations/usecases/generate-chat-oath-url/generate-msteams-oath-url/generate-msteams-oauth-url.usecase.ts:23:  autoLinkUser?: boolean;
apps/api/src/app/integrations/usecases/generate-chat-oath-url/generate-msteams-oath-url/generate-msteams-oauth-url.usecase.ts-24-};
apps/api/src/app/integrations/usecases/generate-chat-oath-url/generate-msteams-oath-url/generate-msteams-oauth-url.usecase.ts-25-
--
apps/api/src/app/integrations/usecases/generate-chat-oath-url/generate-msteams-oath-url/generate-msteams-oauth-url.usecase.ts-60-      command.connectionIdentifier,
apps/api/src/app/integrations/usecases/generate-chat-oath-url/generate-msteams-oath-url/generate-msteams-oauth-url.usecase.ts-61-      command.mode,
apps/api/src/app/integrations/usecases/generate-chat-oath-url/generate-msteams-oath-url/generate-msteams-oauth-url.usecase.ts:62:      command.autoLinkUser
apps/api/src/app/integrations/usecases/generate-chat-oath-url/generate-msteams-oath-url/generate-msteams-oauth-url.usecase.ts-63-    );
apps/api/src/app/integrations/usecases/generate-chat-oath-url/generate-msteams-oath-url/generate-msteams-oauth-url.usecase.ts-64-
--
apps/api/src/app/integrations/usecases/generate-chat-oath-url/generate-msteams-oath-url/generate-msteams-oauth-url.usecase.ts-137-    connectionIdentifier?: string,
apps/api/src/app/integrations/usecases/generate-chat-oath-url/generate-msteams-oath-url/generate-msteams-oauth-url.usecase.ts-138-    mode?: OAuthMode,
apps/api/src/app/integrations/usecases/generate-chat-oath-url/generate-msteams-oath-url/generate-msteams-oauth-url.usecase.ts:139:    autoLinkUser?: boolean
apps/api/src/app/integrations/usecases/generate-chat-oath-url/generate-msteams-oath-url/generate-msteams-oauth-url.usecase.ts-140-  ): Promise<string> {
apps/api/src/app/integrations/usecases/generate-chat-oath-url/generate-msteams-oath-url/generate-msteams-oauth-url.usecase.ts-141-    const { _environmentId, _organizationId, identifier, providerId } = integration;
--
apps/api/src/app/integrations/usecases/generate-chat-oath-url/generate-msteams-oath-url/generate-msteams-oauth-url.usecase.ts-151-      timestamp: Date.now(),
apps/api/src/app/integrations/usecases/generate-chat-oath-url/generate-msteams-oath-url/generate-msteams-oauth-url.usecase.ts-152-      mode,
apps/api/src/app/integrations/usecases/generate-chat-oath-url/generate-msteams-oath-url/generate-msteams-oauth-url.usecase.ts:153:      autoLinkUser,
apps/api/src/app/integrations/usecases/generate-chat-oath-url/generate-msteams-oath-url/generate-msteams-oauth-url.usecase.ts-154-    };
apps/api/src/app/integrations/usecases/generate-chat-oath-url/generate-msteams-oath-url/generate-msteams-oauth-url.usecase.ts-155-
--
apps/api/src/app/integrations/usecases/generate-chat-oath-url/generate-chat-oauth-url.command.ts-44-  `@IsOptional`()
apps/api/src/app/integrations/usecases/generate-chat-oath-url/generate-chat-oauth-url.command.ts-45-  `@IsBoolean`()
apps/api/src/app/integrations/usecases/generate-chat-oath-url/generate-chat-oauth-url.command.ts:46:  readonly autoLinkUser?: boolean;
apps/api/src/app/integrations/usecases/generate-chat-oath-url/generate-chat-oauth-url.command.ts-47-}
--
apps/api/src/app/integrations/usecases/generate-chat-oath-url/generate-slack-oath-url/generate-slack-oauth-url.usecase.ts-32-  mode?: OAuthMode;
apps/api/src/app/integrations/usecases/generate-chat-oath-url/generate-slack-oath-url/generate-slack-oauth-url.usecase.ts-33-  connectionMode?: ConnectionMode;
apps/api/src/app/integrations/usecases/generate-chat-oath-url/generate-slack-oath-url/generate-slack-oauth-url.usecase.ts:34:  autoLinkUser?: boolean;
apps/api/src/app/integrations/usecases/generate-chat-oath-url/generate-slack-oath-url/generate-slack-oauth-url.usecase.ts-35-};
apps/api/src/app/integrations/usecases/generate-chat-oath-url/generate-slack-oath-url/generate-slack-oauth-url.usecase.ts-36-
--
apps/api/src/app/integrations/usecases/generate-chat-oath-url/generate-slack-oath-url/generate-slack-oauth-url.usecase.ts-72-      command.mode,
apps/api/src/app/integrations/usecases/generate-chat-oath-url/generate-slack-oath-url/generate-slack-oauth-url.usecase.ts-73-      command.connectionMode,
apps/api/src/app/integrations/usecases/generate-chat-oath-url/generate-slack-oath-url/generate-slack-oauth-url.usecase.ts:74:      command.autoLinkUser
apps/api/src/app/integrations/usecases/generate-chat-oath-url/generate-slack-oath-url/generate-slack-oauth-url.usecase.ts-75-    );
apps/api/src/app/integrations/usecases/generate-chat-oath-url/generate-slack-oath-url/generate-slack-oauth-url.usecase.ts-76-
--
apps/api/src/app/integrations/usecases/generate-chat-oath-url/generate-slack-oath-url/generate-slack-oauth-url.usecase.ts-167-    mode?: OAuthMode,
apps/api/src/app/integrations/usecases/generate-chat-oath-url/generate-slack-oath-url/generate-slack-oauth-url.usecase.ts-168-    connectionMode?: ConnectionMode,
apps/api/src/app/integrations/usecases/generate-chat-oath-url/generate-slack-oath-url/generate-slack-oauth-url.usecase.ts:169:    autoLinkUser?: boolean
apps/api/src/app/integrations/usecases/generate-chat-oath-url/generate-slack-oath-url/generate-slack-oauth-url.usecase.ts-170-  ): Promise<string> {
apps/api/src/app/integrations/usecases/generate-chat-oath-url/generate-slack-oath-url/generate-slack-oauth-url.usecase.ts-171-    const { _environmentId, _organizationId, identifier, providerId } = integration;
--
apps/api/src/app/integrations/usecases/generate-chat-oath-url/generate-slack-oath-url/generate-slack-oauth-url.usecase.ts-182-      mode,
apps/api/src/app/integrations/usecases/generate-chat-oath-url/generate-slack-oath-url/generate-slack-oauth-url.usecase.ts-183-      connectionMode,
apps/api/src/app/integrations/usecases/generate-chat-oath-url/generate-slack-oath-url/generate-slack-oauth-url.usecase.ts:184:      autoLinkUser,
apps/api/src/app/integrations/usecases/generate-chat-oath-url/generate-slack-oath-url/generate-slack-oauth-url.usecase.ts-185-    };
apps/api/src/app/integrations/usecases/generate-chat-oath-url/generate-slack-oath-url/generate-slack-oauth-url.usecase.ts-186-
--
apps/api/src/app/inbox/inbox.controller.ts-836-        mode: body.mode,
apps/api/src/app/inbox/inbox.controller.ts-837-        connectionMode: body.connectionMode,
apps/api/src/app/inbox/inbox.controller.ts:838:        autoLinkUser: body.autoLinkUser,
apps/api/src/app/inbox/inbox.controller.ts-839-      })
apps/api/src/app/inbox/inbox.controller.ts-840-    );
--
apps/api/src/app/integrations/dtos/generate-chat-oauth-url.dto.ts-129-  `@IsOptional`()
apps/api/src/app/integrations/dtos/generate-chat-oauth-url.dto.ts-130-  `@IsBoolean`()
apps/api/src/app/integrations/dtos/generate-chat-oauth-url.dto.ts:131:  autoLinkUser?: boolean;
apps/api/src/app/integrations/dtos/generate-chat-oauth-url.dto.ts-132-}


Script executed:

cat packages/framework/CHANGELOG.md | head -100

Repository: novuhq/novu


Repository: novuhq/novu
Exit code: 0

stdout:

## 2.10.0 (2026-03-27)

### 🚀 Features

- **framework:** export param types fixes NV-7261 ([`#10407`](https://github.com/novuhq/novu/pull/10407))
- **novu,framework:** align step resolver handlers with framework steps fixes NV-7235 ([`#10286`](https://github.com/novuhq/novu/pull/10286))
- **api-service,dashboard,framework:** align step resolver scaffolding with framework fixes NV-7116 ([`#10136`](https://github.com/novuhq/novu/pull/10136))

### 🩹 Fixes

- **framework:** disable AJV strict mode for user schemas and remove noisy console.error ([`#10426`](https://github.com/novuhq/novu/pull/10426))
- **framework:** Liquid output escaping for special JSON characters including `"` ([`#9730`](https://github.com/novuhq/novu/pull/9730))
- **framework:** repair invalid JSON strings in control data fixes NV-6904 ([`#9632`](https://github.com/novuhq/novu/pull/9632))
- **framework:** fix CORS issue preventing flows from showing in local studio fixes NV-6945 ([`#9626`](https://github.com/novuhq/novu/pull/9626))
- **framework:** security patch for next.js dependency ([`#9753`](https://github.com/novuhq/novu/pull/9753))
- **root:** resolve high liquidjs vulnerability ([`#10263`](https://github.com/novuhq/novu/pull/10263))
- **root:** resolve moderate lodash, ajv, and express vulnerabilities ([`#10360`](https://github.com/novuhq/novu/pull/10360))

### ❤️ Thank You

- Adam Chmara `@ChmaraX`
- Dima Grossman `@scopsy`
- George Djabarov `@djabarovgeorge`

## 2.9.0 (2025-12-02)

### 🚀 Features

- **api,framework:** translations - support liquid filters & nesting fixes NV-6870 ([`#9575`](https://github.com/novuhq/novu/pull/9575))

### 🩹 Fixes

- **worker:** sanitize img tags to prevent xss fixes NV-6883 ([`#9483`](https://github.com/novuhq/novu/pull/9483))

### ❤️ Thank You

- Adam Chmara `@ChmaraX`
- Dima Grossman `@scopsy`

## 2.6.6 (2025-02-25)

### 🚀 Features

- **api-service:** system limits & update pricing pages ([`#7718`](https://github.com/novuhq/novu/pull/7718))
- **root:** add no only github action ([`#7692`](https://github.com/novuhq/novu/pull/7692))

### 🩹 Fixes

- **root:** unhandled promise reject and undefined ff kind ([`#7732`](https://github.com/novuhq/novu/pull/7732))
- **api-service:** remove only on e2e ([`#7691`](https://github.com/novuhq/novu/pull/7691))

### ❤️ Thank You

- GalTidhar `@tatarco`
- George Djabarov `@djabarovgeorge`


## 2.6.5 (2025-02-07)

### 🚀 Features

- Update README.md ([bb63172dd](https://github.com/novuhq/novu/commit/bb63172dd))
- **readme:** Update README.md ([955cbeab0](https://github.com/novuhq/novu/commit/955cbeab0))
- **dashboard:** Digest liquid helper and popover handler ([`#7439`](https://github.com/novuhq/novu/pull/7439))
- quick start updates readme ([88b3b6628](https://github.com/novuhq/novu/commit/88b3b6628))
- **readme:** update readme ([e5ea61812](https://github.com/novuhq/novu/commit/e5ea61812))
- **api-service:** add internal sdk ([`#7599`](https://github.com/novuhq/novu/pull/7599))
- **dashboard:** step conditions editor ui ([`#7502`](https://github.com/novuhq/novu/pull/7502))
- **api:** add query parser ([`#7267`](https://github.com/novuhq/novu/pull/7267))
- **api:** Nv 5033 additional removal cycle found unneeded elements ([`#7283`](https://github.com/novuhq/novu/pull/7283))
- **api:** Nv 4966 e2e testing happy path - messages ([`#7248`](https://github.com/novuhq/novu/pull/7248))
- **dashboard:** Implement email step editor & mini preview ([`#7129`](https://github.com/novuhq/novu/pull/7129))
- **api:** converted bulk trigger to use SDK ([`#7166`](https://github.com/novuhq/novu/pull/7166))
- **application-generic:** add SUBSCRIBER_WIDGET_JWT_EXPIRATION_TIME env variable ([`#7105`](https://github.com/novuhq/novu/pull/7105))

### 🩹 Fixes

- **js:** Await read action in Inbox ([`#7653`](https://github.com/novuhq/novu/pull/7653))
- **api:** duplicated subscribers created due to race condition ([`#7646`](https://github.com/novuhq/novu/pull/7646))
- **api-service:** add missing environment variable ([`#7553`](https://github.com/novuhq/novu/pull/7553))
- **api:** Fix failing API e2e tests ([78c385ec7](https://github.com/novuhq/novu/commit/78c385ec7))
- **api-service:** E2E improvements ([`#7461`](https://github.com/novuhq/novu/pull/7461))
- **novu:** automatically create indexes on startup ([`#7431`](https://github.com/novuhq/novu/pull/7431))
- **api:** `@novu/api` -> `@novu/api-service` ([`#7348`](https://github.com/novuhq/novu/pull/7348))
- **framework:** Remove `@novu/shared` dependency temporarily ([`#7337`](https://github.com/novuhq/novu/pull/7337))

### ❤️ Thank You

- Aminul Islam `@AminulBD`
- Dima Grossman `@scopsy`
- GalTidhar `@tatarco`
- George Desipris `@desiprisg`
- George Djabarov `@djabarovgeorge`
- Lucky `@L-U-C-K-Y`
- Pawan Jain
- Paweł Tymczuk `@LetItRock`
- Sokratis Vidros `@SokratisVidros`


## 2.5.3 (2024-12-24)


Script executed:

# Check if there are multiple CHANGELOG files and search for autoLinkUser mentions
fd -e md -e mdx CHANGELOG -exec grep -l "autoLinkUser" {} \;

Repository: novuhq/novu


Repository: novuhq/novu
Exit code: 1

stderr:

[fd error]: Search path 'grep' is not a directory.
[fd error]: Search path 'autoLinkUser' is not a directory.
[fd error]: Search path '{}' is not a directory.
[fd error]: Search path ';' is not a directory.
[fd error]: No valid search paths given.


Script executed:

# Get the actual SlackConnectButton.tsx file to see full context of the JSDoc and implementation
cat -n packages/js/src/ui/components/slack-connect-button/SlackConnectButton.tsx | head -170

Repository: novuhq/novu


Repository: novuhq/novu
Exit code: 0

stdout:

     1	import { createEffect, createMemo, createSignal, onCleanup, Show } from 'solid-js';
     2	import type { ConnectionMode } from '../../../channel-connections/types';
     3	import type { Context } from '../../../types';
     4	import { useChannelConnection } from '../../api/hooks/useChannelConnection';
     5	import { useNovu } from '../../context';
     6	import { useStyle } from '../../helpers/useStyle';
     7	import { CheckCircleFill } from '../../icons/CheckCircleFill';
     8	import { Loader } from '../../icons/Loader';
     9	import { SlackColored } from '../../icons/SlackColored';
    10	import type { ChannelConnectButtonAppearanceCallback } from '../../types';
    11	import { Button, Motion } from '../primitives';
    12	import { Tooltip } from '../primitives/Tooltip';
    13	import { IconRendererWrapper } from '../shared/IconRendererWrapper';
    14	import { DEFAULT_CONNECTION_IDENTIFIER, DEFAULT_INTEGRATION_IDENTIFIER } from '../slack-constants';
    15	
    16	export type SlackConnectButtonProps = {
    17	  integrationIdentifier?: string;
    18	  connectionIdentifier?: string;
    19	  subscriberId?: string;
    20	  context?: Context;
    21	  scope?: string[];
    22	  connectionMode?: ConnectionMode;
    23	  /**
    24	   * When true (default), after the workspace connection is created the OAuth
    25	   * flow also links the subscriber who clicked "Connect" as a personal Slack
    26	   * endpoint using the authed_user.id already returned by oauth.v2.access.
    27	   * Set to false to skip the user-linking step and only create the workspace
    28	   * connection. Raw API callers must pass true explicitly; this component
    29	   * defaults to true.
    30	   */
    31	  autoLinkUser?: boolean;
    32	  onConnectSuccess?: (connectionIdentifier: string) => void;
    33	  onConnectError?: (error: unknown) => void;
    34	  onDisconnectSuccess?: () => void;
    35	  onDisconnectError?: (error: unknown) => void;
    36	  connectLabel?: string;
    37	  connectedLabel?: string;
    38	};
    39	
    40	const POLL_INTERVAL_MS = 2500;
    41	const POLL_TIMEOUT_MS = 120_000;
    42	
    43	export const SlackConnectButton = (props: SlackConnectButtonProps) => {
    44	  const style = useStyle();
    45	  const novuAccessor = useNovu();
    46	  const integrationIdentifier = () => props.integrationIdentifier ?? DEFAULT_INTEGRATION_IDENTIFIER;
    47	  const connectionIdentifier = () => props.connectionIdentifier ?? DEFAULT_CONNECTION_IDENTIFIER;
    48	
    49	  const { connection, loading, connect, disconnect, mutate } = useChannelConnection({
    50	    integrationIdentifier: integrationIdentifier(),
    51	    connectionIdentifier: connectionIdentifier(),
    52	    subscriberId: props.subscriberId,
    53	  });
    54	
    55	  const [actionLoading, setActionLoading] = createSignal(false);
    56	
    57	  const connectionMode = () => props.connectionMode ?? 'subscriber';
    58	  const resolvedContext = () => props.context ?? novuAccessor().context;
    59	  const isMisconfigured = createMemo(() => connectionMode() === 'shared' && !resolvedContext());
    60	
    61	  createEffect(() => {
    62	    if (isMisconfigured()) {
    63	      console.warn(
    64	        '[Novu] SlackConnectButton: "context" is required when connectionMode is "shared". ' +
    65	          'Provide it via the context prop on SlackConnectButton or on NovuProvider.'
    66	      );
    67	    }
    68	  });
    69	
    70	  const isConnected = () => !!connection();
    71	  const isLoading = () => loading() || actionLoading();
    72	
    73	  const intervalIdRef: { current: ReturnType<typeof setInterval> | null } = { current: null };
    74	
    75	  onCleanup(() => {
    76	    if (intervalIdRef.current !== null) {
    77	      clearInterval(intervalIdRef.current);
    78	      intervalIdRef.current = null;
    79	    }
    80	  });
    81	
    82	  const startPolling = () => {
    83	    if (intervalIdRef.current !== null) {
    84	      clearInterval(intervalIdRef.current);
    85	      intervalIdRef.current = null;
    86	    }
    87	
    88	    const startedAt = Date.now();
    89	
    90	    intervalIdRef.current = setInterval(async () => {
    91	      try {
    92	        const response = await novuAccessor().channelConnections.get({
    93	          identifier: connectionIdentifier(),
    94	        });
    95	
    96	        if (response.data) {
    97	          if (intervalIdRef.current !== null) {
    98	            clearInterval(intervalIdRef.current);
    99	            intervalIdRef.current = null;
   100	          }
   101	
   102	          setActionLoading(false);
   103	          mutate(response.data);
   104	          props.onConnectSuccess?.(connectionIdentifier());
   105	
   106	          return;
   107	        }
   108	      } catch {
   109	        // ignore transient errors during polling
   110	      }
   111	
   112	      if (Date.now() - startedAt >= POLL_TIMEOUT_MS) {
   113	        if (intervalIdRef.current !== null) {
   114	          clearInterval(intervalIdRef.current);
   115	          intervalIdRef.current = null;
   116	        }
   117	
   118	        setActionLoading(false);
   119	        props.onConnectError?.(new Error('Slack OAuth timed out. Please try again.'));
   120	      }
   121	    }, POLL_INTERVAL_MS);
   122	  };
   123	
   124	  const handleClick = async () => {
   125	    if (isConnected()) {
   126	      const identifier = connection()?.identifier;
   127	      if (!identifier) return;
   128	
   129	      const result = await disconnect(identifier);
   130	      if (result.error) {
   131	        props.onDisconnectError?.(result.error);
   132	      } else {
   133	        props.onDisconnectSuccess?.();
   134	      }
   135	    } else {
   136	      setActionLoading(true);
   137	
   138	      const mode = connectionMode();
   139	      const ctx = resolvedContext();
   140	      const resolvedSubscriberId =
   141	        mode === 'subscriber' ? (props.subscriberId ?? novuAccessor().subscriberId) : undefined;
   142	
   143	      const result = await connect({
   144	        integrationIdentifier: integrationIdentifier(),
   145	        connectionIdentifier: connectionIdentifier(),
   146	        subscriberId: resolvedSubscriberId,
   147	        context: ctx,
   148	        scope: props.scope,
   149	        connectionMode: mode,
   150	        autoLinkUser: mode === 'subscriber' ? (props.autoLinkUser ?? true) : false,
   151	      });
   152	
   153	      if (result.error) {
   154	        setActionLoading(false);
   155	        props.onConnectError?.(result.error);
   156	
   157	        return;
   158	      }
   159	
   160	      if (result.data?.url) {
   161	        window.open(result.data.url, '_blank', 'noopener,noreferrer');
   162	        startPolling();
   163	      } else {
   164	        setActionLoading(false);
   165	        props.onConnectError?.(new Error('OAuth URL was not returned. Please try again.'));
   166	      }
   167	    }
   168	  };
   169	
   170	  const buttonContent = () => (


Default autoLinkUser: true is an observable behavior change requiring semver consideration.

Existing apps rendering <SlackConnectButton /> will start creating a SLACK_USER endpoint for every subscriber alongside the workspace connection, without explicit opt-in. While the backend code confirms this as an intentional design decision (SDK users opt-out, raw API users opt-in), the behavior change is still observable and impacts the public API surface.

For a client-facing SDK following semver, address this by either:

  1. Default to false so adoption is opt-in (aligns raw API conventions and avoids surprising existing integrations), or
  2. Keep true but document in changelog with a major version bump if this is a new feature.

The JSDoc accurately documents the default, but the behavior change isn't signaled to consumers upgrading package.json.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/js/src/ui/components/slack-connect-button/SlackConnectButton.tsx`
around lines 23 - 31, The public prop default for autoLinkUser currently flips
behavior for consumers; change the default to false to preserve backwards
compatibility: update the SlackConnectButton component (the autoLinkUser prop,
any defaultProps or destructured default in the function/component) to default
to false, and update the JSDoc comment to reflect the new default; also add a
short changelog note indicating this behavioral choice if you prefer to keep the
current versioning, but implement the code default change in SlackConnectButton
so existing integrations are not opt-into creating SLACK_USER endpoints.

Comment thread playground/nextjs/src/pages/connect-chat/index.tsx
…flows

- Added new endpoints for generating OAuth URLs: `/channel-connections/oauth` for workspace/tenant connections and `/channel-endpoints/oauth` for linking subscriber user identities.
- Updated the `InboxController` and `IntegrationsController` to handle the new OAuth flows with corresponding DTOs and use cases.
- Deprecated the previous `/chat/oauth` endpoint in favor of the new routes, enhancing clarity and functionality in the OAuth process.
- Updated relevant components and hooks to utilize the new OAuth methods, ensuring a seamless integration experience for users.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (2)
packages/js/src/ui/components/msteams-connect-button/MsTeamsConnectButton.tsx (1)

137-163: ⚠️ Potential issue | 🟠 Major

Open the OAuth tab synchronously in the click path.

This still calls window.open(...) only after await generateConnectOAuthUrl(...). In stricter browsers that loses the user gesture, the tab is blocked, and startPolling() waits for a connection that the user never had a chance to authorize.

Suggested fix
     } else {
       setActionLoading(true);
+      const popup = window.open('about:blank', '_blank', 'noopener,noreferrer');
+
+      if (!popup) {
+        setActionLoading(false);
+        props.onConnectError?.(new Error('Popup was blocked. Please allow popups and try again.'));
+
+        return;
+      }
 
       const mode = connectionMode();
       const ctx = resolvedContext();
       const resolvedSubscriberId =
         mode === 'subscriber' ? (props.subscriberId ?? novuAccessor().subscriberId) : undefined;
@@
       });
 
       if (result.error) {
+        popup.close();
         setActionLoading(false);
         props.onConnectError?.(result.error);
 
         return;
       }
 
       if (result.data?.url) {
-        window.open(result.data.url, '_blank', 'noopener,noreferrer');
+        popup.location.href = result.data.url;
         startPolling();
       } else {
+        popup.close();
         setActionLoading(false);
         props.onConnectError?.(new Error('OAuth URL was not returned. Please try again.'));
       }
     }
Do browsers preserve user activation for window.open() after an await inside a click handler, or can popup blockers block it?
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/js/src/ui/components/msteams-connect-button/MsTeamsConnectButton.tsx`
around lines 137 - 163, Open the OAuth popup synchronously to preserve the user
gesture: inside MsTeamsConnectButton's click handler call window.open(...)
immediately (store the returned window reference) before awaiting
generateConnectOAuthUrl, then await generateConnectOAuthUrl(...) and, on
success, set popup.location.href = result.data.url (or popup?.focus()) and call
startPolling(); on error close the popup (popup?.close()),
setActionLoading(false), and call props.onConnectError with the error; update
references to generateConnectOAuthUrl, startPolling, and window.open accordingly
so the popup is created before any await.
packages/js/src/ui/components/msteams-link-user/MsTeamsLinkUser.tsx (1)

147-165: ⚠️ Potential issue | 🟠 Major

Open the OAuth tab before awaiting the URL.

window.open(...) happens only after await generateLinkUserOAuthUrl(...), so browsers can drop the user-activation token and return null. We then start polling anyway, which leaves the button spinning until timeout even though no auth tab was opened.

Suggested fix
       setActionLoading(true);
+      const popup = window.open('about:blank', '_blank', 'noopener,noreferrer');
+
+      if (!popup) {
+        setActionLoading(false);
+        props.onLinkError?.(new Error('Popup was blocked. Please allow popups and try again.'));
+
+        return;
+      }
 
       const result = await generateLinkUserOAuthUrl({
         integrationIdentifier: integrationIdentifier(),
         connectionIdentifier: connectionIdentifier(),
         subscriberId: resolvedSubscriberId,
         context: props.context,
       });
 
       if (result.error) {
+        popup.close();
         setActionLoading(false);
         props.onLinkError?.(result.error);
 
         return;
       }
 
       if (result.data?.url) {
-        window.open(result.data.url, '_blank', 'noopener,noreferrer');
+        popup.location.href = result.data.url;
         startPolling();
       } else {
+        popup.close();
         setActionLoading(false);
         props.onLinkError?.(new Error('OAuth URL was not returned. Please try again.'));
       }
Do browsers preserve user activation for window.open() after an await inside a click handler, or can popup blockers block it?
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/js/src/ui/components/msteams-link-user/MsTeamsLinkUser.tsx` around
lines 147 - 165, Open the OAuth popup synchronously before awaiting
generateLinkUserOAuthUrl to preserve the user activation; call window.open with
a temporary blank URL (e.g., 'about:blank') immediately after
setActionLoading(true) and store the returned window handle, then await
generateLinkUserOAuthUrl(integrationIdentifier(), connectionIdentifier(),
resolvedSubscriberId, props.context) and when result.data?.url arrives navigate
the stored window to that URL (or close it on error). On error, call
props.onLinkError?.(result.error), setActionLoading(false), and close the popup
if it was opened; ensure startPolling() is only called after you successfully
navigated the popup to the OAuth URL.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/api/src/app/integrations/integrations.controller.ts`:
- Around line 475-485: Before calling generateConnectOauthUrlUsecase.execute /
GenerateConnectOauthUrlCommand.create in the controller, normalize requests
where connectionMode === 'shared' by removing/ignoring any subscriberId and
forcing autoLinkUser to false: i.e., if body.connectionMode === 'shared' then do
not pass body.subscriberId (omit it from the command) and set autoLinkUser:
false in the payload passed to GenerateConnectOauthUrlCommand.create; implement
this normalization in the integrations.controller.ts just prior to invoking
generateConnectOauthUrlUsecase.execute so downstream code cannot receive a
shared request with subscriber-scoped fields.

In `@packages/js/src/ui/api/hooks/useChannelConnection.ts`:
- Around line 36-38: Keep a backward-compatible alias named connect that
forwards to generateConnectOAuthUrl in the hook return value to avoid breaking
consumers; add a JSDoc `@deprecated` comment above the alias (e.g., /**
`@deprecated` use generateConnectOAuthUrl instead */) and implement connect as a
thin wrapper that calls generateConnectOAuthUrl(args), ensuring the returned
object from useChannelConnection still contains both generateConnectOAuthUrl and
connect.

---

Duplicate comments:
In
`@packages/js/src/ui/components/msteams-connect-button/MsTeamsConnectButton.tsx`:
- Around line 137-163: Open the OAuth popup synchronously to preserve the user
gesture: inside MsTeamsConnectButton's click handler call window.open(...)
immediately (store the returned window reference) before awaiting
generateConnectOAuthUrl, then await generateConnectOAuthUrl(...) and, on
success, set popup.location.href = result.data.url (or popup?.focus()) and call
startPolling(); on error close the popup (popup?.close()),
setActionLoading(false), and call props.onConnectError with the error; update
references to generateConnectOAuthUrl, startPolling, and window.open accordingly
so the popup is created before any await.

In `@packages/js/src/ui/components/msteams-link-user/MsTeamsLinkUser.tsx`:
- Around line 147-165: Open the OAuth popup synchronously before awaiting
generateLinkUserOAuthUrl to preserve the user activation; call window.open with
a temporary blank URL (e.g., 'about:blank') immediately after
setActionLoading(true) and store the returned window handle, then await
generateLinkUserOAuthUrl(integrationIdentifier(), connectionIdentifier(),
resolvedSubscriberId, props.context) and when result.data?.url arrives navigate
the stored window to that URL (or close it on error). On error, call
props.onLinkError?.(result.error), setActionLoading(false), and close the popup
if it was opened; ensure startPolling() is only called after you successfully
navigated the popup to the OAuth URL.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 482e08ee-2cb7-4e7a-b913-eefcecc2ab71

📥 Commits

Reviewing files that changed from the base of the PR and between 408dd08 and e40487f.

📒 Files selected for processing (24)
  • apps/api/src/app/inbox/inbox.controller.ts
  • apps/api/src/app/integrations/dtos/generate-chat-oauth-url.dto.ts
  • apps/api/src/app/integrations/dtos/generate-connect-oauth-url-request.dto.ts
  • apps/api/src/app/integrations/dtos/generate-link-user-oauth-url-request.dto.ts
  • apps/api/src/app/integrations/integrations.controller.ts
  • apps/api/src/app/integrations/usecases/generate-chat-oath-url/generate-connect-oauth-url.command.ts
  • apps/api/src/app/integrations/usecases/generate-chat-oath-url/generate-connect-oauth-url.usecase.ts
  • apps/api/src/app/integrations/usecases/generate-chat-oath-url/generate-link-user-oauth-url.command.ts
  • apps/api/src/app/integrations/usecases/generate-chat-oath-url/generate-link-user-oauth-url.usecase.ts
  • apps/api/src/app/integrations/usecases/index.ts
  • packages/js/scripts/size-limit.mjs
  • packages/js/src/api/inbox-service.ts
  • packages/js/src/channel-connections/channel-connections.ts
  • packages/js/src/channel-connections/helpers.ts
  • packages/js/src/channel-connections/types.ts
  • packages/js/src/channel-endpoints/channel-endpoints.ts
  • packages/js/src/channel-endpoints/helpers.ts
  • packages/js/src/ui/api/hooks/useChannelConnection.ts
  • packages/js/src/ui/api/hooks/useChannelEndpoint.ts
  • packages/js/src/ui/components/connect-chat/ConnectChat.tsx
  • packages/js/src/ui/components/msteams-connect-button/MsTeamsConnectButton.tsx
  • packages/js/src/ui/components/msteams-link-user/MsTeamsLinkUser.tsx
  • packages/js/src/ui/components/slack-connect-button/SlackConnectButton.tsx
  • packages/js/src/ui/components/slack-link-user/SlackLinkUser.tsx
✅ Files skipped from review due to trivial changes (1)
  • packages/js/scripts/size-limit.mjs
🚧 Files skipped from review as they are similar to previous changes (2)
  • apps/api/src/app/inbox/inbox.controller.ts
  • packages/js/src/channel-connections/types.ts

Comment on lines +475 to +485
const url = await this.generateConnectOauthUrlUsecase.execute(
GenerateConnectOauthUrlCommand.create({
environmentId: user.environmentId,
organizationId: user.organizationId,
subscriberId: body.subscriberId,
integrationIdentifier: body.integrationIdentifier,
connectionIdentifier: body.connectionIdentifier,
context: body.context,
scope: body.scope,
connectionMode: body.connectionMode,
autoLinkUser: body.autoLinkUser,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Normalize shared connect requests before generating the OAuth URL.

This endpoint exposes connectionMode, but the MS Teams path downstream drops that field and only sees subscriberId, context, and autoLinkUser. A caller can therefore send connectionMode: 'shared' together with subscriberId and/or autoLinkUser: true, and the request is no longer guaranteed to stay tenant/context-scoped. Please enforce the mode here (or in the connect use case) by omitting/rejecting subscriberId for shared and forcing autoLinkUser to false server-side.

Suggested normalization
+    const connectionMode = body.connectionMode ?? 'subscriber';
+    const subscriberId = connectionMode === 'subscriber' ? body.subscriberId : undefined;
+    const autoLinkUser = connectionMode === 'subscriber' ? (body.autoLinkUser ?? true) : false;
+
     const url = await this.generateConnectOauthUrlUsecase.execute(
       GenerateConnectOauthUrlCommand.create({
         environmentId: user.environmentId,
         organizationId: user.organizationId,
-        subscriberId: body.subscriberId,
+        subscriberId,
         integrationIdentifier: body.integrationIdentifier,
         connectionIdentifier: body.connectionIdentifier,
         context: body.context,
         scope: body.scope,
-        connectionMode: body.connectionMode,
-        autoLinkUser: body.autoLinkUser,
+        connectionMode,
+        autoLinkUser,
       })
     );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const url = await this.generateConnectOauthUrlUsecase.execute(
GenerateConnectOauthUrlCommand.create({
environmentId: user.environmentId,
organizationId: user.organizationId,
subscriberId: body.subscriberId,
integrationIdentifier: body.integrationIdentifier,
connectionIdentifier: body.connectionIdentifier,
context: body.context,
scope: body.scope,
connectionMode: body.connectionMode,
autoLinkUser: body.autoLinkUser,
const connectionMode = body.connectionMode ?? 'subscriber';
const subscriberId = connectionMode === 'subscriber' ? body.subscriberId : undefined;
const autoLinkUser = connectionMode === 'subscriber' ? (body.autoLinkUser ?? true) : false;
const url = await this.generateConnectOauthUrlUsecase.execute(
GenerateConnectOauthUrlCommand.create({
environmentId: user.environmentId,
organizationId: user.organizationId,
subscriberId,
integrationIdentifier: body.integrationIdentifier,
connectionIdentifier: body.connectionIdentifier,
context: body.context,
scope: body.scope,
connectionMode,
autoLinkUser,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/src/app/integrations/integrations.controller.ts` around lines 475 -
485, Before calling generateConnectOauthUrlUsecase.execute /
GenerateConnectOauthUrlCommand.create in the controller, normalize requests
where connectionMode === 'shared' by removing/ignoring any subscriberId and
forcing autoLinkUser to false: i.e., if body.connectionMode === 'shared' then do
not pass body.subscriberId (omit it from the command) and set autoLinkUser:
false in the payload passed to GenerateConnectOauthUrlCommand.create; implement
this normalization in the integrations.controller.ts just prior to invoking
generateConnectOauthUrlUsecase.execute so downstream code cannot receive a
shared request with subscriber-scoped fields.

Comment on lines +36 to 38
const generateConnectOAuthUrl = async (args: GenerateConnectOAuthUrlArgs) => {
return novuAccessor().channelConnections.generateConnectOAuthUrl(args);
};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Keep connect as a deprecated alias to avoid a breaking SDK API change.

Removing connect from the returned hook contract can break existing package consumers immediately. Keep a backward-compatible alias for at least one deprecation cycle.

Proposed compatibility patch
 const generateConnectOAuthUrl = async (args: GenerateConnectOAuthUrlArgs) => {
   return novuAccessor().channelConnections.generateConnectOAuthUrl(args);
 };
 
+/**
+ * `@deprecated` Use generateConnectOAuthUrl instead.
+ */
+const connect = generateConnectOAuthUrl;
+
- return { connection, loading, mutate, refetch, generateConnectOAuthUrl, disconnect };
+ return { connection, loading, mutate, refetch, generateConnectOAuthUrl, connect, disconnect };

As per coding guidelines: packages/**/*: “Treat all exported symbols in packages as public API; follow semver conventions with breaking changes requiring major bumps…” and packages/**/*.{ts,tsx,js,jsx}: “Deprecate symbols with @deprecated JSDoc before removing them from packages.”

Also applies to: 108-108

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/js/src/ui/api/hooks/useChannelConnection.ts` around lines 36 - 38,
Keep a backward-compatible alias named connect that forwards to
generateConnectOAuthUrl in the hook return value to avoid breaking consumers;
add a JSDoc `@deprecated` comment above the alias (e.g., /** `@deprecated` use
generateConnectOAuthUrl instead */) and implement connect as a thin wrapper that
calls generateConnectOAuthUrl(args), ensuring the returned object from
useChannelConnection still contains both generateConnectOAuthUrl and connect.

…ests

- Refactored tests in `generate-msteams-oauth-url.usecase.spec.ts` to use try-catch blocks for better error handling.
- Updated assertions to check for specific exception types and messages, enhancing clarity in test failures.
- Removed unnecessary imports in `slack-oauth-callback.usecase.spec.ts` to clean up the code.
…tests

- Refactored tests in `msteams-oauth-callback.usecase.spec.ts` to utilize try-catch blocks for improved error handling.
- Updated assertions to verify specific exception types and messages, enhancing clarity in test failures.
- Added new event type for generating link user OAuth URLs in `event-emitter/types.ts` to support upcoming features.
…tion

- Updated tests in `msteams-oauth-callback.usecase.spec.ts` to directly check response types and messages instead of relying on exceptions.
- Enhanced assertions to verify that the correct HTML response is returned when required parameters are missing in link_user mode.
- Improved test clarity and reliability by removing try-catch blocks and focusing on expected outcomes.
@djabarovgeorge djabarovgeorge changed the title feat(api-service): Implement msteams components feat(js,react,api-service): implement MS Teams connect and link-user components Apr 27, 2026
…rationIdentifier handling and remove unused constants

- Updated the MsTeamsConnectButton and SlackConnectButton components to require integrationIdentifier as a mandatory prop, enhancing type safety.
- Removed deprecated constants related to MS Teams and Slack integrations, simplifying the codebase.
- Adjusted connection identifier handling to improve clarity and maintainability across components.
- Refactored the `GenerateMsTeamsOauthUrl` and `GenerateSlackOauthUrl` use cases to return OAuth URLs based on the dynamic `API_ROOT_URL` instead of a hardcoded ngrok URL.
- This change enhances flexibility and ensures that the generated URLs are consistent with the current API environment.
@djabarovgeorge djabarovgeorge merged commit c0055bd into next Apr 27, 2026
29 of 30 checks passed
@djabarovgeorge djabarovgeorge deleted the implement-msteams-components branch April 27, 2026 18:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants