Skip to content

Syncing fork - #1

Open
ShariqueAhmedAtoa wants to merge 2662 commits into
ATOAPaymentsLimited:developfrom
chatwoot:develop
Open

Syncing fork#1
ShariqueAhmedAtoa wants to merge 2662 commits into
ATOAPaymentsLimited:developfrom
chatwoot:develop

Conversation

@ShariqueAhmedAtoa

Copy link
Copy Markdown

Pull Request Template

Description

Please include a summary of the change and issue(s) fixed. Also, mention relevant motivation, context, and any dependencies that this change requires.
Fixes # (issue)

Type of change

Please delete options that are not relevant.

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality not to work as expected)
  • This change requires a documentation update

How Has This Been Tested?

Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration.

Checklist:

  • My code follows the style guidelines of this project
  • I have performed a self-review of my code
  • I have commented on my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • Any dependent changes have been merged and published in downstream modules

tds-1 and others added 30 commits June 12, 2026 12:25
…14718)

## Description

Solves issue #14690

## Type of change

- [ ] Bug fix (non-breaking change which fixes an issue)

## How has this been tested?

- UI flows

## Checklist:

- [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules

---------

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
## Description

First PR of user_sessions feature - enforcement, impersonation and mfa will be handled separately.

Adds an Active Sessions section under Profile where users can see every
device currently logged in and revoke any session they don't recognize.
Helps users lock down stale or unrecognized logins on their own without
needing support.

**Behavior at the limit, by client:**
- **Browser:** returns 409 with a picker overlay; user picks a session
to revoke or chooses "End all sessions" to clear them.

- **Mobile / API client:** silently evicts the oldest session and
proceeds with login (no picker UI to render).
- **Pre-tracking users** (token rows without `user_sessions`, i.e.
anyone already logged in before this ships): silent-evict any untracked
token first, so freshly tracked sessions are never killed in favor of
legacy ones.

Sessions are stored in a new `user_sessions` table keyed on `(user_id,
client_id)` with browser, platform, IP, last activity and (when
configured) geo. Kept in sync with `user.tokens` via an after_save
callback so revoking a token from any path cleans up the row.

Fixes https://linear.app/chatwoot/issue/CW-7169

## Type of change

- [x] New feature (non-breaking change which adds functionality)

## How Has This Been Tested?

- Added specs.
- Manual local testing: browser picker fires at limit; pre-tracking user
silent-evicts; mixed tracked/untracked correctly drops the untracked one
first; profile page revoke succeeds; current session cannot be revoked
from profile.

## Checklist

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [x] Any dependent changes have been merged and published in downstream
modules
## Description

Stabilizes the enterprise help center article builder source URL
validation spec by asserting the custom exception via its class name and
message text. This keeps the spec focused on the intended behavior while
avoiding brittle custom exception constant identity checks in
CI/reloading environments.

A bunch of builds on different PRs have been failing because of this
error, sample traces are below:
*
https://app.circleci.com/pipelines/github/chatwoot/chatwoot/114064/workflows/bdb6eca9-3b65-4c38-b8cf-f2f8564476f8/jobs/158777
*
https://app.circleci.com/pipelines/github/chatwoot/chatwoot/114064/workflows/bdb6eca9-3b65-4c38-b8cf-f2f8564476f8/jobs/158777

Fixes # N/A

## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)

## How Has This Been Tested?

- `/Users/sonymathew/.rbenv/shims/bundle exec rspec
spec/enterprise/services/onboarding/help_center_article_builder_spec.rb`
- `/Users/sonymathew/.rbenv/shims/bundle exec rubocop
spec/enterprise/services/onboarding/help_center_article_builder_spec.rb`
- `git diff --check`

## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] My changes generate no new warnings
- [x] New and existing unit tests pass locally with my changes
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] Any dependent changes have been merged and published in downstream
modules
Bumps [net-imap](https://github.com/ruby/net-imap) from 0.4.24 to
0.6.4.1.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/ruby/net-imap/releases">net-imap's
releases</a>.</em></p>
<blockquote>
<h2>v0.5.15</h2>
<h2>What's Changed</h2>
<h3>🔒 Security</h3>
<p>This release fixes several more security vulnerabilities which are
related to the fixes in <code>v0.5.14</code>. Please see the linked
security advisories for more information.</p>
<ul>
<li><em>(moderate)</em> Command Injection via non-synchronizing literal
in &quot;raw&quot; argument (CVE-2026-47240, GHSA-8p34-64r3-mwg8)
This vulnerability depends how the server interprets non-synchronizing
literals.
The connection is <em>not</em> vulnerable if the server supports
non-synchronizing literals.
<ul>
<li>🥅 Validate non-synchronizing literals support by <a
href="https://github.com/nevans"><code>@​nevans</code></a> in <a
href="https://redirect.github.com/ruby/net-imap/pull/703">ruby/net-imap#703</a>
(backports <a
href="https://redirect.github.com/ruby/net-imap/pull/701">ruby/net-imap#701</a>)</li>
</ul>
</li>
<li><em>(moderate)</em> Command Injection via unvalidated ID and ENABLE
arguments (CVE-2026-47242, GHSA-46q3-7gv7-qmgg)
<ul>
<li>🥅 Validate <code>ID</code> values contain only valid bytes by <a
href="https://github.com/nevans"><code>@​nevans</code></a> in <a
href="https://redirect.github.com/ruby/net-imap/pull/703">ruby/net-imap#703</a>
(backports <a
href="https://redirect.github.com/ruby/net-imap/pull/698">ruby/net-imap#698</a>)</li>
<li>🥅 Validate <code>#enable</code> arguments are all atoms by <a
href="https://github.com/nevans"><code>@​nevans</code></a> in <a
href="https://redirect.github.com/ruby/net-imap/pull/703">ruby/net-imap#703</a>
(backport <a
href="https://redirect.github.com/ruby/net-imap/pull/699">ruby/net-imap#699</a>)
<strong>NOTE:</strong> <code>#enable</code> should
<strong><em>never</em></strong> be called with untrusted input.</li>
</ul>
</li>
<li><em>(low)</em> Denial of Service via incomplete &quot;raw&quot;
argument validation (CVE-2026-47241, GHSA-c4fp-cxrr-mj66)
This results in the affected command hanging until the connection is
closed. If another thread attempts to send a concurrent pipelined
command, the first thread will return with a syntax error and the second
thread will hang until the connection closes.
<ul>
<li><em>Reported by <a
href="https://github.com/fg0x0"><code>@​fg0x0</code></a></em></li>
<li>🐛 Prevent trailing <code>{0}</code> in RawData validation by <a
href="https://github.com/nevans"><code>@​nevans</code></a> in <a
href="https://redirect.github.com/ruby/net-imap/pull/703">ruby/net-imap#703</a>
(backports <a
href="https://redirect.github.com/ruby/net-imap/pull/700">ruby/net-imap#700</a>)</li>
</ul>
</li>
</ul>
<h3>Fixed</h3>
<ul>
<li>🥅 Validate that Atom and Flag are not empty by <a
href="https://github.com/nevans"><code>@​nevans</code></a> in <a
href="https://redirect.github.com/ruby/net-imap/pull/685">ruby/net-imap#685</a>
(backports <a
href="https://redirect.github.com/ruby/net-imap/pull/684">ruby/net-imap#684</a>)</li>
<li>🧵 Fix deadlock in <code>#disconnect</code> by <a
href="https://github.com/nevans"><code>@​nevans</code></a> in <a
href="https://redirect.github.com/ruby/net-imap/pull/697">ruby/net-imap#697</a>
(backports <a
href="https://redirect.github.com/ruby/net-imap/pull/686">ruby/net-imap#686</a>)</li>
</ul>
<h3>Documentation</h3>
<ul>
<li>⚠️ Boost visibility of raw data argument documentation warnings by
<a href="https://github.com/nevans"><code>@​nevans</code></a> in <a
href="https://redirect.github.com/ruby/net-imap/pull/696">ruby/net-imap#696</a>
(backports <a
href="https://redirect.github.com/ruby/net-imap/pull/677">ruby/net-imap#677</a>)</li>
</ul>
<h3>Other Changes</h3>
<ul>
<li>🏷️ Allow 64-bit Integer arguments in <a
href="https://redirect.github.com/ruby/net-imap/pull/696">ruby/net-imap#696</a>
(backports <a
href="https://redirect.github.com/ruby/net-imap/pull/675">ruby/net-imap#675</a>)</li>
<li>🥅 Ensure send_number_data input is an Integer in <a
href="https://redirect.github.com/ruby/net-imap/pull/696">ruby/net-imap#696</a>
(backports <a
href="https://redirect.github.com/ruby/net-imap/pull/676">ruby/net-imap#676</a>)</li>
<li>♻️ Improve <code>RawData.new</code>, Add <code>RawData.split</code>
by <a href="https://github.com/nevans"><code>@​nevans</code></a> in <a
href="https://redirect.github.com/ruby/net-imap/pull/696">ruby/net-imap#696</a>
(backports <a
href="https://redirect.github.com/ruby/net-imap/pull/679">ruby/net-imap#679</a>)</li>
<li>🥅 Validate response literal byte size format by <a
href="https://github.com/nevans"><code>@​nevans</code></a> in <a
href="https://redirect.github.com/ruby/net-imap/pull/696">ruby/net-imap#696</a>
(backports <a
href="https://redirect.github.com/ruby/net-imap/pull/681">ruby/net-imap#681</a>)</li>
</ul>
<h3>Miscellaneous</h3>
<ul>
<li>✅ Improvements to tests' FakeServer in <a
href="https://redirect.github.com/ruby/net-imap/pull/696">ruby/net-imap#696</a>
(backports <a
href="https://redirect.github.com/ruby/net-imap/pull/678">ruby/net-imap#678</a>)</li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/ruby/net-imap/compare/v0.5.14...v0.5.15">https://github.com/ruby/net-imap/compare/v0.5.14...v0.5.15</a></p>
<h2>v0.5.14</h2>
<h2>What's Changed</h2>
<h3>🔒 Security</h3>
<p>This release contains fixes for <strong>multiple
vulnerabilities</strong> concerning <em><strong><code>STARTTLS</code>
stripping</strong></em>, argument validation, and denial of service
attacks.</p>
<blockquote>
<p>[!WARNING]
<a
href="https://redirect.github.com/ruby/net-imap/pull/665">ruby/net-imap#665</a>
fixes a <code>STARTTLS</code> stripping vulnerability
(GHSA-vcgp-9326-pqcp).
Without this fix, a man-in-the-middle attacker can cause
<code>Net::IMAP#starttls</code> to return &quot;successfully&quot;,
<strong><em>without starting TLS</em></strong>.</p>
</blockquote>
<blockquote>
<p>[!IMPORTANT]
Argument validation is significantly improved. Several command injection
vulnerabilities have been fixed:
<a
href="https://redirect.github.com/ruby/net-imap/pull/662">ruby/net-imap#662</a>
fixes CRLF/command/argument injection via Symbol arguments
(GHSA-75xq-5h9v-w6px).
<a
href="https://redirect.github.com/ruby/net-imap/pull/662">ruby/net-imap#662</a>
fixes CRLF/command/argument injection via the <code>attr</code> argument
to <code>#store</code>/<code>#uid_store</code> (GHSA-hm49-wcqc-g2xg)</p>
</blockquote>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/ruby/net-imap/commit/ce20fc8e9974ed87d0848dc411eef490623cdeb9"><code>ce20fc8</code></a>
🔖 Bump version to 0.5.15</li>
<li><a
href="https://github.com/ruby/net-imap/commit/0b7b83c19312dd3f44cb72be2e7681289324d9db"><code>0b7b83c</code></a>
🔀 Merge pull request <a
href="https://redirect.github.com/ruby/net-imap/issues/703">#703</a>
from ruby/backport/v0.5/security-patches</li>
<li><a
href="https://github.com/ruby/net-imap/commit/f22fd6cee01140b53cdb7fc8d42a352a734f4509"><code>f22fd6c</code></a>
🍒 pick 0ea9eba3 (<a
href="https://redirect.github.com/ruby/net-imap/issues/701">#701</a>): ✅
Fix flaky tests for MacOS, TruffleRuby</li>
<li><a
href="https://github.com/ruby/net-imap/commit/12460740451d064cef84f59dd8c91baba0388ae1"><code>1246074</code></a>
🍒 pick ae9f83b5 (<a
href="https://redirect.github.com/ruby/net-imap/issues/701">#701</a>):
♻️ Extract str.bytesize lvar in send_literal</li>
<li><a
href="https://github.com/ruby/net-imap/commit/a2f61af6b7a7e6f0e4f27691531de6e4861318cd"><code>a2f61af</code></a>
🍒 pick 62a0da6d (<a
href="https://redirect.github.com/ruby/net-imap/issues/701">#701</a>): 🥅
Validate non-synchronizing literals support</li>
<li><a
href="https://github.com/ruby/net-imap/commit/e33348ccdc639d0d4414f72e4ba7024bdb24bf29"><code>e33348c</code></a>
🍒 pick d6ddd294 (<a
href="https://redirect.github.com/ruby/net-imap/issues/700">#700</a>): 🐛
Prevent trailing <code>{0}</code> in RawData validation</li>
<li><a
href="https://github.com/ruby/net-imap/commit/4f81b69044297de02ada859fffba6ad9923a43a5"><code>4f81b69</code></a>
🍒 pick 1f97168b (<a
href="https://redirect.github.com/ruby/net-imap/issues/699">#699</a>): 🥅
Validate <code>#enable</code> arguments are all atoms</li>
<li><a
href="https://github.com/ruby/net-imap/commit/69da4a490dd148526fb287a5c82056bfd20c6f95"><code>69da4a4</code></a>
🍒 pick 8d9397ab (<a
href="https://redirect.github.com/ruby/net-imap/issues/698">#698</a>): 🥅
Validate QuotedString contains only valid bytes</li>
<li><a
href="https://github.com/ruby/net-imap/commit/7aab580ad35f2cad1404a58bf0ff015567f2593c"><code>7aab580</code></a>
🍒 pick e3c50fad (<a
href="https://redirect.github.com/ruby/net-imap/issues/698">#698</a>):
♻️ Refactor RawText, add improve test coverage</li>
<li><a
href="https://github.com/ruby/net-imap/commit/fac1733d45ec57da3675c4d266c771c73c7265bb"><code>fac1733</code></a>
🍒 pick aab64f92 (<a
href="https://redirect.github.com/ruby/net-imap/issues/686">#686</a>): 🧵
Fix deadlock in <code>#disconnect</code></li>
<li>Additional commits viewable in <a
href="https://github.com/ruby/net-imap/compare/v0.4.24...v0.5.15">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=net-imap&package-manager=bundler&previous-version=0.4.24&new-version=0.5.15)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/chatwoot/chatwoot/network/alerts).

</details>

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Sony Mathew <sony@chatwoot.com>
Co-authored-by: Sony Mathew <2040199+sony-mathew@users.noreply.github.com>
## Summary

The `GET /api/v1/accounts/:id/integrations/apps` endpoint returns raw
secret values (OpenAI API keys, Google service account private keys,
Linear refresh tokens, etc.) in hook settings within the JSON response.
Although gated behind an administrator check, these secrets are visible
in the browser network tab.

This PR filters hook settings through the existing `visible_properties`
whitelist defined in `config/integration/apps.yml`, and adds explicit
whitelists to integrations that were missing them.

Closes #14042

## Bug reproduction

**Setup:** Created an OpenAI integration hook with a fake API key
(`sk-test-secret-12345`).

**Step 1 — Browser Network tab shows raw secrets:**

<img width="1676" height="869" alt="Screenshot 2026-04-24 at 14 32 28"
src="https://github.com/user-attachments/assets/6fc69463-365e-458b-b216-98a7390bf528"
/>


Navigate to Settings > Integrations as an admin. Open DevTools Network
tab and observe the response from `GET
/api/v1/accounts/:id/integrations/apps`. The hook settings contain the
full API key in plaintext:

```json
"hooks": [
  {
    "id": 1,
    "app_id": "openai",
    "settings": {
      "api_key": "sk-test-secret-12345",
      "label_suggestion": false
    }
  }
]
```

**Step 2 — API call confirms the leak:**

```bash
curl -s "http://localhost:3000/api/v1/accounts/2/integrations/apps" \
  -H "access-token: <token>" \
  -H "client: <client>" \
  -H "uid: user@test.com" \
  | jq '.payload[] | select(.id == "openai") | {id, name, hooks: [.hooks[] | {id, app_id, settings}]}'
```

Response:

```json
{
  "id": "openai",
  "name": "OpenAI",
  "hooks": [
    {
      "id": 1,
      "app_id": "openai",
      "settings": {
        "api_key": "sk-test-secret-12345",
        "label_suggestion": false
      }
    }
  ]
}
```

Any admin user can extract the raw API key from the response. The same
applies to other integrations — Dialogflow exposes full Google service
account credentials, Linear exposes refresh tokens, etc.

## After fix

After applying this change, the GET
/api/v1/accounts/:id/integrations/apps endpoint no longer returns
sensitive secret values in hook settings. Instead, the response is
filtered using each integration’s visible_properties whitelist, ensuring
only safe, user-facing fields are exposed. For example, OpenAI
integrations return non-sensitive fields like label_suggestion while
excluding raw API keys. This prevents secrets from being exposed in the
browser network tab or API responses, even for authenticated admin
users.

```bash
curl -X GET "http://localhost:3000/api/v1/accounts/2/integrations/apps" \
  -H "Accept: application/json" \
  -H "Authorization: Bearer eyJhY2Nlc3MtdG9rZW4iOiJqZG5jOWg4TnljaWFJa3JlZkxGQzRnIiwidG9rZW4tdHlwZSI6IkJlYXJlciIsImNsaWVudCI6Ik81bjVnTDFEOVVhbGpwbWxjaHZNanciLCJleHBpcnkiOiIxNzgyMzMyNTA4IiwidWlkIjoidXNlckB0ZXN0LmNvbSJ9" \
  -H "access-token: jdnc9h8NyciaIkrefLFC4g" \
  -H "client: O5n5gL1D9UaljpmlchvMjw" \
  -H "uid: user@test.com" \
  | jq '.payload[] | select(.id == "openai") | {id, name, hooks: [.hooks[] | {id, app_id, settings}]}'
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100 10174  100 10174    0     0  33232      0 --:--:-- --:--:-- --:--:-- 33357
{
  "id": "openai",
  "name": "OpenAI",
  "hooks": [
    {
      "id": 1,
      "app_id": "openai",
      "settings": {
        "label_suggestion": false
      }
    }
  ]
}
```

## What changed

- Added `visible_properties` accessor to `Integrations::App` model to
expose the whitelist from config
- Updated `_hook.json.jbuilder` to filter `resource.settings` through
the associated app's `visible_properties` instead of returning the full
hash
- Added `visible_properties` to integrations that were missing it:
  - Linear: `[]` (settings contain refresh_token)
  - Notion: `[]` (OAuth-based, no user-facing settings)
- Slack: `['channel_name']` (UI needs this to display connected channel)
  - Shopify: `[]` (no settings)

App config metadata (`_app.json.jbuilder`) is left unchanged —
hook_type, settings_form_schema, etc. are not secrets and the frontend
depends on them.

## How to test

1. Create an OpenAI integration hook with an API key
2. As an admin, call `GET /api/v1/accounts/:id/integrations/apps`
3. Verify hook settings include `api_key` (whitelisted) but not raw
credential objects
4. For Dialogflow hooks, verify `credentials` (private key JSON) is
excluded while `project_id` is included
5. For Slack hooks, verify `channel_name` is still returned
6. For Linear hooks, verify `refresh_token` is not returned

---------

Co-authored-by: Botshelo Nokoane (Konstruktors) <botshelo@entersekt.com>
Co-authored-by: Sony Mathew <sony@chatwoot.com>
Co-authored-by: Sony Mathew <2040199+sony-mathew@users.noreply.github.com>
…s and labels) - CW-7059 (#14509)

## Description

Added ability to collapse conversation sidebar sections (folders, teams,
inboxes and labels)

Fixes #CW-7059

## Type of change

Please delete options that are not relevant.

- [ ] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality not to work as expected)
- [ ] This change requires a documentation update

## How Has This Been Tested?

Tested locally. Added specs. Attaching the loom for them same.


https://github.com/user-attachments/assets/40d613e7-6c82-4078-abf4-79739a00f718


## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules

---------

Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Co-authored-by: iamsivin <iamsivin@gmail.com>
…ct (#14383)

## Description

This PR fixes a data-corruption bug in the new conversation flow that
surfaces in conversation search results as `Subject: undefined`.

### The Problem
When creating a conversation through the "New conversation" modal
without typing a subject (every non-email channel never shows the
subject input, and email channels can be left blank), the conversation
gets persisted with `additional_attributes.mail_subject = "undefined"`
(the literal string). The bogus value shows up in conversation search
results as `Subject: undefined`, which started rendering after #10843.

### Root Cause
`createConversationPayload` in
`app/javascript/dashboard/store/modules/contactConversations.js` appends
the `mail_subject` field unconditionally:

```js
payload.append('additional_attributes[mail_subject]', mailSubject);
```

`composeConversationHelper.js` only sets `payload.mailSubject` when
`subject` is truthy, so when no subject is provided, `mailSubject` is
destructured as `undefined` in `createConversationPayload`.
`FormData.append` coerces `undefined` to the string `"undefined"`, and
the backend persists it as-is into the JSONB column.

### The Fix
Skip the append when `mailSubject` is falsy. The backend already treats
a missing key the same as an empty subject (the email mailer falls back
to a default subject when `mail_subject` is `nil`), so omitting the
field is safe across channels.

### Key Changes
- Guarded the `additional_attributes[mail_subject]` append in
`createConversationPayload`.
- Added a spec covering the case where `mailSubject` is omitted.

> Note: existing rows persisted before this fix will continue to show
`Subject: undefined` in search until cleaned up. A simple SQL cleanup
for non-email inboxes:
>
> ```sql
> UPDATE conversations
> SET additional_attributes = additional_attributes - 'mail_subject'
> FROM inboxes
> WHERE conversations.inbox_id = inboxes.id
>   AND inboxes.channel_type <> 'Channel::Email'
> AND conversations.additional_attributes->>'mail_subject' =
'undefined';
> ```
>
> I'm leaving any data-cleanup migration out of this PR since it's a
maintenance concern that may be handled differently per installation.

## Type of change

- [X] Bug fix (non-breaking change which fixes an issue)

## How Has This Been Tested?

1. Open the "New conversation" modal on a non-email inbox (e.g.,
WhatsApp, SMS, API).
2. Create a conversation without filling any subject (field is not
present on those, so regular flow).
3. Open the global search and search for the conversation.
4. **Before:** the result card shows `Subject: undefined`.
   **After:** the subject row is hidden.
5. Repeat on an email inbox leaving the subject blank — same result.
6. On an email inbox, type a subject and verify it still persists and
renders correctly.
7. Run the new spec: `pnpm test contactConversations`.

## Checklist:

- [X] My code follows the style guidelines of this project
- [X] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [X] My changes generate no new warnings
- [X] I have added tests that prove my fix is effective or that my
feature works
- [X] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules

Co-authored-by: Sojan Jose <sojan@pepalo.com>
Co-authored-by: Sony Mathew <sony@chatwoot.com>
# Pull Request Template

## Description

Prevent deleted email conversations from being synced into Chatwoot
again while they are still within the IMAP sync window.

When an admin explicitly deletes an email conversation, the incoming
email message IDs are stored temporarily in Redis. IMAP sync checks
these recently deleted message IDs in addition to existing message
records. Each Redis key expires automatically after two days.

This applies only to explicit conversation deletion. Individual message
deletion, inbox deletion, and account deletion keep their existing
behavior.

Fixes
[CW-7214](https://linear.app/chatwoot/issue/CW-7214/deleted-mails-in-gmail-inbox-gets-synced-again)
…n (CW-7169) (#14622)

## Description

SuperAdmin impersonation SSO logins no longer create UserSession rows
visible to the customer. Impersonation tokens use a 2-day lifespan
instead of ~2 months, so they naturally evict first and don't linger in
the user's token list.

Server-side detection via Redis value (`'impersonation'` vs `'normal'`)
without changing the `valid_sso_auth_token?` signature. Backward
compatible with in-flight tokens.

Depends on #14556.

## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)

## How Has This Been Tested?

Specs cover: impersonation login skips UserSession creation,
impersonation token has short lifespan, normal SSO login still creates
UserSession row.

## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [x] Any dependent changes have been merged and published in downstream
modules
…14621)

## Description

Cap active sessions at `MAX_USER_SESSIONS` which defaults to existing
value of `25` per user. This ensure existing user login behavior is not
affected for self-hosted installations. Browser users at the cap see a
session picker (409 response) to choose which session to end.
Non-browser clients and partially-tracked users get silent
oldest-session eviction.

Depends on #14556.

## Type of change

- [x] New feature (non-breaking change which adds functionality)

## How Has This Been Tested?

Specs cover: under limit, at limit (browser picker, non-browser
eviction), partial tracking fallback, revoke single/all sessions during
login, session row creation on successful login.

---------

Co-authored-by: Sony Mathew <sony@chatwoot.com>
…14512)

## Description

Added a new option for the sort by option in conversation filters called
unread. This is to filter out unread conversations.
Fixes # CW-7152

## Type of change

Please delete options that are not relevant.

- [ ] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality not to work as expected)
- [ ] This change requires a documentation update

## How Has This Been Tested?

Tested manually in local.

<img width="1397" height="603" alt="Screenshot 2026-05-20 at 10 40
20 PM"
src="https://github.com/user-attachments/assets/6c60263e-907b-419f-a7ed-06010bbe8736"
/>


## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
…hannels and labels (CW-7193) (#14609)

## Description

* Added the ability to sort for 4 sub-sections under conversations
folders, teams, channels and labels.
* the sort options are basically created at, alphabetical and unread
counts along with both directions.
* for folders we don't have an unread count, so we sort it by only
created and alphabetical.
* all the sort preferences are stored on the frontend - easiest
implementation for now.

Fixes # CW-7193

## Type of change

Please delete options that are not relevant.

- [ ] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality not to work as expected)
- [ ] This change requires a documentation update

## How Has This Been Tested?

Tested this locally by visually verifying the changes. Also ran the
newly added tests for component level changes.

Here is the screenshot of the changes:

Added the sort option to sub sections in the conversation sidebar:
<img width="282" height="848" alt="Screenshot 2026-06-02 at 1 58 12 AM"
src="https://github.com/user-attachments/assets/4a7c6061-86e3-438a-92ae-ee643a0128b6"
/>

The sort options looks like this:
<img width="783" height="698" alt="Screenshot 2026-06-02 at 1 58 49 AM"
src="https://github.com/user-attachments/assets/a15bb0a7-b810-4423-a88c-fbd84d0476c0"
/>


## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules

---------

Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Co-authored-by: iamsivin <iamsivin@gmail.com>
Locales are stored with underscores (e.g. pt_BR), but the HTML lang
attribute requires hyphens (pt-BR). Convert via a helper in the public
portal layouts.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Adds support for capturing Click-to-WhatsApp ad referral metadata from
incoming WhatsApp messages.

This stores Meta Cloud API `referral` payloads on the incoming message
`content_attributes` and normalizes Twilio `Referral*` callback fields
into the same shape. The UI display is intentionally deferred until
Instagram, Messenger, and TikTok referral payloads are captured as well,
so we can design one cross-channel referral surface instead of a
WhatsApp-only sidebar block.

Why message-level storage

Meta sends CTWA referral details as part of the inbound message payload,
not as a stable conversation-level webhook. The referral represents the
exact ad click that produced that specific customer message, and later
messages in the same conversation may not carry the same context.
Storing the normalized referral on the message preserves the original
webhook semantics, avoids adding conversation-level attribution that
could become stale or ambiguous, and keeps support for both Cloud API
and Twilio payloads aligned behind `content_attributes.referral`.
Fixes
https://linear.app/chatwoot/issue/CW-7090/surface-meta-ctwa-referral-on-incoming-whatsapp-messages-cloud-api
Closes #13995,
#12560,
#13006


Related community PRs
- #13130
- #14180
- #14121

Related follow-ups
-
https://linear.app/chatwoot/issue/CW-7301/capture-instagram-ad-referral-metadata-on-incoming-messages
-
https://linear.app/chatwoot/issue/CW-7302/capture-messenger-ad-referral-metadata-on-facebook-page-conversations
-
https://linear.app/chatwoot/issue/CW-7303/capture-tiktok-ad-referral-metadata-from-im-referral-msg-events

How to test
1. Send or replay a WhatsApp Cloud API inbound message that contains a
`messages[0].referral` payload from a Click-to-WhatsApp ad.
2. Confirm the generated incoming message stores the payload under
`content_attributes.referral`.
3. Repeat with a Twilio WhatsApp callback containing `Referral*` fields
and confirm the stored message has the same normalized
`content_attributes.referral` structure.

---------

Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com>
Adds a company selector to the contact details form so agents can
associate a contact with an existing company directly from the contact
page.

Closes

- None

Why

Contacts already expose company information through the CRM fields, but
the form only accepted free-text company names. As we split company CRM
work into smaller PRs, this keeps the contact page aligned with the
structured company model while preserving the existing company-name
behavior used by automations.

What changed

- Shows a company dropdown in the contact details form when the
Companies feature is enabled.
- Keeps legacy free-text company names editable when a contact has no
structured `company_id`.
- Allows Enterprise contact create/update APIs to accept account-scoped
`company_id`.
- Syncs `additional_attributes.company_name` when a contact is
associated with a company, including the existing email-domain
auto-association path.
- Serializes `company_id` in the contact model payload so the form can
show the current association.

How to test

1. Enable Companies for an account and open a contact details page.
2. In Edit contact details, use the Company field to select an existing
company.
3. Save the contact and refresh the page.
4. Confirm the selected company remains visible and the contact is
associated with that company.
5. Confirm contacts with only a legacy free-text company name still show
the text input instead of an empty selector.

---------

Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Co-authored-by: iamsivin <iamsivin@gmail.com>
Co-authored-by: Sony Mathew <2040199+sony-mathew@users.noreply.github.com>
Paid plan accounts with many agents were hitting the flat daily email
rate limit cap.
This multiplies the plan base limit by the account's agent seat count,
so larger teams
get proportionally higher limits. Free/hacker plan keeps the flat limit
unchanged.

Fixes https://linear.app/chatwoot/issue/CW-6664

## How to test
1. Set up `ACCOUNT_EMAILS_PLAN_LIMITS` config with plan limits (e.g.,
`{"hacker": 10, "startups": 20, "business": 30, "enterprise": 40}`)
2. Create an account on a paid plan (e.g., startups) with multiple agent
seats
3. Verify `account.email_rate_limit` returns `base_limit × agent_seats`
(e.g., 20 × 5 = 100)
4. Create an account on the hacker plan — verify limit stays flat (10)
5. Set a per-account override via super admin `limits.emails` — verify
it takes priority over the multiplied limit

## What changed
- `plan_email_limit` now multiplies the base plan limit by agent seat
count for paid plans
- Added `free_plan?` helper to skip the multiplier for the default/free
plan
# Pull Request Template

## Description

Storage migration functionality, which allows the transfer of images
from one on-premises provider to another (e.g. AWS, Google, etc.) using
Active Storage.
I ran the unit and integration tests and they all passed correctly.

**The command to execute the migration is `FROM=from_service
TO=to_service rake storage:migrate`**

Fixes #7907

## Type of change

Please delete options that are not relevant.

- [x] New feature (non-breaking change which adds functionality)

## How Has This Been Tested?

I have tested the storage migration functionality by running the
provided unit and integration tests. Additionally, I manually tested the
migration process in a local development environment by following these
steps:

Set up two storage services (e.g., AWS S3 and Google Cloud Storage) with
valid configurations.
Execute the `FROM=local TO=amazon rake storage:migrate` task with
appropriate FROM and TO arguments to migrate blobs between the services.
Verified that the blobs were successfully transferred to the target
storage service.
Checked for any error messages or unexpected behavior during the
migration process.


## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have commented on my code, particularly in hard-to-understand
areas
- [x] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [x] Any dependent changes have been merged and published in downstream
modules

---------

Co-authored-by: Sony Mathew <sony@chatwoot.com>
Co-authored-by: Sony Mathew <2040199+sony-mathew@users.noreply.github.com>
…14750)

## Description

Removes `chatwoot_internal: true` from the `advanced_search` feature
flag so self-hosted enterprise userss can toggle it from the super_admin
UI instead of going through a rails console.
# Pull Request Template

## Description

This PR introduces dedicated color tokens for the floating voice call
widget instead of reusing generic design tokens. This allows the widget
to maintain a distinct dark-card appearance across both light and dark
themes.

Adds the following tokens and applies them to `CallCard`:

* `--call-widget`
* `--call-widget-border`
* `--call-widget-text`
* `--call-widget-sub-text`

Fixes
https://linear.app/chatwoot/issue/CW-7353/call-notification-window-theme-update-design

## Type of change

- [x] New feature (non-breaking change which adds functionality)

## How Has This Been Tested?

### Screenshots

**Light mode**
<img width="1466" height="815" alt="Screenshot 2026-06-16 at 3 47 32 PM"
src="https://github.com/user-attachments/assets/732164b1-5488-4cc1-8c71-01245d906842"
/>


**Dark mode**
<img width="1466" height="815" alt="Screenshot 2026-06-16 at 3 46 54 PM"
src="https://github.com/user-attachments/assets/536ab632-4c7c-486b-8fc4-a061ee40b22a"
/>



## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
## Description

Extends the conversation unread-count system so the left sidebar can
show unread badges for Mentions, Participating, Unattended, and saved
conversation folders. Folder badges reuse the existing `custom_filters`
conversation filter semantics, store user-scoped Redis sets lazily, and
skip unsupported folder filters so invalid saved folders continue to
render without a badge. The Unattended badge counts all visible unread
open conversations that match the existing unattended conversation
scope.

Closes
-
[CW-7262](https://linear.app/chatwoot/issue/CW-7262/unread-counts-for-filters-folders)

## What changed

- Added user-scoped unread-count Redis keys and cache builders for
mentions, participating conversations, unattended conversations, and
saved folder filters.
- Reused `Conversations::FilterService` through a relation-returning
path so folder counts match the folder conversation list behavior.
- Invalidated user filter caches from mention, participant,
custom-filter, and relevant conversation update events.
- Extended the unread-count endpoint payload and sidebar Vuex/sidebar
rendering for the new badge counts, including the Unattended sidebar
item.
- Added Ruby, Enterprise, request, listener, and frontend store coverage
for the new unread-count dimensions.

## Type of change

- [x] New feature (non-breaking change which adds functionality)

## How Has This Been Tested?

- Created local validation folders for `john@acme.inc` and confirmed the
unread-count payload includes open, resolved, and high-priority folder
badges while excluding the invalid unsupported folder.
- Added coverage for the Unattended badge rule: all visible unread open
conversations matching `Conversation.unattended`.
- Ran focused unread-count Ruby specs, including service, listener,
request, and Enterprise counter coverage.
- Ran frontend unread-count store specs.
- Ran RuboCop on the touched Ruby files.
- Ran ESLint through the project script; it completed with warnings in
existing unrelated files and no errors.

<img width="369" height="525" alt="Screenshot 2026-06-13 at 10 51 39 PM"
src="https://github.com/user-attachments/assets/36b1d2c4-dac1-4f6f-9c0e-7ef5a6cc2975"
/>

## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have commented on my code, particularly in hard-to-understand
areas
- [x] Documentation changes are not required for this internal
unread-count behavior
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [x] No dependent downstream changes are required

---------

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
## Description

Sessions created by logins from the Chatwoot Mobile app currently render
as "Unknown Device" in the dashboard sessions UI. The mobile app's HTTP
layer sends:

- Android: `User-Agent: okhttp/4.9.2`
- iOS: `User-Agent: Chatwoot/<build> CFNetwork/<v> Darwin/<v>`

Neither pattern is classifiable by the `browser` gem, so
\`browser_name\`, \`platform_name\`, and \`device_name\` all end up
"Unknown".

This change adds a backend-only fallback in
\`UserSessionTrackingService\`. When \`Browser.new(ua)\` returns
"Unknown Browser" and the UA matches a known Chatwoot Mobile pattern,
the labels are overridden to \`Chatwoot Mobile\` + \`Android\` /
\`iPhone\`. The Vue sessions list (\`ActiveSessions.vue\`) then renders
"Chatwoot Mobile on Android" or "Chatwoot Mobile on iPhone" with the
smartphone icon.

This is the immediate floor. A follow-up will add structured
\`X-Chatwoot-*\` headers from the mobile app so we can render full
version + device model.

Fixes INF-75.

## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)

## How Has This Been Tested?

- Added specs

## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [x] Any dependent changes have been merged and published in downstream
modules
## Description

Reverts [#14726](#14726)
(\"feat: Add sidebar unread counts for filters (CW-7262)\"), which
shipped in 4.15.0.

After 4.15.0 rolled out to prod the unread-counts-for-filters code path
caused a cascading incident:

- `Counter#ensure_filters_cache!` fires on every `/unread_counts/index`
and `update_last_seen` request.
- On cache miss it calls `Builder#build_filters_for!`, which:
- invokes `store.clear_user_filters!` -> `delete_matching` -> a Redis
`SCAN_each` over a per-user pattern keyspace, and
- runs 4 fresh SQL passes per user (mentions, participating, unattended,
and per-folder `Conversations::FilterService` queries).
- Threads blocked in the SCAN held their DB connections, the connection
pool exhausted, Sidekiq jobs were discarded with
`ActiveJob::DeserializationError: could not obtain a connection from the
pool`, and the enqueued queue blew past 200K.


Related:
[CW-7262](https://linear.app/chatwoot/issue/CW-7262/unread-counts-for-filters-folders)

## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality not to work as expected)
- [ ] This change requires a documentation update

## How Has This Been Tested?


## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
# Pull Request Template

## Description

This PR fixes a few issues in Global Search and removes a non-functional
control.

* Fixes an issue in Firefox where characters could be dropped while
typing in the search input. The search now uses the latest input value
directly, preventing searches from running one character behind.

* Removes a stale query sync in `SearchHeader` that could overwrite
recently typed characters during the debounce window, causing the input
to appear out of sync.

* Fixes advanced search filters being removed from the URL on page
reload. The search page now waits for account data to load before
parsing URL parameters, ensuring agent, inbox, and date range filters
are preserved.

* Removes the non-functional "Sort by relevance" button from the search
tabs bar, as it was disabled and had no effect.


Fixes #14684
[CW-7305](https://linear.app/chatwoot/issue/CW-7305/global-search-drops-characters-while-typing-in-firefox-query-truncated)
[CW-7370](https://linear.app/chatwoot/issue/CW-7370/remove-non-functional-relevance-placeholder-button-from-global-search)

## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)

## How Has This Been Tested?

### Screencast

**Before**


https://github.com/user-attachments/assets/48d72a4e-20f4-4f24-91f4-2c9a9c065eba




**After**


https://github.com/user-attachments/assets/0e41a803-b4fd-410d-a739-549f72d418d6





## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
muhsin-k and others added 30 commits July 28, 2026 15:28
WhatsApp coexistence replies can carry a BSUID-scoped `context.id` even
when Chatwoot stored the original message with a phone-scoped WAMID.
Although both identifiers refer to the same message, their complete
values differ, so incoming replies retained the external reference but
did not populate the internal `in_reply_to` relationship. Agents
consequently saw the reply text without the quoted-message preview.

This resolves the original message within the selected conversation and
stores the internal reply relationship. Exact WAMID matches remain the
primary path; scoped identifiers fall back to the unique decoded message
token.

Fixed
https://linear.app/chatwoot/issue/CW-7663/whatsapp-quoted-replies-are-not-linked-across-scoped-wamids
and #14953

## How to reproduce

1. Use a WhatsApp Cloud inbox with coexistence enabled.
2. Send a message whose source ID is stored using the phone-scoped
WAMID.
3. Reply to it from WhatsApp when the webhook carries a BSUID-scoped
`context.id` for the same message.
4. Before this change, the incoming message appears without its
quoted-message preview.
5. After this change, the reply references and displays the original
message.

## What changed

- Resolve incoming reply context IDs against messages in the selected
conversation.
- Keep exact source-ID matching as the first lookup path.
- Decode scoped WAMIDs and match only a unique 20- or 32-character
message token.
- Populate `content_attributes.in_reply_to` while preserving
`in_reply_to_external_id`.
- Leave malformed, unmatched, or ambiguous identifiers unlinked.

## How to test

1. Open a WhatsApp Cloud conversation and send a message to the contact.
2. Reply to that message from WhatsApp through a coexistence identity.
3. Confirm the incoming message displays the original message as a
quoted preview.
4. Confirm ordinary exact-ID replies continue to resolve.
5. Confirm an unknown or malformed context ID does not attach to another
message.

## Things to know

Meta documents `context.id` as the replied-to message identifier, but
does not document the internal WAMID encoding or the phone-versus-BSUID
scope transformation. The fallback is therefore limited to the selected
conversation and succeeds only when one stored message has the decoded
token.

---------

Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com>
## Description

The help center article endpoints permit `author_id` and render the
author through the agent serializer (`_agent.json.jbuilder`), which
exposes `email` plus name, role, and availability. `author_id` was never
scoped to the current account, so a forged or stale author disclosed the
profile of a user in another account. This is the article path of the
same serializer disclosure class as the conversation participants fix.

The fix has two parts:

- **Serializer guard (the disclosure fix).** The article partials now
render the author only when they are a member of the current account
(`article.author&.account`). This closes the leak on every read and for
every row, including articles that already carry a forged or stale
out-of-account author, and mirrors how the conversation assignee is
already handled.
- **Create-only validation.** `author_id` is checked against the
account's users on create, so a new article cannot be forged with an
out-of-account author. Update needs no guard: a non-member author is
simply never rendered, so editing an article whose author has left the
account continues to work.

Fixes https://linear.app/chatwoot/issue/CW-7665

Related: #15180
(https://linear.app/chatwoot/issue/CW-7746).
An API-level issue on Meta’s side is preventing Chatwoot Cloud customers
from completing some Meta inbox setup flows and can affect actions in
existing Instagram conversations. This draft temporarily gates the
affected Facebook, Instagram, and WhatsApp flows behind one incident
flag and gives users clear guidance with a link to the current status
incident.

Self-hosted installations remain unchanged. While WhatsApp Embedded
Signup is unavailable, selecting WhatsApp Cloud takes eligible users
directly to manual setup and explains the current limitations.

Related: https://status.chatwoot.com/incident/976975

Fixes
https://linear.app/chatwoot/issue/CW-7771/guide-users-through-meta-api-incident

### Things to know

- `IS_META_INBOX_CREATION_DISABLED` is the single temporary rollout flag
and is currently enabled.
- The incident behavior applies only to Chatwoot Cloud.
- Facebook, Instagram, and WhatsApp Embedded Signup are removed from the
new onboarding suggestions while the incident is active.
- Existing Instagram inbox settings and conversations show incident
guidance with a status link.
- The WhatsApp Cloud provider routes to manual setup while the flag is
enabled.
- Manual setup works only for numbers already connected to WhatsApp
Cloud API; numbers using WhatsApp Business app coexistence are not
supported by this flow yet.
- The flag should be disabled or removed after the incident is resolved.

### How to test

1. Run Chatwoot with the Cloud installation configuration and open the
new inbox flow.
2. Confirm Facebook and Instagram authentication actions are disabled
and display the amber incident banner.
3. Open the WhatsApp provider selector and confirm WhatsApp Cloud is
described as manual setup with Cloud API credentials.
4. Select WhatsApp Cloud and confirm it opens the manual setup form with
the incident warning and links to the current status incident.
5. Open WhatsApp Embedded Signup directly and confirm the signup action
is disabled while the eligible manual setup option remains available.
6. Open the initial account onboarding flow and confirm Meta channels
are not offered while other configured channels remain available.
7. Open an existing Instagram inbox and conversation and confirm the
incident guidance links to the current status incident.
8. Run the same flows as a self-hosted installation and confirm they
remain available.

---------

Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com>
)

## Description

The dashboard "update to vX.Y.Z is available" banner is a self-hosted
upgrade nudge, but nothing gates it to self-hosted installs. On a
managed cloud instance the banner can still appear:
`latest_chatwoot_version` is populated in Redis from the hub's
advertised latest stable release, and the frontend compares it
(`semver.lt`) against the per-request `appVersion` baked into the
dashboard HTML. When a browser session is holding HTML rendered before
the current deploy, `appVersion` lags the hub's advertised version and
the banner fires, even though the managed instance is already on the
newest build.

This gates the value at the source: `api/v1/accounts#show` no longer
sets `latest_chatwoot_version` when running on cloud, so
`hasAnUpdateAvailable` short-circuits (`semver.valid(null)` is false)
and the banner never renders there. Self-hosted behaviour is unchanged.

Fixes https://linear.app/chatwoot/issue/CW-7763
Chatwoot Cloud customers can now provide a dedicated WhatsApp business
management token when their Embedded Signup credential cannot access
message templates. Once validated, the token is stored securely and used
only for template synchronization.

Existing inboxes continue using their configured WhatsApp API key when
no business management token is present. Sending, receiving, webhooks,
phone-number health, and other WhatsApp operations remain unchanged.

### Things to know

- This option is available only on Chatwoot Cloud.
- Saving the token verifies that `whatsapp_business_management` is
granted through Meta's permissions endpoint; template synchronization
still verifies access to the configured WhatsApp Business Account.
- The token is encrypted using the existing external-credentials
encryption mechanism.
- Self-hosted installations continue using the existing API key flow.

### How to test

1. On Chatwoot Cloud, open a WhatsApp Cloud inbox and go to
**Configuration**.
2. Enter a token with `whatsapp_business_management` access and save it.
3. Confirm the token is accepted and the value is not exposed again in
the UI or API.
4. Select **Sync Templates** and confirm templates are fetched with the
saved business management token.
5. Remove the token and confirm template synchronization falls back to
the inbox API key.
6. Confirm the business management token controls are not shown on a
self-hosted installation.

### What changed

- Added an encrypted `business_management_token` credential to WhatsApp
channels.
- Added Cloud-only endpoints and UI controls to validate the required
permission, save, and remove the token.
- Added template-sync credential selection with API-key fallback.

---------

Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com>
…switches + saner defaults (#14376)

## Description

Consolidates all widget API throttles under the single
`ENABLE_RACK_ATTACK_WIDGET_API` flag, each with its own independent kill
switch and configurable limit, with defaults tuned from real prod
traffic.

Fixes https://linear.app/chatwoot/issue/INF-83
Related to https://linear.app/chatwoot/issue/INF-77

Parent flag stays default-true; installs that disabled it keep today's
behavior (no widget throttling). Each endpoint adds
`ENABLE_RACK_ATTACK_WIDGET_<X>` + `RATE_LIMIT_WIDGET_<X>`.

- **conversations create**: keyed on (IP, website_token), 30/min (was
per-IP 6/12h)
- **messages create**: new throttle, (IP, website_token), 60/min (was
unthrottled)
- **contact update**: fixes a dormant bug. `resource :contact` gives the
singular URL `/api/v1/widget/contact`, but the throttle checked plural
`/api/v1/widget/contacts` so it never fired. Now active (60/1h per IP).
- **widget load**: 5 to 200/1h, tuned from prod (real per-IP loads top
out ~50/hr; the higher tail is crawlers/scrapers)
- **transcript**: 5/1h retained

Token precedence: the (IP, website_token) throttles read `website_token`
via ActionDispatch (query wins), matching the controller, so a
body-supplied token cannot fork the throttle bucket.

## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)

## How Has This Been Tested?

`ruby -c` and rubocop clean. No spec added, matching this file's
existing no-spec convention for throttles.

---------

Co-authored-by: Sony Mathew <2040199+sony-mathew@users.noreply.github.com>
# Pull Request Template

## Description

Adds API-key authorization support for Searchkick/OpenSearch so Elastic
Cloud deployments can configure advanced search with an Elastic API key
instead of embedding basic auth in the URL.

The initializer now accepts `OPENSEARCH_API_KEY` or
`ELASTICSEARCH_API_KEY` and forwards it as an `Authorization: ApiKey
...` header. `.env.example` also documents the
OpenSearch/Elasticsearch-compatible search variables.

Refs
https://linear.app/chatwoot/issue/CW-7511/populate-test-data-set-and-run-experiments

## Type of change

- [ ] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality not to work as expected)
- [x] This change requires a documentation update

## How Has This Been Tested?

- `bundle exec ruby -c config/initializers/searchkick.rb`
- `bundle exec ruby -c spec/config/searchkick_spec.rb`
- `bundle exec rspec spec/config/searchkick_spec.rb`
- `bundle exec rubocop config/initializers/searchkick.rb
spec/config/searchkick_spec.rb`
- `git diff --check`

## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have commented on my code, particularly in hard-to-understand
areas
- [x] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
## Description

Backend CI no longer flakes when the conversation FAQ grouping-lock spec
runs after Rails has reloaded application constants.

The job already raises the intended lock-acquisition error and prevents
concurrent suggestion generation. This updates the assertion to compare
the error class name, preserving that behavior without depending on a
reload-sensitive Ruby `Class` object.

The failure was reproduced in [CircleCI backend job
171299](https://app.circleci.com/pipelines/github/chatwoot/chatwoot/116560/workflows/efcee2f1-5af8-4121-82c3-920002dc420b/jobs/171299).

## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)

## How Has This Been Tested?

The isolated job spec passes, the exact 18-way CircleCI shard selection
passes all 442 examples, and the changed spec passes Ruby lint.

## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] My changes generate no new warnings
- [x] New and existing unit tests pass locally with my changes
## Description

Article edits now ignore an `author_id` that does not belong to the
current account, retain the existing author, and still apply other valid
article changes. Article creation continues to reject cross-account
authors with a generic validation error and without creating a record.

The authenticated article serializers still omit authors without a
current-account membership so existing forged or stale records cannot
expose agent profile fields. The guard now checks `current_account_user`
directly to make that intent explicit.

## Closes

Follow-up to [CW-7665](https://linear.app/chatwoot/issue/CW-7665) and
[#15191](#15191).

## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)

## How Has This Been Tested?

- Creating an article with a cross-account author returns `422` and does
not create an article.
- Updating an article with a cross-account author retains the previous
author while applying other valid attributes.
- Articles whose previous author is no longer an account member can
still be edited without exposing that author's agent profile.
- Existing OSS and Enterprise article request coverage passes locally.

## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [x] Any dependent changes have been merged and published in downstream
modules

Co-authored-by: Vishnu Narayanan <iamwishnu@gmail.com>
Captain now keeps v2 replies within each conversation channel's delivery
limit, preventing generated responses from being rejected by providers
such as Instagram, Facebook, and WhatsApp.

## Closes

-
https://linear.app/chatwoot/issue/AI-188/captain-should-respect-whatsapp-character-limits

## How to reproduce

1. Enable Captain v2 on an Instagram inbox.
2. Ask a question that produces a response longer than 1,000 characters.
3. Observe that Meta rejects the outgoing message with error 100 because
it exceeds Instagram's character limit.

## What changed

- Resolve the outbound character limit from the conversation channel,
including provider-specific Twilio limits.
- Add the resolved limit to the assistant and scenario prompts.
- Apply the same limit to the v2 structured response schema so the model
output conforms before delivery.

---------

Co-authored-by: Sony Mathew <sony@chatwoot.com>
## Description

Resolved conversations now preserve historical SLA misses without
allowing their displayed duration to keep growing. Applied SLAs record a
stable completion timestamp that is shared through REST and realtime
payloads, and the dashboard freezes FRT, NRT, and RT misses at that
point.

Legacy completed SLAs without a reliable timestamp remain visible as a
static missed state. Terminal SLAs remain frozen when a conversation is
reopened; a reopen before finalization continues the same SLA without
resetting its deadlines.

### Closes


[CW-7597](https://linear.app/chatwoot/issue/CW-7597/freeze-sla-miss-durations-after-conversation-resolution)

## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)

## How to reproduce

1. Apply an SLA with a resolution-time threshold to a conversation.
2. Let the threshold breach, then resolve the conversation.
3. Observe that the recorded miss duration continues increasing every
minute even though the conversation is resolved.

## What changed

- Added nullable `applied_slas.completed_at` and exposed it as
`sla_completed_at` in conversation, report, and websocket payloads.
- Captured completion before broadcasting resolution and preserved it
for terminal applied SLAs.
- Frozen recorded FRT, NRT, and RT durations in classic and
next-generation conversation labels, including a static fallback for
legacy rows.
- Added a dry-run-first, resumable Rails runner for account-scoped or
explicitly global historical repair without enqueuing jobs or touching
`updated_at`.

Account-scoped production rollout starts with:

```sh
ACCOUNT_ID=168154 bundle exec rails runner script/backfill_applied_sla_completed_at.rb
ACCOUNT_ID=168154 APPLY=true bundle exec rails runner script/backfill_applied_sla_completed_at.rb
```

## How Has This Been Tested?

- Verified resolution stamping, nonterminal reopen clearing, and
terminal reopen preservation.
- Verified dry-run, apply, account/global scope, resume, skip,
idempotency, and timestamp-preserving backfill behavior.
- Verified all three miss types freeze and existing conversation-card
behavior remains intact.
- 71 focused RSpec examples and 37 focused Vitest examples pass.
- RuboCop, ESLint, and diff checks pass.

## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules

---------

Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
WhatsApp templates with variables in both a text header and body
currently show inputs only for the body. Agents therefore cannot provide
the header value in the template composer, even though the API and
backend support the corresponding `processed_params.header` payload.

The composer now displays text-header variables separately, previews
their substituted values, and sends them alongside body parameters.
Templates using media headers remain unchanged.

Related: chatwoot/utils#65

### Things to know

This PR consumes the released `@chatwoot/utils@0.0.57`, which adds
text-header parameter construction and completeness validation.

### How to reproduce

1. Open a WhatsApp template containing text header `Welcome {{1}}` and
body variables `{{1}}` and `{{2}}`.
2. Observe that the current composer displays only two body inputs and
omits the header input.

### How to test

1. Open the same template in the conversation composer.
2. Confirm one header input and two body inputs are displayed.
3. Fill the values and confirm both the header and body previews update.
4. Send the template and confirm `processed_params` contains `header.1`,
`body.1`, and `body.2`.

---------

Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com>
This is phase one of a rolling deployment for Captain burst handling.

The job now accepts an optional third argument named
`responding_to_message_id`. The job does not use the argument yet, and
no enqueue call changes in this PR. The change lets old workers accept
the future three argument payload while versions overlap during
deployment.

There is no behavior change.

This is a pre-requisite for
#15133 because if we introduce
another argument, and post deployment, older jobs will fail with
argument error and result in no captain replies

## Validation

* `bundle exec rspec
spec/enterprise/jobs/captain/conversation/response_builder_job_spec.rb`
* `bundle exec rubocop
enterprise/app/jobs/captain/conversation/response_builder_job.rb`
`bundle-audit` in CI flags an Active Storage advisory with no patched
release on the Rails 7.1 line, failing lint on every open PR. Adds it to
the existing `.bundler-audit.yml` ignore list. Mitigated locally; remove
once on Rails 7.2.3.1+.

Related to https://linear.app/chatwoot/issue/INF-92

## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)
Captain now handles a burst of customer messages with one reply. If more
messages arrive before Captain replies, the latest job uses the full
conversation history. The change applies only to Captain V2 and works
across every channel that Captain supports.

Blocked on: #15212

## Closes

Closes #14545

## How to reproduce

1. Start a pending conversation with Captain V2.
2. Send several messages while Captain is preparing a reply.
3. Captain can generate and send a separate reply for each message.

## What changed

* Each Captain V2 job records the incoming message that started it.
* A job stops before generation if a newer message already exists.
* Captain discards a generated reply if a newer message arrived during
generation.
* The latest job replies using the full conversation history.
* Langfuse records whether a generation was discarded and whether a
customer credit was used.
* Captain V1 keeps its existing behavior.

## Tradeoffs

Discarded generations still cost money. Message bursts can also increase
background job work and model provider load. A continuous stream of
incoming messages can delay the reply until one generation finishes
without a newer message.

A small timing window remains if a message arrives after the final check
and before Captain saves the reply. A handoff also cannot be undone if a
newer message arrives after Captain has already changed the conversation
status.

## How to test

1. Enable Captain V2 and start a pending Captain conversation.
2. Send several messages while Captain is preparing a reply.
3. Confirm that Captain sends one reply based on the full message
history.
4. Confirm that Langfuse marks discarded runs with `discarded=true` and
`credit_used=false`.
5. Disable Captain V2 and confirm that Captain V1 behavior is unchanged.

---------

Co-authored-by: Sony Mathew <sony@chatwoot.com>
## Description

Add automations that trigger based on how long a conversation has been
in a given state.

## Type of change

- [ ] New feature (non-breaking change which adds functionality)

## How Has This Been Tested?

- UI flows 
- Specs (#15021) 

## Checklist:

- [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules

---------

Co-authored-by: Sony Mathew <sony@chatwoot.com>
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Co-authored-by: iamsivin <iamsivin@gmail.com>
Adds lifecycle log lines across the IMAP email fetch pipeline so we can
trace where a fetch run spends time or silently stops. Some IMAP inboxes
showed increasingly sparse fetch runs in production without matching
errors; existing logs did not cover job start, lock acquisition,
connection setup, or early-exit paths.

## What changed

- `Inboxes::FetchImapEmailInboxesJob`: log when a fetch job is enqueued
per inbox (temporary instrumentation).
- `Inboxes::FetchImapEmailsJob`: log job start, skip reason on early
return, lock attempt/acquisition, fetched count, processing completion,
job completion, and unexpected errors.
- `Imap::BaseFetchEmailService`: log successful IMAP connection and the
start of each header batch fetch.

All lines share the `[IMAP::FETCH_EMAIL_SERVICE]` tag for searchability.
No behavior changes.

---------

Co-authored-by: Sony Mathew <sony@chatwoot.com>
Adds a dashboard-only takeover path for pending conversations currently
handled by an assistant/bot. Agents see the warning by default, stay in
private-note mode while the conversation is pending, and can use Take
over to move the conversation back to human handling.

## Related
- Original scope:
https://linear.app/chatwoot/issue/CW-7450/block-replies-and-add-takeover-for-agent-bot-ownership
- Backend follow-up:
https://linear.app/chatwoot/issue/CW-7779/enforce-backend-reply-blocking-for-agent-bot-owned-conversations

## Why
We want to prevent accidental parallel handling from the dashboard while
an assistant is managing a pending conversation, without expanding this
PR into API-level enforcement. Backend blocking is tracked separately in
CW-7779.

## What changed
- Locks the dashboard composer to private-note mode while the
conversation status is `pending`.
- Shows the takeover banner by default for pending conversations.
- Uses the Agent Bot assignee name when the conversation payload exposes
one, otherwise falls back to `a bot`.
- Simplifies the banner action copy to `Take over`.
- Reopens and self-assigns the conversation from the takeover action.
- Clears stale local `AgentBot` assignee type when assigning the
conversation back to a human in the store.

## How to test
- Open a pending conversation assigned to an Agent Bot and verify the
banner says it is handled by that bot name.
- Verify the reply editor stays in private-note mode and public reply
mode cannot be selected while the conversation is pending.
- Click Take over and verify the conversation moves to open and is
assigned to the current agent.
- Open a pending Captain/Dialogflow-style conversation without an Agent
Bot assignee payload and verify the banner falls back to `a bot`.

---------

Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Co-authored-by: iamsivin <iamsivin@gmail.com>
)

When an agent loops a new address into an ongoing email thread and then
adds a private note, the reply currently goes out with no Cc, and with
To falling back to the conversation contact. The newly added person
never receives the mail. This change makes an outgoing email keep the
To/Cc/Bcc that were entered on that reply, no matter what is added to
the conversation afterwards.

Closes #15193

## How to reproduce

1. Open a conversation on an Email inbox.
2. Add a new address to **Cc** and send a reply.
3. Immediately add a private note to the same conversation.
4. Inspect the delivered mail: `Cc` is empty and `To` is the
conversation contact instead of the addresses entered on the reply.

## What changed

`ConversationReplyMailer#cc_bcc_emails` and
`#to_emails_from_content_attributes` read the addresses from
`@conversation.messages.outgoing.last` rather than from the message they
were handed. Replies are delivered asynchronously
(`SendReplyJob.perform_later`, and `wait: 2.seconds` when the message
has attachments), so any outgoing message created in that window
replaces the recipients of a mail that is already queued. A private note
is the easiest way to hit it: private notes are outgoing messages, and
on an Email inbox `Messages::MessageBuilder#process_emails` stores them
with empty `to_emails` / `cc_emails` / `bcc_emails`.

Both lookups now go through the existing `current_message` helper
(`@message || @conversation.messages.outgoing.last`), which the `from`
and `reply_to` builders already use. `email_reply` sets `@message`, so
it resolves the recipients of the message being delivered;
`reply_with_summary` and `reply_without_summary` leave `@message` nil
and keep their current behaviour.

Added a spec covering the private-note case in
`spec/mailers/conversation_reply_mailer_spec.rb`.

## Note

I could not run the Ruby test suite locally — there is no Ruby toolchain
in the environment I worked in, so the new spec has not been executed on
my side. The change and the spec were verified by reading the code paths
(`Message#send_reply`, `Email::SendOnEmailService`,
`Messages::MessageBuilder#process_emails`,
`ConversationReplyMailerHelper#prepare_mail`). Deferring to CI for the
actual run.
Required checkbox fields in the pre-chat form now remain invalid unless
they are checked. Other required field types keep their existing
validation behavior, and the existing localized required message is
reused.

## Closes

Closes #15128

## How to reproduce

1. Add a required checkbox custom attribute to the pre-chat form.
2. Check and then uncheck it.
3. Submit the form; submission is now blocked until the checkbox is
checked.

## What changed

- Use the FormKit accepted rule for required checkbox fields.
- Map accepted validation failures to the existing pre-chat required
message.

---------

Co-authored-by: Nazmus Samir <nazmussamir@Nazmuss-MacBook-Pro.local>
Co-authored-by: Sojan Jose <sojan@pepalo.com>
## Summary
`Api::V1::Widget::BaseController#conversation_params` had a `FIXME: typo
referrer in additional attributes, will probably require a migration`
comment. On investigation, `additional_attributes[:referer]` is used
consistently across the whole codebase — automation rules,
conversation/contact filters, `ConversationInfo.vue`, and the widget
SDK's `referer_url` param — matching the historical (if misspelled) HTTP
`Referer` header convention. There is no actual inconsistency to fix,
and renaming the key would require a data migration and would break
existing customer automations built on the `referer` key, for no
functional benefit.

## What changed
- Removed the misleading FIXME comment. No behavior change.
Paid Chatwoot Cloud accounts now receive Captain V2 during plan
reconciliation unless they are explicitly held on Captain V1. Older
accounts could otherwise start on V1 when they became paid after the V2
rollout because only newly created accounts carried the rollout
eligibility value.

## Closes

No linked issue.

## How to reproduce

1. Start with a cloud account created before the Captain V2 rollout.
2. Upgrade the account from the default plan to a paid plan.
3. Reconcile the Stripe subscription.
4. Confirm that Captain is enabled but Captain V2 remains disabled.

## What changed

1. Treat a missing rollout eligibility value as eligible for Captain V2
on paid plans.
2. Keep Captain V2 disabled when the rollout eligibility value is
explicitly set to false.
3. Keep the default plan behavior unchanged.
4. Update the billing reconciliation specs to cover existing paid
accounts, new accounts, and explicit V1 exceptions.
## Description

Adds `VIPS_BLOCK_UNTRUSTED=1` to `.env.example`. This tells libvips to
only use its trusted, well-tested loaders when Active Storage generates
image variants, hardening image processing against untrusted uploads.
The setting requires libvips >= 8.13 and is silently ignored on older
versions.

Related to https://linear.app/chatwoot/issue/INF-92

## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)

## How Has This Been Tested?

Verified locally that thumbnail/variant generation for the common raster
formats (JPEG, PNG, GIF, WebP, TIFF, HEIC) is unaffected with the flag
enabled.

## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
…5213)

This introduces a small event layer for the Captain V2 conversation
lifecycle. A new `Captain::ConversationEvents` facade dispatches five
normalized events (`captain.conversation.engaged`,
`captain.conversation.handed_off`, `captain.conversation.resolved`,
`captain.response.completed`, `captain.response.failed`) from the points
where Captain engages a conversation, replies, fails, hands off, or
auto-resolves. Each event carries the conversation, assistant,
timestamp, and a `source`/`reason_category` where relevant.

## Why this, why now

The Captain V2 flow is about to gain several observers at once:
conversation outcome tracking, agent session capture, and analytics all
need to know when Captain engages, replies, fails, hands off, or
resolves. Wiring each of them directly into `ResponseBuilderJob`,
`HookExecutionService`, and the tools would tangle secondary bookkeeping
into the paths that deliver customer-facing behavior, and every future
consumer would deepen that. Landing the event layer first as its own PR
means the flow announces these moments once and stays otherwise
untouched: customer-visible behavior (messages, status changes,
handoffs, usage enforcement) remains synchronous, while secondary
effects subscribe through listeners. The upcoming conversation outcomes
PR then reduces to a listener plus a model instead of another round of
edits to the core flow, which is why this ships now, before that work
merges.

The existing inference reporting behavior is folded into this layer: the
`conversation.captain_inference_*` events and their dispatch helpers on
`Enterprise::Conversation` are removed, and a dedicated
`Captain::ReportingEventListener` (registered on the enterprise async
dispatcher) maps `source: 'inference'` events to the same stored
reporting event names, so recorded analytics and the assistant stats
builder are unaffected.

## What changed
- New `Captain::ConversationEvents` facade and event type constants
- Event emission from `HookExecutionService` (engagement, usage-limit
handoff), `ResponseBuilderJob` (response completed/failed,
generation-failure handoff), `HandoffTool` (tool handoff), and
`InboxPendingConversationsResolutionJob` (inference resolved/handoff)
- A dedicated `Captain::ReportingEventListener` preserves inference
reporting events through the new event names, removing captain logic
from the OSS listener
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.