Skip to content

feat(root): add @novu/chat-sdk-adapter Chat SDK platform adapter fixes NV-8063#11593

Merged
scopsy merged 16 commits into
nextfrom
nv-8063-chat-adapter-novu
Jun 17, 2026
Merged

feat(root): add @novu/chat-sdk-adapter Chat SDK platform adapter fixes NV-8063#11593
scopsy merged 16 commits into
nextfrom
nv-8063-chat-adapter-novu

Conversation

@scopsy

@scopsy scopsy commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Summary

Introduce @novu/chat-sdk-adapter (packages/chat-adapter), a Chat SDK platform adapter that exposes all of Novu's normalized chat channels — Slack, WhatsApp, Microsoft Teams, Telegram, Email — as a single platform. Novu does the per-channel normalization (one Conversation + Subscriber + history) and calls the developer's bridge; the developer's Chat SDK app becomes the brain. One handler set serves every channel with no per-channel code.

sequenceDiagram
  participant U as End-user channel
  participant N as Novu
  participant B as Chat SDK app (@novu/chat-sdk-adapter)
  U->>N: platform webhook
  N->>N: normalize -> Conversation + Subscriber + history
  N->>B: POST AgentBridgeRequest (HMAC novu-signature)
  B->>B: verify HMAC, map to Thread/Message, route to handler
  B->>N: AgentReplyPayload -> POST /v1/agents/:id/reply (apiKey)
  N->>U: route reply back to originating channel
Loading

What's included

  • New package @chat-adapter/novu, peer-deps chat >=4.30.0 (+ optional react for JSX cards).
  • Inbound: HMAC verification over the raw body (novu-signature, t=,v1= scheme); maps AgentBridgeRequest to Chat SDK Thread/Message; server-truth routing (ongoing conversation -> onSubscribedMessage; brand-new -> onNewMention / onDirectMessage via platformContext.isDM); onResolve no-op; dedup via deliveryId.
  • Outbound: postMessage / editMessage / addReaction serialize to reply / edit / addReactions POSTs authenticated with the Novu apiKey; edit-based streaming via the chat package's built-in cadence.
  • Security: the reply URL is derived from config (apiBaseUrl + agentIdentifier); the inbound request's replyUrl is ignored, so a forged request can't exfiltrate the apiKey.
  • State: bundled zero-deps in-memory StateAdapter (single-instance-safe); multi-instance bridges pass a shared state adapter.
  • Opt-in Novu helper: getNovuContext(thread) => { platform, trigger, setMetadata, deleteMetadata, resolve }. Ported Chat SDK bots ignore it and run unchanged.
  • Optional boot-time bridge registration (PUT /v1/agents/:id/bridge).
  • Playground example (playground/nextjs): /api/novu-agent live bridge, /api/novu-agent/simulate credential-free simulator, and /novu-agent test UI.

Out of scope (v1)

deleteMessage, modals, outbound-initiated DMs (openDM), code-driven channel provisioning, and Novu-side per-conversation turn serialization (tracked follow-up so zero-deps in-memory state is correct at horizontal scale).

Follow-ups

  • Secure the @chat-adapter/novu npm name + chat-sdk.dev adapter listing (cross-team).
  • Extend the managed per-conversation turn queue (inbound-ack.service.ts) to the bridge path.

Test plan

  • pnpm --filter @chat-adapter/novu test (unit: mappers, HMAC valid/invalid/replayed, reply-URL derivation)
  • pnpm --filter @chat-adapter/novu build
  • Playground simulator: /novu-agent test UI exercises onNewMention / onDirectMessage / onSubscribedMessage / onAction / onReaction with no credentials
  • Live bridge: point an agent's devBridgeUrl (tunnel) at /api/novu-agent, send a real WhatsApp/Telegram message, confirm the echo returns on that channel
  • Cross-channel: same bot, two channels, identical handler path + correct per-channel reply routing

Closes NV-8063

Made with Cursor

Summary by CodeRabbit

What changed

  • New Features
    • Introduced a new @novu/chat-sdk-adapter package to bridge Novu chat events into Chat SDK threads/messages.
    • Added an opt-in getNovuContext(thread) helper for Novu-specific capabilities.
    • Added a Next.js live bridge endpoint plus a credential-free simulator and a small UI to exercise routing and replies.
  • Security
    • Added HMAC signature verification for inbound webhooks and hardened reply handling to prevent misuse of inbound reply URLs.
    • Added webhook delivery deduplication.
  • Documentation
    • Added comprehensive adapter setup and state guidance.
  • Tests
    • Added Vitest coverage for signatures, thread-id behavior, reply URL/auth, and end-to-end routing/dedup cases.

Greptile Summary

  • Adds a new @novu/chat-sdk-adapter package that bridges Novu agent webhook events into Chat SDK threads, messages, and handlers.
  • Implements HMAC verification, delivery deduplication, thread/message mapping, snapshot caching, outbound replies, edits, reactions, and Novu-specific context helpers.
  • Adds unit/integration coverage for signatures, routing, reply behavior, snapshots, files, and pagination.
  • Adds a Next.js playground endpoint, simulator route, and UI for local and live bridge testing.

Confidence Score: 5/5

The changes appear merge-safe with no code issues identified.

The adapter and playground additions are covered by focused tests for the core bridge behaviors, signing, routing, reply handling, snapshots, and pagination.

T-Rex T-Rex Logs

What T-Rex did

  • T-Rex ran the requested verification and did not upload local artifact references.

T-Rex Ran code and verified through T-Rex

Comments Outside Diff (7)

  1. General comment

    P1 Brand-new mention and DM webhooks ACK without firing Chat handlers

    • Bug
      • For signed onMessage bridge requests with conversation.messageCount: 1, empty history, and platformContext.isDM set to both false and true, handleWebhook returned HTTP 200 but did not invoke onNewMention or onDirectMessage. The PR claim says brand-new non-DM should route to onNewMention and brand-new DM should route to onDirectMessage, but the after artifact's recorded handler calls omit both.
    • Cause
      • packages/chat-adapter/src/adapter.ts routes all message events through this.chat.processMessage(this, threadId, message, options) after only pre-subscribing ongoing conversations at lines 142-151. The adapter does not provide/preserve the platform channel context needed by the Chat SDK to classify a first message as a mention or direct message, so first-message server-truth cases are acknowledged but not dispatched to the expected new-conversation handlers.
    • Fix
      • When dispatching onMessage, pass enough channel/thread context into the Chat SDK (or explicitly choose the Chat SDK event path) so messageCount === 1 && history.length === 0 && !isDM invokes onNewMention, and the same first-message case with isDM === true invokes onDirectMessage. Add an integration assertion for both first-message cases alongside the existing ongoing/action coverage.

    T-Rex Ran code and verified through T-Rex

  2. General comment

    P1 Simulator endpoint fails at runtime because Next.js cannot resolve @novu/chat-sdk-adapter

    • Bug
      • The PR claim requires the credential-free /api/novu-agent/simulate endpoint to let users exercise routing from the browser and display HTTP status, routedTo, and captured replies. In the real playground/nextjs dev server, the page route loads, but POSTing both an ongoing onMessage scenario and a brand-new DM scenario to /api/novu-agent/simulate returns HTTP 500. The response is Next.js' module-not-found error for @novu/chat-sdk-adapter, so the UI fetch would not receive the expected simulator JSON and cannot display the promised routed result.
    • Cause
      • The changed Novu agent route code imports @novu/chat-sdk-adapter, but the actual dev-server compilation could not resolve that package from the playground app in this workspace install/state. The failing import is on the simulator/agent route path (playground/nextjs/src/app/api/novu-agent/simulate/route.ts imports createMemoryState/createNovuAdapter, and agent.ts also imports the package).
    • Fix
      • Ensure @novu/chat-sdk-adapter is resolvable by the Next.js playground in the repository-supported install/dev workflow. Verify the workspace package name/dependency is correct in playground/nextjs/package.json, the lockfile/workspace links are updated, and Next.js can transpile/resolve the package from the app. Then re-run the real dev server and confirm /api/novu-agent/simulate returns 200 JSON with status, routedTo, and replies for ongoing and DM scenarios.

    T-Rex Ran code and verified through T-Rex

  3. General comment

    P1 New DM and mention webhooks are ACKed but not routed to their handlers

    • Bug
      • The PR contract says new direct-message conversations should route to onDirectMessage and new non-DM conversations should route to onNewMention. In the head runtime artifact, the synthetic webhook run delivered an ongoing message, a new DM (messageCount: 1, empty history, isDM: true, distinct conversation), a new non-DM mention (messageCount: 1, empty history, isDM: false, distinct conversation), action, reaction, and resolve. All six calls returned HTTP 200, but the events captured from Chat SDK handlers only include subscribed, action, and reaction; no dm or mention event was delivered.
    • Cause
      • NovuAdapterImpl.handleWebhook relies on subscription pre-seeding at packages/chat-adapter/src/adapter.ts:142-147 and then delegates every onMessage to chat.processMessage at packages/chat-adapter/src/adapter.ts:149-152/189-198. For brand-new conversations it leaves the thread unsubscribed, but the runtime path did not result in Chat SDK invoking the registered new-message handlers.
    • Fix
      • Adjust the onMessage dispatch path for brand-new conversations so it explicitly satisfies Chat SDK's new DM/new mention routing contract, or set the thread/channel state required by chat.processMessage before dispatch. Add a regression test that asserts onDirectMessage fires for messageCount === 1 && history.length === 0 && isDM and onNewMention fires for the corresponding non-DM case.

    T-Rex Ran code and verified through T-Rex

  4. General comment

    P1 Simulator route cannot compile because @novu/chat-sdk-adapter is unresolved

    • Bug
      • The new /novu-agent playground cannot successfully submit simulator requests. In the real Next.js app, POSTing to /api/novu-agent/simulate fails with HTTP 500 because src/app/api/novu-agent/agent.ts imports @novu/chat-sdk-adapter, but that package cannot be resolved at runtime. As a result, the UI cannot display the required HTTP status, routedTo, and captured replies after interaction.
    • Cause
      • playground/nextjs/src/app/api/novu-agent/agent.ts and simulate/route.ts depend on @novu/chat-sdk-adapter, but the workspace install/runtime resolution does not provide that package to the Next.js playground app under the current package graph.
    • Fix
      • Ensure the adapter package is resolvable from playground/nextjs: correct the package name/import if it was renamed, add the actual workspace package to the playground dependencies, and verify pnpm --dir playground/nextjs exec next dev can compile /api/novu-agent/simulate. Then retest the UI button flow for default and direct-message scenarios.

    T-Rex Ran code and verified through T-Rex

  5. General comment

    P1 Claimed @chat-adapter/novu package filter does not match the introduced workspace package

    • Bug
      • On head, packages/chat-adapter/package.json declares the package as @novu/chat-sdk-adapter, while the validation contract and author's documented commands expect @chat-adapter/novu. Running pnpm --filter @chat-adapter/novu test and pnpm --filter @chat-adapter/novu build on head prints No projects matched the filters and exits 0 without executing tests or a build, so the advertised build/test contract is not actually validated by those commands. The playground is also wired to @novu/chat-sdk-adapter, not @chat-adapter/novu.
    • Cause
      • packages/chat-adapter/package.json line 2 uses "name": "@novu/chat-sdk-adapter", and playground/nextjs/package.json line 18 depends on "@novu/chat-sdk-adapter": "workspace:*", but the claimed workspace/package filter is @chat-adapter/novu.
    • Fix
      • Either rename the package and playground dependency to the intended @chat-adapter/novu package name, or update the published PR/test-plan contract and all setup guidance to use @novu/chat-sdk-adapter; ensure the documented pnpm --filter ... test and build commands match a real workspace project and execute scripts.

    T-Rex Ran code and verified through T-Rex

  6. General comment

    P1 Credential-free Novu agent simulator UI is blocked by required Clerk environment

    • Bug
      • The PR claims a credential-free simulator at /novu-agent, but running the real playground app on head without credentials renders the app-level Missing NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY screen instead of the Novu agent playground. Because no simulator controls are visible, Playwright cannot trigger onNewMention, onDirectMessage, onSubscribedMessage, onAction, or onReaction through the real UI in the default credential-free setup.
    • Cause
      • The new route is added under the existing playground app surface, which requires NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY before rendering route content. The changed /novu-agent page and simulator may be credential-free with respect to Novu, but the playground route is not credential-free overall unless Clerk env is supplied.
    • Fix
      • Make /novu-agent render independently of the Clerk-gated app shell, provide safe local defaults/stubs for Clerk in the playground, or update .env.example/startup contract so the route can be exercised without external credentials as claimed.

    T-Rex Ran code and verified through T-Rex

  7. General comment

    P1 Simulator onNewMention request does not reliably return a successful structured response

    • Bug
      • The claimed credential-free simulator should exercise onNewMention, but a representative request POST /api/novu-agent/simulate with body {"event":"onMessage","ongoing":false,"isDM":false,"text":"hello mention"} returned HTTP/1.1 404 Not Found and an HTML app not-found/missing-Clerk response in the real Next.js server. Other simulator paths in the same run returned structured 200 OK JSON responses, so this is a contract mismatch for the claimed onNewMention event type.
    • Cause
      • The simulator route logic labels first non-ongoing messages as onNewMention in playground/nextjs/src/app/api/novu-agent/simulate/route.ts lines 129-134, but the runtime request did not complete through that JSON handler path for the onNewMention representative request; it fell through to the app not-found/Clerk layout response.
    • Fix
      • Ensure POST /api/novu-agent/simulate consistently handles first-message/non-DM onMessage requests and returns the same structured JSON envelope as the other simulator event types. Add an endpoint test that posts the onNewMention body and asserts 200 OK, JSON content type, routedTo:"onNewMention", and a captured reply.

    T-Rex Ran code and verified through T-Rex

Reviews (4): Last reviewed commit: "chore(chat-adapter): align package metad..." | Re-trigger Greptile

…-8063

Introduce @chat-adapter/novu (packages/chat-adapter), a Chat SDK platform
adapter that exposes Novu's normalized channels (Slack, WhatsApp, Teams,
Telegram, Email) as one platform. The developer's Chat SDK app becomes the
Novu bridge: verify inbound HMAC, map AgentBridgeRequest to Thread/Message,
route to native handlers, and POST AgentReplyPayload to /v1/agents/:id/reply.

- Inbound: raw-body HMAC verify, server-truth routing, deliveryId dedup, onResolve no-op
- Outbound: reply/edit/addReactions POSTs (apiKey auth), edit-based streaming
- Security: reply URL derived from config; request replyUrl ignored
- Bundled zero-deps in-memory state; opt-in getNovuContext (trigger/metadata/resolve)
- Next.js playground example (live bridge + credential-free simulator + test UI)
@linear-code

linear-code Bot commented Jun 17, 2026

Copy link
Copy Markdown

NV-8063

@netlify

netlify Bot commented Jun 17, 2026

Copy link
Copy Markdown

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

Name Link
🔨 Latest commit e6a3334
🔍 Latest deploy log https://app.netlify.com/projects/dashboard-v2-novu-staging/deploys/6a32b91298446e0008808b94

@coderabbitai

coderabbitai Bot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

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

Introduces the new @novu/chat-sdk-adapter package with comprehensive type contracts, thread-ID encoding, HMAC signature verification, webhook parsing, outbound HTTP client, bidirectional message mapping, NovuAdapterImpl orchestration, and end-to-end integration tests. Integrates the adapter into the Next.js playground with a live webhook route, HMAC-signed webhook simulation endpoint, and interactive UI for testing.

Sequence Diagrams

sequenceDiagram
  participant NovuBridge
  participant NovuAdapterImpl
  participant WebhookHandler
  participant StateAdapter
  participant Chat
  participant ReplyClient

  NovuBridge->>NovuAdapterImpl: handleWebhook(request)
  NovuAdapterImpl->>WebhookHandler: parseAndVerify(request)
  WebhookHandler-->>NovuAdapterImpl: ParseResult {status, AgentBridgeRequest?}
  NovuAdapterImpl->>StateAdapter: setIfNotExists(deliveryId TTL key)
  StateAdapter-->>NovuAdapterImpl: deduplicated?
  NovuAdapterImpl->>StateAdapter: set(thread snapshot, TTL 7d)
  NovuAdapterImpl->>Chat: processMessage / processAction / processReaction
  Chat-->>NovuAdapterImpl: handler reply content
  NovuAdapterImpl->>ReplyClient: send(AgentReplyPayload)
  ReplyClient->>NovuBridge: POST /v1/agent/reply (ApiKey auth)
  ReplyClient-->>NovuAdapterImpl: SentMessageInfo or null
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~65 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 29.03% 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
Title check ✅ Passed The title 'feat(root): add @chat-adapter/novu Chat SDK platform adapter fixes NV-8063' clearly describes the main change—adding a new Novu Chat SDK platform adapter—and references the related issue.
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.

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

return timingSafeEqual(Buffer.from(receivedHmac, 'hex'), Buffer.from(expectedHmac, 'hex'));
}

export function getSignatureHeader(request: Request): string | null {

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.

🔒 Agentic Security Review
Severity: MEDIUM
The signature verifier can throw on malformed novu-signature values: timingSafeEqual(Buffer.from(receivedHmac, 'hex'), ...) is called after checking only the hex-string length, but invalid hex can decode to a shorter buffer and cause timingSafeEqual to raise.

Impact: unauthenticated callers can reliably trigger 500 responses on the webhook verification path instead of a clean 401 reject, creating an avoidable availability/abuse vector on a public endpoint.

Fix in Cursor Fix in Web

Reviewed by Cursor Security Reviewer for commit 3fd05cd. Configure here.

@cursor cursor Bot requested review from ChmaraX and djabarovgeorge June 17, 2026 11:36

@cursor cursor Bot left a comment

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.

Stale comment

Risk: medium. Not approving: Cursor Security Agent reported an unresolved medium-severity finding in webhook signature verification (malformed hex can throw and return 500s instead of 401). Assigned ChmaraX and djabarovgeorge for human review.

Open in Web View Automation 

Sent by Cursor Approval Agent: Pull Request Approver

Replace @chat-adapter/novu with @novu/chat-sdk-adapter across package metadata,
playground imports, and lockfile.

@cursor cursor Bot left a comment

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.

Stale comment

Risk: medium. Not approving: an unresolved medium-severity Security Agent finding remains on webhook signature verification (malformed hex can throw and return 500s). ChmaraX and djabarovgeorge are already assigned for human review.

Open in Web View Automation 

Sent by Cursor Approval Agent: Pull Request Approver

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 7

🧹 Nitpick comments (2)
playground/nextjs/src/app/novu-agent/page.tsx (1)

13-13: 💤 Low value

Consider using a named export in addition to the default export.

The coding guidelines prefer named exports for all components. While Next.js App Router pages require a default export, you can satisfy both requirements by using a named export and then re-exporting it as default.

♻️ Optional refactor pattern
-export default function NovuAgentPlayground() {
+export function NovuAgentPlayground() {
   // ... component body ...
 }
+
+export default NovuAgentPlayground;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@playground/nextjs/src/app/novu-agent/page.tsx` at line 13, The
NovuAgentPlayground component currently uses only a default export, but the
coding guidelines prefer named exports. Refactor the component by first defining
it as a named export function NovuAgentPlayground, then create a separate
default export statement that re-exports the named export. This satisfies both
the coding guideline requirement for named exports and the Next.js App Router
requirement for a default export on page components.

Source: Coding guidelines

packages/chat-adapter/src/thread-id.spec.ts (1)

28-32: ⚡ Quick win

Consider adding test coverage for empty platform and invalid DM flag.

The malformed id tests don't cover cases like 'novu::int:conv:0' (empty platform) or 'novu:slack:int:conv:X' (invalid DM flag). Adding these would strengthen validation and align with the suggested improvement in thread-id.ts line 26.

📋 Suggested additional test cases
   it('throws on malformed ids', () => {
-    for (const bad of ['', 'novu', 'novu:slack', 'email:foo:bar', 'novu:slack:int::0']) {
+    for (const bad of [
+      '',
+      'novu',
+      'novu:slack',
+      'email:foo:bar',
+      'novu:slack:int::0',
+      'novu::int:conv:0',
+      'novu:slack:int:conv:X',
+    ]) {
       expect(() => decodeThreadId(bad)).toThrow();
     }
   });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/chat-adapter/src/thread-id.spec.ts` around lines 28 - 32, The test
for malformed ids in the it block labeled 'throws on malformed ids' does not
include test cases for empty platforms or invalid DM flag values. Add the two
missing test cases to the bad array that is iterated over in the for loop: one
case with an empty platform segment (novu::int:conv:0) and one case with an
invalid DM flag value (novu:slack:int:conv:X). This will ensure that the
decodeThreadId function properly validates these edge cases as required by the
validation logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/chat-adapter/package.json`:
- Line 13: The `afterinstall` script in the scripts section of package.json is
not a recognized npm lifecycle hook and will not execute automatically during
installation. Rename the `afterinstall` script to `prepare`, which is a standard
npm lifecycle hook that will run automatically before pack/publish operations
and during local install, ensuring the build command executes when needed.

In `@packages/chat-adapter/src/adapter.ts`:
- Around line 132-166: The dedupe marker is being set via state.setIfNotExists
with deliveryKey before the actual message processing occurs. Move the dedupe
marker logic to execute after all dispatching operations (dispatchMessage,
dispatchAction, dispatchReaction) and the subscription setup complete
successfully. This ensures that if any of the dispatch or processing steps fail,
the delivery will not be marked as deduped and can be retried on the next
attempt without being treated as a duplicate.
- Around line 157-158: The dispatchReaction method call at line 157 is not
awaited, causing a race condition where the webhook returns 200 before async
reaction handlers complete. This is inconsistent with dispatchMessage and
dispatchAction which are both awaited. Make the dispatchReaction method async,
add await before the dispatchReaction call at line 157, and also await the
internal this.chat.processReaction call within the dispatchReaction method to
ensure all async operations complete before the webhook response is sent.

In `@packages/chat-adapter/src/message-mapper.ts`:
- Around line 77-98: The buildHistoryMessage method is setting conversationId to
an empty string in the raw message object, which loses the actual conversation
identity even though the threadId parameter is available and contains the real
conversation identifier. Replace the empty string value for conversationId with
the threadId parameter to preserve the conversation identity in the history raw
messages and maintain consistency with the thread state information.

In `@packages/chat-adapter/src/novu-context.ts`:
- Around line 18-33: The validation guard in getNovuContext() checks for the
existence of emitSignals and decodeThreadId functions but does not validate
emitResolve, which is called on line 32 in the resolve function. Add a
validation check for source.emitResolve to the existing guard clause that
validates adapter ownership, ensuring that all three methods (emitSignals,
decodeThreadId, and emitResolve) are present and are functions before proceeding
with the rest of the function logic.

In `@packages/chat-adapter/src/thread-id.ts`:
- Around line 24-36: The decodeThreadId function has incomplete validation that
allows invalid thread IDs to pass. The validation check currently verifies that
parts[2] and parts[3] are non-empty but does not validate parts[1] (platform) or
parts[4] (isDM flag). You need to extend the condition in the validation check
to also ensure that parts[1] is non-empty by adding it to the falsy checks
alongside parts[2] and parts[3]. Additionally, add validation for parts[4] to
ensure it is strictly '0' or '1' before setting the isDM property, rejecting any
other value as an invalid format to prevent silent defaulting to false for
malformed thread IDs.

In `@playground/nextjs/src/app/novu-agent/page.tsx`:
- Around line 7-11: The SimResult type definition is using the `interface`
keyword, but frontend code should use `type` instead. Change the `interface
SimResult` declaration to `type SimResult` to align with the frontend coding
guidelines where `type` is used for type definitions in client components and
`interface` is reserved for backend code.

---

Nitpick comments:
In `@packages/chat-adapter/src/thread-id.spec.ts`:
- Around line 28-32: The test for malformed ids in the it block labeled 'throws
on malformed ids' does not include test cases for empty platforms or invalid DM
flag values. Add the two missing test cases to the bad array that is iterated
over in the for loop: one case with an empty platform segment (novu::int:conv:0)
and one case with an invalid DM flag value (novu:slack:int:conv:X). This will
ensure that the decodeThreadId function properly validates these edge cases as
required by the validation logic.

In `@playground/nextjs/src/app/novu-agent/page.tsx`:
- Line 13: The NovuAgentPlayground component currently uses only a default
export, but the coding guidelines prefer named exports. Refactor the component
by first defining it as a named export function NovuAgentPlayground, then create
a separate default export statement that re-exports the named export. This
satisfies both the coding guideline requirement for named exports and the
Next.js App Router requirement for a default export on page components.
🪄 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: ab8d09b5-a66d-4d09-94e1-300e2129e8b5

📥 Commits

Reviewing files that changed from the base of the PR and between 288b5ab and 3fd05cd.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (26)
  • packages/chat-adapter/.gitignore
  • packages/chat-adapter/README.md
  • packages/chat-adapter/package.json
  • packages/chat-adapter/project.json
  • packages/chat-adapter/src/adapter.integration.spec.ts
  • packages/chat-adapter/src/adapter.ts
  • packages/chat-adapter/src/index.ts
  • packages/chat-adapter/src/message-mapper.ts
  • packages/chat-adapter/src/novu-context.ts
  • packages/chat-adapter/src/reply-client.spec.ts
  • packages/chat-adapter/src/reply-client.ts
  • packages/chat-adapter/src/signature.spec.ts
  • packages/chat-adapter/src/signature.ts
  • packages/chat-adapter/src/state-memory.ts
  • packages/chat-adapter/src/thread-id.spec.ts
  • packages/chat-adapter/src/thread-id.ts
  • packages/chat-adapter/src/types.ts
  • packages/chat-adapter/src/webhook-handler.ts
  • packages/chat-adapter/tsconfig.json
  • packages/chat-adapter/vitest.config.ts
  • playground/nextjs/.env.example
  • playground/nextjs/package.json
  • playground/nextjs/src/app/api/novu-agent/agent.ts
  • playground/nextjs/src/app/api/novu-agent/route.ts
  • playground/nextjs/src/app/api/novu-agent/simulate/route.ts
  • playground/nextjs/src/app/novu-agent/page.tsx

Comment thread packages/chat-adapter/package.json Outdated
Comment thread packages/chat-adapter/src/adapter.ts
Comment thread packages/chat-adapter/src/adapter.ts Outdated
Comment thread packages/chat-adapter/src/message-mapper.ts
Comment on lines +18 to +33
if (typeof source?.emitSignals !== 'function' || typeof source?.decodeThreadId !== 'function') {
throw new Error('getNovuContext() requires a thread owned by the Novu adapter');
}

const threadId = thread.id;
const { platform } = source.decodeThreadId(threadId);

const emit = (signal: Signal) => source.emitSignals(threadId, [signal]);

return {
platform,
trigger: (workflowId, opts) => emit({ type: 'trigger', workflowId, to: opts?.to, payload: opts?.payload }),
setMetadata: (key, value) => emit({ type: 'metadata', action: 'set', key, value }),
deleteMetadata: (key) => emit({ type: 'metadata', action: 'delete', key }),
resolve: (summary) => source.emitResolve(threadId, summary),
};

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 | ⚡ Quick win

Guard emitResolve in adapter ownership validation.

Line 32 calls source.emitResolve(...), but Lines 18-19 only validate emitSignals and decodeThreadId. If an adapter passes those two checks but lacks emitResolve, resolve() fails at runtime.

Proposed fix
-  if (typeof source?.emitSignals !== 'function' || typeof source?.decodeThreadId !== 'function') {
+  if (
+    typeof source?.emitSignals !== 'function' ||
+    typeof source?.decodeThreadId !== 'function' ||
+    typeof source?.emitResolve !== 'function'
+  ) {
     throw new Error('getNovuContext() requires a thread owned by the Novu adapter');
   }
📝 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
if (typeof source?.emitSignals !== 'function' || typeof source?.decodeThreadId !== 'function') {
throw new Error('getNovuContext() requires a thread owned by the Novu adapter');
}
const threadId = thread.id;
const { platform } = source.decodeThreadId(threadId);
const emit = (signal: Signal) => source.emitSignals(threadId, [signal]);
return {
platform,
trigger: (workflowId, opts) => emit({ type: 'trigger', workflowId, to: opts?.to, payload: opts?.payload }),
setMetadata: (key, value) => emit({ type: 'metadata', action: 'set', key, value }),
deleteMetadata: (key) => emit({ type: 'metadata', action: 'delete', key }),
resolve: (summary) => source.emitResolve(threadId, summary),
};
if (
typeof source?.emitSignals !== 'function' ||
typeof source?.decodeThreadId !== 'function' ||
typeof source?.emitResolve !== 'function'
) {
throw new Error('getNovuContext() requires a thread owned by the Novu adapter');
}
const threadId = thread.id;
const { platform } = source.decodeThreadId(threadId);
const emit = (signal: Signal) => source.emitSignals(threadId, [signal]);
return {
platform,
trigger: (workflowId, opts) => emit({ type: 'trigger', workflowId, to: opts?.to, payload: opts?.payload }),
setMetadata: (key, value) => emit({ type: 'metadata', action: 'set', key, value }),
deleteMetadata: (key) => emit({ type: 'metadata', action: 'delete', key }),
resolve: (summary) => source.emitResolve(threadId, summary),
};
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/chat-adapter/src/novu-context.ts` around lines 18 - 33, The
validation guard in getNovuContext() checks for the existence of emitSignals and
decodeThreadId functions but does not validate emitResolve, which is called on
line 32 in the resolve function. Add a validation check for source.emitResolve
to the existing guard clause that validates adapter ownership, ensuring that all
three methods (emitSignals, decodeThreadId, and emitResolve) are present and are
functions before proceeding with the rest of the function logic.

Comment on lines +24 to +36
export function decodeThreadId(threadId: string): NovuThreadId {
const parts = threadId.split(':');
if (parts.length !== 5 || parts[0] !== PREFIX || !parts[2] || !parts[3]) {
throw new Error(`Invalid Novu thread id format: ${threadId}`);
}

return {
platform: decodeURIComponent(parts[1] ?? ''),
integrationIdentifier: decodeURIComponent(parts[2]),
conversationId: decodeURIComponent(parts[3]),
isDM: parts[4] === '1',
};
}

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 | ⚡ Quick win

Incomplete validation in decodeThreadId.

The validation on line 26 checks that parts[2] (integrationIdentifier) and parts[3] (conversationId) are non-empty, but does not validate parts[1] (platform). A thread id like 'novu::int:conv:0' would pass validation and return an empty platform, potentially causing downstream issues when constructing channel ids or routing messages.

Additionally, parts[4] (the DM flag) is not validated to be '0' or '1'—any other value silently defaults to false.

🛡️ Suggested validation improvement
 export function decodeThreadId(threadId: string): NovuThreadId {
   const parts = threadId.split(':');
-  if (parts.length !== 5 || parts[0] !== PREFIX || !parts[2] || !parts[3]) {
+  if (
+    parts.length !== 5 ||
+    parts[0] !== PREFIX ||
+    !parts[1] ||
+    !parts[2] ||
+    !parts[3] ||
+    (parts[4] !== '0' && parts[4] !== '1')
+  ) {
     throw new Error(`Invalid Novu thread id format: ${threadId}`);
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/chat-adapter/src/thread-id.ts` around lines 24 - 36, The
decodeThreadId function has incomplete validation that allows invalid thread IDs
to pass. The validation check currently verifies that parts[2] and parts[3] are
non-empty but does not validate parts[1] (platform) or parts[4] (isDM flag). You
need to extend the condition in the validation check to also ensure that
parts[1] is non-empty by adding it to the falsy checks alongside parts[2] and
parts[3]. Additionally, add validation for parts[4] to ensure it is strictly '0'
or '1' before setting the isDM property, rejecting any other value as an invalid
format to prevent silent defaulting to false for malformed thread IDs.

Comment on lines +7 to +11
interface SimResult {
status: number;
routedTo: string;
replies: Array<{ url: string; payload: unknown }>;
}

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 | ⚡ Quick win

Use type instead of interface for frontend type definitions.

Per coding guidelines, frontend code should use type for type definitions, while interface is reserved for backend code. This is a client component (frontend), so SimResult should be defined with type.

♻️ Proposed fix
-interface SimResult {
+type SimResult = {
   status: number;
   routedTo: string;
   replies: Array<{ url: string; payload: unknown }>;
-}
+};
📝 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
interface SimResult {
status: number;
routedTo: string;
replies: Array<{ url: string; payload: unknown }>;
}
type SimResult = {
status: number;
routedTo: string;
replies: Array<{ url: string; payload: unknown }>;
};
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@playground/nextjs/src/app/novu-agent/page.tsx` around lines 7 - 11, The
SimResult type definition is using the `interface` keyword, but frontend code
should use `type` instead. Change the `interface SimResult` declaration to `type
SimResult` to align with the frontend coding guidelines where `type` is used for
type definitions in client components and `interface` is reserved for backend
code.

Source: Coding guidelines

Comment thread packages/chat-adapter/src/signature.ts Outdated
Comment thread packages/chat-adapter/src/adapter.ts
Comment thread packages/chat-adapter/src/adapter.ts
Comment on lines +75 to +86
async appendToList(key: string, value: unknown, options?: { maxLength?: number; ttlMs?: number }): Promise<void> {
const list = this.lists.get(key) ?? [];
list.push(value);
if (options?.maxLength && list.length > options.maxLength) {
list.splice(0, list.length - options.maxLength);
}
this.lists.set(key, list);
}

async getList<T = unknown>(key: string): Promise<T[]> {
return [...((this.lists.get(key) as T[]) ?? [])];
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Honor list TTLs

appendToList() accepts ttlMs, but the value is ignored and getList() never expires list keys. Chat SDK list-backed features such as history, transcripts, or adapter message-reference lists rely on this TTL being refreshed and enforced, so long-running processes can keep stale list data forever, return expired references, and grow memory beyond the configured retention.

T-Rex Ran code and verified through T-Rex

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/chat-adapter/src/state-memory.ts
Line: 75-86

Comment:
**Honor list TTLs**

`appendToList()` accepts `ttlMs`, but the value is ignored and `getList()` never expires list keys. Chat SDK list-backed features such as history, transcripts, or adapter message-reference lists rely on this TTL being refreshed and enforced, so long-running processes can keep stale list data forever, return expired references, and grow memory beyond the configured retention.

How can I resolve this? If you propose a fix, please make it concise.

Fix in Cursor

Comment thread packages/chat-adapter/src/adapter.ts Outdated
Comment thread packages/chat-adapter/src/webhook-handler.ts
Comment thread packages/chat-adapter/src/message-mapper.ts Outdated
Comment thread packages/chat-adapter/src/message-mapper.ts
Comment thread packages/chat-adapter/src/reply-client.ts
Comment thread packages/chat-adapter/src/adapter.ts
…typing fixes NV-8063

Drop the bundled in-memory StateAdapter in favor of the official
@chat-adapter/state-memory package; consumers and the playground import
createMemoryState from it directly. Also widen NovuAdapter to the base Adapter
type so instances are assignable to new Chat({ adapters }).
@socket-security

socket-security Bot commented Jun 17, 2026

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Added@​chat-adapter/​state-memory@​4.30.0731008697100

View full report

@cursor cursor Bot left a comment

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.

Stale comment

Risk: medium. Not approving: an unresolved medium-severity Security Agent finding remains on webhook signature verification (malformed hex can throw and return 500s instead of 401). ChmaraX and djabarovgeorge are already assigned for human review.

Open in Web View Automation 

Sent by Cursor Approval Agent: Pull Request Approver

@coderabbitai coderabbitai Bot left a comment

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.

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 (1)
packages/chat-adapter/src/adapter.ts (1)

277-284: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Generate unique fallback ids for empty Novu reply responses.

When ReplyClient.send returns no messageId, every postMessage in the same conversation returns novu-reply:${conversationId}. Distinct outbound replies can then collide in any state/UI keyed by RawMessage.id.

Proposed fix
   private readonly webhookHandler: WebhookHandler;
   private readonly replyClient: ReplyClient;
+  private fallbackMessageCounter = 0;
   private chat: ChatInstance | null = null;
@@
-    const messageId = info?.messageId ?? `novu-reply:${decoded.conversationId}`;
+    const messageId = info?.messageId ?? this.nextFallbackMessageId(decoded.conversationId);
@@
   private outboundRaw(decoded: NovuThreadId, messageId: string): NovuRawMessage {
@@
   }
+
+  private nextFallbackMessageId(conversationId: string): string {
+    this.fallbackMessageCounter += 1;
+
+    return `novu-reply:${conversationId}:${Date.now()}:${this.fallbackMessageCounter}`;
+  }
 
   private emojiName(emoji: EmojiValue | string): string {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/chat-adapter/src/adapter.ts` around lines 277 - 284, The postMessage
method generates the same fallback messageId for all messages in the same
conversation when ReplyClient.send returns no messageId, causing collisions when
messages are keyed by id. Replace the fallback id generation at the messageId
assignment (where it currently uses novu-reply:${decoded.conversationId}) with a
mechanism that generates a unique identifier for each call, such as by appending
a timestamp, UUID, or counter to ensure each message gets a distinct id even
when they share the same conversationId.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/chat-adapter/src/index.ts`:
- Line 52: Add a clarifying comment to the return statement in the
createNovuAdapter function explaining why the explicit `as NovuAdapter` cast is
necessary. The comment should document that NovuAdapterImpl implements
NovuTypedAdapter with specific generic type parameters (NovuThreadId and
NovuRawMessage), but the function return type requires the untyped
consumer-facing NovuAdapter interface, making the type widening cast required
and intentional. This prevents future developers from mistakenly removing the
cast thinking it is redundant.

---

Outside diff comments:
In `@packages/chat-adapter/src/adapter.ts`:
- Around line 277-284: The postMessage method generates the same fallback
messageId for all messages in the same conversation when ReplyClient.send
returns no messageId, causing collisions when messages are keyed by id. Replace
the fallback id generation at the messageId assignment (where it currently uses
novu-reply:${decoded.conversationId}) with a mechanism that generates a unique
identifier for each call, such as by appending a timestamp, UUID, or counter to
ensure each message gets a distinct id even when they share the same
conversationId.
🪄 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: d8418d44-6ca0-4bf4-a3d6-81a394608935

📥 Commits

Reviewing files that changed from the base of the PR and between a5bcaac and 934fd65.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (10)
  • packages/chat-adapter/README.md
  • packages/chat-adapter/package.json
  • packages/chat-adapter/src/adapter.integration.spec.ts
  • packages/chat-adapter/src/adapter.ts
  • packages/chat-adapter/src/index.ts
  • packages/chat-adapter/src/novu-context.ts
  • packages/chat-adapter/src/types.ts
  • playground/nextjs/package.json
  • playground/nextjs/src/app/api/novu-agent/agent.ts
  • playground/nextjs/src/app/api/novu-agent/simulate/route.ts
✅ Files skipped from review due to trivial changes (1)
  • packages/chat-adapter/README.md
🚧 Files skipped from review as they are similar to previous changes (6)
  • packages/chat-adapter/package.json
  • playground/nextjs/src/app/api/novu-agent/agent.ts
  • packages/chat-adapter/src/novu-context.ts
  • packages/chat-adapter/src/types.ts
  • packages/chat-adapter/src/adapter.integration.spec.ts
  • playground/nextjs/src/app/api/novu-agent/simulate/route.ts

Comment thread packages/chat-adapter/src/index.ts Outdated
Comment thread packages/chat-adapter/src/message-mapper.ts Outdated
…User fixes NV-8063

Surface the Novu subscriber to Chat SDK handlers two ways: the full rich
profile (email, phone, avatar, locale, custom data) through the Novu-only
escape hatch getNovuContext(thread).getSubscriber(), and the portable
identity through the SDK-native adapter.getUser(userId) -> UserInfo.

Align the inbound human author identity to the subscriber so
message.author.userId === subscriberId (consistent across getParticipants
and getUser); the platform-native author is preserved on message.raw.author.

@cursor cursor Bot left a comment

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.

Stale comment

Risk: medium. Not approving: an unresolved medium-severity Security Agent finding remains on webhook signature verification (malformed hex can throw and return 500s instead of 401). ChmaraX and djabarovgeorge are already assigned for human review.

Open in Web View Automation 

Sent by Cursor Approval Agent: Pull Request Approver

@cursor cursor Bot left a comment

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.

Stale comment

Risk: medium. Not approving: an unresolved medium-severity Security Agent finding remains on webhook signature verification (malformed hex can throw and return 500s instead of 401). ChmaraX and djabarovgeorge are already assigned for human review.

Open in Web View Automation 

Sent by Cursor Approval Agent: Pull Request Approver

…ges NV-8063

Commit delivery dedupe only after successful dispatch so transient failures can retry. Reject malformed HMAC hex without throwing, and honor fetchMessages limit/cursor pagination.

@cursor cursor Bot left a comment

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.

Stale comment

Risk: medium. Not approving: an unresolved medium-severity Security Agent finding remains on webhook signature verification (malformed hex can throw and return 500s instead of 401). ChmaraX and djabarovgeorge are already assigned for human review.

Open in Web View Automation 

Sent by Cursor Approval Agent: Pull Request Approver

scopsy added 2 commits June 17, 2026 17:48
Run biome format on remaining chat-adapter files using single quotes. Add VS Code quoteStyle preferences so TypeScript imports match biome.

@cursor cursor Bot left a comment

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.

Stale comment

Risk: medium. Not approving: Cursor Bugbot and Security Agent did not run on this update, so there is no fresh automated sign-off, and GitHub still requires human review. ChmaraX and djabarovgeorge are already assigned.

Open in Web View Automation 

Sent by Cursor Approval Agent: Pull Request Approver

@cursor cursor Bot left a comment

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.

Risk: medium. Not approving: an unresolved medium-severity Security Agent finding remains on webhook signature verification (malformed hex can throw and return 500s instead of 401). ChmaraX and djabarovgeorge are already assigned for human review.

Open in Web View Automation 

Sent by Cursor Approval Agent: Pull Request Approver

Bump version and update README with example app link and accurate usage docs.
…NV-8063

Add exports map, publish metadata, license, and keywords; tighten chat peer to ^4.30.0. Support env-var fallbacks in createNovuAdapter, and add @chat-adapter/shared dependency for upcoming shared error/util adoption.
@scopsy scopsy merged commit daf2654 into next Jun 17, 2026
34 checks passed
@scopsy scopsy deleted the nv-8063-chat-adapter-novu branch June 17, 2026 15:28
@scopsy scopsy changed the title feat(root): add @chat-adapter/novu Chat SDK platform adapter fixes NV-8063 feat(root): add @novu/chat-sdk-adapter Chat SDK platform adapter fixes NV-8063 Jun 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant