mirror of
https://github.com/chatwoot/chatwoot.git
synced 2026-06-04 21:02:35 +08:00
7c16071fc7
6198 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
7c16071fc7
|
fix: Support allowlisted private API inbox webhooks (#14548)
Self-hosted installations can now opt SafeFetch into private-network access after SSRF hardening. The default remains unchanged: private IP destinations are blocked unless the instance owner explicitly enables private-network requests with `SAFE_FETCH_ALLOW_PRIVATE_NETWORK=true`. Fixes https://linear.app/chatwoot/issue/CW-7131 Fixes https://github.com/chatwoot/chatwoot/issues/14489 Fixes https://github.com/chatwoot/chatwoot/issues/14494 ## How to use For self-hosted installations that need API inbox webhooks, or other SafeFetch-backed requests, to call trusted private services, enable private-network access with a single environment variable: ```bash SAFE_FETCH_ALLOW_PRIVATE_NETWORK=true ``` This is disabled by default. Enable it only when the instance owner controls the deployment network and trusts the configured URLs. |
||
|
|
b981ba766f
|
feat: support bulk label removal (#14534)
Adds bulk label removal alongside the existing assign-label action for conversations and contacts, so teams can clean up labels across selected records without opening each item individually. For conversations, the remove dropdown is scoped to labels that are actually applied across the current selection — so agents no longer see (or accidentally "remove") labels that aren't on any of the selected items. For contacts, the dropdown still lists all account labels for now; label data isn't carried on the contact list payload today, so scoping the contact remove menu cleanly is being tracked as a follow-up. ## Closes N/A ## How to test - Open the conversation list, select multiple conversations, open **Remove labels**, and confirm the dropdown only lists labels that are applied to at least one selected conversation. Pick a label and confirm it's removed from the selection. - Open Contacts, select multiple contacts, use **Remove Labels**, choose a label, and confirm the selected contacts are refreshed without that label. - Verify **Assign Labels** still works for conversations and contacts, and continues to show every available label. ## What changed - Adds an `action` prop to the shared `BulkLabelActions` dropdown so it can render in `assign` or `remove` mode. - Wires conversation bulk remove to the existing `labels.remove` backend path and filters the dropdown to the union of labels applied across the selected conversations. - Adds contact bulk remove support through `Contacts::BulkRemoveLabelsService`, routed by `Contacts::BulkActionService`. - Raises contact label save failures instead of reporting a successful bulk action when a contact update is invalid. ## Follow-ups - Scope the contact remove dropdown to applied labels (needs a lightweight endpoint, or eventually `cached_label_list` on `Contact`). ## Verification Conversation bulk remove selector: <img width="1680" height="1050" alt="Conversation bulk remove label selector" src="https://github.com/user-attachments/assets/2dba4a06-c497-45e1-85b0-e700164b6b2f" /> Contact bulk remove selector: <img width="1680" height="1050" alt="Contact bulk remove label selector" src="https://github.com/user-attachments/assets/b3b89959-5978-4064-b5f9-82b1a3e571dc" /> Video proof: https://github.com/user-attachments/assets/fffafe19-4e1c-4e2a-a135-c7182c06bb4d --------- Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Co-authored-by: iamsivin <iamsivin@gmail.com> |
||
|
|
37c8e7e699
|
fix: firecrawl long external link (#14566)
# Pull Request Template ## Description Fixes urls going past 255 chars, this is because of arabic urls, where each character balloons to 8-9 characters and goes past the 255 limit ## Type of change Please delete options that are not relevant. - [x] Bug fix (non-breaking change which fixes an issue) ## 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. 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 |
||
|
|
75c2f91019
|
chore: Enable Tiktok on paid plans Automatically (#13628)
This PR add the ability enable Tiktok integration on all paid plans. |
||
|
|
56e30102eb
|
fix(whatsapp): store and surface unavailable coexistence messages (CW-7166) (#14547)
In WhatsApp coexistence setups (Business App + Cloud API on the same
number), some inbound customer messages arrive from Meta as `type:
unsupported` with error `131060` ("This message is unavailable") and no
content — typically the first message of a Click-to-WhatsApp /
Instagram-ad conversation, or a message synced from a companion device.
Chatwoot was dropping these webhooks entirely, so no contact,
conversation, or message was created. The conversation only surfaced
once an agent replied (via an `smb_message_echoes` event), starting
"headless" with zero customer context.
This change persists a placeholder message for these events so the
contact and conversation are created, and renders it with the dedicated
unsupported-message bubble that points agents to the WhatsApp app —
where the original message is still visible.
Fixes
https://linear.app/chatwoot/issue/CW-7166/whatsapp-coexistence-inbound-messages-are-silently-dropped
and https://github.com/chatwoot/chatwoot/issues/13464
<img width="3448" height="1604" alt="CleanShot 2026-05-22 at 17 49
35@2x"
src="https://github.com/user-attachments/assets/0a90ec84-9085-4cba-883d-08d9de33fa3c"
/>
## How to reproduce
1. Connect a WhatsApp Cloud (coexistence) inbox.
2. Receive an inbound message that Meta delivers as `type: unsupported`
with error `131060` (e.g. a Click-to-WhatsApp ad message, or a message
handled on a companion/primary device that fails to sync to the API).
3. **Before:** nothing is created — the conversation only appears after
an agent replies, with no record of the customer's first message.
4. **After:** the contact and conversation are created with an incoming
placeholder message rendered as the amber "unsupported" bubble: _"This
message is unsupported. You can view this message on the WhatsApp app."
---------
Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Sojan Jose <sojan@pepalo.com>
|
||
|
|
6fbff026eb
|
fix: skip AutoAssignment bulk loop when no agents are online (#14500)
## Description When an inbox has `enable_auto_assignment` and `assignment_v2` enabled but no agents are currently online, `AutoAssignment::AssignmentService#perform_bulk_assignment` still loaded up to 100 unassigned conversations and iterated each one, calling `inbox.available_agents` per conversation. Each call hits Redis presence lookups that return empty, no conversations get assigned, and the loop finishes having done only wasted work. For a busy inbox with a long unassigned backlog and offline agents, this is hundreds of Redis ops per job, multiplied by every `AutoAssignment::AssignmentJob` enqueue from the per-save handler. The pressure is significant when inbound volume is high. This adds a single early-return guard: if `inbox.available_agents.empty?`, return `0` immediately. Existing semantics are preserved (jobs are still enqueued on conversation events; they just exit cheaply when there is no one to assign to). ## Type of change - [x] Performance improvement (non-breaking change) ## Test coverage - [x] Added specs |
||
|
|
52da165cb7
|
feat: add timeout for imap email job and skip problematic emails (#11981)
# Pull Request Template ## Description Large emails (2MB+ with multiple attachments) were causing IMAP email processing jobs to timeout silently, blocking all subsequent emails from being processed. This created an infinite loop where: - Problematic emails were repeatedly fetched but never successfully processed - Other emails in the queue were never processed as we iterated sequentially - silent failures ### Solution Enhanced the FetchImapEmailsJob with individual email processing isolation: ### Key Changes 1. Individual Email Processing: Changed from map to each for better memory efficiency 2. Timeout Protection: Added configurable timeout per email (default: 60 seconds) 3. Failure Tracking: Track failed emails with 6-hour expiry for retry opportunities 4. Skip Logic: Skip emails that have failed 3+ times to prevent infinite loops 5. Error Isolation: Each email is processed in its own error boundary ### Configuration - Timeout: Configurable via EMAIL_PROCESSING_TIMEOUT_SECONDS using GlobalConfigService - Default: 60 seconds per email - Failure Limit: 3 attempts before skipping - Retry Window: 6 hours so that emails get 8 more chances in the 2 day window ### Benefits - Prevents queue blocking: One problematic email cannot stop others - Maintains email order: Older emails (customers waiting longer) processed first - Automatic recovery: Failed emails get retry opportunities - Better monitoring: Clear logging when emails timeout or are skipped - Configurable: Deployments can adjust the timeout based on their needs This fix ensures email processing reliability while maintaining existing functionality. ## Type of change Please delete options that are not relevant. - [x] 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? 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 |
||
|
|
03fb6591e0
|
chore: relax conversation meta polling for high-volume accounts (#14518)
On high-volume accounts, the dashboard sidebar's conversation count badges fall behind because a meaningful share of `/api/v1/accounts/:id/conversations/meta` requests get rate-limited (per-user throttle, default 30 req/min). Root cause is in `conversationStats.js`. The tiered debounce uses `allCount` from the last response to pick a wait interval. `allCount` reflects the user's *current filtered scope*, not the account's true volume — so an agent viewing a small filter on a busy account falls into the most aggressive tier (500ms wait / 1.5s maxWait → up to 40 calls/min/tab) and trips the throttle. ## What changed `app/javascript/dashboard/store/modules/conversationStats.js`: - Short-tier `maxWait`: `1500 → 2000` (caps short-tier at 30/min/tab instead of 40) - Super-long-tier threshold: `allCount > 5000 → > 2000` (more high-volume accounts fall into the safe 3/min/tab tier) - Middle-tier threshold unchanged (`> 100`) | Tier (allCount) | wait / maxWait | Calls/min/tab | |---|---|---| | `> 2000` | 10s / 20s | 3 | | `> 100` | 5s / 10s | 6 | | else | 500ms / 2s | 30 | ## Trade-off Badge updates (including those triggered by the agent's own action) may lag by up to the tier's `maxWait` — worst case 20s for accounts with > 2000 open conversations in the active scope. The conversation list itself and push notifications continue to update in real time; only the numeric badge is debounced. ## Not in scope - Sticky-max `allCount` to fix the underlying tier-selection signal — defer until the simpler tuning is validated in production - Optimistic count updates on local user actions — adds non-trivial state management for a cosmetic lag |
||
|
|
c4a6a19e9b
|
feat(voice): WhatsApp Cloud Calling — UI [6] (#14346)
Some checks failed
Frontend Lint & Test / test (push) Has been cancelled
Publish Chatwoot EE docker images / build (linux/amd64, ubuntu-latest) (push) Has been cancelled
Publish Chatwoot EE docker images / build (linux/arm64, ubuntu-22.04-arm) (push) Has been cancelled
Publish Chatwoot CE docker images / build (linux/amd64, ubuntu-latest) (push) Has been cancelled
Publish Chatwoot CE docker images / build (linux/arm64, ubuntu-22.04-arm) (push) Has been cancelled
Run Chatwoot CE spec / lint-backend (push) Has been cancelled
Run Chatwoot CE spec / lint-frontend (push) Has been cancelled
Run Chatwoot CE spec / frontend-tests (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (0, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (1, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (10, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (11, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (12, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (13, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (14, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (15, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (2, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (3, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (4, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (5, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (6, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (7, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (8, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (9, 16) (push) Has been cancelled
Publish Chatwoot EE docker images / merge (push) Has been cancelled
Publish Chatwoot CE docker images / merge (push) Has been cancelled
## Summary Frontend for WhatsApp Cloud Calling: header / contact-panel call buttons, ringing widget, accept/reject/hangup, mute, in-bubble audio player + transcript, recording-on-hangup upload, mid-call reload warning. WebRTC is browser-direct to Meta — no media server bridge. ## Closes - https://linear.app/chatwoot/issue/PLA-150 ## How to test Requires backend support — the controller, services, model changes, and routes ship in **#14334** (`feature/pla-150`). Merge / deploy that first (or simultaneously); the FE alone won't function without those endpoints. Then on staging, for a WhatsApp Cloud + embedded-signup inbox with the new \`Configuration → Enable voice calling\` toggle ON and webhook registered: 1. **Outbound** — open a conversation, click the phone icon in the conversation header (or contact panel), grant mic, your phone rings, answer, audio both ways, hang up. Recording + transcript land in the bubble within ~10s. 2. **Inbound** — call the business number from your phone. The FloatingCallWidget appears bottom-right with caller name. Click accept, audio both ways, hang up. Recording + transcript appear. 3. **Mute** — during an active WhatsApp call, click the mic icon next to hangup. Speech stops reaching Meta until you click again. 4. **Mid-call reload guard** — try `Cmd-R` during an active call; browser shows a confirm prompt. --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: iamsivin <iamsivin@gmail.com> Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> |
||
|
|
b9757447a8
|
fix(openapi): document webhook secret in API schema (#14199)
Fixes #13862 Updates the webhook OpenAPI schema to match the current API behavior for webhook secrets and supported subscription events. ## Why Current source already creates per-webhook secrets, returns `secret` from the account webhook API, and uses that secret to sign outbound webhook requests with `X-Chatwoot-Signature`. The OpenAPI schema was behind that contract: - `components.schemas.webhook` did not include the returned `secret` field. - Webhook subscription enums did not include the typing events that are already available in the dashboard webhook form and handled by `WebhookListener`. ## What this change does - Documents `secret` on the webhook response schema. - Documents the outbound webhook signing headers associated with `secret`: `X-Chatwoot-Timestamp`, `X-Chatwoot-Signature`, and `X-Chatwoot-Delivery`. - Adds `conversation_typing_on` and `conversation_typing_off` to webhook subscription enums. - Regenerates the main and tag-group swagger JSON files. ## Validation - Ran `bundle exec rails swagger:build`. - Ran `bundle exec rspec spec/swagger/openapi_spec.rb`. - Verified generated swagger JSON includes `secret`, `conversation_typing_on`, and `conversation_typing_off` in the webhook schemas. --------- Co-authored-by: Syed Muhammad Bilal <sdmhbilal@users.noreply.github.com> Co-authored-by: Sojan Jose <sojan@pepalo.com> |
||
|
|
e5d66020fd
|
chore: made the design for unread-counts more subtle [DESN-43] (#14542)
# Pull Request Template ## Description Made the design for unread-counts more subtle Fixes # DESN-43 ## 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. Here are the screenshots. Light theme: <img width="273" height="605" alt="Screenshot 2026-05-22 at 1 28 31 PM" src="https://github.com/user-attachments/assets/cbeccf11-41c4-4899-bbb5-f870df530260" /> Dark theme: <img width="280" height="606" alt="Screenshot 2026-05-22 at 1 27 59 PM" src="https://github.com/user-attachments/assets/3740f57d-3392-435d-9d84-75caf42df610" /> ## 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 |
||
|
|
6b1d8203c6
|
fix: remove unused working hours endpoint (#13839)
Fixes #13752 Removes the standalone `working_hours` API endpoint instead of fixing only the callback typo in `WorkingHoursController`. ## Why The route is not used by the dashboard. The supported product flow already updates business hours through `PATCH /api/v1/accounts/:account_id/inboxes/:id`. The standalone endpoint was already unusable in practice: - The controller callback pointed to the wrong method. - Fixing that callback alone would still leave the endpoint blocked by missing `WorkingHourPolicy` authorization. - Keeping the route would preserve unsupported API surface without making the product flow better. ## What this change does - Removes `PATCH/PUT /api/v1/accounts/:account_id/working_hours/:id`. - Deletes `Api::V1::Accounts::WorkingHoursController`. - Leaves the inbox working-hours update path unchanged. Compatibility note: this removes an undocumented endpoint that was already unusable in practice. Working-hours updates should continue to go through the supported inbox update API. ## Validation - Ran `bin/rails routes -g working_hours` and confirmed the standalone working-hours API route is no longer present. - Searched for remaining `WorkingHoursController` and `resources :working_hours` references. --------- Co-authored-by: easonysliu <easonysliu@tencent.com> Co-authored-by: Sojan Jose <sojan@pepalo.com> |
||
|
|
4550c4130b
|
fix: order labels, teams, channels in sidebar by unread count (CW-7151) (#14510)
# Pull Request Template ## Description Ordered the conversation sidebar labels, teams and channels by the unread count. Fixes # CW-7151 ## 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? Verified manually. Adding the screenshot below. <img width="625" height="833" alt="Screenshot 2026-05-20 at 10 24 30 PM" src="https://github.com/user-attachments/assets/ad04464d-0fc3-4ac7-b8cc-786e9647a299" /> ## 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 |
||
|
|
e09496078b
|
fix(widget): improve dark mode select options (#14538)
Fixes the web widget select dropdown styling in dark mode so native select options remain readable against the widget's dark UI. Closes None ## Screenshots Previous state: <img width="426" height="764" alt="Previous dark mode dropdown issue" src="https://github.com/user-attachments/assets/812fa88c-ae5a-4769-be14-748fbbaf7dfe" /> Current state: <img width="1210" height="610" alt="Current dark mode dropdown styling" src="https://github.com/user-attachments/assets/0ec9b6d7-025d-4b97-b43e-ef026857f9c4" /> ## How to test 1. Enable the web widget pre-chat form with a list/select field. 2. Load the widget with dark mode enabled. 3. Open the select field and verify the select control and option text remain readable in dark mode. ## What changed - Adds light/dark color-scheme handling for widget native selects. - Applies explicit option background and text colors so dark-mode select options do not inherit unreadable colors from the browser popup. --------- Co-authored-by: iamsivin <iamsivin@gmail.com> |
||
|
|
4cdfe4168c
|
chore: resolve sass and vue compiler deprecation warnings (#13794) | ||
|
|
0722750a55
|
chore: Captain reply actions not showing correctly with content (#14160) | ||
|
|
bef25781de
|
feat(attachments): add XML and PFX file support (#14539)
Update frontend allowed file types and FileIcon mapping, and backend Attachment constants to accept .xml and .pfx files # Pull Request Template ## Description Customer also wanted XML support along with .pfx Following up on #14456 ## Type of change - [x] New feature (non-breaking change which adds functionality) ## 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. locally <img width="864" height="512" alt="CleanShot 2026-05-22 at 11 43 20@2x" src="https://github.com/user-attachments/assets/4cbf65d4-b919-4a4b-bf75-a4f2e8690586" /> <img width="870" height="1440" alt="CleanShot 2026-05-22 at 11 44 03@2x" src="https://github.com/user-attachments/assets/e763b49d-4365-4c45-9b43-b0c39af87656" /> ## 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 |
||
|
|
1d7a9093d2
|
fix: clarify agent availability swagger fields (#14533)
Clarifies the agent availability API documentation so request payloads use the writable `availability` field, while `availability_status` remains documented as a read-only response field. ## Closes Closes #13873 ## Why The backend already supports updating an agent's configured availability through `availability`, but the Swagger request payloads documented `availability_status`. That made clients follow a read-only response field and see successful requests without the intended availability change. ## What changed - Replaces `availability_status` with `availability` in agent create/update request schemas. - Updates the availability enum to `online`, `busy`, and `offline`. - Marks response `availability_status` as read-only and explains that it is derived from configured availability, auto-offline, and presence. - Regenerates the combined and tag-group Swagger JSON files. ## Validation - `bundle exec rails swagger:build` - `bundle exec rspec spec/swagger/openapi_spec.rb` - `git diff --check` |
||
|
|
3c67c41544
|
chore: support PFX filetype in attachment uploads (#14456)
# Pull Request Template ## Description This PR expands the default upload rules to support PFX certificate files (`application/x-pkcs12`, `application/pkcs12`, `.pfx`) across private notes, Website, Email, and Telegram channels. Also adds `.xls` / `.xlsx` extension fallbacks for cases where browsers upload Excel files with an empty or generic MIME type. ### Utils Repo PR: https://github.com/chatwoot/utils/pull/61 Fixes https://linear.app/chatwoot/issue/CW-7085/support-more-file-types-in-private-notes-and-in-app ## Type of change - [x] New feature (non-breaking change which adds functionality) ## How Has This Been Tested? ### Screenshots <img width="330" height="218" alt="image" src="https://github.com/user-attachments/assets/80823250-893e-4509-adb9-61f845359151" /> ## 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 - [ ] 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: aakashb95 <aakashbakhle@gmail.com> |
||
|
|
d0ecdc14d8
|
feat(webhooks): Emit inbox_updated when an inbox is disconnected (#14504)
Chatwoot now lets external apps know when an inbox loses its connection and needs re-authentication. When a channel's authorization expires (for example, an email inbox disconnects), Chatwoot fires an `inbox_updated` webhook reflecting the new `reauthorization_required` status, and fires it again once the inbox is re-authenticated. Integrators can keep their own view of which inboxes are healthy without polling the API. This is gated behind the `ENABLE_INBOX_EVENTS` installation flag — the **Inbox updated** webhook subscription only appears in the dashboard when that flag is enabled, so no event is offered that the backend wouldn't dispatch. Fixes https://linear.app/chatwoot/issue/CW-7148/emit-inbox-webhook-when-an-inbox-is-disconnected ## How to test 1. Set `ENABLE_INBOX_EVENTS=true` and restart the app. 2. In **Settings → Integrations → Webhooks**, add a webhook and subscribe to **Inbox updated**. 3. Disconnect an inbox — let an email/Instagram channel hit its auth-error threshold, or run `inbox.channel.prompt_reauthorization!` in a console. 4. The endpoint receives an `inbox_updated` event whose `changed_attributes` shows `reauthorization_required` flipping to `true`. 5. Re-authenticate the inbox (or run `inbox.channel.reauthorized!`) — the endpoint receives the `true → false` transition. 6. Confirm the **Inbox updated** option is hidden when `ENABLE_INBOX_EVENTS` is unset. --------- Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com> |
||
|
|
b1db6c3e9b
|
fix: make zadd function optimised to stay in rubocop limits (#14520)
## Description Fixes rubocop for alfred.rb file on develop ## Type of change - [x] Bug fix (non-breaking change which fixes an issue) ## 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 |
||
|
|
3d20a7b049
|
feat: generate Help Center for Onboarding (#14370)
## Manually triggering help center generation
Open a Rails console (`bundle exec rails console`):
```ruby
account = Account.find(<ACCOUNT_ID>)
user = account.users.first
# Optional: refresh brand info from the customer's website
domain = 'example.com'
result = WebsiteBrandingService.new("noreply@#{domain}").perform
account.update!(
name: result[:title].presence || account.name,
custom_attributes: account.custom_attributes.merge('website' => domain, 'brand_info' => result)
)
# Optional: wipe existing portals so a fresh one is created
account.portals.destroy_all
Onboarding::HelpCenterCreationService.new(account, user).perform
```
Sidekiq must be running — articles are written by
`Onboarding::HelpCenterArticleGenerationJob`. Avoid running on
production; generation calls the LLM provider.
### Generation flow (Happy Path)
```mermaid
sequenceDiagram
autonumber
participant Kickoff as HelpCenterCreationService
participant DB as DB
participant GenJob as HelpCenterArticleGenerationJob
participant Curator as HelpCenterCurator
participant Firecrawl as Firecrawl
participant CuratorLLM as Curation LLM
participant Redis as Redis Progress
participant WriterJob as HelpCenterArticleWriterJob
participant Builder as HelpCenterArticleBuilder
participant WriterLLM as Writer LLM
participant Cable as ActionCable
Kickoff->>DB: Create portal for account<br/>homepage_link=https://chatwoot.com
Kickoff->>DB: Attach brand logo if available
Kickoff->>GenJob: Enqueue generation job<br/>account_id, portal_id, user_id, generation_id
GenJob->>Curator: Curate help center plan
Curator->>Firecrawl: map https://chatwoot.com<br/>search: docs help support faq
Firecrawl-->>Curator: Return discovered links
Curator->>CuratorLLM: Select categories + article plans<br/>from discovered links only
CuratorLLM-->>Curator: Return categories, articles, allowed_urls
GenJob->>DB: Create portal categories
GenJob->>GenJob: Stamp articles with category_id
GenJob->>GenJob: Filter article URLs against allowed_urls
GenJob->>GenJob: Drop articles with no category<br/>or no approved source URLs
GenJob->>Redis: Start progress<br/>status=generating, total=N, finished=0
loop For each approved article
GenJob->>WriterJob: Enqueue writer job<br/>title, category_id, approved URLs
end
par Writer jobs run independently
WriterJob->>Builder: Build article from approved URLs
Builder->>Firecrawl: batch_scrape approved URLs
Firecrawl-->>Builder: Return Markdown source pages
Builder->>WriterLLM: Rewrite sources into one article
WriterLLM-->>Builder: Return title, description, Markdown content
Builder->>DB: Create draft portal article<br/>meta.source_urls
WriterJob->>Redis: Increment finished count
WriterJob->>Cable: Broadcast help_center.article_generated
end
WriterJob->>Redis: If finished >= total<br/>mark status=completed
WriterJob->>Cable: Broadcast help_center.generation_completed
```
### Redis State Management
```mermaid
stateDiagram-v2
[*] --> active_pointer_set
active_pointer_set --> generating: generation job creates valid plan
active_pointer_set --> skipped: curation skipped/failed
generating --> generating: each writer job increments finished
generating --> completed: finished == total
generating --> ignored_completion: generation_id superseded
skipped --> [*]
completed --> [*]
ignored_completion --> [*]
```
|
||
|
|
3cd8cf43ce
|
fix: atomically claim conversation to prevent duplicate assignment (#14495)
## Description Fixes a bug under Assignment V2 where a single conversation could be reassigned dozens of times in a row by the system, producing long stacks of "Assigned to X by Automation System via <policy>" activity messages alternating between agents. After this change each unassigned conversation is assigned exactly once, even on busy inboxes. ## Fixes # (issue) ## Type of change - [ ] Bug fix (non-breaking change which fixes an issue) ## How Has This Been Tested? ## How to reproduce 1. Enable `assignment_v2` on an account with at least 2 online agents in an inbox. 2. Generate sustained resolve/snooze activity in the inbox (each one enqueues `AutoAssignment::AssignmentJob` for the whole inbox). 3. Watch any one unassigned conversation while the jobs drain — pre-fix it picks up multiple back-to-back "Assigned to …" activity rows alternating between agents. ## 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 |
||
|
|
f33e469e9a
|
feat: Unread Count: Frontend changes for showing unread count badges (3/3)[CW-6851] (#14372)
# Pull Request Template ## Description This is the third and final PR in a series of PRs for Introducing unread counts in the sidebar for inboxes and labels. In this PR: * Added frontend changes to show the badges for unread counts for Inboxes and Labels * Added specs for the changes Issue: https://linear.app/chatwoot/issue/CW-6851/support-unread-conversation-counts ## 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. Cases to test: * Send a message from the widget and see if the count changes * Mark a conversation as unread and see the count change for inbox * Open an unread conversation as agent and see the count go down * Add a label to an unread conversation from sidebar right click action without opening the conversation and see the count of un-reads on the label change Added the screenshot of how it will look like <img width="614" height="990" alt="Screenshot 2026-05-05 at 7 00 11 PM" src="https://github.com/user-attachments/assets/99fbaa9f-bcf2-4d8d-86e2-5727f652a9dd" /> ## 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> |
||
|
|
27f2c2b392
|
feat: Unread Count: added api, store refresher, invalidation and events (2/3)[CW-6851] (#14369)
Some checks failed
Frontend Lint & Test / test (push) Has been cancelled
Publish Chatwoot EE docker images / build (linux/amd64, ubuntu-latest) (push) Has been cancelled
Publish Chatwoot EE docker images / build (linux/arm64, ubuntu-22.04-arm) (push) Has been cancelled
Publish Chatwoot CE docker images / build (linux/amd64, ubuntu-latest) (push) Has been cancelled
Publish Chatwoot CE docker images / build (linux/arm64, ubuntu-22.04-arm) (push) Has been cancelled
Run Chatwoot CE spec / lint-backend (push) Has been cancelled
Run Chatwoot CE spec / lint-frontend (push) Has been cancelled
Run Chatwoot CE spec / frontend-tests (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (0, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (1, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (10, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (11, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (12, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (13, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (14, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (15, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (2, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (3, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (4, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (5, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (6, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (7, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (8, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (9, 16) (push) Has been cancelled
Publish Chatwoot EE docker images / merge (push) Has been cancelled
Publish Chatwoot CE docker images / merge (push) Has been cancelled
# Pull Request Template ## Description This is the second PR in a series of PRs for Introducing unread counts in the sidebar for inboxes and labels. In this PR: * added api for unread counts * Added the store refresher and invalidation with event listeners * Added action cable event * Added specs for the changes Issue: https://linear.app/chatwoot/issue/CW-6851/support-unread-conversation-counts ## 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? 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: - [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> |
||
|
|
40deaef458
|
feat: Store WhatsApp BSUID identifiers from inbound webhooks (#14436)
Adds storage support for WhatsApp business-scoped user identifiers received from Meta Cloud API and Twilio WhatsApp webhooks. The change keeps existing phone-based behavior intact, stores BSUID and parent BSUID values as additional `contact_inboxes.source_id` rows for the same contact, and allows BSUID-only inbound messages to create contacts, conversations, and messages without requiring a phone number. Related: https://github.com/chatwoot/chatwoot/issues/13837 **What changed** - Extended WhatsApp source ID validation to accept regular BSUID and parent BSUID formats. - For Meta Cloud API, stores phone, `user_id`, and `parent_user_id` identifiers as contact inbox source IDs when they are present. - For Twilio WhatsApp, stores phone, `ExternalUserId`, and `ParentExternalUserId` identifiers as contact inbox source IDs while preserving the existing `whatsapp:` Twilio source ID shape. - Supports BSUID-only inbound messages by creating a contact, contact inbox, conversation, and message even when the phone number is missing. - Links phone-first and later BSUID-only messages to the same contact when the first payload contains both phone and BSUID. - Stores WhatsApp usernames in contact `additional_attributes`, matching existing social channel patterns. - Keeps existing phone-based outbound and new-conversation behavior unchanged for this milestone. **How to test** 1. Send a Meta Cloud webhook payload with both `wa_id` and `user_id`. 2. Verify Chatwoot creates or finds the phone `contact_inbox` and also creates a BSUID `contact_inbox` for the same contact. 3. Send a later Meta Cloud payload for the same user with only `user_id` / `from_user_id`. 4. Verify Chatwoot finds the BSUID `contact_inbox` and creates the inbound message without requiring a phone number. 5. Send a Twilio WhatsApp webhook with `From: whatsapp:+E164`, `ExternalUserId`, and optionally `ParentExternalUserId`. 6. Verify Chatwoot stores the Twilio phone and BSUID identifiers as `whatsapp:`-prefixed source IDs for the same contact. 7. Send a Twilio WhatsApp webhook where `From` is `whatsapp:<BSUID>` and there is no phone number. 8. Verify Chatwoot creates the contact, contact inbox, conversation, and message without a phone number. --------- Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com> |
||
|
|
3fae800936
|
feat: base layer for unread counts (store, counter and builder) (1/3)[CW-6851] (#14368)
## Description This is the first PR in a series of PRs for Introducing unread counts in the sidebar for inboxes and labels. In this PR: * Added the unread store, counter and builder modules * Added redis keys for unread count management * Added specs for all 3 modules, some specs are for testing enterprise only feature like specific roles and permissions which are added in the respective enterprise folder itself. **Note** None of this changes affect anything else and nothing is wired to existing modules. Issue: https://linear.app/chatwoot/issue/CW-6851/support-unread-conversation-counts ## 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? 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: - [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> |
||
|
|
1913ccadfa
|
fix: [CW-7141] fix gem audit issue for sidekiq-cron and devise (#14497)
# Pull Request Template ## Description * sidekiq-cron upgraded to 2.4.0 * Sidekiq constrained to stay on 7.3.x * Devise advisory ignored in .bundler-audit.yml with the reason: Chatwoot does not enable Timeoutable, so the timeout redirect path is not reachable ### Details The sidekiq-cron upgrade is from 1.12.0 to 2.4.0. What changed that matters for us: Fixes the reported Sidekiq Web UI reflected XSS in 2.4.0. Adds namespace handling changes from the 2.x series. Chatwoot does not use custom cron namespaces in config/schedule.yml, so the migration guide says no action is needed for our usage. Drops support for old Sidekiq/Redis versions. We are still on Sidekiq 7.3.1, which is supported. Adds new dependencies: cronex and unicode. Keeps the same APIs we use: Sidekiq::Cron::Job.load_from_hash!(schedule, source: 'schedule'), Sidekiq::Cron::Job.destroy(name), and require 'sidekiq/cron/web' still exist. Chance of breakage: low, based on the current Chatwoot usage. The main thing I would watch after deploy is scheduled job registration in Sidekiq. The one subtle area is namespace behavior: if production has custom, manually-created cron jobs using non-default namespaces, load_from_hash! cleanup behavior could matter. For the committed config/schedule.yml jobs, which do not specify namespaces, they should continue in the default namespace. For concerns around Devise, it does not look exploitable in current Chatwoot, because Chatwoot does not enable Devise :timeoutable. I checked: app/models/user.rb (line 59) lists the Devise modules, and :timeoutable is not included. config/initializers/devise.rb (line 164) has the timeoutable section, but config.timeout_in is commented out. SuperAdmin inherits from User, so it does not add a separate timeoutable path either. So from a practical security perspective: the vulnerable redirect path requires warden_message == :timeout, which is only produced by Devise Timeoutable. Since Chatwoot does not use Timeoutable, this warning is not currently reachable. Is the patch really needed? Strictly for current exploitability: no. Fixes #CW-7141 ## Type of change Please delete options that are not relevant. - [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? Spec and lints and change-log checks with codex. ## 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 - [ ] 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: Vishnu Narayanan <iamwishnu@gmail.com> |
||
|
|
bca95efb82
|
feat: add image resize support in articles (#14293) | ||
|
|
6560dbb68d
|
feat: Add an option on the dashboard to allow switching help center layout (#14491)
<img width="633" height="431" alt="Screenshot 2026-05-18 at 12 32 55 PM" src="https://github.com/user-attachments/assets/682d4c5f-4c76-465b-8d2f-92fbc2bb2a40" /> --------- Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com> Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Co-authored-by: iamsivin <iamsivin@gmail.com> |
||
|
|
497d34c097
|
fix: render markdown in CSAT survey messages (#14468) | ||
|
|
4371793741
|
fix: captain-v2 cannot see image attachments shared via email (#14449)
## Description Inbound email attachments are stored with `file_type: 'file'` regardless of their actual MIME type. As a result, image screenshots shared by customers via email are not exposed to Captain V2's multimodal pipeline — `Captain::OpenAiMessageBuilderService#attachment_parts` selects images via `attachments.where(file_type: :image)` and emits a placeholder `"User has shared an attachment"` text part instead of an `image_url` part. The model never gets the image, so Captain keeps asking the customer to retype information that is already visible in the screenshot. This PR makes the email mailbox derive `file_type` from the blob's `content_type` using the existing shared `FileTypeHelper`, matching how every other inbound channel (`twilio`, `sms`, `telegram`, `line`, `tiktok`, `twitter`, `messenger`) and `MessageBuilder` already classify attachments. Fixes #14448 ## Type of change - [x] Bug fix (non-breaking change which fixes an issue) ## How Has This Been Tested? Reproduced and verified on a self-hosted production instance: 1. Real customer reply via email with a PNG screenshot of an in-app error. Before: ```ruby a = Message.find(<id>).attachments.first a.file_type # => "file" a.file.blob.content_type # => "image/png" Captain::OpenAiMessageBuilderService.new(message: a.message).generate_content # => [{type: 'text', text: '...'}, # {type: 'text', text: 'User has shared an attachment'}] ❌ no image_url ``` Captain reply: "Please copy and paste the full error text…" (model never saw the image). 2. After the patch + force-recreate, same conversation: ```ruby a.file_type # => "image" Captain::OpenAiMessageBuilderService.new(message: a.message).generate_content # => [{type: 'text', text: '...'}, # {type: 'image_url', image_url: {url: 'https://.../<blob>.png'}}] ✅ ``` Captain reply now correctly references the on-screen error text from the screenshot via the multimodal vision path — no more deflection. 3. Regression sanity-check on non-image attachments (PDF / Office docs): `file_type` falls through to `:file`, behavior unchanged. ## Notes for self-hosted operators Existing email image attachments in the DB will still have `file_type: 'file'`. A one-shot backfill is straightforward and safe (no data loss, only metadata): ```ruby Attachment.joins(message: :conversation) .where(messages: { content_type: 'incoming_email' }) .where(file_type: 'file') .find_each do |a| next unless a.file.attached? ct = a.file.blob.content_type.to_s next unless ct.start_with?('image/', 'audio/', 'video/') new_type = ct.start_with?('image/') ? :image : (ct.start_with?('video/') ? :video : :audio) a.update_columns(file_type: Attachment.file_types[new_type]) end ``` ## 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 - [ ] I have added tests that prove my fix is effective — happy to add a `mailbox_helper_spec` example for `process_regular_attachments` if maintainers prefer; existing specs in that file focus on inline-image handling. --------- Co-authored-by: Aakash Bakhle <48802744+aakashb95@users.noreply.github.com> |
||
|
|
64585faff0
|
feat: Add a documentation layout design for public help center portal (#14403)
https://github.com/user-attachments/assets/fc4d15f9-2b54-4627-940f-94772ec739b1 --------- Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com> Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> |
||
|
|
e05008e0a4 | Merge branch 'release/4.14.0' into develop | ||
|
|
92f8c13ce5 | Bump version to 4.14.0 | ||
|
|
c4089f2226
|
fix(csat): Require confirmation before submitting rating (#14450)
Customers reported that the CSAT survey was recording their rating the instant they tapped a star — leaving no chance to correct an accidental pick. This change lets the customer freely change their selection until they actually submit the comment/feedback. The rating still saves on click (so we don't lose ratings when a customer never types a comment), but it stays editable until the comment form is submitted. Once that happens, the rating locks. The flow on both surfaces: - Customer taps a star/emoji → rating is saved. - Customer taps a different star/emoji → previous save is overwritten with the new value. - Customer types a comment and submits → latest rating + comment are saved together. - After that submit, the rating is locked and can't be changed. Two surfaces are updated: - **Standalone survey page** (`/survey/responses/:uuid`) — the rating buttons remain re-tappable until the Feedback form is submitted; once submitted, both rating and feedback lock. - **In-conversation widget CSAT** — same behavior, the inline arrow-submit button on the feedback form is the locking action. In-flight guards prevent a race where the customer changes their pick mid-network-call (raised by the codex review on the earlier revision): while a save is in flight, the rating controls are temporarily disabled so the request and the displayed selection can't diverge. ## Closes - https://linear.app/chatwoot/issue/CW-7061/csat-star-rating-submits-on-first-click-needs-confirmation-step ## How to test **Standalone survey page** 1. Enable CSAT on any inbox (Settings → Inboxes → Configuration → CSAT survey). 2. Resolve a conversation in that inbox so a CSAT message is generated. 3. Open the survey URL: `{FRONTEND_URL}/survey/responses/{conversation.uuid}` (easiest: `bundle exec rails runner 'puts Conversation.joins(:messages).where(messages: { content_type: "input_csat" }).last.csat_survey_link'`). 4. Tap a star/emoji — confirm the rating saves (Network panel shows a PUT to `/public/api/v1/csat_survey/{uuid}`). 5. Tap a different star/emoji — confirm a second PUT goes out with the new value; the latest selection is reflected. 6. Type a comment and hit Submit feedback — confirm rating + feedback persist; both controls now lock. 7. Reload the page — the locked state is rehydrated correctly. **Widget CSAT** 1. From an inbox with CSAT enabled, resolve a conversation that has an active widget session. 2. In the widget, the CSAT card appears with stars/emojis + the inline comment box. 3. Tap a star/emoji — confirm a PATCH goes out and the selection visibly updates. 4. Tap different stars/emojis — confirm each overrides the previous save. 5. Type a comment and click the arrow — rating + comment submit together; stars lock. Both display types (emoji and 5-star) should behave consistently. ## What changed - `app/javascript/survey/views/Response.vue` — `selectRating()` saves on every tap and short-circuits while a save is in flight (or after feedback was submitted). Rating components are disabled by `isFeedbackSubmitted || isUpdating` so the lock follows the feedback submission, not the first rating tap. - `app/javascript/survey/components/Rating.vue` — new `isDisabled` prop. The disabled / hover styling and click guard key off it instead of the presence of `selectedRating`, so emojis stay re-clickable until the feedback step locks them. - `app/javascript/shared/components/CustomerSatisfaction.vue` — same shape for the widget: rating click auto-submits and re-clicks override the previous save; controls disabled while a submit is in flight; emoji-button styling and the inline `StarRating` lock on `isFeedbackSubmitted || isUpdating`. --------- Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com> Co-authored-by: Sony Mathew <2040199+sony-mathew@users.noreply.github.com> |
||
|
|
bcb66cdcc0
|
chore: Support creating articles from category view (#14406)
# Pull Request Template ## Description This PR adds support for creating articles directly from the category view. Previously, articles could only be created from the main articles page. With this change, users can now create a new article while browsing a specific category, making the workflow faster and more convenient. Fixes https://linear.app/chatwoot/issue/CW-7050/create-an-article-when-inside-a-category ## Type of change - [x] New feature (non-breaking change which adds functionality) ## How Has This Been Tested? ### Screencast https://github.com/user-attachments/assets/e5a72a85-676e-4747-948a-6b1a19d2089f ## 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 - [ ] 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 |
||
|
|
7f0d5caca4
|
chore: Update translations (#14276)
Co-authored-by: Sojan Jose <sojan@pepalo.com> |
||
|
|
1d2f3e86dd
|
feat(companies): track company last activity (#14435)
Tracks company recency from linked contact activity so the Companies list and detail page can show/sort by real customer engagement instead of generic record updates. ## Closes None. ## Why Company recency should reflect activity from people associated with the company. This keeps the signal tied to persisted contact activity, without treating passive online presence or widget heartbeat pings as company activity. ## What Changed - Adds a company helper to record `last_activity_at` from linked contact activity. - Rolls up `Contact#last_activity_at` changes to the associated company. - Initializes company activity when an already-active contact is associated with a company, including the business-email auto-association path. - Throttles company activity rollups to once every 5 minutes per company to avoid unnecessary writes during active conversations. - Treats company activity as monotonic: unlinking, moving, or deleting contacts does not move a company's activity timestamp backwards. - Leaves historical backfill, online presence tracking, widget visit tracking, and richer activity attribution out of scope. ## How to Test 1. Open an account with Companies enabled and visit the Companies list. 2. Trigger activity for a contact that belongs to a company, for example by receiving or sending a message in that contact's conversation. 3. Confirm the linked company shows a recent activity timestamp in the Companies list/detail page after the contact activity updates. 4. Associate an already-active contact with a company and confirm the company receives that contact's existing activity timestamp. 5. Confirm repeated contact activity within a short window does not continuously rewrite the company timestamp. --------- Co-authored-by: Sony Mathew <2040199+sony-mathew@users.noreply.github.com> |
||
|
|
3253e863ed
|
fix: validate OpenAI hook credentials (#14068)
# Pull Request Template ## Description - Validates openai key while configuring hooks - added backfill logic Fixes # (issue) ## Type of change - [x] New feature (non-breaking change which adds functionality) ## 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. locally <img width="1710" height="1234" alt="CleanShot 2026-04-15 at 16 15 02@2x" src="https://github.com/user-attachments/assets/3d319fe0-19f9-4fd0-9308-74987daac2e1" /> <img width="2884" height="1136" alt="CleanShot 2026-05-11 at 19 22 53@2x" src="https://github.com/user-attachments/assets/5eae8650-985b-4c4a-af42-35f7175ff52d" /> ## 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 --------- Co-authored-by: Vishnu Narayanan <iamwishnu@gmail.com> |
||
|
|
059d840272
|
feat: Refresh llm settings when superadmin configs change [AI-151] (#14388)
# Pull Request Template ## Description fixes: https://linear.app/chatwoot/issue/AI-151/captains-super-admin-config-dont-get-applied-into-rails-without ## Type of change - [x] Bug fix (non-breaking change which fixes an issue) ## 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. specs and locally To test locally: go to super admin -> settings -> captain -> Change endpoint to something incorrect go to local app -> captain -> playground -> try chatting (should fail due to incorrect endpoint) now in super admin captain settings, set the correct endpoint then chat in playground. Now it should work. Current develop code doesn't reflect the changes in installation config for captain instantly, needs a server restart. ## 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 |
||
|
|
b8deb89613
|
fix: make SAML callback session independent (#14467)
This PR makes SAML login independent of Rails session cookies ## Problem The normal SAML login flow should be straightforward: - User opens Chatwoot. - Chatwoot creates `_chatwoot_session`. - User starts SSO. - Chatwoot redirects the browser to the SAML provider. - The provider authenticates the user. - The provider sends the browser back to Chatwoot's ACS URL. - Chatwoot reads the SAML response, finds or creates the user, and logs them in. The fragile step is the ACS callback. Most SSO flows return to the app through browser redirects where cookies usually pass through as expected. **ADFS commonly returns the SAML response with a cross-site POST**. With Chatwoot's session cookie using `SameSite=Lax`, browsers may not send `_chatwoot_session` on that POST. SAML validation itself does not need the old Rails session cookie. The problem was our callback handoff after validation. DeviseTokenAuth stores the verified OmniAuth payload in Rails session, then redirects to a second callback route. If the browser does not preserve that session, Chatwoot has already received a valid SAML response but can no longer finish login. ## Solution This PR removes the session-backed handoff for SAML only: - The SAML callback completes login in the same request where OmniAuth validates the SAML response. - Chatwoot reads the verified auth payload directly from `request.env['omniauth.auth']`. - Account context and RelayState come from callback params or OmniAuth env data, not Rails session. - Other OmniAuth providers continue using the existing DeviseTokenAuth flow. - Mobile SAML still works when the IdP returns `RelayState=mobile`; the callback redirects to the mobile deep link with the generated SSO token. The previous SAML override used `303 See Other` to avoid replaying the SAML POST into the second callback route. This change keeps that intent, but removes the second callback route for SAML entirely. ## Screen recording ### SP Initiated https://github.com/user-attachments/assets/b0735e93-3864-4cc3-b6fc-419fff4b549e ### IDP Initiated https://github.com/user-attachments/assets/3ded0246-933c-4c85-9b7c-fa15fdc34883 ## Testing Manual validation: - Complete a SAML login. - In the browser network trace, find the IdP POST to `/omniauth/saml/callback?account_id=<account-id>`. - Confirm it redirects directly to `/app/login?...sso_auth_token=...` for web login. - For mobile, confirm `RelayState=mobile` redirects to the configured mobile deep link. - Confirm there is no intermediate `/auth/saml/callback` request. Testing with mocksaml.com: - Configure Chatwoot with a public `FRONTEND_URL`. - Set the mocksaml ACS URL to: ```text https://<chatwoot-host>/omniauth/saml/callback?account_id=<account-id> ``` - Set the mocksaml audience/SP entity ID to the value shown in Chatwoot SAML settings, usually: ```text https://<chatwoot-host>/saml/sp/<account-id> ``` - Use an email returned by mocksaml that exists in the SAML-enabled account. - Start login from Chatwoot's SSO login page. - Confirm the callback redirects directly to the app login URL with an SSO token. --------- Co-authored-by: Sojan Jose <sojan@pepalo.com> |
||
|
|
ef27e571f7
|
feat: enable quoted reply for everyone (#14469)
Some checks failed
Frontend Lint & Test / test (push) Has been cancelled
Publish Chatwoot EE docker images / build (linux/amd64, ubuntu-latest) (push) Has been cancelled
Publish Chatwoot EE docker images / build (linux/arm64, ubuntu-22.04-arm) (push) Has been cancelled
Publish Chatwoot CE docker images / build (linux/amd64, ubuntu-latest) (push) Has been cancelled
Publish Chatwoot CE docker images / build (linux/arm64, ubuntu-22.04-arm) (push) Has been cancelled
Run Chatwoot CE spec / lint-backend (push) Has been cancelled
Run Chatwoot CE spec / lint-frontend (push) Has been cancelled
Run Chatwoot CE spec / frontend-tests (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (0, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (1, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (10, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (11, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (12, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (13, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (14, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (15, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (2, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (3, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (4, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (5, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (6, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (7, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (8, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (9, 16) (push) Has been cancelled
Publish Chatwoot EE docker images / merge (push) Has been cancelled
Publish Chatwoot CE docker images / merge (push) Has been cancelled
Quoted email replies is now available to every account by default.
Previously this was gated behind the `quoted_email_reply` account-level
feature flag, so accounts needed it toggled on (via Super Admin) before
agents saw the toggle in the reply box.
## How to test
1. Open any conversation on an email inbox.
2. Confirm the **Quote previous email** toggle is visible in the reply
box (and is **not** visible on private notes or non-email channels).
3. Toggle it on, type a reply, and send — the outbound email should
include the quoted prior email below your message.
4. Toggle it off and send another reply — the quoted block should not
appear.
5. The toggle preference should persist per channel type (UI setting),
as before.
6. Verify the toggle works on a brand new account with no feature flags
flipped on (previously it would have been hidden).
## What changed
- Removed all `isFeatureEnabledonAccount(..., QUOTED_EMAIL_REPLY)` gates
from `ReplyBox.vue`, so the toggle and quoted-content behavior are
unconditional on email channels.
- Removed the `QUOTED_EMAIL_REPLY` constant from
`dashboard/featureFlags.js`.
- Marked the flag as `deprecated: true` in `config/features.yml` (kept
the entry in place to preserve FlagShihTzu bit positions on existing
accounts; `deprecated: true` hides it from the Super Admin UI).
- Dropped the now-unnecessary
`account.enable_features('quoted_email_reply')` setup from the message
builder spec.
|
||
|
|
1528fcde0c
|
fix: missing widget es translations (#14375)
# Pull Request Template ## Description There were English strings in the Spanish i18n file for the widget. This PR translates them. ## Type of change Please delete options that are not relevant. - [x] Bug fix (non-breaking change which fixes an issue) ## 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 - [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 - [ ] 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: Sojan Jose <sojan@pepalo.com> |
||
|
|
5f6bd951b9
|
fix: portals#create returns 500 when custom_domain is omitted (#14400)
## Description
`POST /api/v1/accounts/:account_id/portals` returns a generic 500
(`{"status":500,"error":"Internal Server Error"}`) whenever the request
body omits `custom_domain`. Root cause: `parsed_custom_domain` calls
`URI.parse(@portal.custom_domain)` and `URI.parse(nil)` raises
`URI::InvalidURIError`. Existing callers either had to know to pass
`"custom_domain": ""` as a workaround or hit a 500 with no useful
diagnostic.
This PR guards `parsed_custom_domain` against blank values so the
existing fall-through (`else @portal.custom_domain`) applies —
equivalent to passing an empty string.
It also moves the `process_attached_logo` guard from the helper into the
`create` call site so `create` mirrors `update` (`process_attached_logo
if params[:blob_id].present?`) and avoids an unnecessary signed-blob
lookup on every create that doesn't include a logo.
Fixes #14397
## Type of change
- [x] Bug fix (non-breaking change which fixes an issue)
## How Has This Been Tested?
Two new request specs in
`spec/controllers/api/v1/accounts/portals_controller_spec.rb` covering
the regression:
- `creates portal when custom_domain is omitted from request body` — the
previously-broken case, now returns 200.
- `creates portal when custom_domain is blank` — verifies the existing
workaround (`"custom_domain": ""`) still works after the change.
Manually verified against `chatwoot/chatwoot:latest` Docker image before
the fix (500) and against this branch (200) using the curl repro from
the issue.
```bash
curl -X POST "https://<host>/api/v1/accounts/<account_id>/portals" \
-H "Content-Type: application/json" \
-H "api_access_token: <token>" \
-d '{"name":"Test Portal","slug":"test-portal","color":"#3b82f6"}'
```
Before: `{"status":500,"error":"Internal Server Error"}`
After: `200 OK` with the portal payload.
## 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 (no doc
change needed — controller behaviour, fully backward-compatible)
- [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: Sojan Jose <sojan@pepalo.com>
|
||
|
|
cfc7699b7e
|
chore(deps): bump net-imap from 0.4.20 to 0.4.24 (#14361)
Bumps [net-imap](https://github.com/ruby/net-imap) from 0.4.20 to 0.4.24. <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.4.24</h2> <blockquote> <p>[!IMPORTANT] <em>The <code>0.4.x</code> release branch will only receive critical security fixes, and will be unsupported when ruby 3.3 is EOL. Please upgrade to a newer version.</em></p> </blockquote> <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/666">ruby/net-imap#666</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 "successfully", <strong><em>without starting TLS</em></strong>.</p> </blockquote> <blockquote> <p>[!IMPORTANT] Argument validation is significantly improved. Several injection vulnerabilities have been fixed: <a href="https://redirect.github.com/ruby/net-imap/pull/663">ruby/net-imap#663</a> fixes CRLF/command/argument injection via Symbol arguments (GHSA-75xq-5h9v-w6px). <a href="https://redirect.github.com/ruby/net-imap/pull/663">ruby/net-imap#663</a> fixes CRLF/command/argument injection via the <code>attr</code> argument to <code>#store</code>/<code>#uid_store</code> (GHSA-hm49-wcqc-g2xg) <a href="https://redirect.github.com/ruby/net-imap/pull/663">ruby/net-imap#663</a> fixes CRLF/command/argument injection via the <code>storage_limit</code> argument to <code>#setquota</code> (GHSA-hm49-wcqc-g2xg). <a href="https://redirect.github.com/ruby/net-imap/pull/663">ruby/net-imap#663</a> fixes CRLF/command injection via <code>RawData</code> (GHSA-hm49-wcqc-g2xg):</p> <ul> <li><code>#search</code> and <code>#uid_search</code> send <code>criteria</code> as raw data, when it is a String</li> <li><code>#fetch</code> and <code>#uid_fetch</code> send <code>attr</code> as raw data, when it is a String. When <code>attr</code> is an Array, its String members are sent as raw data.</li> </ul> </blockquote> <blockquote> <p>[!CAUTION] <code>RawData</code> does not defend against <em>other</em> forms of argument injection! It is an intentionally low-level API.</p> </blockquote> <blockquote> <p>[!NOTE] Two denial of service vulnerabilities have been addressed. These are generally only relevant when connecting to an <em>untrusted hostile server</em> (or without TLS).</p> <p><a href="https://redirect.github.com/ruby/net-imap/pull/651">ruby/net-imap#651</a> fixes quadratic time complexity when reading large responses containing many string literals (GHSA-q2mw-fvj9-vvcw). <a href="https://redirect.github.com/ruby/net-imap/pull/655">ruby/net-imap#655</a> adds a configurable <code>max_iterations</code> count for <code>SCRAM-*</code> authentication (GHSA-87pf-fpwv-p7m7).</p> <p>The default <code>ScramAuthenticator#max_iterations</code> is <code>2**31 - 1</code> (max 32-bit signed int), which was already OpenSSL's maximum value. <em>It provides no protection</em> against hostile servers unless it is explicitly set to a lower value by the user.</p> </blockquote> <h3>Added</h3> <ul> <li>🔒 Add <code>ScramAuthenticator#max_iterations</code> (backports <a href="https://redirect.github.com/ruby/net-imap/issues/654">#654</a>) in <a href="https://redirect.github.com/ruby/net-imap/pull/655">ruby/net-imap#655</a>, reported by <a href="https://github.com/Masamuneee"><code>@Masamuneee</code></a></li> </ul> <h3>Fixed</h3> <ul> <li>🔒 Fix STARTTLS stripping vulnerability (backports <a href="https://redirect.github.com/ruby/net-imap/issues/664">#664</a>) in <a href="https://redirect.github.com/ruby/net-imap/pull/666">ruby/net-imap#666</a>, reported by <a href="https://github.com/Masamuneee"><code>@Masamuneee</code></a></li> <li>🔒 Fix CRLF injection vulnerabilities (backports <a href="https://redirect.github.com/ruby/net-imap/issues/657">#657</a>, <a href="https://redirect.github.com/ruby/net-imap/issues/658">#658</a>, <a href="https://redirect.github.com/ruby/net-imap/issues/659">#659</a>, <a href="https://redirect.github.com/ruby/net-imap/issues/660">#660</a>, <a href="https://redirect.github.com/ruby/net-imap/issues/636">#636</a>, <a href="https://redirect.github.com/ruby/net-imap/issues/661">#661</a>) in <a href="https://redirect.github.com/ruby/net-imap/pull/663">ruby/net-imap#663</a>, reported by <a href="https://github.com/manunio"><code>@manunio</code></a></li> <li>⚡ Much faster ResponseReader performance (backports <a href="https://redirect.github.com/ruby/net-imap/issues/642">#642</a>) in <a href="https://redirect.github.com/ruby/net-imap/pull/651">ruby/net-imap#651</a>, reported by <a href="https://github.com/Masamuneee"><code>@Masamuneee</code></a></li> <li>🐛 Wait to continue RawData literals (backports <a href="https://redirect.github.com/ruby/net-imap/issues/660">#660</a>) by <a href="https://github.com/nevans"><code>@nevans</code></a> in <a href="https://redirect.github.com/ruby/net-imap/pull/663">ruby/net-imap#663</a></li> </ul> <h3>Other Changes</h3> <ul> <li>♻️ Improve internal literal sending (partially backports <a href="https://redirect.github.com/ruby/net-imap/issues/358">#358</a>, <a href="https://redirect.github.com/ruby/net-imap/issues/616">#616</a>, <a href="https://redirect.github.com/ruby/net-imap/issues/649">#649</a>) by <a href="https://github.com/nevans"><code>@nevans</code></a> in <a href="https://redirect.github.com/ruby/net-imap/pull/653">ruby/net-imap#653</a></li> </ul> <p><strong>Full Changelog</strong>: <a href="https://github.com/ruby/net-imap/compare/v0.4.23...v0.4.24">https://github.com/ruby/net-imap/compare/v0.4.23...v0.4.24</a></p> <!-- raw HTML omitted --> </blockquote> <p>... (truncated)</p> </details> <details> <summary>Commits</summary> <ul> <li><a href=" |
||
|
|
dc332dd93e
|
feat: add attachments endpoint for contact media view (#14391)
# Pull Request Template ## Description This PR adds an endpoint to fetch all attachments shared with or by a contact across all of their conversations. Results are scoped based on the access: * Admins can access all attachments * Agents can access attachments only from inboxes they belong to * Custom role agents are further filtered based on their conversation permissions Each attachment payload includes `conversation_id`, allowing the UI to deep-link back to the source conversation. Added `GET /api/v1/accounts/:account_id/contacts/:contact_id/attachments` under the existing contacts scope. Fixes https://linear.app/chatwoot/issue/CW-7021/add-media-view-to-the-contact-details-page ## Type of change - [x] New feature (non-breaking change which adds functionality) ## 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 - [ ] Any dependent changes have been merged and published in downstream modules --------- Co-authored-by: Sojan Jose <sojan@pepalo.com> |
||
|
|
13f66e3a88
|
fix: incorrect scope across controllers (#14459)
Co-authored-by: Sojan Jose <sojan@pepalo.com> |
||
|
|
fbcb89e955
|
fix(swagger): prevent path traversal in docs controller (#14458)
This hardens the development/test Swagger docs endpoint by ensuring requested files are resolved only within the `swagger/` directory. This did not affect production security because the Swagger controller only renders files in development or test environments; production already returns `404`. The change still closes the scanner finding and prevents future automated reports from flagging the development-only path. ## Closes Addresses: GHSA-xhp7-ggjq-p2rg ## How to reproduce 1. Start Chatwoot locally in development. 2. Visit `/swagger/%2Fetc%2Fpasswd`. 3. Before this change, the endpoint could render files outside the Swagger directory in development/test. ## What changed - Resolve Swagger file requests relative to `Rails.root/swagger`. - Return `404` when the resolved path is outside the Swagger directory or does not point to a file. - Strip leading slashes from derived request paths. - Add a request spec for the encoded absolute-path case. ## How to test 1. Start the app locally. 2. Visit `/swagger` and confirm the ReDoc page loads. 3. Visit `/swagger/swagger.json` and confirm the Swagger JSON loads. 4. Visit `/swagger/%2Fetc%2Fpasswd` and confirm it returns `404` with no file contents. Note: `bundle exec rspec spec/controllers/swagger_controller_spec.rb` was passing locally earlier during this fix. A final rerun before opening the PR was blocked because local Postgres on `localhost:5432` was not accepting connections. Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com> |
||
|
|
8712879681
|
test: Stabilize SafeFetch spec against constant-identity flake (#14454)
Some checks failed
Frontend Lint & Test / test (push) Has been cancelled
Publish Chatwoot EE docker images / build (linux/amd64, ubuntu-latest) (push) Has been cancelled
Publish Chatwoot EE docker images / build (linux/arm64, ubuntu-22.04-arm) (push) Has been cancelled
Publish Chatwoot CE docker images / build (linux/amd64, ubuntu-latest) (push) Has been cancelled
Publish Chatwoot CE docker images / build (linux/arm64, ubuntu-22.04-arm) (push) Has been cancelled
Run Chatwoot CE spec / lint-backend (push) Has been cancelled
Run Chatwoot CE spec / lint-frontend (push) Has been cancelled
Run Chatwoot CE spec / frontend-tests (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (0, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (1, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (10, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (11, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (12, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (13, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (14, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (15, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (2, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (3, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (4, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (5, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (6, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (7, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (8, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (9, 16) (push) Has been cancelled
Publish Chatwoot EE docker images / merge (push) Has been cancelled
Publish Chatwoot CE docker images / merge (push) Has been cancelled
`spec/lib/safe_fetch_spec.rb` has been flaking intermittently under full-suite runs with errors like: ``` expected SafeFetch::FileTooLargeError, got #<SafeFetch::FileTooLargeError: exceeded 1048576 bytes> ``` The class name on both sides is identical — yet RSpec reports a mismatch. This PR replaces the constant-identity assertions in this spec with a name-based matcher so the comparison stops depending on the live Class object's identity. We have made a similar fix earlier, but that wasn't addressing the core of the issue in #14139. **How to reproduce:** The flake only surfaces under load. Running the spec in isolation almost always passes, here's a [run failing in CI](https://github.com/chatwoot/chatwoot/actions/runs/25852294516/job/75961520248?pr=14370) ## What's actually going on Three facts combine: 1. **Test env reloads classes.** `config/environments/test.rb` sets `cache_classes = false` so Zeitwerk reloads autoloadable code on demand. 2. **`lib/` is on the reloadable autoload tree.** `config/application.rb` adds `lib` to `eager_load_paths`, which (with `eager_load = false` in test) makes it lazily loaded by Zeitwerk's *main* (reloading) autoloader. `lib/safe_fetch/` lives under that umbrella. 3. **RSpec's `raise_error(Klass)` snapshots the Class object.** `raise_error` matcher captures `Klass` (a specific `Class` instance) when the matcher is built. At raise time it compares with `Module#===`, which is identity-based. When the executor between examples triggers `Rails.application.reloader.reload!`, Zeitwerk does `remove_const(:SafeFetch)` and re-installs an autoload trigger. The next access produces a **fresh** `Class` object for `SafeFetch::FileTooLargeError` — same name, different identity. The matcher's snapshot now points at the dead Class, and the live raise produces the new one. Identity fails, even though the error is semantically correct. This bites SafeFetch specifically because: - SafeFetch is a *namespace* with 7 nested error classes — one reload invalidates all of them. - The spec contains 14+ `raise_error(SafeFetch::Foo)` assertions — many chances to land in a reload window. - SafeFetch is exercised by request-driven code (webhook delivery, avatar fetch). Earlier specs in the suite warm up the reloader machinery, which then fires during this spec. Other custom-error specs don't visibly flake because their consumers `rescue ConstName => e` (dynamic class lookup at raise time, walks the ancestor chain via `Module#===` *at the moment of raise*, with no captured snapshot), rather than RSpec's snapshot-then-compare pattern. ## What this PR does Adds a small local helper to the spec that matches by class name string, not by Class identity: ```ruby def safe_fetch_error(name, message_pattern = nil) satisfy("raise SafeFetch::#{name}#{" matching #{message_pattern.inspect}" if message_pattern}") do |error| error.class.name == "SafeFetch::#{name}" && (message_pattern.nil? || message_pattern.match?(error.message)) end end ``` Every `raise_error(described_class::FooError)` becomes `raise_error(safe_fetch_error('FooError'))`. Class names are strings; string equality survives any number of reloads. The semantic assertion ("this raised the right kind of error") is preserved. This is the pattern CLAUDE.md already endorses for this codebase: > Specs in parallel/reloading environments: prefer comparing `error.class.name` over constant class equality when asserting raised errors. ## Why this approach (even though it's suboptimal) To be honest with reviewers: **this is a workaround, not a root-cause fix.** Future spec authors who use `raise_error(SafeFetch::Foo)` in other files will hit the same flake. The "real" fix removes the failure mode at the source rather than dodging it per-spec. Here's the menu of options we considered and the tradeoffs: ### Option A — Spec-side name matcher (this PR) - **What:** the helper above. - **Pro:** one-file change, zero blast radius beyond the spec. - **Pro:** matches CLAUDE.md's documented stance. - **Con:** every new spec touching reloadable error classes needs to remember this pattern. It's a discipline tax, not a structural fix. - **Verdict:** chosen for this PR. ### Option B — Pin SafeFetch outside Zeitwerk ```ruby # config/application.rb Rails.autoloaders.main.ignore( Rails.root.join('lib/safe_fetch.rb'), Rails.root.join('lib/safe_fetch'), ) require Rails.root.join('lib/safe_fetch') ``` - **Pro:** real root-cause fix. `SafeFetch::*` constants become process-lifetime stable. The spec helper becomes unnecessary, every assertion in any spec works correctly. - **Pro:** preserves the public API exactly. - **Con:** loses hot-reload for SafeFetch in dev (need server restart to see edits). - **Con:** modifies `application.rb`, which has cross-team review weight. - **Verdict:** rejected for scope reasons in this PR; a sensible follow-up. ### Option C — Move error classes to a non-reloadable location Define top-level error classes in `config/initializers/safe_fetch_errors.rb`. Initializers run once at boot, constants are never reloaded. - **Pro:** identity-stable errors, no spec helper needed. - **Con:** API change — `SafeFetch::FileTooLargeError` becomes `SafeFetchFileTooLargeError` (or similar). Every consumer's `rescue` clause has to update. - **Con:** namespace pollution at top-level. - **Verdict:** rejected. The constraint of touching initializers is what motivated the simpler PR. ### Option D — Vendor SafeFetch as a path gem Move `lib/safe_fetch/` → `vendor/gems/safe_fetch/` with a gemspec. Bundler `require`s gems once; Zeitwerk has zero involvement. - **Pro:** structurally the most correct fix. Same identity stability as Option B, no Zeitwerk plumbing required. - **Pro:** zero API change for consumers. - **Con:** larger refactor (6+ files moved, gemspec authored, Gemfile/Gemfile.lock updated). - **Con:** SafeFetch becomes harder to iterate on in dev (gem-style edit-then-restart loop). - **Verdict:** rejected for scope in this PR; the cleanest long-term home for a security primitive. ### Option E — Drop custom errors, return a `Result` Refactor SafeFetch to yield a result hash (`{ ok: true, ... }` / `{ ok: false, kind: :unsafe_url, ... }`) instead of raising for anticipated outcomes. Failure kinds become symbols, which are interned for the process lifetime. - **Pro:** eliminates the failure mode at its true root — there are no custom exception classes to reload, anywhere. - **Pro:** forces explicit, exhaustive handling at every call site — a feature for a security primitive. - **Pro:** spec assertions become data assertions (`expect(result).to match(ok: false, kind: :too_large)`), which are robust against any reload, any reordering. - **Con:** paradigm shift away from the exception-driven style used everywhere else in the codebase. - **Con:** every consumer rewrites their `rescue` block into pattern-matched handling. - **Verdict:** rejected for scope in this PR; the architecturally cleanest answer if we ever revisit SafeFetch's API. ## Why we're shipping A despite knowing B–E are better This flake has been chewing CI time intermittently and previously took a partial fix (#14139). We need it stable *now*. Options B–E are real refactors with broader review surface (`application.rb`, consumer code, or the lib's public contract). Option A: - Costs nothing — one helper, mechanical replacements. - Doesn't preclude any of B/C/D/E later. The helper goes away cleanly once a root-cause fix lands. - Aligns with the project's documented guidance for this scenario. When SafeFetch next gets a substantive change, that's the right moment to fold in B or D. Until then, the spec is stable and CI gets its time back. ## What changed - `spec/lib/safe_fetch_spec.rb`: added `safe_fetch_error(name, message_pattern = nil)` helper; converted all 14 `raise_error(described_class::FooError[, /regex/])` assertions to `raise_error(safe_fetch_error('FooError'[, /regex/]))`. |