Skip to content

refactor(api-service): polish inbox channel endpoints#10781

Merged
djabarovgeorge merged 4 commits intonextfrom
polish-inbox-channel-endpoints
Apr 19, 2026
Merged

refactor(api-service): polish inbox channel endpoints#10781
djabarovgeorge merged 4 commits intonextfrom
polish-inbox-channel-endpoints

Conversation

@djabarovgeorge
Copy link
Copy Markdown
Contributor

@djabarovgeorge djabarovgeorge commented Apr 17, 2026

What changed? Why was the change needed?

What changed

This PR refactors the inbox channel endpoints to improve separation of concerns by removing channel connection and endpoint creation from the inbox controller and simplifying response DTOs. Creation of these resources is now handled exclusively through OAuth callback flows in the integrations and subscribers modules, while the inbox controller only handles read operations (list and get).

Affected areas

@novu/api-service: The inbox controller now only exposes list and get endpoints for channel connections and endpoints, removing the POST /channel-connections and POST /channel-endpoints handlers. New simplified DTOs (InboxChannelConnectionResponseDto, InboxChannelEndpointResponseDto) were introduced that reduce response payloads to essential fields: identifier for connections and identifier/type for endpoints. Pagination shapes were normalized by dropping totalCount and totalCountCapped. A minor fix to the Slack OAuth URL generation usecase ensures subscriberId is always included in the secure state regardless of connection mode.

@novu/js: Response types for channel connections and endpoints were simplified to match the new API shape, removing redundant fields like integrationIdentifier, providerId, channel, timestamps, and other metadata.

Key technical decisions

  • Channel connection and endpoint creation logic is now exclusively handled in OAuth callback usecases (ChatOauthCallback, SlackOauthCallback) rather than direct inbox endpoints, providing a more appropriate flow for these operations.
  • Response DTOs were minimized to expose only identifier (and type for endpoints), reducing API surface and improving contract stability.
  • Pagination cursors (next/previous) are normalized to nullable strings with no cardinality counters.

Testing

No tests are mentioned in the PR. Given that this is a refactoring that moves creation logic to existing callback flows (which already handle these operations), existing tests for those callback usecases should provide coverage. The inbox controller changes primarily involve removing endpoints and updating response mappers, which may require updates to existing inbox endpoint tests.

Screenshots

Expand for optional sections

Related enterprise PR

Special notes for your reviewer

@netlify
Copy link
Copy Markdown

netlify Bot commented Apr 17, 2026

Deploy preview added

Name Link
🔨 Latest commit 9c718c7
🔍 Latest deploy log https://app.netlify.com/projects/dashboard-v2-novu-staging/deploys/69e4b9dd9e74e80008d0c73e
😎 Deploy Preview https://deploy-preview-10781.dashboard-v2.novu-staging.co
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 17, 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: refactor(api-service): polish inbox channel endpoints

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

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 17, 2026

📝 Walkthrough

Walkthrough

The PR simplifies inbox channel-connection and channel-endpoint APIs by introducing new DTO classes that expose only essential fields (identifier, type), removing creation endpoints from the controller, and updating response mapping logic while also removing most detailed response fields from client-side types.

Changes

Cohort / File(s) Summary
New DTO Definitions
apps/api/src/app/inbox/dtos/inbox-channel-connection-response.dto.ts, apps/api/src/app/inbox/dtos/inbox-channel-endpoint-response.dto.ts
Added new DTO classes for inbox responses; InboxChannelConnectionResponseDto exports identifier, while InboxChannelEndpointResponseDto exports identifier and type; both include paginated list response DTOs with data, next, and previous fields decorated with Swagger metadata.
DTO Mapping Logic
apps/api/src/app/inbox/dtos/inbox-dto.mapper.ts
New mapper module with mapChannelConnectionToInboxDto and mapChannelEndpointToInboxDto functions converting entities to corresponding inbox DTOs.
Controller Updates
apps/api/src/app/inbox/inbox.controller.ts
Removed POST endpoints for channel-connections and channel-endpoints creation; removed GET endpoint for channel-endpoint retrieval; updated list and get-by-identifier endpoints to return new inbox DTOs and map responses via mapper functions; removed creation usecase dependencies and dropped totalCount/totalCountCapped fields from paginated responses.
Slack OAuth Usecase
apps/api/src/app/integrations/usecases/.../generate-slack-oauth-url.usecase.ts
Modified execute control flow to always pass subscriberId to createSecureState regardless of connection mode; removed prior conditional omission of subscriberId for shared mode.
Client Type Simplification
packages/js/src/channel-connections/types.ts
Reduced ChannelConnectionResponse and ChannelEndpointResponse type shapes; ChannelConnectionResponse now contains only identifier, while ChannelEndpointResponse contains only identifier and type; removed fields like integrationIdentifier, providerId, channel, workspace, timestamps, and other metadata.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested reviewers

  • scopsy
  • LetItRock
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Title check ⚠️ Warning The title does not follow the required Conventional Commits format with a valid scope. The scope 'api-service' is not in the list of valid scopes (api, dashboard, worker, ws, webhook, shared, dal, application-generic, js, react, react-native, nextjs, providers, framework, root). Update the title to use a valid scope from the approved list, such as 'refactor(api): polish inbox channel endpoints' to match the Conventional Commits requirements.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ 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.

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented Apr 17, 2026

Open in StackBlitz

@novu/js

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

@novu/nextjs

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

novu

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

@novu/react

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

@novu/react-native

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

commit: 9c718c7

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: 1

Caution

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

⚠️ Outside diff range comments (2)
apps/api/src/app/integrations/usecases/generate-chat-oath-url/generate-slack-oath-url/generate-slack-oauth-url.usecase.ts (1)

221-229: ⚠️ Potential issue | 🔴 Critical

Critical: Same hardcoded ngrok URL leaked into Slack OAuth redirect.

Mirrors the issue in generate-msteams-oauth-url.usecase.ts: buildRedirectUri() returns a hardcoded personal ngrok origin while still requiring and computing baseUrl from API_ROOT_URL only to throw it away. For Slack this is especially risky — the redirect URI must match the value registered on the Slack app, so every non-local environment (and every CI/e2e run) will fail OAuth, and once the ngrok tunnel is gone the subdomain could be claimed by someone else.

Also note the stray 22 in the commented-out fallback on line 228 — clearly a local editing artifact, which reinforces that this is debug code that shouldn't be merged.

Please revert buildRedirectUri() to the env-driven implementation and drop the commented-out fallback. As per coding guidelines for apps/api/**, this segment should be reviewed with focus on security and authentication.

🔒 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
-    return `https://c9f4-79-177-157-205.ngrok-free.app${CHAT_OAUTH_CALLBACK_PATH}`;
-    // return `${baseUrl}${CHAT_OAUTH_CALLBACK_PATH}`; 22
+    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 221 - 229, The buildRedirectUri() function currently returns a
hardcoded ngrok origin; change it to use the API_ROOT_URL env var instead:
validate process.env.API_ROOT_URL, normalize it by removing a trailing slash
into baseUrl, and return `${baseUrl}${CHAT_OAUTH_CALLBACK_PATH}` (remove the
hardcoded ngrok URL). Also delete the commented-out fallback line containing the
stray "22" so no debug artifact remains. Ensure you edit the static method
buildRedirectUri in the generate-slack-oauth-url.usecase class/function.
apps/api/src/app/integrations/usecases/generate-chat-oath-url/generate-msteams-oath-url/generate-msteams-oauth-url.usecase.ts (1)

149-157: ⚠️ Potential issue | 🔴 Critical

Critical: Remove hardcoded personal ngrok URL from OAuth redirect URI.

buildRedirectUri() now returns a hardcoded personal ngrok origin (https://c9f4-79-177-157-205.ngrok-free.app) for every environment, while still validating API_ROOT_URL and computing baseUrl only to discard it. This looks like a local-development artifact that must not land on next:

  • In any non-local environment (staging/prod), the MS Teams OAuth redirect URI will no longer match the app's real callback, so admin consent will fail or, worse, redirect admins to a tunnel you don't control once the ngrok session expires and the subdomain is reused.
  • It's a security-relevant change in an auth/OAuth flow and is outside the stated scope of this PR ("polish inbox channel endpoints"), so it almost certainly shouldn't be merged together with the inbox refactor.
  • baseUrl is now dead code, and the previous behavior is only preserved as a comment, which will silently rot.

Please restore the env-driven behavior (and drop the dead local variable + commented fallback) before merging. As per coding guidelines for apps/api/**, this segment should be reviewed with focus on security and authentication.

🔒 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
-    return `https://c9f4-79-177-157-205.ngrok-free.app${CHAT_OAUTH_CALLBACK_PATH}`;
-    // return `${baseUrl}${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 149 - 157, The buildRedirectUri function currently returns a
hardcoded ngrok URL; restore environment-driven behavior by removing the
hardcoded origin and returning the computed baseUrl combined with
CHAT_OAUTH_CALLBACK_PATH (i.e., compute baseUrl from process.env.API_ROOT_URL
after trimming trailing slash and return
`${baseUrl}${CHAT_OAUTH_CALLBACK_PATH}`), remove the dead/commented fallback and
ensure the existing API_ROOT_URL presence check/throw remains intact; reference:
buildRedirectUri and CHAT_OAUTH_CALLBACK_PATH.
🤖 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/inbox/inbox.controller.ts`:
- Around line 692-696: The API no longer returns totalCount and totalCountCapped
but the internal SDK Zod schemas still require them; restore compatibility by
including those fields in the controller responses for
listChannelConnections/listChannelEndpoints. In the inbox controller (where
result is transformed with mapChannelConnectionToInboxDto) add totalCount:
result.totalCount ?? null and totalCountCapped: result.totalCountCapped ?? null
(or appropriate numeric defaults) to the returned object so the SDK validation
succeeds, or alternatively update the SDK Zod schemas in libs/internal-sdk to
make totalCount and totalCountCapped optional — choose one approach and apply it
consistently for both listChannelConnections and listChannelEndpoints.

---

Outside diff comments:
In
`@apps/api/src/app/integrations/usecases/generate-chat-oath-url/generate-msteams-oath-url/generate-msteams-oauth-url.usecase.ts`:
- Around line 149-157: The buildRedirectUri function currently returns a
hardcoded ngrok URL; restore environment-driven behavior by removing the
hardcoded origin and returning the computed baseUrl combined with
CHAT_OAUTH_CALLBACK_PATH (i.e., compute baseUrl from process.env.API_ROOT_URL
after trimming trailing slash and return
`${baseUrl}${CHAT_OAUTH_CALLBACK_PATH}`), remove the dead/commented fallback and
ensure the existing API_ROOT_URL presence check/throw remains intact; reference:
buildRedirectUri and CHAT_OAUTH_CALLBACK_PATH.

In
`@apps/api/src/app/integrations/usecases/generate-chat-oath-url/generate-slack-oath-url/generate-slack-oauth-url.usecase.ts`:
- Around line 221-229: The buildRedirectUri() function currently returns a
hardcoded ngrok origin; change it to use the API_ROOT_URL env var instead:
validate process.env.API_ROOT_URL, normalize it by removing a trailing slash
into baseUrl, and return `${baseUrl}${CHAT_OAUTH_CALLBACK_PATH}` (remove the
hardcoded ngrok URL). Also delete the commented-out fallback line containing the
stray "22" so no debug artifact remains. Ensure you edit the static method
buildRedirectUri in the generate-slack-oauth-url.usecase class/function.
🪄 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: 8028d06e-9fab-4018-8a7d-e099798fe944

📥 Commits

Reviewing files that changed from the base of the PR and between 0ad1fe5 and a89c3ee.

📒 Files selected for processing (7)
  • apps/api/src/app/inbox/dtos/inbox-channel-connection-response.dto.ts
  • apps/api/src/app/inbox/dtos/inbox-channel-endpoint-response.dto.ts
  • apps/api/src/app/inbox/dtos/inbox-dto.mapper.ts
  • apps/api/src/app/inbox/inbox.controller.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.usecase.ts
  • packages/js/src/channel-connections/types.ts
💤 Files with no reviewable changes (1)
  • packages/js/src/channel-connections/types.ts

Comment thread apps/api/src/app/inbox/inbox.controller.ts
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: 1

🤖 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/generate-chat-oath-url/generate-slack-oath-url/generate-slack-oauth-url.usecase.ts`:
- Around line 65-72: The state encoder/decoder currently splits on '.' which
breaks when subscriberId, context, or connectionIdentifier contain dots; change
the parsing to locate the last '.' (use lastIndexOf('.') on the state string)
and split into payload = state.slice(0, lastIndex) and signature =
state.slice(lastIndex + 1), then JSON.parse the payload and verify the signature
as before. Update the parsing logic wherever split('.') is used (e.g., in the
Slack and MSTEAMS callback handlers and any state parsing functions referenced
by createSecureState and generateSlackOauthUrl/generateMsteamsOauthUrl usecases)
so they use lastIndexOf('.') and handle missing/invalid separators robustly.
🪄 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: 59be5d00-8ce8-4365-b632-6d36fa9314ad

📥 Commits

Reviewing files that changed from the base of the PR and between a89c3ee and 9c718c7.

📒 Files selected for processing (1)
  • apps/api/src/app/integrations/usecases/generate-chat-oath-url/generate-slack-oath-url/generate-slack-oauth-url.usecase.ts

Comment on lines 65 to 72
const secureState = await this.createSecureState(
command.integration,
subscriberId,
command.subscriberId,
command.context,
command.connectionIdentifier,
command.mode,
command.connectionMode
);
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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Inspect subscriberId constraints and OAuth state delimiter parsing.

rg -n -C4 --type=ts '\bsubscriberId\b|decoded\.split\('\''\.'\''\)'

Repository: novuhq/novu

Length of output: 50369


🏁 Script executed:

#!/bin/bash
# Get the exact file and lines mentioned in the review
file_path="apps/api/src/app/integrations/usecases/generate-chat-oath-url/generate-slack-oath-url/generate-slack-oauth-url.usecase.ts"

# Check file exists and show relevant lines
if [ -f "$file_path" ]; then
  echo "=== File context around lines 65-72 ==="
  sed -n '60,75p' "$file_path"
  echo ""
  echo "=== File context around lines 197-202 ==="
  sed -n '190,210p' "$file_path"
else
  echo "File not found: $file_path"
  # Try to find the file with a flexible search
  find . -name "*generate-slack-oauth-url.usecase.ts" -type f
fi

Repository: novuhq/novu

Length of output: 1492


🏁 Script executed:

#!/bin/bash
# Search for the split('.') pattern in the usecase file and related state handling
rg -n "split\('\.'\)|createSecureState|validateAndDecodeState" apps/api/src/app/integrations --type=ts -A 5 -B 2 | head -150

Repository: novuhq/novu

Length of output: 14838


🏁 Script executed:

#!/bin/bash
# Check if there are any validations on subscriberId format in the codebase
rg -n "subscriberId.*pattern|subscriberId.*validate|subscriberId.*format" apps/api/src --type=ts | head -50

Repository: novuhq/novu

Length of output: 172


🏁 Script executed:

#!/bin/bash
sed -n '159,195p' apps/api/src/app/integrations/usecases/generate-chat-oath-url/generate-slack-oath-url/generate-slack-oauth-url.usecase.ts

Repository: novuhq/novu

Length of output: 1234


🏁 Script executed:

#!/bin/bash
# Check if there's any validation logic for subscriberId format
rg -n "subscriberId" apps/api/src/app/integrations/usecases/generate-chat-oath-url/generate-slack-oath-url/generate-slack-oauth-url.usecase.ts -B 2 -A 2 | head -80

Repository: novuhq/novu

Length of output: 1727


Fix OAuth state parsing to handle subscriber IDs and context with dot delimiters.

Line 200 uses split('.') which fails if subscriberId, context, or connectionIdentifier contain dots (e.g., email-like subscriber IDs, domain names). The payload is JSON that may contain dots; splitting at the first dot instead of the last one breaks signature verification for otherwise valid states.

This issue also affects:

  • apps/api/src/app/integrations/usecases/generate-chat-oath-url/generate-msteams-oath-url/generate-msteams-oauth-url.usecase.ts:128
  • Callback handlers in slack-oauth-callback and msteams-oauth-callback
🔒 Proposed fix (use last dot as separator)
-      const [payload, signature] = decoded.split('.');
+      const separatorIndex = decoded.lastIndexOf('.');
+      if (separatorIndex === -1) {
+        throw new Error('Invalid state format');
+      }
+
+      const payload = decoded.slice(0, separatorIndex);
+      const signature = decoded.slice(separatorIndex + 1);
+
+      if (!payload || !signature) {
+        throw new Error('Invalid state format');
+      }
 
       const expectedSignature = createHash(environmentApiKey, payload);
🤖 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 65 - 72, The state encoder/decoder currently splits on '.' which
breaks when subscriberId, context, or connectionIdentifier contain dots; change
the parsing to locate the last '.' (use lastIndexOf('.') on the state string)
and split into payload = state.slice(0, lastIndex) and signature =
state.slice(lastIndex + 1), then JSON.parse the payload and verify the signature
as before. Update the parsing logic wherever split('.') is used (e.g., in the
Slack and MSTEAMS callback handlers and any state parsing functions referenced
by createSecureState and generateSlackOauthUrl/generateMsteamsOauthUrl usecases)
so they use lastIndexOf('.') and handle missing/invalid separators robustly.

@djabarovgeorge djabarovgeorge merged commit a62baa5 into next Apr 19, 2026
36 checks passed
@djabarovgeorge djabarovgeorge deleted the polish-inbox-channel-endpoints branch April 19, 2026 11:39
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.

1 participant