deb259c8d2ee09cdbd3856d93b12bb7cd0bceec6
6112
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
deb259c8d2 |
chore(deps): bump rack from 3.2.5 to 3.2.6 (#13987)
Bumps [rack](https://github.com/rack/rack) from 3.2.5 to 3.2.6. <details> <summary>Release notes</summary> <p><em>Sourced from <a href="https://github.com/rack/rack/releases">rack's releases</a>.</em></p> <blockquote> <h2>v3.2.6</h2> <p><strong>Full Changelog</strong>: <a href="https://github.com/rack/rack/compare/v3.2.5...v3.2.6">https://github.com/rack/rack/compare/v3.2.5...v3.2.6</a></p> </blockquote> </details> <details> <summary>Changelog</summary> <p><em>Sourced from <a href="https://github.com/rack/rack/blob/main/CHANGELOG.md">rack's changelog</a>.</em></p> <blockquote> <h2>[3.2.6] - 2026-04-01</h2> <h3>Security</h3> <ul> <li><a href="https://github.com/advisories/GHSA-7mqq-6cf9-v2qp">CVE-2026-34763</a> Root directory disclosure via unescaped regex interpolation in <code>Rack::Directory</code>.</li> <li><a href="https://github.com/advisories/GHSA-v569-hp3g-36wr">CVE-2026-34230</a> Avoid O(n^2) algorithm in <code>Rack::Utils.select_best_encoding</code> which could lead to denial of service.</li> <li><a href="https://github.com/advisories/GHSA-qfgr-crr9-7r49">CVE-2026-32762</a> Forwarded header semicolon injection enables Host and Scheme spoofing.</li> <li><a href="https://github.com/advisories/GHSA-vgpv-f759-9wx3">CVE-2026-26961</a> Raise error for multipart requests with multiple boundary parameters.</li> <li><a href="https://github.com/advisories/GHSA-q4qf-9j86-f5mh">CVE-2026-34786</a> <code>Rack::Static</code> <code>header_rules</code> bypass via URL-encoded path mismatch.</li> <li><a href="https://github.com/advisories/GHSA-q2ww-5357-x388">CVE-2026-34831</a> <code>Content-Length</code> mismatch in <code>Rack::Files</code> error responses.</li> <li><a href="https://github.com/advisories/GHSA-x8cg-fq8g-mxfx">CVE-2026-34826</a> Multipart byte range processing allows denial of service via excessive overlapping ranges.</li> <li><a href="https://github.com/advisories/GHSA-g2pf-xv49-m2h5">CVE-2026-34835</a> <code>Rack::Request</code> accepts invalid Host characters, enabling host allowlist bypass.</li> <li><a href="https://github.com/advisories/GHSA-qv7j-4883-hwh7">CVE-2026-34830</a> <code>Rack::Sendfile</code> header-based <code>X-Accel-Mapping</code> regex injection enables unauthorized <code>X-Accel-Redirect</code>.</li> <li><a href="https://github.com/advisories/GHSA-h2jq-g4cq-5ppq">CVE-2026-34785</a> <code>Rack::Static</code> prefix matching can expose unintended files under the static root.</li> <li><a href="https://github.com/advisories/GHSA-8vqr-qjwx-82mw">CVE-2026-34829</a> Multipart parsing without <code>Content-Length</code> header allows unbounded chunked file uploads.</li> <li><a href="https://github.com/advisories/GHSA-v6x5-cg8r-vv6x">CVE-2026-34827</a> Multipart header parsing allows denial of service via escape-heavy quoted parameters.</li> <li><a href="https://github.com/advisories/GHSA-rx22-g9mx-qrhv">CVE-2026-26962</a> Improper unfolding of folded multipart headers preserves CRLF in parsed parameter values.</li> </ul> </blockquote> </details> <details> <summary>Commits</summary> <ul> <li><a href="https://github.com/rack/rack/commit/e1f22fdbe99afd2126b6fbf05bb12399359574b7"><code>e1f22fd</code></a> Bump patch version.</li> <li><a href="https://github.com/rack/rack/commit/31989fd7bb6f806fdb3cfa4e9aec1fe8434f47d1"><code>31989fd</code></a> Fix typo in test.</li> <li><a href="https://github.com/rack/rack/commit/d268165e390e17b83573fec916dcdef6304a8b4b"><code>d268165</code></a> Fix test expectation.</li> <li><a href="https://github.com/rack/rack/commit/8f425de0ee75a2f3cdfbfdd57858c1910b7645ff"><code>8f425de</code></a> Add Ruby v4.0 to the test matrix.</li> <li><a href="https://github.com/rack/rack/commit/bf830426ce5b3daccb5a226b733703c86504ceba"><code>bf83042</code></a> Drop EOL Rubies from external tests.</li> <li><a href="https://github.com/rack/rack/commit/d50c4d3dab62fa80b2a276271d0d4fb338cfa7df"><code>d50c4d3</code></a> Implement OBS unfolding for multipart requests per RFC 5322 2.2.3</li> <li><a href="https://github.com/rack/rack/commit/bfb69142dbe2a1e3298ad52d12935938d1b58205"><code>bfb6914</code></a> Limit the number of quoted escapes during multipart parsing</li> <li><a href="https://github.com/rack/rack/commit/b3e5945c648c5a5b6982e5072b26e51990991229"><code>b3e5945</code></a> Add Content-Length size check in Rack::Multipart::Parser</li> <li><a href="https://github.com/rack/rack/commit/7a8f32696609b88e2c4c1f09d473a1d2d837ed4b"><code>7a8f326</code></a> Fix root prefix bug in Rack::Static</li> <li><a href="https://github.com/rack/rack/commit/a57bc140247f904dc1e3302badedcb73645072c7"><code>a57bc14</code></a> Only do a simple substitution on the x-accel-mapping paths</li> <li>Additional commits viewable in <a href="https://github.com/rack/rack/compare/v3.2.5...v3.2.6">compare view</a></li> </ul> </details> <br /> 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> |
||
|
|
9c8cfc40b6 |
chore(deps): bump dompurify from 3.3.2 to 3.4.0 (#14074)
Bumps [dompurify](https://github.com/cure53/DOMPurify) from 3.3.2 to 3.4.0. <details> <summary>Release notes</summary> <p><em>Sourced from <a href="https://github.com/cure53/DOMPurify/releases">dompurify's releases</a>.</em></p> <blockquote> <h2>DOMPurify 3.4.0</h2> <p><strong>Most relevant changes:</strong></p> <ul> <li>Fixed a problem with <code>FORBID_TAGS</code> not winning over <code>ADD_TAGS</code>, thanks <a href="https://github.com/kodareef5"><code>@kodareef5</code></a></li> <li>Fixed several minor problems and typos regarding MathML attributes, thanks <a href="https://github.com/DavidOliver"><code>@DavidOliver</code></a></li> <li>Fixed <code>ADD_ATTR</code>/<code>ADD_TAGS</code> function leaking into subsequent array-based calls, thanks <a href="https://github.com/1Jesper1"><code>@1Jesper1</code></a></li> <li>Fixed a missing <code>SAFE_FOR_TEMPLATES</code> scrub in <code>RETURN_DOM</code> path, thanks <a href="https://github.com/bencalif"><code>@bencalif</code></a></li> <li>Fixed a prototype pollution via <code>CUSTOM_ELEMENT_HANDLING</code>, thanks <a href="https://github.com/trace37labs"><code>@trace37labs</code></a></li> <li>Fixed an issue with <code>ADD_TAGS</code> function form bypassing <code>FORBID_TAGS</code>, thanks <a href="https://github.com/eddieran"><code>@eddieran</code></a></li> <li>Fixed an issue with <code>ADD_ATTR</code> predicates skipping URI validation, thanks <a href="https://github.com/christos-eth"><code>@christos-eth</code></a></li> <li>Fixed an issue with <code>USE_PROFILES</code> prototype pollution, thanks <a href="https://github.com/christos-eth"><code>@christos-eth</code></a></li> <li>Fixed an issue leading to possible mXSS via Re-Contextualization, thanks <a href="https://github.com/researchatfluidattacks"><code>@researchatfluidattacks</code></a> and others</li> <li>Fixed an issue with closing tags leading to possible mXSS, thanks <a href="https://github.com/frevadiscor"><code>@frevadiscor</code></a></li> <li>Fixed a problem with the type dentition patcher after Node version bump</li> <li>Fixed freezing BS runs by reducing the tested browsers array</li> <li>Bumped several dependencies where possible</li> <li>Added needed files for OpenSSF scorecard checks</li> </ul> <p><strong>Published Advisories are here:</strong> <a href="https://github.com/cure53/DOMPurify/security/advisories?state=published">https://github.com/cure53/DOMPurify/security/advisories?state=published</a></p> <h2>DOMPurify 3.3.3</h2> <ul> <li>Fixed an engine requirement for Node 20 which caused hiccups, thanks <a href="https://github.com/Rotzbua"><code>@Rotzbua</code></a></li> </ul> </blockquote> </details> <details> <summary>Commits</summary> <ul> <li><a href="https://github.com/cure53/DOMPurify/commit/5b16e0b892e82b1779d62b9928b43c4c4ff290b9"><code>5b16e0b</code></a> Getting 3.x branch ready for 3.4.0 release (<a href="https://redirect.github.com/cure53/DOMPurify/issues/1250">#1250</a>)</li> <li><a href="https://github.com/cure53/DOMPurify/commit/8bcbf73ae7eb56e7b4f1300b66cf543342c7ee27"><code>8bcbf73</code></a> chore: Preparing 3.3.3 release</li> <li><a href="https://github.com/cure53/DOMPurify/commit/5faddd60af7b4d612f32a0c6b44432b77c8c490c"><code>5faddd6</code></a> fix: engine requirement (<a href="https://redirect.github.com/cure53/DOMPurify/issues/1210">#1210</a>)</li> <li><a href="https://github.com/cure53/DOMPurify/commit/0f91e3add5c028bc4110c513b0c2571b284c35af"><code>0f91e3a</code></a> Update README.md</li> <li><a href="https://github.com/cure53/DOMPurify/commit/d5ff1a8c605df1df998c2e7df2c4c8ac762b0dea"><code>d5ff1a8</code></a> Merge branch 'main' of github.com:cure53/DOMPurify</li> <li><a href="https://github.com/cure53/DOMPurify/commit/c3efd489010366e755de9d65fd741888fd8b7462"><code>c3efd48</code></a> fix: moved back from jsdom 28 to jsdom 20</li> <li><a href="https://github.com/cure53/DOMPurify/commit/988b888108c8df911ef37e68d0e26c85ad90e885"><code>988b888</code></a> fix: moved back from jsdom 28 to jsdom 20</li> <li><a href="https://github.com/cure53/DOMPurify/commit/2726c74e9c6a0645127d1630e5ca49f64bc9fe67"><code>2726c74</code></a> chore: Preparing 3.3.2 release</li> <li><a href="https://github.com/cure53/DOMPurify/commit/6202c7e43e9df01ba606396aed60fbae5583f7a1"><code>6202c7e</code></a> build(deps): bump <code>@tootallnate/once</code> and jsdom (<a href="https://redirect.github.com/cure53/DOMPurify/issues/1204">#1204</a>)</li> <li><a href="https://github.com/cure53/DOMPurify/commit/302b51de22535cc90235472c52e3401bedd46f80"><code>302b51d</code></a> fix: Expanded the regex ever so slightly to also cover script</li> <li>Additional commits viewable in <a href="https://github.com/cure53/DOMPurify/compare/3.3.2...3.4.0">compare view</a></li> </ul> </details> <br /> [](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: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> |
||
|
|
8f532f45ff | feat: add attachments section to conversation sidebar (#14371) | ||
|
|
00ba468486 | fix(macros): disable public visibility for agents (#14349) | ||
|
|
b8108b71c1 |
fix(tiktok): Resolve media upload failures and gate attachments by conversation capability (#13643)
This PR fixes TikTok attachment send failures and adds a capability-based guard so attachments are only enabled for conversations that support media sending. - Fixed TikTok media upload request formatting so TikTok accepts image uploads reliably. - Added TikTok capability check (IMAGE_SEND) during conversation creation. - Stored capability in conversation.additional_attributes.tiktok_capabilities. - Updated reply composer UI to disable/hide attachment upload for TikTok conversations where image_send is false. Fixes https://linear.app/chatwoot/issue/CW-6532/enable-attachments-based-on-the-conversation-capability and https://linear.app/chatwoot/issue/CW-6996/unable-to-send-image-attachments-to-tiktok-customer-400-parsing-error --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com> |
||
|
|
cc5974da9b |
feat(inbox): Add beta badge for TikTok and Voice channels (#14378)
TikTok and Voice channels in the inbox creation flow now display a small "Beta" badge next to their title, signaling that these integrations are still being polished while keeping them available for users to try. Fixes https://linear.app/chatwoot/issue/CW-7026/add-beta-label-for-tiktok-and-voice-inboxes --------- Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
2192af80f4 |
fix: html-escape captured values in helpcenter article markdown embeds (#14140)
Embed templates interpolate regex captures from user-authored article URLs into HTML attribute values. CommonMark's angle-bracket link destination syntax allows characters that the capture regexes don't filter, so the unescaped substitution could produce malformed attribute output. Escaping at substitution time keeps the render deterministic regardless of the URL. ### How was this tested? Added specs. Fixes [CW-6934](https://linear.app/chatwoot/issue/CW-6934/) Co-authored-by: Sony Mathew <sony@chatwoot.com> Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> |
||
|
|
941c8a86b4 |
fix: use a dedicated PAT for ghsa linear sync gh action (#14364)
The default `GITHUB_TOKEN` cannot read `security-advisories`; that endpoint requires the `repository_advisories` permission, which is not available to the GitHub Actions installation token. Switched to a fine-grained PAT stored in `GHSA_READ_TOKEN`. Tested locally: the same PAT returns the full triage list Changes ---- - Switch to custom token - Add a discord alert for new advisories - Switch to python |
||
|
|
a9ac1c633d |
fix: added HMAC validation for Whatsapp and Instagram webhooks (#14280)
## Description * Added Meta webhook HMAC validation in meta_token_verify_concern.rb. * Wired it into instagram_controller.rb and whatsapp_controller.rb. * WhatsApp now verifies X-Hub-Signature-256 with WHATSAPP_APP_SECRET. * Instagram now verifies with either FB_APP_SECRET or INSTAGRAM_APP_SECRET. * Updated request specs so missing/invalid signatures return 401 and valid signatures still enqueue jobs. Fixes # (issue): [CW-6786](https://linear.app/chatwoot/issue/CW-6786/ghsa-7rw7-pc8v-mrr3-unauthenticated-message-injection-via-missing) ## 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? * Updated the controller specs and ran them successfully. * The original issue is no longer reproducible. ## 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: Muhsin Keloth <muhsinkeramam@gmail.com> |
||
|
|
70f799ab35 |
fix(captain): add v1 handoff classifier [AI-137] (#14337)
# Pull Request Template ## Description Captain (v1) makes false promises by saying it will handoff but doesn't. This happens due to an exact string match comparison and the prompt gives the model a lot of responsibilities: - identity - what to respond - obey custom instructions - decide on tool calls This PR decouples responsibility, the core prompt responds, and an additional llm call evaluates if handoff was needed or not after that message. ## 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. Locally ## 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 |
||
|
|
8cc36e1938 | feat: inline url embeds in article editor (#14284) | ||
|
|
c1d167bd64 |
fix: prevent -- signature delimiter rendering as \ in bubble (#14134)
# Pull Request Template ## Description Fixes https://linear.app/chatwoot/issue/CW-6903/signature-delimiter-renders-as-h2-when-using-enter-line-before **1**. Fixes an issue where the signature delimiter `--` gets parsed as an H2 when using **Enter** (new paragraph) before or after it, causing it to render as a bold `\` in the message bubble. * Ensures `--` renders as plain text * Aligns renderer with parser behavior (both disable `lheading`) * Prevents stray `\` from appearing as heading text **2**. Also fixes a related editor issue where toggling signature **off** leaves behind a stray `\` or `-- \`. * Strips blank paragraph markers (`\`) and dangling hard breaks (`\<newline>`) from ProseMirror serializer * Applied in both `appendSignature` and `removeSignature` * Replaces `trimEnd()` with shared helpers (`trimTrailingBlanks` / `stripTrailingBlankMarkers`) ## Type of change - [x] Bug fix (non-breaking change which fixes an issue) ## How Has This Been Tested? #### Screenshots **Before** <img width="194" height="204" alt="image" src="https://github.com/user-attachments/assets/b286ab50-7f89-4910-a552-1568902b93b3" /> **After** <img width="194" height="220" alt="image" src="https://github.com/user-attachments/assets/658cd543-bce2-46e2-a319-35e5374f1aef" /> **Editor** https://linear.app/chatwoot/issue/CW-6903/signature-delimiter-renders-as-in-h2-when-using-enter-line#comment-5814b882 ### Steps #### Editor 1. Enable agent signature 2. Add and remove new lines around the signature using Enter/shift enter 3. Toggle signature off 4. Notice stray `\` or `-- \` remains #### Bubble 1. Enable agent signature 2. Send a message using Enter between lines 3. Verify `--` renders correctly (no H2, no bold `\`) ## 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: Muhsin Keloth <muhsinkeramam@gmail.com> |
||
|
|
6386eec5e7 |
fix: regex validation not applied for custom text attributes in UI (#14110)
# Pull Request Template ## Description This PR fixes multiple issues related to regex patterns and validation for custom attributes. 1. Fixed regex patterns being double-escaped when saving from Add and Edit flows 2. Fixed regex validation not being enforced in the widget pre-chat form 3. Minor UI improvements in the Add/Edit custom attribute dialog Fixes [CW-6625](https://linear.app/chatwoot/issue/CW-6625/bug-report-custom-attribute-regex-validation-not-working-in-ui), https://github.com/chatwoot/chatwoot/issues/13771 ## Type of change - [x] Bug fix (non-breaking change which fixes an issue) ## How Has This Been Tested? **Loom video** **Before** https://www.loom.com/share/14f1983a8bc84f9fabc3663afd83cd50 **After** https://www.loom.com/share/867c0484741140c1944fcbd43914c9c0 ## 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 |
||
|
|
21c0f4dc52 |
fix(transcription): guard Whisper 25MB limit and zero temperature for stable output (#14335)
Two production-grade fixes to the existing audio transcription service. **Independent of the WhatsApp Calling work** — these affect every audio attachment that goes through Whisper (voice notes, call recordings, voicemails, etc.). ## Closes - [PLA-151 — PR-5: Recording Upload + Transcription Pipeline](https://linear.app/chatwoot/issue/PLA-151/pr-5-recording-upload-transcription-pipeline) ## Why this is needed ### 1. Whisper rejects payloads larger than 25 MB OpenAI's [Whisper API](https://platform.openai.com/docs/guides/speech-to-text) hard-caps file uploads at 25 MB. Long audio recordings — voice notes from chatty contacts, ~70+ min Opus call recordings — currently hit OpenAI with the full payload and 413 (\`Payload Too Large\`). The job retries via the existing \`Faraday::BadRequestError\` discard path, but the agent still sees a transcription failure for an attachment we knew was too big up front. This PR adds a pre-flight \`audio_too_large?\` check via the blob's \`byte_size\` and returns a controlled error without hitting OpenAI. The audio attachment is preserved (agents can still listen), only the transcription is skipped. ### 2. Whisper hallucinates on silence at non-zero temperature At \`temperature: 0.4\` (the previous value), Whisper produces well-documented hallucinated repeats on silence and near-silent segments — e.g. \`Oh, dear. Oh, dear. Oh, dear.\` filling the transcript. This shows up in real recordings whenever there's a hold or quiet moment. \`temperature: 0.0\` matches OpenAI's recommended default for transcription and eliminates the spirals. Reference: [openai/whisper#928](https://github.com/openai/whisper/discussions/928), [openai-python#1010](https://github.com/openai/openai-python/issues/1010). ## Are WhatsApp call recordings already handled? Yes — by the existing pipeline, **before this PR**: \`\`\` Browser MediaRecorder → upload_recording (PR-4) → @call.message.attachments.create!(file_type: :audio, ...) → Enterprise::Concerns::Attachment#enqueue_audio_transcription (after_create_commit hook) → Messages::AudioTranscriptionJob.perform_later(attachment.id) → Messages::AudioTranscriptionService → Whisper \`\`\` The \`after_create_commit\` hook already fires for every audio attachment regardless of source. PR-4's \`upload_recording\` endpoint creates the attachment; the existing job/service take it from there. No new wiring needed. This PR just makes the existing service more robust: - Calls longer than ~70 min (Opus 48 kbps) no longer 413 against OpenAI - Quiet recordings no longer produce hallucinated transcripts ## How to test \`\`\`ruby # In rails console with a real audio attachment: service = Messages::AudioTranscriptionService.new(Attachment.audio.last) # Normal-sized audio: unchanged behaviour service.perform # => { success: true, transcriptions: ... } # Large audio: new guard returns error instead of 413-ing OpenAI allow(attachment.file.blob).to receive(:byte_size).and_return(30.megabytes) service.perform # => { error: 'Audio too large for Whisper' } \`\`\` Existing transcription specs cover the happy path; one new spec exercises the byte-limit guard. ## Risk Low. Both changes are pre-flight guards or parameter values — they reduce the surface of OpenAI calls that can fail. Failure to transcribe is already non-fatal (the audio attachment is preserved either way). |
||
|
|
624c6c90fd |
chore: sync GitHub security advisories to Linear (#14359)
Sync new GHSA reports to Linear. Fixes https://linear.app/chatwoot/issue/CW-7006 |
||
|
|
0bd0cab868 |
feat(voice): Attach call recordings + show call duration on the bubble (#14344)
When an inbound voice call ends, the conversation bubble now (1) renders an inline audio player as soon as Twilio finishes the recording and (2) shows the call duration alongside "Call ended" so the agent gets the at-a-glance summary without opening the recording. Fixes https://linear.app/chatwoot/issue/PLA-118/feat-recordings-on-calls-should-be-attached-on-the-conversation and https://linear.app/chatwoot/issue/PLA-119/duration-of-the-call-is-not-visible-on-the-chat-bubble ## How to test 1. Set up a Twilio voice inbox and trigger an inbound call. 2. Answer the call from an agent, talk for a few seconds, then hang up. 3. As soon as the call ends, the bubble should read **"Call ended — 0:NN"** (where NN is the call duration in seconds). 4. Wait a few seconds for Twilio to finish processing the recording (usually <30s after hangup). 5. The same bubble should now show an inline audio player below the duration. Press play; the recording should be audible. 6. Refresh the page — both the duration and the player should still be there. 7. End a second call on the same conversation — its bubble should get its own duration + player, independent of the first. --------- Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com> |
||
|
|
2dee7457cd |
fix: set minimal top-level permissions on workflows (#14358)
- Fix CodeQL alerts by declaring read-only GITHUB_TOKEN scope at the workflow level. The codespace image publish workflow additionally needs packages: write to push to ghcr.io. |
||
|
|
ea87610999 |
feat(voice): Join active call from the conversation bubble (#14343)
Agents can now click **Join call** directly on the incoming call bubble in the conversation timeline. If they refresh the page or miss the floating widget while a call is still ringing, the bubble becomes the recovery affordance — one click joins the conference, no need to wait for the next event. The button only appears when the call is still ringing, no other agent has claimed it, and the conversation is unassigned or assigned to the current agent (mirroring the floating widget's eligibility rules). It disappears as soon as anyone joins the call or it ends. Fixes https://linear.app/chatwoot/issue/PLA-117/ability-to-join-the-call-by-clicking-on-call-bubble-in-a-conversation ## How to test 1. Set up a Twilio voice inbox and trigger an inbound call to it. 2. As an agent who is eligible to answer (unassigned conversation, or assigned to you), open the conversation **without answering from the floating widget**. The bubble should show a teal **Join call** link under "Not answered yet". 3. Refresh the page mid-ring — the link should still be there. 4. Click **Join call** — you should be connected to the conference, the bubble should flip to "Call in progress / You answered", and the link should disappear. 5. As a second agent who is **not** eligible (conversation assigned to someone else), open the same conversation — the link should not appear. 6. Wait for the call to end — the bubble should show "Call ended" with no Join link. --------- Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com> |
||
|
|
d00867d636 | fix: captain auto sync scheduler config (#14336) | ||
|
|
a01adf860a |
fix: [CW-7001] Limit emails fetch (#14354)
This PR limits IMAP email fetching to 500 messages per sync run to avoid expensive/long-running mailbox scans. It also filters out already-imported emails and Chatwoot-generated notification emails during the header fetch phase, before fetching full email bodies, reducing unnecessary IMAP work. Fixes #CW-7001 (issue) : https://linear.app/chatwoot/issue/CW-7001/emails-not-syncing |
||
|
|
2a30e7b082 |
fix: render agent variables in automation messages (#14338)
# Pull Request Template
## Description
This PR fixes an issue where agent variables like
`{{agent.name}}`,`{{agent.first_name}}`, `{{agent.last_name}}`, and
`{{agent.email}}` were not rendering in automation messages.
In automation, these either showed blank or returned `Liquid error:
internal`, while the same variables worked fine in macros.
**Cause**
Automation messages are created without a sender, so agent data was
missing during variable rendering. This also caused errors in name
handling, and `email` was not defined at all.
**Solution**
* Handle missing agent data safely to avoid errors
* Add support for `{{agent.email}}`
* Fallback to conversation assignee when sender is not present
Fixes
https://linear.app/chatwoot/issue/CW-6979/template-variables-not-working-in-automated-messages
## Type of change
- [x] Bug fix (non-breaking change which fixes an issue)
## How Has This Been Tested?
### Screenshots
**Automation**
<img width="759" height="284" alt="image"
src="https://github.com/user-attachments/assets/61a877b7-4984-4a7f-bbef-b8c510dcbdfe"
/>
**Before**
<img width="404" height="105" alt="image"
src="https://github.com/user-attachments/assets/da665ce8-137d-4249-8ee5-a1acc11391db"
/>
**After**
<img width="564" height="132" alt="image"
src="https://github.com/user-attachments/assets/6a80d67c-49c8-4658-b782-ae4acbc77256"
/>
## 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
|
||
|
|
28ec1794f4 |
feat(voice): add WhatsApp Cloud Calling provider methods (#14312)
Adds the Meta WhatsApp Cloud API surface needed for browser-based calling. This is the second slice of the WhatsApp calling feature, sitting on top of `feat/voice-call-model-wiring` and consumed by later PRs (incoming-webhook pipeline, call service, frontend). This PR ships only the provider-level HTTP wrapper and one error class. It is feature-flag-free and does not change any user-visible behaviour on its own — without later PRs, no caller invokes these methods. ## Linear - https://linear.app/chatwoot/issue/PLA-148/pr-2-meta-cloud-api-provider-methods ## What changed - Add `Whatsapp::Providers::WhatsappCloudCallMethods` (`enterprise/app/services/whatsapp/providers/whatsapp_cloud_call_methods.rb`) wrapping six Meta endpoints: - `pre_accept_call`, `accept_call`, `reject_call`, `terminate_call` — `POST /{phone_id}/calls` with the relevant action payload. - `send_call_permission_request` — `POST /{phone_id}/messages` interactive `call_permission_request`. - `initiate_call` — `POST /{phone_id}/calls` with `audio`/`offer` session. - Prepend the module into `Whatsapp::Providers::WhatsappCloudService` only if defined, so OSS continues to work without the enterprise overlay. - Add `Voice::CallErrors::NoCallPermission` (`enterprise/lib/voice/call_errors.rb`) — raised when Meta returns error code `138006` from `initiate_call`. The remaining call-service errors (`NotRinging`, `AlreadyAccepted`, `CallFailed`) will land with PR-4. ## How to test There is no UI in this PR. Smoke-test from a Rails console with a WhatsApp inbox configured for calling: ```ruby inbox = Inbox.find(<id>) svc = inbox.channel.provider_service svc.respond_to?(:initiate_call) # => true svc.respond_to?(:send_call_permission_request) # => true # Optional live calls (require a real phone + Meta call-permission opt-in): svc.send_call_permission_request('15551234567') svc.initiate_call('15551234567', '<sdp_offer>') ``` Failure path: `initiate_call` against a contact who has not granted call permission should raise `Voice::CallErrors::NoCallPermission` with Meta's user-facing message. |
||
|
|
64790ea204 |
fix: Redirect to the conversation URL if custom_view is not available (#14340)
When an agent shares a conversation link copied from a custom view (e.g.
/custom_view/{id}/conversations/{id}), the link previously broke for
recipients who didn't have access to that custom view. The conversation
now loads regardless — if the custom view isn't available to the
recipient, they're redirected to the direct conversation URL.
### How to reproduce
1. As Agent A, open a conversation from inside a personal custom view
and copy the URL from the address bar.
2. Share the URL with Agent B who does not have access to that custom
view.
3. Before this fix, the link failed to load the conversation. After this
fix, Agent B lands on the conversation via the direct URL.
### What changed
- Added a beforeEnter guard on the conversations_through_folders route.
It checks the user's available conversation custom views (fetching them
on demand for deep links), and if the foldersId in the URL isn't among
them, redirects to the inbox_conversation route with the same
conversation_id.
---------
Co-authored-by: iamsivin <iamsivin@gmail.com>
|
||
|
|
517b67dfd6 |
feat: Add rich template preview for WhatsApp & Twilio Templates (#13206)
This PR introduces a comprehensive, display-only template preview system for both WhatsApp and Twilio templates rendering of all supported template types. This lays the foundation for rich template previews across: - Template selection modals - Campaign creation flows - Message composer - Message screen. <img width="1818" height="2058" alt="template-preview" src="https://github.com/user-attachments/assets/9833b28c-f824-4568-8f74-6da09ac61f62" /> --------- Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Co-authored-by: iamsivin <iamsivin@gmail.com> Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com> |
||
|
|
353089473e |
feat(voice): Assignment aware visibility and join conflict for inbound calls (#14333)
### Description Inbound voice calls now route ownership cleanly: the call widget is hidden from agents who aren't the conversation assignee, the first agent to pick up becomes the assignee, and any later join attempt by another agent is rejected with a clear "<agent> is already handling the call." alert. Closes https://linear.app/chatwoot/issue/PLA-98/inbound-voice-calls-assignment-aware-visibility-auto-assignment-on ### How to test 1. As Agent A and Agent B, open the dashboard for the same voice inbox in two browsers. 2. Place an inbound call to the inbox with the conversation **unassigned** — both agents should see the call widget. 3. Have Agent A click **Join**. Agent A's widget transitions to the active call; Agent B's widget disappears (conversation is now assigned to Agent A). 4. While the call is in progress, attempt to join from a third agent (e.g., via the bubble in the conversation timeline) — the join is rejected with the toast `Agent A is already handling the call.` 5. Resolve the conversation, then place a second call to a conversation that is already manually assigned to Agent A — only Agent A sees the widget; nobody else does. 6. Race test: trigger two near-simultaneous join attempts (two agents click Join within a few hundred ms of each other) — exactly one wins; the other gets the conflict alert. --------- Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com> |
||
|
|
1124c1b4c2 |
feat(voice): Wire Twilio voice flow through unified call model (#14091)
Twilio voice now uses first-class `Call` records as the source of truth for call state, instead of storing it on `conversation.additional_attributes` and `conversation.identifier`. Each call gets its own record, its own `voice_call` bubble matched by `call_sid`, and its own conference name keyed off `Call.id`. Multiple calls on the same conversation (for `lock_to_single_conversation` inboxes) now work correctly, and the conversation card stays in sync with the real latest message. Fixes https://linear.app/chatwoot/issue/PLA-121/lock-to-single-thread --------- Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com> |
||
|
|
cd9c8e3303 |
fix: skip self-mention notification in private notes (#14318)
When an agent mentions themselves in a private note, they no longer receive a redundant notification for their own mention. Closes: #4096 # Pull Request Template ## Description Agents who mention themselves in a private note no longer receive a conversation_mention notification. Previously, the mention service would generate a notification for every mentioned user without checking whether the sender and the mentioned user were the same person. |
||
|
|
80fccbc526 |
fix: render slack emoji shortcodes as unicode characters (#12928)
This PR fixes an issue where Slack emojis are rendered as text shortcodes (e.g. 🚀) instead of the actual emoji characters in Chatwoot messages. It introduces a new EmojiFormatter class that uses the emoji-data mapping to convert shortcodes to unicode characters. --------- Co-authored-by: Sony Mathew <sony@chatwoot.com> Co-authored-by: Sony Mathew <2040199+sony-mathew@users.noreply.github.com> |
||
|
|
bcdb73502e |
chore(deps): bump addressable from 2.8.7 to 2.9.0 (#14019)
Bumps [addressable](https://github.com/sporkmonger/addressable) from 2.8.7 to 2.9.0. <details> <summary>Changelog</summary> <p><em>Sourced from <a href="https://github.com/sporkmonger/addressable/blob/main/CHANGELOG.md">addressable's changelog</a>.</em></p> <blockquote> <h2>Addressable 2.9.0 <!-- raw HTML omitted --></h2> <ul> <li>fixes ReDoS vulnerability in Addressable::Template#match (fixes incomplete remediation in 2.8.10)</li> </ul> <h2>Addressable 2.8.10 <!-- raw HTML omitted --></h2> <ul> <li>fixes ReDoS vulnerability in Addressable::Template#match</li> </ul> <h2>Addressable 2.8.9 <!-- raw HTML omitted --></h2> <ul> <li>Reduce gem size by excluding test files (<a href="https://redirect.github.com/sporkmonger/addressable/issues/569">#569</a>)</li> <li>No need for bundler as development dependency (<a href="https://redirect.github.com/sporkmonger/addressable/issues/571">#571</a>, <a href="https://github.com/sporkmonger/addressable/commit/5fc1d93">5fc1d93</a>)</li> <li>idna/pure: stop building the useless <code>COMPOSITION_TABLE</code> (removes the <code>Addressable::IDNA::COMPOSITION_TABLE</code> constant) (<a href="https://redirect.github.com/sporkmonger/addressable/issues/564">#564</a>)</li> </ul> <p><a href="https://redirect.github.com/sporkmonger/addressable/issues/569">#569</a>: <a href="https://redirect.github.com/sporkmonger/addressable/pull/569">sporkmonger/addressable#569</a> <a href="https://redirect.github.com/sporkmonger/addressable/issues/571">#571</a>: <a href="https://redirect.github.com/sporkmonger/addressable/pull/571">sporkmonger/addressable#571</a> <a href="https://redirect.github.com/sporkmonger/addressable/issues/564">#564</a>: <a href="https://redirect.github.com/sporkmonger/addressable/pull/564">sporkmonger/addressable#564</a></p> <h2>Addressable 2.8.8 <!-- raw HTML omitted --></h2> <ul> <li>Replace the <code>unicode.data</code> blob by a ruby constant (<a href="https://redirect.github.com/sporkmonger/addressable/issues/561">#561</a>)</li> <li>Allow <code>public_suffix</code> 7 (<a href="https://redirect.github.com/sporkmonger/addressable/issues/558">#558</a>)</li> </ul> <p><a href="https://redirect.github.com/sporkmonger/addressable/issues/561">#561</a>: <a href="https://redirect.github.com/sporkmonger/addressable/pull/561">sporkmonger/addressable#561</a> <a href="https://redirect.github.com/sporkmonger/addressable/issues/558">#558</a>: <a href="https://redirect.github.com/sporkmonger/addressable/pull/558">sporkmonger/addressable#558</a></p> </blockquote> </details> <details> <summary>Commits</summary> <ul> <li><a href="https://github.com/sporkmonger/addressable/commit/0c3e8589b23d4402903a9b4e1fdeba4e43c52ca4"><code>0c3e858</code></a> Revving version and changelog</li> <li><a href="https://github.com/sporkmonger/addressable/commit/91915c1f7aafa3e2c9f42e2f4e21d948c7a861b8"><code>91915c1</code></a> Fixing additional vulnerable paths</li> <li><a href="https://github.com/sporkmonger/addressable/commit/a091e39ff02fc321b21dea3a0df585bef2ba3744"><code>a091e39</code></a> Add many more adversarial test cases to ensure we don't have any ReDoS regres...</li> <li><a href="https://github.com/sporkmonger/addressable/commit/463a819665a3b85ce5ce894c90bd7bfa3b9d2e15"><code>463a819</code></a> Regenerate gemspec on newer rubygems</li> <li><a href="https://github.com/sporkmonger/addressable/commit/0afcb0b9672bee301e5e96ed850fec05b2fcabb0"><code>0afcb0b</code></a> Improve from O(n^2) to O(n)</li> <li><a href="https://github.com/sporkmonger/addressable/commit/c87f768f22ab00376ed2f8cb106f59c9d0652d3a"><code>c87f768</code></a> Fix a ReDoS vulnerability in URI template matching</li> <li><a href="https://github.com/sporkmonger/addressable/commit/0d7e9b259fb0940d1a85064b04f678a7984409a5"><code>0d7e9b2</code></a> Fix links for 2.8.9 in CHANGELOG (<a href="https://redirect.github.com/sporkmonger/addressable/issues/573">#573</a>)</li> <li><a href="https://github.com/sporkmonger/addressable/commit/e2091200b31553f19248eb871f071852409796f8"><code>e209120</code></a> Update version, gemspec, and CHANGELOG for 2.8.9 (<a href="https://redirect.github.com/sporkmonger/addressable/issues/572">#572</a>)</li> <li><a href="https://github.com/sporkmonger/addressable/commit/387587492b6536748ed12a11c3fdb44a48885f28"><code>3875874</code></a> Reduce gem size by excluding test files (<a href="https://redirect.github.com/sporkmonger/addressable/issues/569">#569</a>)</li> <li><a href="https://github.com/sporkmonger/addressable/commit/3e57cc6018f94231aabb47fd341acd1b40f1e71a"><code>3e57cc6</code></a> CI: back to <code>windows-2022</code> for MRI job</li> <li>Additional commits viewable in <a href="https://github.com/sporkmonger/addressable/compare/addressable-2.8.7...addressable-2.9.0">compare view</a></li> </ul> </details> <br /> [](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> |
||
|
|
d634ced4e9 |
chore(deps-dev): bump uuid from 13.0.0 to 14.0.0 in /tests/playwright (#14294)
Bumps [uuid](https://github.com/uuidjs/uuid) from 13.0.0 to 14.0.0. <details> <summary>Release notes</summary> <p><em>Sourced from <a href="https://github.com/uuidjs/uuid/releases">uuid's releases</a>.</em></p> <blockquote> <h2>v14.0.0</h2> <h2><a href="https://github.com/uuidjs/uuid/compare/v13.0.0...v14.0.0">14.0.0</a> (2026-04-19)</h2> <h3>⚠ BREAKING CHANGES</h3> <ul> <li>expect <code>crypto</code> to be global everywhere (requires node@20+) (<a href="https://redirect.github.com/uuidjs/uuid/issues/935">#935</a>)</li> <li>drop node@18 support (<a href="https://redirect.github.com/uuidjs/uuid/issues/934">#934</a>)</li> </ul> <h3>Features</h3> <ul> <li>drop node@18 support (<a href="https://redirect.github.com/uuidjs/uuid/issues/934">#934</a>) (<a href="https://github.com/uuidjs/uuid/commit/dc4ddb87272ed2843faccd130bcc41d492688bd3">dc4ddb8</a>)</li> </ul> <h3>Bug Fixes</h3> <ul> <li>expect <code>crypto</code> to be global everywhere (requires node@20+) (<a href="https://redirect.github.com/uuidjs/uuid/issues/935">#935</a>) (<a href="https://github.com/uuidjs/uuid/commit/f2c235f93059325fa43e1106e624b5291bb523c4">f2c235f</a>)</li> <li>Use GITHUB_TOKEN for release-please and enable npm provenance (<a href="https://redirect.github.com/uuidjs/uuid/issues/925">#925</a>) (<a href="https://github.com/uuidjs/uuid/commit/ffa31383e8e4e1f0b4e22e504561272041b8738c">ffa3138</a>)</li> </ul> </blockquote> </details> <details> <summary>Changelog</summary> <p><em>Sourced from <a href="https://github.com/uuidjs/uuid/blob/main/CHANGELOG.md">uuid's changelog</a>.</em></p> <blockquote> <h2><a href="https://github.com/uuidjs/uuid/compare/v13.0.0...v14.0.0">14.0.0</a> (2026-04-19)</h2> <h3>Security</h3> <ul> <li>Fixes <a href="https://github.com/uuidjs/uuid/security/advisories/GHSA-w5hq-g745-h8pq">GHSA-w5hq-g745-h8pq</a>: <code>v3()</code>, <code>v5()</code>, and <code>v6()</code> did not validate that writes would remain within the bounds of a caller-supplied buffer, allowing out-of-bounds writes when an invalid <code>offset</code> was provided. A <code>RangeError</code> is now thrown if <code>offset < 0</code> or <code>offset + 16 > buf.length</code>.</li> </ul> <h3>⚠ BREAKING CHANGES</h3> <ul> <li><code>crypto</code> is now expected to be globally defined (requires node@20+) (<a href="https://redirect.github.com/uuidjs/uuid/issues/935">#935</a>)</li> <li>drop node@18 support (<a href="https://redirect.github.com/uuidjs/uuid/issues/934">#934</a>)</li> <li>upgrade minimum supported TypeScript version to 5.4.3, in keeping with the project's policy of supporting TypeScript versions released within the last two years</li> </ul> </blockquote> </details> <details> <summary>Commits</summary> <ul> <li><a href="https://github.com/uuidjs/uuid/commit/7c1ea087a8149b57380fc8bb7f68c3a215cb6e4b"><code>7c1ea08</code></a> chore(main): release 14.0.0 (<a href="https://redirect.github.com/uuidjs/uuid/issues/926">#926</a>)</li> <li><a href="https://github.com/uuidjs/uuid/commit/3d2c5b0342f0fcb52a5ac681c3d47c13e7444b34"><code>3d2c5b0</code></a> Merge commit from fork</li> <li><a href="https://github.com/uuidjs/uuid/commit/f2c235f93059325fa43e1106e624b5291bb523c4"><code>f2c235f</code></a> fix!: expect <code>crypto</code> to be global everywhere (requires node@20+) (<a href="https://redirect.github.com/uuidjs/uuid/issues/935">#935</a>)</li> <li><a href="https://github.com/uuidjs/uuid/commit/529ef0899f5dd503d2ee90d690585d63d78bc212"><code>529ef08</code></a> chore: upgrade TypeScript and fixup types (<a href="https://redirect.github.com/uuidjs/uuid/issues/927">#927</a>)</li> <li><a href="https://github.com/uuidjs/uuid/commit/086fd7976f11433edf9ac80be876b3ad243fe087"><code>086fd79</code></a> chore: update dependencies (<a href="https://redirect.github.com/uuidjs/uuid/issues/933">#933</a>)</li> <li><a href="https://github.com/uuidjs/uuid/commit/dc4ddb87272ed2843faccd130bcc41d492688bd3"><code>dc4ddb8</code></a> feat!: drop node@18 support (<a href="https://redirect.github.com/uuidjs/uuid/issues/934">#934</a>)</li> <li><a href="https://github.com/uuidjs/uuid/commit/0f1f9c9c9cedbae5a1d363d5406c5dfbabe81404"><code>0f1f9c9</code></a> chore: switch to Biome for parsing and linting (<a href="https://redirect.github.com/uuidjs/uuid/issues/932">#932</a>)</li> <li><a href="https://github.com/uuidjs/uuid/commit/e2879e64bf125add903c1eff6e0860542c605013"><code>e2879e6</code></a> chore: use maintained version of npm-run-all (<a href="https://redirect.github.com/uuidjs/uuid/issues/930">#930</a>)</li> <li><a href="https://github.com/uuidjs/uuid/commit/ffa31383e8e4e1f0b4e22e504561272041b8738c"><code>ffa3138</code></a> fix: Use GITHUB_TOKEN for release-please and enable npm provenance (<a href="https://redirect.github.com/uuidjs/uuid/issues/925">#925</a>)</li> <li><a href="https://github.com/uuidjs/uuid/commit/0423d49df2dc8efc300c804731d25f4d7e0fccc4"><code>0423d49</code></a> docs: remove obsolete v1 option notes (<a href="https://redirect.github.com/uuidjs/uuid/issues/915">#915</a>)</li> <li>Additional commits viewable in <a href="https://github.com/uuidjs/uuid/compare/v13.0.0...v14.0.0">compare view</a></li> </ul> </details> <details> <summary>Maintainer changes</summary> <p>This version was pushed to npm by <a href="https://www.npmjs.com/~GitHub%20Actions">GitHub Actions</a>, a new releaser for uuid since your current version.</p> </details> <br /> [](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> |
||
|
|
e723c6b6f2 |
fix: Prevent platform banners cloud check from breaking app boot (#14321)
The `ChatwootApp.chatwoot_cloud?` gate on the platform banners route in #13943 reads `InstallationConfig` from the database. Because `routes.rb` is evaluated during `Rails.application.initialize!`, this ran before the database existed on a fresh setup, breaking `bundle exec rake db:create` in CI and first-time installs with `ActiveRecord::NoDatabaseError: We could not find your database: chatwoot_test`. The route is now always mounted, and the cloud check moved to where the database is guaranteed to be available — the controller (`before_action`) and the super admin sidebar partial. Closes the CI failure introduced by #13943. ## How to test 1. Drop your local databases: `bundle exec rake db:drop` 2. Run `bundle exec rake db:create` — it should succeed (previously failed with `NoDatabaseError`) 3. Bring the DB back: `bundle exec rake db:setup` 4. On a non-cloud install, visit `/super_admin/platform_banners` — should 404, and the sidebar entry should be hidden 5. With `DEPLOYMENT_ENV=cloud` configured (cloud install), the page and sidebar entry should work as before ## What changed - `config/routes.rb` — always mount `resources :platform_banners` (no DB call at boot) - `app/controllers/super_admin/platform_banners_controller.rb` — `before_action` raises `ActionController::RoutingError` (404) when not on Chatwoot Cloud - `app/views/super_admin/application/_navigation.html.erb` — hides the sidebar entry on non-cloud installs --------- Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com> |
||
|
|
5325e05143 |
feat: Add platform-wide status banners for outage notifications (#13943)
Adds a platform-wide status banner system to notify all users about external service outages. Super Admins can create, edit, and manage banners via the Super Admin console. Banners support markdown for links and are dismissible by users. <img width="1099" height="236" alt="image" src="https://github.com/user-attachments/assets/047a7994-d885-4a8a-b9c4-aeb32f15474a" /> ## How to test 1. Set `ENABLE_PLATFORM_BANNERS=true` in your environment 2. Go to Super Admin → Platform Banners 3. Create a banner with a message like: `Elevated error rates from Meta APIs. [Check status](https://metastatus.com)` 4. Select a banner type: `info` (blue), `warning` (amber), or `error` (red) 5. Visit the dashboard — the banner should appear at the top 6. Click "Dismiss" — the banner hides and stays dismissed across page reloads 7. Deactivate the banner in Super Admin — it disappears on next page load ## What changed - New `PlatformBanner` model with `banner_message`, `banner_type` (info/warning/error), and `active` flag - Super Admin CRUD via Administrate (controller, dashboard, routes, sidebar icon) - `DashboardController` serves active banners via `globalConfig` - `StatusBanner.vue` component renders banners with markdown support and per-banner localStorage dismiss - Feature gated behind `ENABLE_PLATFORM_BANNERS` env var 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Co-authored-by: iamsivin <iamsivin@gmail.com> Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com> |
||
|
|
c09206c22e |
fix: flaky time based document sync spec (#14320)
fixes: https://app.circleci.com/pipelines/github/chatwoot/chatwoot/111572/workflows/210e3618-301a-4a8f-a411-826214965441/jobs/146263 |
||
|
|
568aae875b |
feat: wire up auto-sync job backend [AI-150] (#14117)
# Pull Request Template ## Description - Wires up Controllers to auto-sync job - adds plan based sync schedule - a scheduler that runs every hour to check syncable documents - guards the whole feature behind feature flag by reclaiming `twilio_content_templates` - Adds a global and account level cap on how many documents to enqueue to prevent sudden burst at first run - some refactor to simplify code - specs 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. specs and locally ## 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 |
||
|
|
2324a344dc | fix: keep agents action visible in teams edit/add view (#14304) | ||
|
|
7c7d67fd06 | fix: show all matches when filtering by multiple labels (#14303) | ||
|
|
b058d84034 | fix: prevent Escape from opening formatting toolbar in editor (#14133) | ||
|
|
0e122188e9 |
feat: Add voice calling as a capability on Twilio SMS channel(Enterprise) (#13963)
Voice calling is now a capability on the existing TwilioSms rather than a separate Voice model. A single Twilio phone number handles both SMS and voice calls through one inbox. Fixes https://linear.app/chatwoot/issue/CW-6683/add-voice-calling-as-a-capability-on-twilio-sms-channel and https://linear.app/chatwoot/issue/PLA-120/add-the-support-for-sms **What changed** - Replaced Channel::Voice with voice_enabled flag on Channel::TwilioSms - Added voice_enabled, twiml_app_sid, api_key_secret columns to channel_twilio_sms table - Dropped channel_voice table (no production data) - All voice logic lives in Enterprise layer via prepend_mod_with('Channel::TwilioSms') - Added Voice settings tab on Twilio SMS inbox settings to enable/disable voice - Validates Twilio number voice capability before provisioning - Teardown service cleans up TwiML app and credentials when voice is disabled - Frontend voice detection uses isVoiceCallEnabled() / getVoiceCallProvider() helpers — extensible to future providers - Gated by channel_voice feature flag **How to test** 1. Enable feature flag: Account.find(<id>).enable_features('channel_voice') 2. Create voice inbox: Inboxes → Voice tile → enter Twilio credentials → verify incoming/outgoing calls and SMS work 3. Enable voice on existing SMS inbox: Inboxes → select Twilio SMS inbox → Voice tab → toggle on → provide API key credentials → verify calls work 4. Disable voice: Voice tab → toggle off → verify TwiML app is deleted, credentials cleared, SMS still works 5. Re-enable voice: Toggle on again → must provide api_key_secret again → new TwiML app provisioned --------- Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com> |
||
|
|
f8f0caf443 |
feat(campaigns): Add variable support to WhatsApp campaigns (#13649)
Fixes https://linear.app/chatwoot/issue/CW-5641/add-the-support-for-variables-in-whatsapp-campaign-templates This PR adds liquid variable support to WhatsApp campaigns, enabling dynamic per-contact personalization. It supports the same liquid variables as SMS campaigns ({{contact.name}}, {{contact.email}}, etc.). Variables are processed per-contact when the campaign executes, allowing personalized messages at scale. --------- Co-authored-by: Sojan Jose <sojan@pepalo.com> Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com> Co-authored-by: Sony Mathew <sony@chatwoot.com> |
||
|
|
6aeda0ddf6 |
feat: add Playwright setup and login flow test (#13578)
## Description Adds Playwright E2E testing infrastructure with project configuration and a login flow test. This is Phase 1 of the Playwright E2E suite, kept minimal with only the core setup and login component. Ref: Discussion #13500, PR #13067 ## Type of change Please delete options that are not relevant. - [x] New feature (non-breaking change which adds functionality) ## How Has This Been Tested? - Verified all imports resolve correctly (`login-flow-ui-validation.spec.ts` only imports `Login` from `@components/ui`) - Ran login flow test locally against a running Chatwoot instance ## Checklist: - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my code - [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 --------- Co-authored-by: Sony Mathew <sony@chatwoot.com> Co-authored-by: Sony Mathew <2040199+sony-mathew@users.noreply.github.com> |
||
|
|
fed32d5964 |
fix(perf): force better index for pending_reponses longest first sort (#14291)
## Description Fix a Postgres planner trap on the "Pending Response: Longest first" sort that causes the conversation list to hang on busy accounts. The current `sort_on_waiting_since` generates query with `ORDER BY waiting_since ASC NULLS LAST, created_at ASC`. That order-by is exactly the shape of the single-column `index_conversations_on_waiting_since` btree, so the planner picks a forward index walk thinking `LIMIT 25` will stop early. In practice the per-account matches are spread along the global waiting_since timeline, so the scan reads tens of millions of rows from other accounts and discards them via the filter before producing any results which in turn causes the requests to time out and the conversation list spinner never resolves. DESC direction and every other sort (`priority`, `created_at`, `last_activity_at`) are unaffected. They fall through to `conv_acid_inbid_stat_asgnid_idx` (account-scoped composite), which is the right index for this access pattern. This change leads the ORDER BY with the expression `(waiting_since IS NULL)`, which no column-only btree can satisfy. The planner falls back to the same account-scoped index used by every other sort, and sorts in memory. Same logical NULLS LAST output for both directions; no behavior change for users. Fixes CW-6965 ## 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? - [x] Existing specs pass - [x] Added new specs to cover NULL case - [x] Verify results and order for old and new query in prod - [x] Tested in prod since staging data was not sufficient `EXPLAIN (ANALYZE, BUFFERS)` for the same query (status=0, ASC, LIMIT 25) on a representative production account, before vs after: | Metric | Before | After | | --- | --- | --- | | Execution time | 34,679 ms | 0.71 ms | | Rows discarded by filter | 36,906,962 | 0 | | Shared buffer hits | 12,699,924 | 11 | | Blocks read from disk | 851,226 | 112 | | I/O read time | 17,785 ms | 0.3 ms | | Pages dirtied / written | 98 / 31,043 | 0 / 0 | Verified on two production accounts: identical row IDs in identical order between the old and new ORDER BY for both ASC and DESC. A NULL-bucket regression spec was added covering ASC/DESC tail ordering when some conversations have a null `waiting_since`. Roughly `49,000×` faster on this query (34,679 ms → 0.71 ms), and trivially less I/O and buffer pressure on the cluster while it runs. ## 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: Tanmay Deep Sharma <32020192+tds-1@users.noreply.github.com> |
||
|
|
735bc73c96 |
fix: Preserve single newlines in outgoing email messages (#14138)
# Pull Request Template ## Description This PR fixes an issue where outgoing Email messages (via API) do not preserve single line breaks in rendered HTML. #### Cause Messages are stored with `\n`, but rendering differs: * **Other channel** (`markdown-it`, `breaks: true`) → `\n` → `<br>` * **Email** (CommonMark) without `HARDBREAKS` → `\n` collapsed into spaces Result: multi-line messages appear as a single paragraph in Email. #### Solution * Added `hardbreaks:` option to `render_message` (default: false) * Enabled `hardbreaks: true` in `EmailHelper#render_email_html` This ensures `\n` renders as `<br />` in Email, matching web widget behavior. Fixes https://linear.app/chatwoot/issue/CW-6941/outgoing-email-messages-strip-single-newlines-from-plain-text-content ## Type of change - [x] Bug fix (non-breaking change which fixes an issue) ## How Has This Been Tested? #### Screenshots **Before** <img width="604" height="104" alt="image" src="https://github.com/user-attachments/assets/f9086ffb-a5c7-4688-99aa-97ea5edcccde" /> **After** <img width="604" height="210" alt="image" src="https://github.com/user-attachments/assets/a8f21c76-bcb8-4058-937a-dd185fb6745c" /> ## 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: Muhsin Keloth <muhsinkeramam@gmail.com> |
||
|
|
5e79dd699e |
fix: prevent country defaulting to Zimbabwe in search result contacts (#14098)
## Context Same root-cause as #7822 / #14078, but in a second file that PR #14078 doesn't touch: \`app/javascript/dashboard/modules/search/components/SearchResultContactItem.vue\`. \`countries.json\` entries only have \`id\` (e.g. \`"AF"\`) — there is no \`code\` field. So: \`\`\`js const countriesMap = countries.reduce((acc, country) => { acc[country.code] = country; // country.code is undefined → acc[undefined] = country acc[country.id] = country; return acc; }, {}); \`\`\` …overwrites \`acc[undefined]\` on every iteration, leaving whichever country comes last in the list (Zimbabwe, \`id: "ZW"\`). Later, the \`countryDetails\` lookup falls back to that value whenever the contact's \`country\` / \`countryCode\` is missing or unknown, and Zimbabwe is displayed incorrectly. ## Fix One-line delete — drop the dead \`acc[country.code] = country\` write. Lookups by ISO code continue to work via \`country.id\`. ## Scope Only the search-results card. The Contacts card is already being fixed in #14078 with the same one-line delete. It's worth patching this surface too so the same symptom doesn't reappear when the same contact is accessed via \`Ctrl+K\` search. ## Test plan - [ ] Reproduce: search for a contact with \`country: null\` / \`countryCode: null\` — before this patch the flag renders as Zimbabwe; after, it renders as no country (current expected fallback). - [ ] Search for a contact with a valid ISO \`countryCode\` (e.g. \`"IN"\`) — country still resolves correctly. - [ ] Contacts list page (\`ContactsCard.vue\`, fixed in #14078) — no regression. ## Follow-up (not in this PR) Both components keep rebuilding the same \`countriesMap\` per mount. A small \`shared/constants/countries.js\` export (\`export const byId = countries.reduce(...)\`, computed once at module load) would save the per-mount cost and centralise the shape so this bug can't return. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Sojan Jose <sojan@pepalo.com> |
||
|
|
05dd31389e |
fix(whatsapp): Prevent duplicate conversations from concurrent uploads (#14060)
When a WhatsApp contact starts a new conversation by sending multiple images at once (an album), each image arrives as a separate webhook. Because no conversation exists yet, the concurrent workers each pass the "does a conversation exist?" check and each create their own conversation — producing one conversation per image instead of one grouped conversation. This fix serializes webhook processing per `(inbox, contact)` using a Redis lock at the job level, so only one webhook at a time can create the initial conversation for a given contact. Concurrent workers retry with backoff and append to the same conversation once the lock is released. ## Closes - Closes #13261 ## How to test 1. On a WhatsApp inbox, ensure there is no active (open) conversation with a specific test contact — resolve or delete any existing one. 2. From a phone, select 6+ images in the WhatsApp gallery and send them as a single album to the Chatwoot-connected number. 3. Open the Chatwoot dashboard and confirm exactly **one** new conversation is created, with all images grouped under it. 4. Repeat the test with a mix of attachment types (XMLs, PDFs, images) sent in rapid succession — still one conversation. ## What changed - New Redis key `WHATSAPP_MESSAGE_CREATE_LOCK::<inbox_id>::<sender_id>` in `lib/redis/redis_keys.rb`. - `Webhooks::WhatsappEventsJob` now inherits from `MutexApplicationJob` and wraps event processing in `with_lock(key)`, matching the pattern already used by `FacebookEventsJob`, `InstagramEventsJob`, and `TiktokEventsJob`. - Uses `retry_on LockAcquisitionError, wait: 1.second, attempts: 8` so concurrent webhooks retry until the lock is free instead of poll-waiting inside the service. - Sender ID is derived from the webhook payload (contact's `from`, or `to` for SMB echo events); status-only webhooks bypass the lock. - Issue 1 from the report (same `source_id` redelivery) was already handled previously by `Whatsapp::MessageDedupLock` (atomic `SET NX EX`); no changes needed there. --------- Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
f7bbd40816 |
fix(slack): Sync bot interactive responses (#14076)
When a customer responds to a bot's interactive prompt (input_select, input_csat, form, input_email) from the widget, the response shows up in the Chatwoot agent UI but is not reflected in the linked Slack channel — Slack only ever shows the original question. This happens because the widget submits the answer as an UPDATE to the original message (writing `content_attributes.submitted_values` or `submitted_email`), but the Slack hook only listened to `message.created`, so updates were ignored. Closes https://linear.app/chatwoot/issue/PLA-147 ### Preview <img width="1290" height="1106" alt="CleanShot 2026-04-21 at 13 19 19@2x" src="https://github.com/user-attachments/assets/cd2a9d3f-89d3-4e81-9230-5b078e1b7b44" /> ### How to test 1. Connect a web widget inbox to a Slack channel. 2. Trigger each bot message type (input_select, form, input_csat, input_email) in a conversation. 3. Submit responses from the widget. 4. Verify each response now appears in the Slack thread, appended to the original bot question. --------- Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> |
||
|
|
7820739f3a |
fix: prevent Country from being set to Zimbabwe in the contacts list (#14078)
## Description This PR resolves an issue that was causing Zimbabwe country being selected in the Contacts list. The root cause is objects in `countries.json` file are missing the `code` property which was causing the countries map to always have the last country in the list also map to an `undefined` key. In turn, this was causing an error when contact's `country` was undefined. Additional thing to keep in mind: I am not sure if there is a case when contact's `country` property is used or if it can be removed as well, but in my Chatwoot installation contacts only have a `countryCode` property, and there is no way to set `country` from the widget sdk. Fixes #7822 ## Type of change - [x] Bug fix (non-breaking change which fixes an issue) ## How Has This Been Tested? Simply running the vue application locally targeting my live chatwoot API. ## 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 - [x] Any dependent changes have been merged and published in downstream modules |
||
|
|
224556fd1b |
feat: onboarding account details with enriched data [UPM-17][UPM-18] (#13979)
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> |
||
|
|
51eb626b88 |
feat: allow disabling 2FA with a backup code (#14102)
## Linear Ticket - https://linear.app/chatwoot/issue/CW-6883/allow-disabling-2fa-using-a-backup-code ## Description When a user loses access to their authenticator app, they can now disable 2FA using one of their saved backup codes (in addition to their password), so they can re-enroll a new authenticator. The disable dialog includes a toggle to switch between entering a verification code and a backup code. ## Type of change - [x] Bug fix (non-breaking change which fixes an issue) ## How Has This Been Tested? - Via UI flows <img width="495" height="423" alt="Screenshot 2026-04-20 at 2 17 21 PM" src="https://github.com/user-attachments/assets/cc6b3dc5-39e6-4104-b5b9-cdabdc46947e" /> <img width="475" height="409" alt="Screenshot 2026-04-20 at 2 17 36 PM" src="https://github.com/user-attachments/assets/97c7304d-4adb-42ed-b7b4-50f5b38585a3" /> ## 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 |
||
|
|
b0aa844a32 |
fix(portals): handle integer blob_id in process_attached_logo without 500 (#14274)
Updating portal settings (name, header text, page title, homepage link) on a portal that already has a logo attached returns 500. The error is \`NoMethodError: undefined method 'valid_encoding?' for an instance of Integer\`. The fix is a two-character change in \`process_attached_logo\`. Closes #13300 ## Root cause \`ActiveStorage::Blob.find_signed\` expects a signed ID string (e.g. \`"eyJfcmFpbH..."\`). Internally it calls \`valid_encoding?\` on the argument to validate the signature payload — a method that exists on \`String\` but not \`Integer\`. When a portal already has a logo, the frontend includes the blob's raw database integer ID (e.g. \`blob_id: 170\`) in the update request payload. The controller passes this integer directly to \`find_signed\`, which immediately raises \`NoMethodError\` before any database query is made. \`\`\`ruby # before, crashes when blob_id is an Integer blob_id = params[:blob_id] blob = ActiveStorage::Blob.find_signed(blob_id) # NoMethodError here @portal.logo.attach(blob) \`\`\` ## What changed \`\`\`ruby # after, safe for any input type blob = ActiveStorage::Blob.find_signed(params[:blob_id].to_s) @portal.logo.attach(blob) if blob \`\`\` \`.to_s\` on an Integer produces a plain decimal string (\`"170"\`), which is not a valid signed ID. \`find_signed\` returns \`nil\` for any invalid signature rather than raising, so the nil guard prevents a broken \`attach\` call. The existing logo remains attached and the settings update succeeds. ## Trade-offs considered | Option | Decision | |---|---| | \`find(blob_id)\` when input is an Integer | Bypasses signature verification — any authenticated user knowing a blob ID could attach arbitrary files to a portal. Security risk. Rejected. | | Raise a 422 for non-string blob_id | Overly strict — the frontend sending an integer is pre-existing behaviour this PR shouldn't break. | | Silently no-op for invalid blob_id (chosen) | Correct product behaviour: if no valid signed upload is provided, leave the logo unchanged. The settings update still succeeds. | ## Known limitation The correct long-term fix is also on the frontend: it should only send \`blob_id\` when attaching a **new** upload (using the signed ID from the direct-upload flow), not when re-submitting the existing logo's raw database integer ID. This PR makes the server robust against the current frontend behaviour without requiring a coordinated frontend change. ## How to reproduce 1. Create a Help Center portal and upload a logo 2. Update any text field via \`PUT /api/v1/accounts/:id/portals/:slug\` while including \`blob_id: <integer>\` in the payload 3. Observe 500 with \`NoMethodError: undefined method 'valid_encoding?' for an instance of Integer\` After this fix, the request returns 200, settings are updated, and the existing logo is preserved. Co-authored-by: Ramalau Debeila <rdebeila@datacentrix.co.za> |
||
|
|
c8e551820b |
fix: [CW-6940] Fix SSRF issue for webhook trigger used by macros and automations (#14155)
This routes external downloads used by webhook fetch used by macros and acutomations through SafeFetch. It closes the SSRF exposure from raw Down.download paths, preserves provider-specific auth and header flows, and adds regression coverage for blocked internal URLs plus authenticated downloads. Fixes # (issue): [CW-6940](https://linear.app/chatwoot/issue/CW-6940/ssrf-via-webhooksautomationmacros-non-upload-non-avatar) |