0d5617d446ce6100285f6ebc2bbbcdcf73277b2c
12079
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
0d5617d446 |
chore(server): drop unused postgresCredentials feature (#20573)
## Summary Drops the `postgresCredentials` legacy feature: a never-finished "postgres proxy" that would have let users query their workspace data over a standard Postgres connection. Nothing — frontend, e2e, Zapier, docs, other server code — calls these mutations/query. ## History - **Introduced** June 2024 (#5767, Thomas Trompette) as "first step for creating credentials for database proxy", alongside the Postgres FDW / remote-server work and the custom `twenty-postgres-spilo` image. Planned follow-ups (provisioning a DB on the proxy, mapping users, exposing it as a remote server) never landed. - **Abandoned** January 2026 (#17001, Weiko) when the sibling "remote integration" feature was removed as a BREAKING CHANGE — "not maintained for more than a year and never officially launched". The spilo image was then replaced with vanilla `postgres:16` (#19182, March 2026), retiring the FDW infrastructure entirely. - This PR finishes the cleanup: removes the orphaned module, the `allPostgresCredentials` relation, `JwtTokenTypeEnum.POSTGRES_PROXY` + payload, the reserved metadata keywords, and adds a 2.5.0 fast instance command that drops `core.postgresCredentials` (reversible `down`). Regenerated frontend GraphQL types + SDK metadata client. ## Test plan - [x] `tsgo --noEmit` clean on twenty-server + twenty-front; lint + prettier clean on touched files. - [x] `database:migrate:generate` reports no pending schema diff; server boots and serves the new schema. |
||
|
|
61683d8bda |
[Website] Replace product page hero visual with interactive CRM depicting AI chat in action. (#20566)
Before: <img width="1439" height="518" alt="image" src="https://github.com/user-attachments/assets/4d294a1b-c5a0-43b2-9895-61a8ee19da62" /> After: https://github.com/user-attachments/assets/c019586f-ef9f-4ae0-8afe-14f08e8cb057 |
||
|
|
ab705b14d7 |
i18n - translations (#20569)
Created by Github action --------- Co-authored-by: github-actions <github-actions@twenty.com> |
||
|
|
90537b3b88 |
i18n - translations (#20567)
Created by Github action --------- Co-authored-by: github-actions <github-actions@twenty.com> |
||
|
|
a754a95d38 | Regrant id token write to claude for oidc swap (#20564) | ||
|
|
8dd0aa46e3 |
Update OAuth consent modal design (#20540)
## Summary Updates the OAuth consent screen to match the provided modal treatment, including the header artwork, app-to-Twenty logo layout, scope icons, and a content-hugging title that stays on one line until its max width. Also preserves `returnToPath` through Google and Microsoft social SSO so users who sign in from `/authorize` return to the OAuth consent flow with the original OAuth query parameters intact. ## Reference  ## Validation - `yarn nx test twenty-front --testFile=src/modules/auth/hooks/__tests__/useAuth.test.tsx --coverage=false` - `yarn nx test twenty-server --testFile=src/engine/core-modules/auth/services/auth.service.spec.ts --coverage=false` - `yarn nx typecheck twenty-front` - `yarn nx typecheck twenty-server` - `git diff --check` ## Notes The reference screenshot is linked from a separate branch-hosted image commit so it renders in the PR body without adding that screenshot to the product diff. Co-authored-by: Charles Bochet <charles@twenty.com> |
||
|
|
91b2390ee8 |
chore(deps-dev): bump vite-plugin-svgr from 4.3.0 to 4.5.0 (#20561)
Bumps [vite-plugin-svgr](https://github.com/pd4d10/vite-plugin-svgr) from 4.3.0 to 4.5.0. <details> <summary>Release notes</summary> <p><em>Sourced from <a href="https://github.com/pd4d10/vite-plugin-svgr/releases">vite-plugin-svgr's releases</a>.</em></p> <blockquote> <h2>v4.5.0</h2> <p><em>No significant changes</em></p> <h5> <a href="https://github.com/pd4d10/vite-plugin-svgr/compare/v4.4.0...v4.5.0">View changes on GitHub</a></h5> </blockquote> </details> <details> <summary>Commits</summary> <ul> <li><a href="https://github.com/pd4d10/vite-plugin-svgr/commit/d1f8887202a6edd119053c665f0679419df70025"><code>d1f8887</code></a> 4.5.0</li> <li><a href="https://github.com/pd4d10/vite-plugin-svgr/commit/b326999d5a2546227bb4b2d2cdcf6092392f4d11"><code>b326999</code></a> ci: fix npm publish</li> <li><a href="https://github.com/pd4d10/vite-plugin-svgr/commit/79300b869297ab2063a730f33ec6f7984b8acbe8"><code>79300b8</code></a> 4.4.0</li> <li><a href="https://github.com/pd4d10/vite-plugin-svgr/commit/b7d3c36dd658a6b8344b82307f36dc3acb0d1479"><code>b7d3c36</code></a> chore: update deps</li> <li><a href="https://github.com/pd4d10/vite-plugin-svgr/commit/6f9d9af421ec891c1594b5511d4865cea6c8f79a"><code>6f9d9af</code></a> feat: support rolldown-vite via <code>transformWithOxc</code> (<a href="https://redirect.github.com/pd4d10/vite-plugin-svgr/issues/130">#130</a>)</li> <li><a href="https://github.com/pd4d10/vite-plugin-svgr/commit/cef5adead59da0abfc0b81edf4b2022b193e4607"><code>cef5ade</code></a> ci: fix release note</li> <li>See full diff in <a href="https://github.com/pd4d10/vite-plugin-svgr/compare/v4.3.0...v4.5.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) </details> Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|
|
b36345a699 |
chore(deps-dev): bump @mui/material from 7.3.8 to 7.3.11 (#20562)
Bumps [@mui/material](https://github.com/mui/material-ui/tree/HEAD/packages/mui-material) from 7.3.8 to 7.3.11. <details> <summary>Release notes</summary> <p><em>Sourced from <a href="https://github.com/mui/material-ui/releases">@mui/material's releases</a>.</em></p> <blockquote> <h2>v7.3.11</h2> <p>A big thanks to the 5 contributors who made this release possible.</p> <h3><code>@mui/material@7.3.11</code></h3> <ul> <li>[autocomplete] Fix highlight sync and scroll preservation (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48350">#48350</a>) <a href="https://github.com/mj12albert"><code>@mj12albert</code></a></li> <li>[autocomplete] Fix popper rendering issues (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48343">#48343</a>) <a href="https://github.com/mj12albert"><code>@mj12albert</code></a></li> <li>[autocomplete] Improve highlight tracking and selection state (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48318">#48318</a>) <a href="https://github.com/mj12albert"><code>@mj12albert</code></a></li> <li>[button] Fix <code>startIcon</code> alignment (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48339">#48339</a>) <a href="https://github.com/mj12albert"><code>@mj12albert</code></a></li> <li>[button] Remove duplicated className entries (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48284">#48284</a>) <a href="https://github.com/silviuaavram"><code>@silviuaavram</code></a></li> <li>[checkbox] Set <code>aria-checked=mixed</code> when indeterminate (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48286">#48286</a>) <a href="https://github.com/mj12albert"><code>@mj12albert</code></a></li> <li>[dialog][drawer][focus trap] Fix initial focus target (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48324">#48324</a>) <a href="https://github.com/mj12albert"><code>@mj12albert</code></a></li> <li>[drawer] Fix transition jump (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48340">#48340</a>) <a href="https://github.com/mj12albert"><code>@mj12albert</code></a></li> <li>[input] Fix layout shift with display: flex (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48359">#48359</a>) <a href="https://github.com/oliviertassinari"><code>@oliviertassinari</code></a></li> <li>[inputs] Fix autofocus in SSR environment (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48307">#48307</a>) <a href="https://github.com/mj12albert"><code>@mj12albert</code></a></li> <li>[popper] Persist positioning styles when popperOptions changes reference (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48302">#48302</a>) <a href="https://github.com/mj12albert"><code>@mj12albert</code></a></li> <li>[switch] Fix incorrect <code>role</code> with <code>slotProps.input</code> (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48472">#48472</a>) <a href="https://github.com/mj12albert"><code>@mj12albert</code></a></li> <li>[utils] Add shadow dom utils (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48309">#48309</a>) <a href="https://github.com/mj12albert"><code>@mj12albert</code></a></li> </ul> <h3>Docs</h3> <ul> <li>[docs] Update banner to announce v9 (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48299">#48299</a>) <a href="https://github.com/siriwatknp"><code>@siriwatknp</code></a></li> <li>[docs] Add v9 in the versions select in v7.mui.com (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48233">#48233</a>) <a href="https://github.com/alexfauquette"><code>@alexfauquette</code></a></li> </ul> <h3>Core</h3> <ul> <li>[internal] Update some host-reference entries (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48225">#48225</a>) <a href="https://github.com/silviuaavram"><code>@silviuaavram</code></a></li> </ul> <p>All contributors of this release in alphabetical order: <a href="https://github.com/alexfauquette"><code>@alexfauquette</code></a>, <a href="https://github.com/mj12albert"><code>@mj12albert</code></a>, <a href="https://github.com/oliviertassinari"><code>@oliviertassinari</code></a>, <a href="https://github.com/silviuaavram"><code>@silviuaavram</code></a>, <a href="https://github.com/siriwatknp"><code>@siriwatknp</code></a></p> <h2>v7.3.10</h2> <p>A big thanks to the 15 contributors who made this release possible. A few highlights ✨:</p> <ul> <li>📖 Added the Menubar component page to the docs.</li> </ul> <h3><code>@mui/material@7.3.10</code></h3> <ul> <li>[alert][dialog] Accessibility improvements (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48161">#48161</a>) <a href="https://github.com/silviuaavram"><code>@silviuaavram</code></a></li> <li>[autocomplete] Add <code>root</code> slot (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/47916">#47916</a>) <a href="https://github.com/GerardasB"><code>@GerardasB</code></a></li> <li>[autocomplete] Fix helper text focusing input when clicked (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48162">#48162</a>) <a href="https://github.com/mj12albert"><code>@mj12albert</code></a></li> <li>[autocomplete] Fix popup reopening on window focus regain with openOnFocus (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/47914">#47914</a>) <a href="https://github.com/aman44444"><code>@aman44444</code></a></li> <li>[autocomplete] Optimize selected option lookup (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48027">#48027</a>) <a href="https://github.com/anchmelev"><code>@anchmelev</code></a></li> <li>[autocomplete] Support full slots for clearIndicator and popupIndicator (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/47913">#47913</a>) <a href="https://github.com/silviuaavram"><code>@silviuaavram</code></a></li> <li>[button-base] Fix native button detection (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/47994">#47994</a>) <a href="https://github.com/mj12albert"><code>@mj12albert</code></a></li> <li>[input] Fix high contrast cutoff on first character (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48160">#48160</a>) <a href="https://github.com/silviuaavram"><code>@silviuaavram</code></a></li> <li>[list item text][card header] Revert cleanup of duplicated CSS rules (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/47938">#47938</a>) <a href="https://github.com/sai6855"><code>@sai6855</code></a></li> <li>[popper] Add missing classes export (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48033">#48033</a>) <a href="https://github.com/mj12albert"><code>@mj12albert</code></a></li> <li>[select] Fix focus visible always set on menu item (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48022">#48022</a>) <a href="https://github.com/silviuaavram"><code>@silviuaavram</code></a></li> <li>[slider] Accept readonly array for the value prop (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/47961">#47961</a>) <a href="https://github.com/pcorpet"><code>@pcorpet</code></a></li> <li>[switch] Add border to make it visible in high contrast mode (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48210">#48210</a>) <a href="https://github.com/silviuaavram"><code>@silviuaavram</code></a></li> </ul> <!-- raw HTML omitted --> </blockquote> <p>... (truncated)</p> </details> <details> <summary>Changelog</summary> <p><em>Sourced from <a href="https://github.com/mui/material-ui/blob/v7.3.11/CHANGELOG.md">@mui/material's changelog</a>.</em></p> <blockquote> <h2>7.3.11</h2> <!-- raw HTML omitted --> <p><em>May 6, 2026</em></p> <p>A big thanks to the 5 contributors who made this release possible.</p> <h3><code>@mui/material@7.3.11</code></h3> <ul> <li>[autocomplete] Fix highlight sync and scroll preservation (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48350">#48350</a>) <a href="https://github.com/mj12albert"><code>@mj12albert</code></a></li> <li>[autocomplete] Fix popper rendering issues (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48343">#48343</a>) <a href="https://github.com/mj12albert"><code>@mj12albert</code></a></li> <li>[autocomplete] Improve highlight tracking and selection state (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48318">#48318</a>) <a href="https://github.com/mj12albert"><code>@mj12albert</code></a></li> <li>[button] Fix <code>startIcon</code> alignment (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48339">#48339</a>) <a href="https://github.com/mj12albert"><code>@mj12albert</code></a></li> <li>[button] Remove duplicated className entries (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48284">#48284</a>) <a href="https://github.com/silviuaavram"><code>@silviuaavram</code></a></li> <li>[checkbox] Set <code>aria-checked=mixed</code> when indeterminate (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48286">#48286</a>) <a href="https://github.com/mj12albert"><code>@mj12albert</code></a></li> <li>[dialog][drawer][focus trap] Fix initial focus target (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48324">#48324</a>) <a href="https://github.com/mj12albert"><code>@mj12albert</code></a></li> <li>[drawer] Fix transition jump (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48340">#48340</a>) <a href="https://github.com/mj12albert"><code>@mj12albert</code></a></li> <li>[input] Fix layout shift with display: flex (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48359">#48359</a>) <a href="https://github.com/oliviertassinari"><code>@oliviertassinari</code></a></li> <li>[inputs] Fix autofocus in SSR environment (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48307">#48307</a>) <a href="https://github.com/mj12albert"><code>@mj12albert</code></a></li> <li>[popper] Persist positioning styles when popperOptions changes reference (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48302">#48302</a>) <a href="https://github.com/mj12albert"><code>@mj12albert</code></a></li> <li>[switch] Fix incorrect <code>role</code> with <code>slotProps.input</code> (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48472">#48472</a>) <a href="https://github.com/mj12albert"><code>@mj12albert</code></a></li> <li>[utils] Add shadow dom utils (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48309">#48309</a>) <a href="https://github.com/mj12albert"><code>@mj12albert</code></a></li> </ul> <h3>Docs</h3> <ul> <li>[docs] Update banner to announce v9 (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48299">#48299</a>) <a href="https://github.com/siriwatknp"><code>@siriwatknp</code></a></li> <li>[docs] Add v9 in the versions select in v7.mui.com (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48233">#48233</a>) <a href="https://github.com/alexfauquette"><code>@alexfauquette</code></a></li> </ul> <h3>Core</h3> <ul> <li>[internal] Update some host-reference entries (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48225">#48225</a>) <a href="https://github.com/silviuaavram"><code>@silviuaavram</code></a></li> </ul> <p>All contributors of this release in alphabetical order: <a href="https://github.com/alexfauquette"><code>@alexfauquette</code></a>, <a href="https://github.com/mj12albert"><code>@mj12albert</code></a>, <a href="https://github.com/oliviertassinari"><code>@oliviertassinari</code></a>, <a href="https://github.com/silviuaavram"><code>@silviuaavram</code></a>, <a href="https://github.com/siriwatknp"><code>@siriwatknp</code></a></p> <h2>7.3.10</h2> <!-- raw HTML omitted --> <p><em>Apr 8, 2026</em></p> <p>A big thanks to the 15 contributors who made this release possible. A few highlights ✨:</p> <ul> <li>📖 Added the Menubar component page to the docs.</li> </ul> <h3><code>@mui/material@7.3.10</code></h3> <ul> <li>[alert][dialog] Accessibility improvements (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48161">#48161</a>) <a href="https://github.com/silviuaavram"><code>@silviuaavram</code></a></li> <li>[autocomplete] Add <code>root</code> slot (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/47916">#47916</a>) <a href="https://github.com/GerardasB"><code>@GerardasB</code></a></li> <li>[autocomplete] Fix helper text focusing input when clicked (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48162">#48162</a>) <a href="https://github.com/mj12albert"><code>@mj12albert</code></a></li> </ul> <!-- raw HTML omitted --> </blockquote> <p>... (truncated)</p> </details> <details> <summary>Commits</summary> <ul> <li><a href="https://github.com/mui/material-ui/commit/6ddda377e979d1783b2cc00226098fde4509bb2e"><code>6ddda37</code></a> [release] v7.3.11 (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48478">#48478</a>)</li> <li><a href="https://github.com/mui/material-ui/commit/a2c7fa0eaeec48767ef7728a1d418609e0bf44ea"><code>a2c7fa0</code></a> [switch] Fix incorrect <code>role</code> with <code>slotProps.input</code> (<a href="https://github.com/mj12albert"><code>@mj12albert</code></a>) (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48472">#48472</a>)</li> <li><a href="https://github.com/mui/material-ui/commit/c3628bd4930e9089fdf41cba78fc2b5798a2bc76"><code>c3628bd</code></a> [input] Fix layout shift with display: flex (<a href="https://github.com/oliviertassinari"><code>@oliviertassinari</code></a>) (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48359">#48359</a>)</li> <li><a href="https://github.com/mui/material-ui/commit/290c89694eae625cf4ad20f874098f0b8889ceb4"><code>290c896</code></a> [autocomplete] Fix highlight sync and scroll preservation (<a href="https://github.com/mj12albert"><code>@mj12albert</code></a>) (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48350">#48350</a>)</li> <li><a href="https://github.com/mui/material-ui/commit/6e172b760815dc3dc8254f58e0dcd793fe690979"><code>6e172b7</code></a> [autocomplete] Fix popper rendering issues (<a href="https://github.com/mj12albert"><code>@mj12albert</code></a>) (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48343">#48343</a>)</li> <li><a href="https://github.com/mui/material-ui/commit/3f2323ce5e780134dcbe75802cf5ab5035822dd4"><code>3f2323c</code></a> [drawer] Fix transition jump (<a href="https://github.com/mj12albert"><code>@mj12albert</code></a>) (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48340">#48340</a>)</li> <li><a href="https://github.com/mui/material-ui/commit/1afbdfebd521eb96c2bd4d43ec1f9b2f48960a00"><code>1afbdfe</code></a> [button] Fix <code>startIcon</code> alignment (<a href="https://github.com/mj12albert"><code>@mj12albert</code></a>) (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48339">#48339</a>)</li> <li><a href="https://github.com/mui/material-ui/commit/dcef24179ffd7a08f47e4c6b656cd58181c34fdc"><code>dcef241</code></a> [dialog][drawer][focus trap] Fix initial focus target (<a href="https://github.com/mj12albert"><code>@mj12albert</code></a>) (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48324">#48324</a>)</li> <li><a href="https://github.com/mui/material-ui/commit/4b0275ea82d1807324eb04034db50501eac1f802"><code>4b0275e</code></a> [autocomplete] Improve highlight tracking and selection state (<a href="https://github.com/mj12albert"><code>@mj12albert</code></a>) (...</li> <li><a href="https://github.com/mui/material-ui/commit/47e41decac18a891f329f854b0d22e2585fa95be"><code>47e41de</code></a> [internal] Update some host-reference entries (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48225">#48225</a>)</li> <li>Additional commits viewable in <a href="https://github.com/mui/material-ui/commits/v7.3.11/packages/mui-material">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) </details> Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|
|
34d9fcaba1 |
chore(auth): drop unused workspacePersonalInviteToken from SSO state (#20557)
## Summary
Pure dead-code removal. The Google and Microsoft SSO strategies have
been packing `workspacePersonalInviteToken` into the OAuth `state` blob
and re-emitting it on `validate()`, but
[`signInUpWithSocialSSO`](packages/twenty-server/src/engine/core-modules/auth/services/auth.service.ts)
never destructures or reads it from the user object. The SSO flow
resolves invitations by the IdP-verified email instead:
```ts
const invitation =
currentWorkspace && email
? await this.findInvitationForSignInUp({
currentWorkspace,
email, // ← matched against appToken.context.email
})
: undefined;
```
So the strategy plumbing is write-only and confusing for readers.
Removed from:
-
[`SocialSSOState`](packages/twenty-server/src/engine/core-modules/auth/types/social-sso-state.type.ts)
- `GoogleRequest['user']` and `MicrosoftRequest['user']`
- The `state` JSON in both strategies' `authenticate()`
- The user object in both strategies' `validate()`
No frontend change needed — `useAuth.buildRedirectUrl` still sets the
`inviteToken` query param when a personal invite token is present (used
by other paths), and nothing on the SSO server side was reading it.
The token-based invitation lookup is preserved for the password signup
flow via `auth.resolver.signUp` → `findInvitationForSignInUp({
currentWorkspace, workspacePersonalInviteToken })`. Unrelated,
untouched.
## Test plan
- [x] `npx jest engine/core-modules/auth` (twenty-server) — 26 suites /
178 tests pass.
- [x] `tsgo -p tsconfig.json --noEmit` — no new errors on the touched
files (pre-existing `IS_REST_METADATA_API_NEW_FORMAT_DIRECT` errors on
main are unrelated).
- [x] `oxlint` + `prettier --check` on touched files — clean.
- [ ] Manual smoke: Google sign-in still works (workspace selection /
verify flow unaffected since `workspaceInviteHash`, `workspaceId`,
`action`, `locale`, `billingCheckoutSessionState`, `returnToPath` still
flow correctly).
|
||
|
|
8edc76d3d0 |
chore(deps): bump react-dropzone from 14.2.3 to 14.4.1 (#20560)
Bumps [react-dropzone](https://github.com/react-dropzone/react-dropzone) from 14.2.3 to 14.4.1. <details> <summary>Release notes</summary> <p><em>Sourced from <a href="https://github.com/react-dropzone/react-dropzone/releases">react-dropzone's releases</a>.</em></p> <blockquote> <h2>v14.4.1</h2> <h2><a href="https://github.com/react-dropzone/react-dropzone/compare/v14.4.0...v14.4.1">14.4.1</a> (2026-02-10)</h2> <h3>Bug Fixes</h3> <ul> <li>prevent tarball from being included in published package (<a href="https://github.com/react-dropzone/react-dropzone/commit/7919a235412ee5a224e76b9e9bef25fdb5e8ff0b">7919a23</a>)</li> </ul> <h2>v14.4.0</h2> <h1><a href="https://github.com/react-dropzone/react-dropzone/compare/v14.3.8...v14.4.0">14.4.0</a> (2026-01-29)</h1> <h3>Bug Fixes</h3> <ul> <li>accept files with empty type during drag events (<a href="https://github.com/react-dropzone/react-dropzone/commit/eaa8ba54963480afbba50415b1dd792514fefac1">eaa8ba5</a>)</li> <li>correct dragLeave filter logic and add dragend test (<a href="https://github.com/react-dropzone/react-dropzone/commit/273aff4a151aba05ddd473cbc49dc3db59132f2f">273aff4</a>)</li> </ul> <h3>Features</h3> <ul> <li>add isDragGlobal state for document-level drag detection (<a href="https://github.com/react-dropzone/react-dropzone/commit/f0874b0ad8e94dbf662b16bc82d0aa2b082ec8ee">f0874b0</a>)</li> </ul> <h2>v14.3.8</h2> <h2><a href="https://github.com/react-dropzone/react-dropzone/compare/v14.3.7...v14.3.8">14.3.8</a> (2025-02-24)</h2> <h3>Bug Fixes</h3> <ul> <li><strong>event_type:</strong> 🎨 Update drop event type to include FileSystemFileHandle (<a href="https://github.com/react-dropzone/react-dropzone/commit/d6911c991e077151e302b599b92269432ab0472b">d6911c9</a>)</li> </ul> <h2>v14.3.7</h2> <h2><a href="https://github.com/react-dropzone/react-dropzone/compare/v14.3.6...v14.3.7">14.3.7</a> (2025-02-24)</h2> <h3>Bug Fixes</h3> <ul> <li>make ESM build compatible with native Node.js (<a href="https://github.com/react-dropzone/react-dropzone/commit/201687900724b45ec98d26cde3626a1c6687c9e1">2016879</a>)</li> </ul> <h2>v14.3.6</h2> <h2><a href="https://github.com/react-dropzone/react-dropzone/compare/v14.3.5...v14.3.6">14.3.6</a> (2025-02-23)</h2> <h3>Bug Fixes</h3> <ul> <li><strong>types:</strong> fix React 19 incompatible JSX type import (<a href="https://github.com/react-dropzone/react-dropzone/commit/356d9d5cb604c47e393f332f3dfe0e8d12c58d95">356d9d5</a>)</li> </ul> <h2>v14.3.5</h2> <h2><a href="https://github.com/react-dropzone/react-dropzone/compare/v14.3.4...v14.3.5">14.3.5</a> (2024-11-04)</h2> <!-- raw HTML omitted --> </blockquote> <p>... (truncated)</p> </details> <details> <summary>Commits</summary> <ul> <li><a href="https://github.com/react-dropzone/react-dropzone/commit/9247d71a4dd6d9f36f177fee31c90a327a34d297"><code>9247d71</code></a> chore: add *.tgz to .gitignore</li> <li><a href="https://github.com/react-dropzone/react-dropzone/commit/7919a235412ee5a224e76b9e9bef25fdb5e8ff0b"><code>7919a23</code></a> fix: prevent tarball from being included in published package</li> <li><a href="https://github.com/react-dropzone/react-dropzone/commit/e2ae36d0c910e64ea7e269e741f26c909f758000"><code>e2ae36d</code></a> build: remove <code>@semantic-release/git</code></li> <li><a href="https://github.com/react-dropzone/react-dropzone/commit/84ff2d6671f95c925cb3e5c4b785fabd1a13c9fc"><code>84ff2d6</code></a> build: update deps for semantic-release and node version</li> <li><a href="https://github.com/react-dropzone/react-dropzone/commit/65095ea5c15b34724793ad38248bf10cd3fef147"><code>65095ea</code></a> build: add --ignore-engines flag to test workflow for Node 18 compatibility</li> <li><a href="https://github.com/react-dropzone/react-dropzone/commit/d70ea9632b70770b8d5b68d214f9532842120005"><code>d70ea96</code></a> build: enable passwordless publishing with NPM trusted publishers</li> <li><a href="https://github.com/react-dropzone/react-dropzone/commit/2c2c56ee46aefaaa3e81778d822dca4eb2e3c7f7"><code>2c2c56e</code></a> build: add full git history fetch and build step to release workflow</li> <li><a href="https://github.com/react-dropzone/react-dropzone/commit/a9e97413c8a7684d5495ba4c869a8c629e210c59"><code>a9e9741</code></a> build: add semantic-release configuration with trusted publishing support</li> <li><a href="https://github.com/react-dropzone/react-dropzone/commit/1455f4cc0a3c389a056532027017e92874d41fba"><code>1455f4c</code></a> build: Update checkout action to version 4 in release workflow</li> <li><a href="https://github.com/react-dropzone/react-dropzone/commit/f4e67064ebc9310ac0e6e04d6f04cc7c4b4da057"><code>f4e6706</code></a> build: Update Node.js setup action to version 4</li> <li>Additional commits viewable in <a href="https://github.com/react-dropzone/react-dropzone/compare/v14.2.3...v14.4.1">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 react-dropzone 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) </details> Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|
|
7fa136f305 |
feat(twenty-server): migrate remaining at-rest encryption sites to versioned envelope (#20550)
## Summary Second PR in the encryption key rotation series. The previous PR (#20528) introduced `ENCRYPTION_KEY` + the versioned `enc:v2:<keyId>:<base64>` envelope inside `SecretEncryptionService` and migrated `ConnectedAccountTokenEncryptionService` as the first consumer. This PR routes every remaining at-rest encryption site through the versioned envelope so that `ENCRYPTION_KEY` (and the future `FALLBACK_ENCRYPTION_KEY`) actually covers them. The legacy unprefixed CTR ciphertext remains readable as a fallback during the rollout window — every migrated read site uses `decryptVersioned`, which transparently delegates to the legacy CTR decrypt when it sees an unprefixed payload. ### Service migrations - **`ApplicationVariableEntityService` (#8)** — workspace-scoped. HKDF info is bound to each row's `workspaceId`. A new `decryptAndMaskVersioned` helper lands on `SecretEncryptionService` for the resolver display path. - **`ApplicationRegistrationVariableService` (#7)** + consumers — **instance-scoped**. Registration variables are server-level config readable by every workspace that installs the application, so HKDF info is `instance`. Updated consumers: - `LogicFunctionExecutorService.buildServerVariableEnvMap` - `ConnectionProviderService.getClientCredentials` - **`LogicFunctionExecutorService.buildEnvVar` (#9)** — workspace-scoped. Each variable's `workspaceId` is threaded into `decryptVersioned`, so per-workspace HKDF contexts are honoured at execution time. - **`UpdateApplicationVariableActionHandlerService`** (workspace-migration runner) — threads `workspaceId` through the secret/non-secret toggle. - **`JwtKeyManagerService` (#3)** — instance-scoped. Signing keys are shared across the JWKS. - **`ConfigStorageService` (#6)** — instance-scoped sensitive STRING config variables. ### Slow instance commands (2.5.0) Each migrated site has a paired backfill that re-encrypts existing rows into the v2 envelope before the column is constrained: | timestamp | command | scope | CHECK constraint | |---|---|---|---| | `1798000005000` | encrypt-application-variable | workspaceId | `"isSecret" = false OR value = '' OR value LIKE 'enc:v2:%'` | | `1798000006000` | encrypt-application-registration-variable | instance | `"encryptedValue" = '' OR value LIKE 'enc:v2:%'` | | `1798000007000` | encrypt-signing-key-private-keys | instance | `"privateKey" IS NULL OR value LIKE 'enc:v2:%'` | | `1798000008000` | encrypt-sensitive-config-storage | instance | _none_ — heterogeneous jsonb column | All backfills are idempotent (the SELECT filter skips rows already in v2 form) and run before their respective `up()` adds the CHECK constraint. Every `down()` deliberately stops at dropping the CHECK constraint — they intentionally do not re-introduce plaintext on rollback. ### Tests - Unit specs for each new slow command cover the v2 upgrade path, the idempotency invariant, and the instance vs workspace HKDF scope. - New `JwtKeyManagerService` spec asserts `decryptVersioned`/`encryptVersioned` are called without `workspaceId` (instance scope). - Updated existing specs for `ApplicationVariableEntityService`, `ConfigStorageService`, and `buildEnvVar` to assert the versioned API and the workspace HKDF context plumbing. - New `SecretEncryptionService.decryptAndMaskVersioned` cases in the service spec. - Updated the `applicationRegistrationVariable` integration spec to assert the column now stores `enc:v2:<keyId>:<base64>` instead of raw legacy CTR. ### Out of scope (future PRs) - `PostgresCredentialsService` — bespoke `jwtWrapperService.generateAppSecret`–derived key + `encryptText`/`decryptText` from `auth.util.ts`; deserves its own migration. - `SimpleSecretEncryptionUtil` (TOTP) — entirely different `aes-256-cbc` `iv:enc` format; deserves its own migration. ## Test plan - [x] `npx nx typecheck twenty-server` - [x] `npx nx lint:diff-with-main twenty-server` (oxlint + prettier) - [x] Local jest run for `secret-encryption | connected-account-token | application-variable | application-registration-variable | build-env-var | jwt-key-manager | config-storage | encrypt-application-variable | encrypt-application-registration-variable | encrypt-signing-key | encrypt-sensitive-config-storage` — 17 suites, 106 tests pass. - [x] Local jest run for `upgrade | instance-command` — 12 suites, 86 tests pass. - [ ] CI green - [ ] Manual review of CHECK constraint shapes by a server reviewer (each one matches `enc:v2:%` rather than `enc:v_:%` since none of the migrated columns can legitimately hold `enc:v1:` ciphertext). |
||
|
|
04eb913551 |
chore(page-layout): remove IS_RECORD_PAGE_LAYOUT_* feature flags (#20556)
## Summary
- Both \`IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED\` and
\`IS_RECORD_PAGE_LAYOUT_GLOBAL_EDITION_ENABLED\` are force-enabled on
every existing workspace by the 1.23.0 upgrade command
\`BackfillRecordPageLayoutsCommand\` and seeded enabled for new
workspaces via \`DEFAULT_FEATURE_FLAGS\` +
\`seed-feature-flags.util.ts\`. They are no longer load-bearing.
- Unwrap all \`if (flag) { … }\` conditionals to their enabled branch on
both server and front.
- Delete legacy fallback files that only the disabled branch reached:
\`PageLayoutRelationWidgetsSyncEffect\`,
\`usePageLayoutWithRelationWidgets\`,
\`reInjectDynamicRelationWidgetsFromDraft\`,
\`injectRelationWidgetsIntoLayout\`, \`isDynamicRelationWidget\` (and
their tests).
- Strip the two \`enableFeatureFlags\` calls from the 1.23 upgrade
command — the page-layout backfill data logic itself is kept intact
since old workspaces upgrading from < 1.23 still need it.
- No DB cleanup migration: stale \`featureFlag\` rows are left in place,
matching the precedent set by #20531 and #20460.
Net diff: 37 files, +106 / -1727.
## Test plan
- [x] \`npx nx typecheck twenty-shared twenty-server twenty-front\` —
all pass
- [x] \`npx nx lint:diff-with-main twenty-server twenty-front\` — all
pass
- [x] \`cd packages/twenty-front && npx jest page-layout\` — 1240 tests,
all pass
- [x] \`cd packages/twenty-server && npx jest
workspace-entity-manager.spec\` — pass
- [ ] Manual smoke: open a record page, verify tabs render and \"Edit
Layout\" command-menu action is available
- [ ] Manual smoke: Settings → Data model → object → Layout tab is
visible (and hidden for remote / Dashboard objects)
- [ ] Manual smoke: edit a tab title, save, reload — confirm persistence
|
||
|
|
6dd1e8a471 |
feat(upgrade): expose twenty_upgrade_workspaces_up_to_date_total (#20555)
## Summary Adds a fourth gauge alongside the existing `twenty_upgrade_workspaces_behind_total` / `twenty_upgrade_workspaces_failed_total` so dashboards can show how many workspaces are currently healthy, not just the ones that need attention. - New gauge: `twenty_upgrade_workspaces_up_to_date_total` - New count is computed during `UpgradeStatusService.refreshInstanceAndAllWorkspacesStatus` (cheap — we already iterate over every workspace), persisted in the existing `UpgradeStatusCacheService` so the cache-hit path stays a single round trip, and surfaced via `InstanceAndAllWorkspacesUpgradeStatusDTO` for the admin panel. ## Files - `packages/twenty-server/src/engine/core-modules/upgrade/upgrade-gauge.service.ts` — register the new ObservableGauge - `packages/twenty-server/src/engine/core-modules/upgrade/services/upgrade-status.service.ts` — count UP_TO_DATE workspaces during refresh, propagate through cached path - `packages/twenty-server/src/engine/core-modules/upgrade/services/upgrade-status-cache.service.ts` — persist `upToDateWorkspaceCount` next to behind/failed sets - `packages/twenty-server/src/engine/core-modules/upgrade/dtos/instance-and-all-workspaces-upgrade-status.dto.ts` — `Int` field on the admin DTO - Tests: extended `upgrade-status.service.spec.ts` (14/14 green) — cached and refresh paths both assert on `upToDateWorkspaceCount` ## Follow-up A companion `twenty-infra` PR adds the new tile + line on the upgrade-status Grafana dashboard. |
||
|
|
d81756f2e8 |
Add default value to apiKey for authentication method (#20552)
- fix authentication method default value when docker instance to apiKey |
||
|
|
49b9660420 |
fix(auth): preserve returnToPath across Google/Microsoft SSO redirects (#20537)
## Summary Fixes the consent-modal-not-reopening half of [#20535](https://github.com/twentyhq/twenty/issues/20535): when a signed-out user opens an OAuth `/authorize?...` URL (e.g. ChatGPT connecting to `api.twenty.com/mcp`) and signs in with **Google or Microsoft**, the original `/authorize` request was lost and the consent screen never reopened. ### Root cause `PageChangeEffect` already saves the deep link as `returnToPath` (Jotai atom) before navigating to `/welcome`. That atom is in-memory: it survives SPA navigation, and the cross-subdomain workspace hop is handled by `useBuildSearchParamsFromUrlSyncedStates` round-tripping the value through the URL. But the social-SSO path leaves `app.twenty.com` entirely — `app.twenty.com/welcome` → `api.twenty.com/auth/google` → Google → `api.twenty.com/auth/google/redirect` → frontend — so the atom is wiped. None of the existing code paths plumbed `returnToPath` through that hop: - `useAuth.buildRedirectUrl` packed `workspaceInviteHash`/`action`/etc. but not `returnToPath`. - `SocialSSOState` / the Google + Microsoft strategies didn't carry it through the OAuth `state` blob. - `signInUpWithSocialSSO` + `computeRedirectURI` didn't re-emit it on the redirect back to the frontend. The email path worked because all transitions stay on the default frontend domain, so the atom survives until `SignInUpGlobalScopeForm` bakes it into the workspace URL. ### What changed Plumb `returnToPath` through the SSO state the same way `workspaceInviteHash` and `action` already flow: - **Frontend** (`useAuth.buildRedirectUrl`): read `returnToPath` from the Jotai store and append it to `/auth/google` / `/auth/microsoft` when set and structurally valid. - **Server types** (`SocialSSOState`, `GoogleRequest['user']`, `MicrosoftRequest['user']`): add optional `returnToPath`. - **Strategies** (`google.auth.strategy.ts`, `microsoft.auth.strategy.ts`): include `returnToPath: req.query.returnToPath` in the JSON `state` and read it back in `validate`. - **auth.service.ts** (`signInUpWithSocialSSO`, `computeRedirectURI`): forward `returnToPath` on both branches — the multi-workspace redirect to `AppPath.SignInUp?tokenPair=...` and the single-workspace redirect to `<workspace>/verify?loginToken=...`. Validated via a new `isValidReturnToPath` helper so a tampered query value can't become an open-redirect vector. After the round-trip, `useInitializeQueryParamState` rehydrates the atom from the URL and `usePageChangeEffectNavigateLocation` resolves it as the post-auth destination — same mechanism the email path already relied on. Out of scope: the OAuth `resource` parameter handling tracked in [#20296](https://github.com/twentyhq/twenty/issues/20296) is independent and not addressed here. ## Test plan - [x] `npx jest src/engine/core-modules/auth` (twenty-server) — 27 suites / 183 tests pass, including new `is-valid-return-to-path.util.spec.ts`. - [x] `npx jest src/modules/auth` (twenty-front) — 13 suites / 52 tests pass, including two new cases in `useAuth.test.tsx` covering the happy path and the protocol-relative open-redirect guard. - [x] `npx nx typecheck twenty-server` / `twenty-front` — clean. - [x] `npx oxlint` + `prettier --check` on touched files — clean. - [ ] Manual: signed-out user opens `https://app.twenty.com/authorize?client_id=...` → Continue with Google → completes Google → selects workspace → consent screen renders. - [ ] Manual: same flow, single workspace — lands on consent screen directly after Verify. - [ ] Manual: email path still works (regression). - [ ] Manual: tamper `returnToPath=//evil.com` on the `/auth/google` URL → server validation rejects, user lands at default home, not at `evil.com`. E2E note: existing `return-to-path.spec.ts` already covers deep links with query params through the email path. A mock OAuth provider would be needed to cover the SSO path end-to-end; unit coverage stands in for now. |
||
|
|
2a9fef2341 |
feat(upgrade): emit structured logfmt logs for upgrade flow (#20539)
## Summary
Adds a small helper that lets every log line in the upgrade flow stay
human-readable while emitting a structured tail that Loki / the
upgrade-status Grafana dashboard can filter on.
Output shape per `logger.log()` call:
```
<humanMessage as-is, may span multiple lines>
[upgrade] event=<event> key=value … ← always single line
```
Same call produces **one** structured Loki event regardless of how
chatty the human-readable part gets — the dashboard's `|= "[upgrade]"`
filter only matches the trailing line.
## Helper API
```ts
formatUpgradeLog({
humanMessage: string, // free-form, multi-line OK, for engineers scrolling raw pod logs
event: string, // required anchor for Loki filtering / dashboards
logFields?: Record<string, // short structured key=value tail
string | number | boolean | null | undefined
>,
});
```
- `humanMessage` is preserved as-is. A thrown `new Error('line one\nline
two')` surfacing through `humanMessage` stays human-readable across
multiple lines.
- `logFields` values are logfmt-escaped: whitespace / `=` / `"` get
quoted, embedded `\` / `"` / `\n` / `\r` / `\t` are escaped, `null` /
`undefined` emit literally (`key=null`, `key=undefined`) instead of
being silently dropped — caught via `isDefined` from
`twenty-shared/utils`.
- `event` itself runs through the same escape so an event name with
whitespace or `=` can't break parsing.
## Example output
```
Initialized upgrade sequence: 8 step(s)
[upgrade] event=sequence.initialized stepCount=8 dryRun=false
Upgrading workspace abc-123 1/10
[upgrade] event=workspace.start workspaceId=abc-123 index=1 total=10 dryRun=false
Upgrade for workspace abc-123 completed.
[upgrade] event=workspace.success workspaceId=abc-123 executedByVersion=1.4.0 dryRun=false
Upgrade summary: 42 workspace(s) succeeded, 1 workspace(s) failed
[upgrade] event=summary totalSuccesses=42 totalFailures=1 dryRun=false
Upgrade failed: Workspace migration runner failed:
- Option id is required
- Option id is invalid
[upgrade] event=aborted totalSuccesses=41 totalFailures=2 dryRun=false
```
Loki query for the dashboard: `{namespace="twenty"} |= "[upgrade]" |
logfmt event, workspaceId, command, executedByVersion`
## Scope
Only the **upgrade-specific** call sites carry the tag:
- `upgrade.command.ts` — `sequence.initialized`, `sequence.step`
(verbose), `summary`, `aborted`
- `upgrade-sequence-runner.service.ts` — `sequence.stopped`,
`sequence.aborted`
- `workspace-command-runner.service.ts` — `workspace.start`,
`workspace.success`, `cache.invalidate.failed`
`instance-command-runner.service.ts` is intentionally **not** tagged —
`runFastInstanceCommand` / `runSlowInstanceCommand` are also invoked
from `RunInstanceCommandsCommand` (DB init / `run-instance-commands`),
so an `[upgrade]` tag there would mislead at init time. Those lines stay
plain-text; stacks still flow on their own via NestJS
`logger.error(message, error.stack)`.
`chalk` is dropped from `upgrade.command.ts` — ANSI escapes break log
parsers and chalk is a no-op without a TTY anyway.
## Tests
9 inline-snapshot tests in `format-upgrade-log.util.spec.ts` surface the
actual output of every interesting shape (summary call site, multi-line
humanMessage, quoted / escaped / control-character logField values,
null/undefined fields, event name escaping). Snapshots double as
documentation of what a real upgrade log line looks like.
## Test plan
- [x] Unit tests green (`jest format-upgrade-log`)
- [x] oxlint + prettier clean
- [x] tsgo typecheck clean on the upgrade module
- [x] CI green
- [ ] Smoke test on staging: run `upgrade` command, confirm `[upgrade]`
structured lines surface in Loki and `| logfmt` extracts fields
|
||
|
|
0dc8426727 |
i18n - translations (#20554)
Created by Github action --------- Co-authored-by: github-actions <github-actions@twenty.com> |
||
|
|
e16977f97b |
[breaking: deploy server before front] feat(view-sort): pick sort sub-field inline on the chip (#20445)
## Summary Lets users choose which sub-field of a composite column to sort by — directly from the sort chip — by clicking the sub-field label and picking from a dropdown. Persists per view via a new nullable \`subFieldName\` column on \`ViewSort\`. Replaces #20438, which proposed a field-settings (admin) configuration for the same problem. The chip-level approach is more discoverable (the option lives where the user is looking) and per-view, so different views on the same object can sort by different sub-fields. ### What changes for users - **FullName columns**: previously sorted by \`firstName\` and \`lastName\` together as a stable dual-key sort. Now the user can pick which sub-field is primary (the other is the tie-breaker). Default remains \`firstName\` primary, \`lastName\` tie-breaker. - **Address columns**: previously not sortable at all (not in \`SORTABLE_FIELD_METADATA_TYPES\`). Now sortable, with a chip dropdown listing each enabled sub-field. Default is \`addressCity\` if enabled, else the first enabled sub-field. Disabling a sub-field at the field-metadata level (existing setting) removes it from the dropdown. - **Other composite types** (Currency, Phones, Emails, Links, Actor) and scalar fields keep their existing single-key sort behavior. ### UX ``` ┌─────────────────────────┐ ┌─────────────────────────┐ │ ↑ Name · Last name ✕ │ │ ↑ Address · City ✕ │ └────────┬────────────────┘ └────────┬────────────────┘ ▼ (click sub-field) ▼ ┌────────────┐ ┌────────────┐ │ First name │ │ Address 1 │ │ Last name ✓│ │ Address 2 │ └────────────┘ │ City ✓│ │ State │ │ Postcode │ │ Country │ └────────────┘ ``` The chip body still toggles direction on click — the \`Dropdown\`'s internal wrapper calls \`stopPropagation\` so the sub-field click doesn't bubble to the chip's onClick. ## What changed **Backend:** - \`ViewSortEntity\` — new nullable \`subFieldName: varchar\` column - \`ViewSortDTO\`, \`CreateViewSortInput\`, \`UpdateViewSortInputUpdates\` — new \`@Field(() => String, { nullable: true })\` - \`FLAT_VIEW_SORT_EDITABLE_PROPERTIES\` — \`'subFieldName'\` added so the property flows through the update merge path - \`ALL_ENTITY_PROPERTIES_CONFIGURATION_BY_METADATA_NAME.viewSort\` — new \`subFieldName\` entry with \`toCompare: true\` so cache diffs notice it - \`fromCreateViewSortInputToFlatViewSortToCreate\` — threads \`subFieldName\` through - Instance command migration (\`add-sub-field-name-to-view-sort\`) — single \`ALTER TABLE core.viewSort ADD subFieldName varchar\` / \`DROP\` **Frontend:** - \`RecordSort\` and \`ViewSort\` types — \`subFieldName?: string | null\` - \`VIEW_SORT_FRAGMENT\` — adds \`subFieldName\` so the field round-trips - \`mapRecordSortToViewSort\` + \`areViewSortsEqual\` — carry the new field through, include it in the diff so the usual \`useSaveRecordSortsToViewSorts\` create/update flow fires when it changes - \`useSaveRecordSortsToViewSorts\` — passes \`subFieldName\` in both \`CreateViewSortInput\` and \`UpdateViewSortInputUpdates\` - \`getOrderByForFieldMetadataType(field, direction, subFieldName?)\` — new optional third arg. \`turnSortsIntoOrderBy\` threads \`sort.subFieldName\` into it. - \`Address\` added to \`SORTABLE_FIELD_METADATA_TYPES\` - New helpers: \`getEnabledAddressSubFields\` (filters by the field's \`subFields\` setting, falls back to the 6 default visible address sub-fields), \`getDefaultSortSubFieldForAddress\`, \`getDefaultSortSubFieldForFullName\` - New shared types/constants: \`AllowedFullNameSubField\`, \`ALLOWED_FULL_NAME_SUBFIELDS\`, \`DEFAULT_VISIBLE_ADDRESS_SUBFIELDS\` - \`SortOrFilterChip\` — new \`labelSubField?: ReactNode\` slot; renders as \` · {sub-field}\` with subdued weight after the main label - \`EditableSortChip\` — builds options from field metadata (\`ALLOWED_FULL_NAME_SUBFIELDS\` for FullName, \`getEnabledAddressSubFields\` for Address), uses i18n-wrapped labels, persists picks via \`upsertRecordSort\` ## Test plan - [x] \`npx nx typecheck\` passes for twenty-shared, twenty-front, twenty-server - [x] \`oxlint --type-aware\` on all 19 frontend + 9 server changed files: 0 errors - [x] \`prettier --check\`: clean - [x] 16 unit tests pass — \`getOrderByForFieldMetadataType\` covers the new \`subFieldName\` override branch for FULL_NAME and ADDRESS; \`getDefaultSortSubFieldForAddress\` covers the city/first-enabled fallback path; \`getDefaultSortSubFieldForFullName\` exercises its constant - [ ] Manual: sort a People view by Full Name → click the chip's sub-field label → switch between First name and Last name → reload page → choice is preserved - [ ] Manual: sort a Company view by Address → confirm dropdown lists only enabled sub-fields → disable Address \`addressCity\` in field settings → confirm dropdown options update and runtime falls back to the first enabled sub-field 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> |
||
|
|
8bc21ec434 |
i18n - docs translations (#20553)
Created by Github action Co-authored-by: github-actions <github-actions@twenty.com> |
||
|
|
915ce37d20 |
i18n - translations (#20551)
Created by Github action Co-authored-by: github-actions <github-actions@twenty.com> |
||
|
|
0cc2194399 |
Simplify create-twenty-app command (#20512)
## Simplify `create-twenty-app` for zero-interaction use Makes `npx create-twenty-app@latest my-app` a fully non-interactive, single-command experience suitable for automated environments (Codex, Claude plugins). ### Changes - **Remove all interactive prompts** — app name, display name, description, and scaffold confirmation are now derived from CLI args with sensible defaults. `inquirer` dependency removed entirely. - **Replace OAuth with API key auth** — use the seeded dev API key (`DEV_API_KEY`) to authenticate against the Docker instance as `tim@apple.dev`, eliminating the browser-based OAuth flow. - **Docker-first with early validation** — check Docker is installed before scaffolding; if missing, print the install URL and exit. Detect alternative runtimes (Podman, nerdctl). - **Parallel image pull** — `docker pull` runs in the background during scaffold + dependency install, saving 10-30s on typical runs. - **Always pull latest image** — ensures the dev server is up-to-date on every run. - **Stop detecting port 3000** — only check port 2020 (Docker instance). - **Update CLI flags** — remove `--skip-local-instance` and `--yes`; add `--skip-docker`. - **Update CI workflows and docs** — align e2e workflows, package README, and template README/cd.yml with the new flow. |
||
|
|
70a3b25680 |
i18n - docs translations (#20549)
Created by Github action Co-authored-by: github-actions <github-actions@twenty.com> |
||
|
|
6a9509ec01 |
i18n - translations (#20548)
Created by Github action --------- Co-authored-by: github-actions <github-actions@twenty.com> |
||
|
|
fcd2d586ee |
chore(billing) - remove feature flag (#20531)
- remove feature flag - remove old enforce cap usage logic |
||
|
|
695a374efd |
fix - nav drawer expansion (#20545)
[PR](https://github.com/twentyhq/twenty/pull/20505), I merge, does not fix the whole issue Closes https://github.com/twentyhq/twenty/issues/20502 |
||
|
|
dea1f89904 |
Inject none secret env variables into front components (#20511)
## Summary - Inject non-secret application variables (`isSecret: false`) into front component `process.env` via the existing Web Worker `setWorkerEnv` mechanism - Filter secret variables server-side in the resolver so they never reach the browser - Set application variables before system variables (`TWENTY_API_URL`, `TWENTY_APP_ACCESS_TOKEN`) to prevent override - Wire up environment variable keys in the logic function code editor for TypeScript autocomplete ## Test plan - [x] Unit tests for `buildNonSecretEnvVar` (6 passing) - [x] Typecheck passes for `twenty-front` and `twenty-server` - [x] Install an app with both `isSecret: false` and `isSecret: true` variables, open a front component, verify only non-secret vars appear in `process.env` - [x] Open a logic function editor, verify autocomplete suggests declared variable keys |
||
|
|
59b993bdb3 |
i18n - translations (#20547)
Created by Github action --------- Co-authored-by: github-actions <github-actions@twenty.com> |
||
|
|
e0b4c9918b |
feat(twenty-server): introduce ENCRYPTION_KEY env var with versioned envelope (#20528)
## Summary
- Adds `ENCRYPTION_KEY` (primary) and `FALLBACK_ENCRYPTION_KEY`
(decrypt-only fallback for rotation) env vars to twenty-server, with
backward-compatible fallback to `APP_SECRET` when `ENCRYPTION_KEY` is
unset.
- Introduces a versioned ciphertext envelope `enc:v2:<keyId>:<base64>`
using AES-256-GCM with HKDF-SHA256 derived per-context keys. The 8-hex
`keyId` fingerprint lets every row identify which physical key encrypted
it, so rotation routes directly to primary or fallback without trial
decryption; GCM's auth tag gives true integrity (legacy CTR has none).
- Migrates `ConnectedAccountTokenEncryptionService` to the new envelope
and plumbs `workspaceId` through every caller, so per-workspace HKDF
context binds each row to its tenant.
The remaining encryption sites (`jwt-key-manager`, `config-storage`,
`postgres-credentials`, `application-variable`, TOTP) stay on the legacy
unprefixed CTR path and will be migrated in follow-up PRs. The
operator-facing rotation runbook is out of scope here.
### Format details
`enc:v{N}:{keyId}:{base64}` — `N=2` is the only version produced by new
writes (`v1` exists for backward-compatible decryption of existing
connected-account rows). `keyId =
sha256(rawKey).slice(0,4).toString('hex')`. The CHECK constraint on
`core.connectedAccount.{accessToken,refreshToken}` is relaxed from `LIKE
'enc:v1:%'` to `LIKE 'enc:v_:%'` so both versions pass.
### Key resolution
| `ENCRYPTION_KEY` | `FALLBACK_ENCRYPTION_KEY` | `APP_SECRET` | Encrypt
with | Decrypt try order |
|---|---|---|---|---|
| set | set | (any) | `ENCRYPTION_KEY` | match `keyId` → primary →
fallback |
| set | unset | (any) | `ENCRYPTION_KEY` | match `keyId` → primary |
| unset | set | set | `APP_SECRET` | match `keyId` → `APP_SECRET` →
fallback |
| unset | unset | set | `APP_SECRET` | match `keyId` → `APP_SECRET` |
| unset | unset | unset | startup error | n/a |
## Test plan
- [x] `npx nx typecheck twenty-server` — clean
- [x] `npx jest
'secret-encryption|connected-account-token-encryption|connected-account-refresh-tokens|encrypt-connected-account-tokens|connection-provider-oauth-flow'`
— 87 tests pass
- [x] New `secret-encryption.service.versioned.spec.ts` covers: key
resolution table (no-key error, APP_SECRET fallback, ENCRYPTION_KEY
precedence), v2 round-trip with/without workspaceId, GCM tamper
rejection, workspaceId-mismatch rejection, keyId-based primary→fallback
routing, missing-key error names the fingerprint, v1 legacy decryption,
no-prefix legacy decryption, malformed envelope rejection.
- [x] Updated `connected-account-token-encryption.service.spec.ts`
covers workspaceId binding and HKDF context isolation.
- [x] Updated slow instance command spec verifies workspaceId is
threaded through encryption and the relaxed `enc:v_:%` LIKE pattern
matches both v1 and v2.
- [ ] Manual E2E: connect a Gmail account on a freshly deployed instance
with `APP_SECRET` only → confirm `core.connectedAccount.accessToken` is
`enc:v2:<keyId>:<base64>`.
- [ ] Manual E2E: rotate — set `ENCRYPTION_KEY=<new>` and
`FALLBACK_ENCRYPTION_KEY=<old APP_SECRET>`, restart, confirm
pre-rotation rows still decrypt and new rows carry the new `keyId`.
- [ ] Manual E2E: missing key — set `ENCRYPTION_KEY=<new>` without the
fallback, confirm decrypt error names the old `keyId` so the operator
can identify the missing key.
|
||
|
|
aec2e01662 |
fix(server): handle ImapFlow socket errors instead of crashing the process (#20510)
## Summary `ImapFlow` is an `EventEmitter`; per Node.js semantics, an emitted `'error'` event with no listener becomes an uncaught exception that exits the process. Both ImapFlow construction sites in `twenty-server` (`ImapClientProvider` used by all messaging flows, and `testImapConnection` in the connection-wizard validator) currently build the client without attaching a permanent `'error'` listener, so a transient socket condition (idle timeout, network blip, server-side disconnect) crashes `twenty-server` and triggers a container restart with a ~1 min HTTP 502 window for end users. This patch attaches an `'error'` listener at each call site that logs the error and lets `imapflow`'s internal reconnect handle recovery. Same shape / same precedent as #20143 (Redis session-store client) which fixed #20144. Closes #20509. ## What changed - `packages/twenty-server/src/modules/messaging/message-import-manager/drivers/imap/providers/imap-client.provider.ts`: `ImapClientProvider.createConnection` now attaches `client.on('error', ...)` between construction and `connect()`. - `packages/twenty-server/src/engine/core-modules/imap-smtp-caldav-connection/services/imap-smtp-caldav-connection.service.ts`: `testImapConnection` does the same on its short-lived test client. Both listeners log via the existing `Logger` instance (matching the resolver-level logging already in `ImapClientProvider.getClient`) and surface `error.stack` so transient socket conditions are observable but no longer fatal. ## Crash this fixes (real production stack) ``` node:events:487 throw er; // Unhandled 'error' event ^ Error: Socket timeout at TLSSocket.<anonymous> (/app/node_modules/imapflow/lib/imap-flow.js:795:29) at TLSSocket.emit (node:events:509:28) at Socket._onTimeout (node:net:610:8) ... Emitted 'error' event on ImapFlow instance at: at ImapFlow.emitError (/app/node_modules/imapflow/lib/imap-flow.js:397:14) code: 'ETIMEOUT', ``` End-user impact: server process exits cleanly (code 0), Docker / k8s restarts it; the DB, worker, redis, and caddy containers are unaffected — only the API server dies, taking the GraphQL/REST surface offline for a ~1 min health-check warmup. ## Test plan - [x] `npx nx typecheck twenty-server` (planned — relying on CI for verification) - [x] `npx nx lint:diff-with-main twenty-server` (planned — relying on CI for verification) - [x] Manually reproduced the crash on `v2.2` by hitting an IMAP/SMTP/CalDAV outbound flow with Gmail; with the patch applied locally to the running container (verified the listener fires and logs without process exit), the server stays up across the same trigger sequence. - [ ] Unit-level coverage: behavior is "listener exists, doesn't throw" — not easily covered without a contrived socket-mock test. Existing call sites have no unit tests today; happy to add one if a reviewer prefers, otherwise mirroring the convention from #20143 which merged without a new test. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: neo773 <neo773@protonmail.com> |
||
|
|
aecfe699f4 |
feat(ai-chat) - Stop ai thinking if credits exhausted (#20526)
Billing is now decremented per-step, not per-turn. The onStepFinish callback in chat-execution.service.ts calls a new decrementAndCheckAvailableCredits method on each model step, so Redis is debited incrementally as the agent runs rather than all at once at the end. Credit exhaustion stops the stream mid-run. When a step depletes the remaining credits, a hasNoMoreAvailableCredits flag is set and passed into the stopWhen predicate of streamText, causing the agent to halt before starting the next step. A new credits-exhausted event is introduced. After the stream drains and the response is persisted, if credits ran out the job publishes a dedicated credits-exhausted event to the frontend instead of the normal message-persisted event. The frontend handles this new event. useAgentChatSubscription has a new credits-exhausted case that sets a BILLING_CREDITS_EXHAUSTED-coded error on the atom, closes the writer, and stops the streaming state — triggering the existing AiChatCreditsExhaustedMessage UI. |
||
|
|
068fc9363d |
fix(navigation): settings drawer should never appear collapsed (#20505)
## Summary Fixes #20502 /claim #20502 The navigation drawer's collapsed state is persisted to \`localStorage\` via \`isNavigationDrawerExpandedState\`. When a user collapses the drawer in the main app and then opens settings via a **direct URL, refresh, or new tab**, the settings layout renders in collapsed mode — no \`useOpenSettingsMenu\` call is made in those paths to force expansion. \`StyledAnimatedContainer\` (which controls the outer drawer width) used raw \`isNavigationDrawerExpanded\` with no settings-route override. Inner components (\`NavigationDrawerItemsCollapsableContainer\`) already guard with \`isExpanded = isNavigationDrawerExpanded || isSettingsPage\` — the outer container simply needed the same treatment. **Fix:** derive \`isExpanded = isSettingsDrawer || isNavigationDrawerExpanded\` using the already-in-scope \`useIsSettingsDrawer()\` result and pass it to both \`StyledAnimatedContainer\` and \`StyledContainer\`. 1 derived variable, 2 prop changes, no new hooks or state. **Changed files:** - \`packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawer.tsx\` ## Test plan Verified via code review and CI. The fix is structurally identical to the existing \`isSettingsPage\` guard already used in \`NavigationDrawerItemsCollapsableContainer\` — the outer container simply lacked the same treatment. No new hooks, no side effects, no state mutations. Manual UI verification (collapse → navigate to settings → confirm expanded) was not performed against a running instance. If the maintainers want to verify, the logic path is the same as the inner component guard that already ships in production. > **Note (2026-05-12):** PR #20508 was submitted after this PR with a \`useEffect\`-based approach. That approach has already received a review comment from a team member noting that \`useEffect\` should be a last resort per the project's own React guidelines. This fix uses no effects or imperative state — only a derived variable. --- > [!NOTE] > **AI-Assisted Contribution** > This patch was generated by [Mesopredator](https://github.com/GusFromSpace), an autonomous code intelligence system. > Static analysis located the bug, the fix was written and verified with \`tsc --noEmit\` + \`node\` test suite, and reviewed by a human before submission. > Please treat this as a community contribution and request changes if needed. |
||
|
|
99a5a038bc |
fix(server): add Apple seed workspace as fallback for single-workspace mode (#20498)
## Issue
As per the developer docs, the local setup uses the `npx nx
database:reset twenty-server` command, which seeds 4 workspaces. This
works correctly for multi-workspace mode
(`IS_MULTIWORKSPACE_ENABLED=true`) and integration tests but causes
issues in single-workspace mode (`IS_MULTIWORKSPACE_ENABLED=false`) or
when switching from multi-workspace mode back to single-workspace mode.
Also, the default mode is single-workspace but 4 workspaces are already
seeded in the database. As a result,
`WorkspaceDomainsService.getDefaultWorkspace()` selects the newest
workspace (Empty4), which is intended only for integration testing and
contains no user data.
There is also an existing warning log mentioning fallback to the Apple
seed workspace when multiple workspaces are found in single-workspace
mode i.e `IS_MULTIWORKSPACE_ENABLED=true`, but it was never implemented.
```
if (workspaces.length > 1) {
Logger.warn(
` ${workspaces.length} workspaces found in database. In single-workspace mode, there should be only one workspace. Apple seed workspace will be used as fallback if it found.`,
);
}
```
Although we could replace with `"nx command-no-deps --
workspace:seed:dev --light"` in `project.json` for `database:reset`,
which will only seed one workspace but it wont resolve issue when
switching from multi-workspace mode back to single-workspace mode or for
integration test `with-db-reset`.
## Changes
- Implement fallback behavior already hinted by existing warning logs.
- Ensure the Apple seed workspace is used as the fallback in
single-workspace mode when multiple workspaces exist.
This improves:
- Local developer onboarding experience.
- Switching between multi-workspace and single-workspace development
modes.
- Consistency during local development and integration testing.
## Related PR
- #19822
- #20464
These PRs only resolves the issue for docker environments but not for
local development setup.
---------
Co-authored-by: Charles Bochet <charles@twenty.com>
|
||
|
|
3d90b3882b |
fix(ai-agent-node) - agent node execution error (#20534)
**Root cause:** getWorkflowRunContext(stepInfos) builds a Record<string, unknown> from the previous steps' results. There is no workspaceId key in it, so context.workspaceId as string silently evaluated to undefined. That undefined was then passed all the way down to WorkspaceCacheService.getOrRecompute, **which correctly throws** when workspaceId is not a valid UUID. Before : <img width="525" height="130" alt="Screenshot 2026-05-13 at 14 58 54" src="https://github.com/user-attachments/assets/0549b4dc-7063-44e5-95a1-00a460a6d7f1" /> Introduced with billing v2 yesterday, since then, workspaceId is needed to bill credit usage |
||
|
|
643ec121a7 |
[twenty-server] no-misused-promise lint (#20529)
# Introduction
Adding `no-miused-promise` lint rule to the twenty-server
In order to flag such pattern
```ts
// ❌ Flagged — forEach doesn't await the callback
items.forEach(async (item) => {
await process(item);
});
```
## What happened
- Refactored the code-interpreter driver to have a async onResult (
which is also expected by e2b )
- Workspace manager still dirty solution including force cast
- Basic fixes
|
||
|
|
27fd124c2e |
Dedicated REST controllers for object & field metadata (#20364)
## Summary
- Replace the dynamic `RestApiMetadataController` (which parsed
`/rest/metadata/*path` and proxied to internal GraphQL) with two
dedicated controllers: `ObjectMetadataController` and
`FieldMetadataController`.
- Drop the GraphQL hop: reads hit Postgres directly via TypeORM
repositories; writes call the existing
`{create,update,delete}One{Object,Field}` service methods.
- Introduce a new clean response shape behind a workspace feature flag
(`IS_REST_METADATA_API_NEW_FORMAT_DIRECT`) — see grace period below.
- Update the OpenAPI spec so the REST playground reflects the (default)
legacy shape during the grace period.
## Why
The legacy metadata controller was over-complex: it routed every method
through a path parser, a set of GraphQL query-builder factories, an
internal GraphQL call, and a
`cleanGraphQLResponse` post-processor. Operation names from GraphQL
(`createOneObject`, `updateOneField`, …) leaked straight into REST
responses. The internal-GraphQL hop also gave us
nothing on metadata reads — pagination, filtering, and serialization all
happen against the same Postgres tables either way.
## Feature flag & grace period
`IS_REST_METADATA_API_NEW_FORMAT_DIRECT` (workspace-scoped):
- **Existing workspaces:** flag absent → resolves to `false` → **legacy
response shape** (no behavior change).
- **Newly created workspaces:** flag seeded to `true` via
`DEFAULT_FEATURE_FLAGS` → **new response shape** from day one.
- **Toggle:** support-assisted (no frontend); customers contact us to
opt into the new shape early.
- **Removal:** the flag, the legacy adapter utils
(`to-legacy-{object,field}-metadata-response.util.ts`), and the
parametrized test wrapper get deleted after the grace window. New shape
becomes the only shape; OpenAPI flips to new shape; POST loses the
conditional and reverts to a declarative response.
## Response shapes
| Operation | Legacy (flag OFF, default for existing) | New (flag ON) |
|-----------|-----------------------------------------|---------------|
| `GET /rest/metadata/objects` | `{ data: { objects: [...] }, pageInfo,
totalCount }` | `{ data: [...], pageInfo, totalCount }` |
| `GET /rest/metadata/objects/:id` | `{ data: { object: {...} } }` | `{
... }` |
| `POST /rest/metadata/objects` | `201 { data: { createOneObject: {...}
} }` | `201 { ... }` |
| `PATCH/PUT /rest/metadata/objects/:id` | `{ data: { updateOneObject:
{...} } }` | `{ ... }` |
| `DELETE /rest/metadata/objects/:id` | `{ data: { deleteOneObject: {
... } } }` | `{ ... }` |
Same matrix for `/rest/metadata/fields`. Cursor params
(`starting_after`, `ending_before`, `limit`) and `totalCount` are
preserved across both shapes. POST returns `201` in both (old
controller already did — the doc on main saying `200` was wrong).
## Implementation notes
- Reads go straight to Postgres with TypeORM cursor pagination
(`paginateByIdCursor` util, mutually-exclusive `starting_after` /
`ending_before`). No cache on this path — caching +
filterable pagination didn't combine cleanly.
- Object endpoints inline `fields[]` via a single follow-up `WHERE
objectMetadataId IN (...)` query.
- Controllers read the flag via `FeatureFlagService.isFeatureEnabled`
and conditionally pass the result through a legacy-shape adapter util
before returning.
- Per-domain REST exception filters
(`{Object,Field}MetadataRestApiExceptionFilter`); the `exceptionCode →
httpStatus` switch is extracted to a util so it can be merged with the
existing GraphQL handler later.
- New controllers live inside the metadata domain modules
(`metadata-modules/{object,field}-metadata/controllers/`) to match
existing precedent (view-field, view, page-layout, …).
- Removes: `RestApiMetadataController`, `RestApiMetadataService`,
`metadata/query-builder/`, `clean-graphql-response.utils.ts`.
- Integration tests are parametrized over both flag values via
`describe.each` — both shapes are asserted in CI.
- OpenAPI fixes inherited from the migration (kept as-is): documents
flat `fields: [...]` rather than the obsolete `{edges:{node:[...]}}`
wrapping; always emits `totalCount`; POST
status `201`. These match what customers actually receive on both
shapes.
Note: Next goal is to implement something similar for graphql and remove
nestjs-query dependency for those 2 entities, then generalise it.
Note2: We have the same issue with Core Rest API such as
```json
{
"data": {
"createCompany": {
"id": "123e4567-e89b-12d3-a456-426614174000",
"createdAt": "2026-05-07T12:14:52.769Z",
"updatedAt": "2026-05-07T12:14:52.769Z",
"deletedAt": "2026-05-07T12:14:52.769Z",
...
```
with "createCompany" here which is odd compared to REST standards (FYI
@etiennejouan @charlesBochet)
## Before (Without feature flag)
<img width="1346" height="712" alt="Screenshot 2026-05-12 at 20 50 38"
src="https://github.com/user-attachments/assets/316ce225-1045-4aac-97a9-60fd537eb1ec"
/>
<img width="1378" height="729" alt="Screenshot 2026-05-12 at 20 52 24"
src="https://github.com/user-attachments/assets/a621ab6f-e4f8-44d5-817c-1efd25d33c30"
/>
## After (With feature flag)
<img width="1376" height="728" alt="Screenshot 2026-05-12 at 20 50 46"
src="https://github.com/user-attachments/assets/2424d9c5-e4ed-497c-8e5c-6b54d78675e4"
/>
<img width="1375" height="727" alt="Screenshot 2026-05-12 at 20 51 47"
src="https://github.com/user-attachments/assets/101d957f-38ed-45d9-ab7b-f4f4eb983397"
/>
---------
Co-authored-by: prastoin <paul@twenty.com>
|
||
|
|
bbc55193f5 |
Fix phone unique constraints (#20261)
## Summary Closes #20195 Fix phone field unique constraints so phone numbers are considered unique by both `primaryPhoneNumber` and `primaryPhoneCallingCode`. - Include `primaryPhoneCallingCode` in the shared phone composite unique constraint metadata - Align the frontend settings composite field config with the backend metadata - Return all included unique composite subfields when building create-many conflict fields - Match composite unique conflict fields as a group during create-many upserts ## Root Cause Phone composite metadata only marked `primaryPhoneNumber` as part of the unique constraint. That made different international phone numbers with the same national number conflict, for example `+1 123456789` and `+32 123456789`. ## Test Plan - `yarn workspace twenty-shared build` - `jest --runTestsByPath <index action handler and create-many utility specs>` - `prettier --check <touched files>` - `oxlint --type-aware <touched files>` - `nx run twenty-shared:typecheck` - `nx run twenty-server:typecheck` - `nx run twenty-front:typecheck` --------- Co-authored-by: mkdev11 <MkDev11@users.noreply.github.com> Co-authored-by: Etienne <45695613+etiennejouan@users.noreply.github.com> Co-authored-by: prastoin <paul@twenty.com> |
||
|
|
ac653182b2 |
feat(server): migrate all remaining JWT token types to ES256 (#20513)
## Summary Extends the asymmetric signing work from #20467 to cover **every remaining `JwtTokenTypeEnum` value**: `LOGIN`, `WORKSPACE_AGNOSTIC`, `FILE`, `API_KEY`, `APPLICATION_ACCESS`, `APPLICATION_REFRESH`, `APP_OAUTH_STATE`, plus the ACCESS-shaped session token issued by the code interpreter tool. After this PR, every JWT the server signs is ES256 with a `kid` pointing at the current `core."signingKey"` row, while legacy HS256 tokens (no `kid` header) remain verifiable indefinitely through the existing fallback in `JwtWrapperService.resolveVerificationKey`. No new entity / migration / config: this is a pure routing change on top of the infrastructure that already shipped. ## Why `#20467` only flipped `ACCESS` and `REFRESH` to ES256. Every other JWT type was still HS256-signed against the global `APP_SECRET`, which kept the original blast radius (a leaked `APP_SECRET` invalidates *every* JWT type forever). Migrating the rest unifies the sign path on rotatable per-server private keys without forcing any token reissue. ## Mechanical changes ### Sign side (8 services) - `LoginTokenService.generateLoginToken` - `TransientTokenService.generateTransientToken` - `WorkspaceAgnosticTokenService.generateWorkspaceAgnosticToken` - `ApplicationTokenService.signApplicationToken` (`APPLICATION_ACCESS` + `APPLICATION_REFRESH`) - `ApiKeyService.generateApiKeyToken` - `FileUrlService.signFileByIdUrl` / `signWorkspaceLogoUrl` - `ConnectionProviderOAuthFlowService.signState` (`APP_OAUTH_STATE`) - `CodeInterpreterTool.generateSessionToken` Each call site swaps `jwtWrapperService.sign(payload, { secret: generateAppSecret(...), ... })` for `await jwtWrapperService.signAsync(payload, { expiresIn, [jwtid] })`. The `generateAppSecret` calls on the sign side are dropped (verifier-side `generateAppSecret` stays in `resolveVerificationKey` for the HS256 fallback). ### Verifier side - `WorkspaceAgnosticTokenService.validateToken` now goes through `verifyJwtToken` instead of the bespoke `verify({ secret })` path, so new ES256 tokens are accepted while the legacy HS256 fallback inside `resolveVerificationKey` still serves the old shape. - `JwtWrapperService.sign()` is kept (legacy compat / tests) but is now strictly deprecated — there are no remaining production callers. ### Async ripple (`signFileByIdUrl` was synchronous) - `FileUrlService.signFileByIdUrl` and `signWorkspaceLogoUrl` are now `async`; the `signUrl` callback used by `getRecordImageIdentifier` is widened to accept `Promise<string | null>`. - Every direct/indirect caller is updated: admin panel (user lookup + statistics + top workspaces), search service (`computeSearchObjectResults`, `getImageIdentifierValue`), workspace resolver (`logo` resolver, public workspace by domain/id), `WorkspaceMemberTranspiler` (now `async toWorkspaceMemberDto[s]` / `toDeletedWorkspaceMemberDto[s]` / `generateSignedAvatarUrl`), `UserService.loadSignedAvatarUrlsByUserId`, `UserWorkspaceService.castWorkspaceToAvailableWorkspace`, workspace-invitation, approved-access-domain, agent-chat-streaming, agent-message-part resolver, navigation-menu-item record identifier, file-ai-chat / file-core-picture / file-email-attachment / file-workflow / files-field services, rich-text & files-field query result getters, and the code-interpreter tool. ## Backward compatibility - **Legacy HS256 tokens (no `kid`)** keep verifying via `resolveVerificationKey` → `extractAppSecretBody` → `generateAppSecret` for both `workspaceId`-bearing and `userId`-bearing payloads. - The `API_KEY` HS256-via-ACCESS-secret fallback (#16504) still kicks in inside `verifyJwtToken` for pre-2025-12-12 API keys. - No payload shape changes, no DB writes, no env var changes — old tokens issued by `main` continue to authenticate. ## Tests ### Unit (all green locally — 63/63) Updated specs for every migrated service to mock `signAsync` instead of `sign` and assert the new option shape: - `login-token.service.spec.ts`, `transient-token.service.spec.ts`, `workspace-agnostic-token.service.spec.ts`, `application-token.service.spec.ts`, `api-key.service.spec.ts`, `connection-provider-oauth-flow.service.spec.ts`. ### Integration (`jwt-key-rotation.integration-spec.ts`) - Existing ACCESS coverage (current key, legacy HS256 fallback, rotated-out key, revoked key, unknown kid) is preserved. - New `it.each` assertion: `REFRESH`, `WORKSPACE_AGNOSTIC`, and `LOGIN` tokens emitted by the real signUp → signUpInNewWorkspace → getAuthTokensFromLoginToken pipeline are ES256 with a `kid` matching the current signing key — proves end-to-end that the migration didn't regress those flows. ## Open question (separate decision) This PR keeps the legacy HS256 verification fallback **forever**. We may eventually want to sunset it for `API_KEY` once telemetry shows pre-migration tokens are gone, but that's a separate product/security decision and not part of this change. ## Test plan - [ ] CI green - [ ] `npx nx lint:diff-with-main twenty-server` passes - [ ] `npx nx typecheck twenty-server` passes - [ ] `jwt-key-rotation` integration suite passes (new + existing assertions) - [ ] Manually verify: signing in issues an ES256 ACCESS / REFRESH token, generating an API key issues an ES256 token with `kid`, signed file URL JWT is ES256 with `kid` - [ ] Pre-existing HS256 tokens still authenticate (covered by integration test, but worth a manual check with a token from `main`) |
||
|
|
a159a68e2c |
[twenty-server] no floating promises lint rule (#20499)
## Introduction
That's an audit + RFC
## Fire-and-forget (`void`) -- Intentional, correct
These are telemetry, metrics, and audit logging in hot paths or
non-critical contexts. `void` is the right choice.
| File | What was voided |
|---|---|
| `sign-in-up.service.ts` | `metricsService.incrementCounter` (sign-up
metric) + `auditService.insertWorkspaceEvent` (workspace created) |
| `use-graphql-error-handler.hook.ts` | 5x
`metricsService.incrementCounter` (GraphQL operation metrics) |
| `bullmq.driver.ts` | 2x `metricsService.incrementCounter` (job
completed/failed metrics) |
| `call-webhook.job.ts` | 2x `auditService.insertWorkspaceEvent` + 1x
`metricsService.incrementCounter` |
| `custom-domain-manager.service.ts` | `analytics.insertWorkspaceEvent`
(domain activation event) |
| `logic-function-executor.service.ts` |
`auditService.insertWorkspaceEvent` (function execution) |
| `workflow-runner.workspace-service.ts` |
`metricsService.incrementCounter` (throttle metric) |
| `cleaner.workspace-service.ts` | `metricsService.incrementCounter`
(deleted workspace metric) |
| `stream-agent-chat.job.ts` | Detached IIFE for streaming chunks
(intentional concurrent pipeline) |
| `workspace-auth-context.middleware.ts` |
`withWorkspaceAuthContext(...)` (AsyncLocalStorage, returns void anyway)
|
## Top-level script entry points (`void bootstrap()`)
These are module-level calls where the promise has no consumer. `void`
makes the lint rule happy and documents the intent.
| File | What changed |
|---|---|
| `main.ts` | `void bootstrap()` |
| `command.ts` | `void bootstrap()` |
| `queue-worker.ts` | `void bootstrap()` |
| `truncate-db.ts` | `void dropSchemasSequentially()` |
| `codegen/index.ts` | `void generateTests(forceArg)` |
## Now properly awaited -- Real bug fixes
These were floating promises that could silently fail, lose data, or
cause race conditions.
| File | What was fixed |
|---|---|
| `billing-sync-plans-data.command.ts` | `meters.map(async ...)` wrapped
in `Promise.all` -- was returning before upserts finished |
| `cache-storage.service.ts` | `setAdd` and `setPop` had `.then()`
chains that weren't returned/awaited |
| `create-audit-log-from-internal-event.ts` | 4x
`auditService.createObjectEvent` now awaited inside a job |
| `cleaner.workspace-service.ts` | 2x `emailService.send(...)` now
awaited -- emails could silently fail |
| `agent-async-executor.service.ts` | `calculateAndBillUsage` +
`billNativeWebSearchUsage` in `finally` block now awaited |
| `repair-tool-call.util.ts` | `calculateAndBillUsage` now awaited |
| `agent-title-generation.service.ts` | `calculateAndBillUsage` now
awaited |
| `chat-execution.service.ts` | `billNativeWebSearchUsage` now awaited |
| `ai-generate-text.controller.ts` | `calculateAndBillUsage` in
`finally` block now awaited |
| `agent-turn.resolver.ts` | `messageQueueService.add(...)` now awaited
|
| `command.ts` | `app.close()` now awaited (was exiting before graceful
shutdown) |
| `i18n.service.ts` | `loadTranslations()` in `onModuleInit` now awaited
|
| `workspace-query-hook.explorer.ts` | `explore()` in `onModuleInit` now
awaited |
| `message-queue.explorer.ts` | `handleProcessorGroupCollection` in
`onModuleInit` now awaited |
| `ai-billing.service.spec.ts` | Test now properly `await`s the async
call |
| `messaging-messages-import.service.spec.ts` | `expect(...)` now
properly `await`ed for async assertion |
| `archive.finalize()` (3 files) | Voided -- promise resolution already
handled by `pipeline()` / `on('end')` |
## Impersonation & security audit trail -- Upgraded from `void` to
`await`
These were previously fire-and-forget but are
security/compliance-critical events that must be reliably persisted.
| File | What was fixed |
|---|---|
| `impersonation.service.ts` | 4x `auditService.insertWorkspaceEvent`
now awaited (impersonation attempt, token generation
attempt/success/failure) |
| `auth.resolver.ts` | 5x `auditService.insertWorkspaceEvent` now
awaited (impersonation token exchange attempt/success/failure at server
and workspace levels) |
| `auth.service.ts` | 2x `analytics.insertWorkspaceEvent` now awaited
(impersonation attempted/issued) |
## Billing audit -- Upgraded from `void` to `await`
Payment events should be reliably persisted for financial/compliance
reporting.
| File | What was fixed |
|---|---|
| `billing-webhook-invoice.service.ts` |
`auditService.insertWorkspaceEvent(PAYMENT_RECEIVED_EVENT)` now awaited
inside Stripe webhook handler |
## Fire-and-forget with proper error handling -- Upgraded from bare
`void`
These remain non-blocking but now catch and log errors instead of
risking unhandled rejections.
| File | What was fixed |
|---|---|
| `logic-function-executor.service.ts` |
`applicationLogsService.writeLogs` now uses `.catch()` instead of bare
`void` -- user-facing logs should surface errors |
## Systemic infrastructure fixes
| File | What was fixed |
|---|---|
| `metrics.service.ts` | `incrementCounter`: Redis cache write
(`metricsCacheService.updateCounter`) now uses `.catch()` internally
instead of raw `await` -- prevents unhandled rejections across all `void
metricsService.incrementCounter(...)` call sites when Redis is unhealthy
|
| `audit.service.ts` | `preventIfDisabled`: made properly `async` with
`await` and consistent `Promise<{ success: boolean }>` return type.
Removed broken `catch` that returned an `AuditException` as a value
(wrong constructor args, unreachable dead code). Removed unused
`AuditException` import |
## Fixed in this session (beyond original PR)
| File | What changed |
|---|---|
| `telemetry.listener.ts` | Removed misleading `Promise.all` + `void`
combo; replaced with simple `for...of` + `void` |
| `message-queue.explorer.ts` | Changed from `void` to `await` so
startup crashes on registration failure |
|
||
|
|
38d9eacff8 |
chore: sync AI model catalog from models.dev (#20523)
Automated daily sync of `ai-providers.json` from [models.dev](https://models.dev). This PR updates pricing, context windows, and model availability based on the latest data. New models meeting inclusion criteria (tool calling, pricing data, context limits) are added automatically. Deprecated models are detected based on cost-efficiency within the same model family. **Please review before merging** — verify no critical models were incorrectly deprecated. Co-authored-by: FelixMalfait <6399865+FelixMalfait@users.noreply.github.com> |
||
|
|
7ade9e3aab |
Fix application variable issue (#20500)
Fixes wrong formatting converting secret value to ******** before saving Issued by https://discord.com/channels/1130383047699738754/1423290797079662602/1502674904770936903 |
||
|
|
c4e897a7b5 |
Improve linear app (#20453)
- Add front component form to create linear issue <img width="1512" height="831" alt="image" src="https://github.com/user-attachments/assets/ffbb223f-30a8-4c64-ac6d-002c29b604c1" /> <img width="1512" height="829" alt="image" src="https://github.com/user-attachments/assets/a5ed2464-35a9-4a60-804c-5f15eb0043b4" /> - improve marketplace Linear app page <img width="1302" height="834" alt="image" src="https://github.com/user-attachments/assets/cdec7ec2-953d-4a49-a797-5369834b03c1" /> - update admin settings to display non secret values <img width="861" height="473" alt="image" src="https://github.com/user-attachments/assets/41dadf02-aa5d-4eb6-befe-0ad8ad4049b2" /> |
||
|
|
70bb011daa |
fix: map FlatEntityMaps and WorkspaceMigrationRunner exceptions to proper status codes on REST and GraphQL (#20494)
## Context Calling `POST /rest/views` (and other metadata mutations) currently returns a generic `500` for user-input failures: Ex: 1. **Invalid `objectMetadataId`** — `resolveEntityRelationUniversalIdentifiers` throws `FlatEntityMapsException(RELATION_UNIVERSAL_IDENTIFIER_NOT_FOUND)`. Should be `404`. 2. **Missing required field** (e.g. `icon`) — Postgres raises a `NOT NULL` violation, wrapped as `WorkspaceMigrationRunnerException(EXECUTION_FAILED)` carrying a `QueryFailedError`. Should be `400`. Neither was caught by `ViewRestApiExceptionFilter`, so both fell through to `UnhandledExceptionFilter` and were emitted as `500`s without reaching Sentry. Same gap existed on most metadata GraphQL resolvers — only `page-layout*` and `role` resolvers covered `WorkspaceMigrationRunnerException` via `WorkspaceMigrationGraphqlApiExceptionInterceptor`. ## Changes ### New filters REST (`HttpExceptionHandlerService` + Sentry-aware): - `FlatEntityMapsRestApiExceptionFilter` — maps `RELATION_UNIVERSAL_IDENTIFIER_NOT_FOUND` / `ENTITY_NOT_FOUND` → `404`, `ENTITY_ALREADY_EXISTS` → `409`, others → `500`. - `WorkspaceMigrationRunnerRestApiExceptionFilter` — for `EXECUTION_FAILED`, unwraps the underlying `metadata` / `workspaceSchema` / `actionTranspilation` error; if it's a `QueryFailedError` it gets remapped to `400` via `HttpExceptionHandlerService`. `APPLICATION_NOT_FOUND` → `404`, `DDL_LOCKED` → `503`, otherwise `500`. GraphQL (graphql-errors + existing formatter): - `FlatEntityMapsGraphqlApiExceptionFilter` — kept as the GraphQL-shaped counterpart (`NotFoundError` / `InternalServerError`). - `WorkspaceMigrationRunnerGraphqlApiExceptionFilter` — reuses `workspaceMigrationRunnerExceptionFormatter` for parity with the existing interceptor. ### Wiring Filters are now declared **per controller / resolver** via `@UseFilters` (no global `APP_FILTER` registration) so they participate in the normal NestJS filter chain instead of being preempted by `UnhandledExceptionFilter`. REST: - `view.controller.ts` — adds `FlatEntityMapsRestApiExceptionFilter` and `WorkspaceMigrationRunnerRestApiExceptionFilter`. GraphQL (14 resolvers, all that mutate flat entities): - `FlatEntityMapsGraphqlApiExceptionFilter` added to: `view`, `view-field`, `view-field-group`, `view-sort`, `view-group`, `view-filter`, `view-filter-group`, `page-layout`, `page-layout-tab`, `page-layout-widget`, `role`, `object-metadata`, `field-metadata`, `index-metadata`. - `WorkspaceMigrationRunnerGraphqlApiExceptionFilter` added to the same list **except** the four already covered by `WorkspaceMigrationGraphqlApiExceptionInterceptor` (`page-layout`, `page-layout-tab`, `page-layout-widget`, `role`) — to avoid double-handling. ## Why per-resolver / per-controller instead of global Earlier attempt to register the filters globally via `APP_FILTER` regressed: NestJS reverses the global filter list and `selectExceptionFilterMetadata` is first-match-wins, so `UnhandledExceptionFilter` (registered last via `app.useGlobalFilters` in `main.ts`) ended up first in the iteration order and preempted every domain-specific filter. The per-resolver / per-controller approach is explicit and predictable. ## Before <img width="953" height="450" alt="Screenshot 2026-05-12 at 15 31 40" src="https://github.com/user-attachments/assets/3c3bc6a8-f6bc-4032-97d0-7243540cfb90" /> ## After <img width="1050" height="598" alt="Screenshot 2026-05-12 at 15 31 17" src="https://github.com/user-attachments/assets/c66c9ce5-d1ea-4f1d-b2fe-07979e2261f7" /> <img width="1068" height="503" alt="Screenshot 2026-05-12 at 15 31 09" src="https://github.com/user-attachments/assets/ddd9eed8-812b-47d6-96cb-b019b807991b" /> |
||
|
|
9e515afb13 |
feat(server): asymmetric JWT signing with kid + key rotation table (#20467)
## Context Today every JWT issued by Twenty (access, refresh, login, file, etc.) is HMAC-signed with a per-token-type secret derived from the global `APP_SECRET`. Rotating that secret invalidates **every** active token at once and there is no way to scope a leak to a subset of tokens. This PR is the first slice of a broader effort to **decouple stateful encryption (`APP_SECRET`-derived secrets) from stateless encryption (JWTs)**. It introduces an asymmetric (private/public key) signing path for `ACCESS` and `REFRESH` tokens and a signing-key registry to enable **safe rotation**: leaked keys can be revoked by flipping `revokedAt`/`isCurrent` on the matching row without invalidating tokens issued by other keys. > Out of scope (intentionally): swapping stateful encryption for `APP_SECRET`, asymmetric signing for token types other than `ACCESS`/`REFRESH`, an admin-panel rotation UI, and an enterprise re-encryption command. Those will land in follow-up PRs. ## What changes - **New `core.signingKey` table** (instance command `2.5.0` / `1778550000000`) storing both the public key (PEM, in clear) and the private key (PEM, encrypted with `APP_SECRET` via `SecretEncryptionService`). One row is marked `isCurrent = true` (enforced by a partial unique index). The row's UUID `id` is used directly as the JWT `kid`. - When a key is rotated out, its `privateKey` is nulled (we never keep historical private keys) but the `publicKey` row stays so previously issued tokens can still be verified. - **`JwtKeyManagerService`** lazily loads-or-generates the current signing key on first use: - If a row with `isCurrent = true` exists → decrypts and uses it. - Otherwise → generates a fresh EC P-256 keypair, encrypts the private key, inserts the row (UUID id = kid). Handles concurrent insert races via the unique constraint. - **`JwtWrapperService.signAsync()`** signs `ACCESS`/`REFRESH` payloads with `ES256` and a `kid` header. Falls back to `HS256` if no signing key is available (boot-time DB error, transient failure). - **Dual-path verification** in both `JwtWrapperService.verifyJwtToken` and the Passport `JwtAuthStrategy.secretOrKeyProvider`: - JWT with a `kid` header → resolve the public key PEM by id and verify with `ES256`, - otherwise → fall back to the existing `APP_SECRET`-derived `HS256` path (unchanged). - **`AccessTokenService` / `RefreshTokenService`** now sign through `signAsync` (single public surface; the routing detail stays internal to the wrapper). - **Public key cache**: a new `SigningKeyEntityCacheProviderService` plugs into `CoreEntityCacheService` (`signingKeyPublicKey` namespace) and serves PEMs by id, with the standard local-memo + Redis layering. - **PEM strings end-to-end**: `jsonwebtoken` accepts PEM strings directly for both sign and verify, so the manager never converts to a Node `KeyObject` and the cache hands the PEM straight to `jwt.verify`. ## Why ES256 (and not EdDSA / RS256) - `@nestjs/jwt` is backed by `jsonwebtoken`, which does **not** support EdDSA today. - ES256 keys are tiny (~120 bytes vs 1.6 kB for RS256), signatures are short (~64 bytes), and signing/verification is fast — important since JWT verification runs on every authenticated request. - ES256 is widely supported and standardized (RFC 7518), with mature ecosystem support. ## Why store the private key in DB (not env) - No new secret to provision: existing instances already have `APP_SECRET`, which we reuse to encrypt the private key at rest. - Self-healing: a fresh instance auto-generates its first signing key on first boot. Nothing to copy/paste. - Rotation is a SQL operation against `core.signingKey`, not a redeploy + env mutation. ## Backward compatibility - All previously-issued tokens (no `kid`) keep verifying through the legacy HS256 path with their existing `APP_SECRET`-derived secret. No forced re-login. - Token types not in scope (`WORKSPACE_AGNOSTIC`, `API_KEY`, `FILE`, `LOGIN`, `EMAIL_VERIFICATION`, etc.) keep their current HS256 behavior unchanged — they still go through the synchronous `JwtWrapperService.sign(payload, options)` with a caller-supplied secret. - `signWithAppSecret` is kept intentionally as the HS256 fallback path; it will be deprecated in a follow-up PR. - If the DB lookup/generation fails for any reason, the wrapper logs the error and falls back to HS256 — no startup crash, no silent regression. ## Rotation story 1. Bootstrap: first signing call lazily inserts a row in `core.signingKey` with `isCurrent = true`, `privateKey = encrypt(pem_A)`. New tokens carry `kid_A`. 2. Rotate: `UPDATE core."signingKey" SET "isCurrent" = false, "privateKey" = NULL WHERE id = '<kid_A>';` then insert a new row with `isCurrent = true`. New tokens carry `kid_B`. Tokens still in flight with `kid_A` keep verifying because the public-key row for `kid_A` is still there. 3. Revoke: `UPDATE core."signingKey" SET "revokedAt" = now() WHERE id = '<kid_A>';`. All tokens with `kid_A` now fail verification cleanly with `UNAUTHENTICATED` (no 500). 4. Tokens with no `kid` (legacy) are unaffected throughout. ## Test plan - [x] Unit: `JwtWrapperService` dual-path verification (HS256 no-kid vs ES256 with-kid), unknown-kid → `UNAUTHENTICATED`, `signAsync` happy path + `null` when no key, `signAsync` rejection for non-rotatable types. - [x] Unit: `JwtAuthStrategy` `secretOrKeyProvider` dual-path resolution and algorithm validation. - [x] All existing JWT/auth/application unit tests adjusted to the renamed public method. - [x] Integration (`jwt-key-rotation.integration-spec.ts`): - **Happy path**: signed-up user's `ACCESS` token has `alg=ES256` + correct UUID `kid`, the `isCurrent=true` row exists in `core.signingKey`, `getCurrentUser` resolves. - **Legacy fallback**: hand-crafted no-kid HS256 token verifies via the legacy `APP_SECRET`-derived path. - **Previous-key rotation**: token signed by a hardcoded *previous* key whose row is pre-inserted with `privateKey = NULL` (rotated-out) still verifies — proves the leaked-key revocation flow works in both directions. - **Unknown kid**: token signed with an orphan UUID `kid` is cleanly rejected (no 500). - [x] `npx nx typecheck twenty-server` - [x] `npx nx test twenty-server` - [x] `npx nx run twenty-server:lint` |
||
|
|
a34bf11dae |
Upgrade cli tools (#20496)
as title upgrade version to 2.4.0 |
||
|
|
ce54f69a4e |
i18n - website translations (#20495)
Created by Github action --------- Co-authored-by: github-actions <github-actions@twenty.com> |
||
|
|
3904addeb0 |
chore: add an icon to why-twenty page and update preview (#20482)
Added light-bulb icon and replaced the preview with kanban image. <img width="751" height="391" alt="image" src="https://github.com/user-attachments/assets/ac406e84-512f-4dcc-8068-cdb9e3978e92" /> |
||
|
|
d53f3e4ccc |
fix(kanban): give title full width when card is not hovered (#20455)
## Summary On kanban cards, the title was being truncated even when the checkbox wasn't displayed. The checkbox is hidden via `opacity: 0` on the card's non-hovered state, which keeps it in flex flow and still reserves its ~24px of width — so the title's flex item was shrinking unnecessarily. This change collapses the checkbox container's `max-width` to `0` (with `overflow: hidden`) while it's hidden, and expands it back to the checkbox's natural size (`spacing[6]` = 24px) on hover or when selected. The existing `transition: all ease-in-out 160ms` animates the title expanding into the freed space. ### Before Title truncates with ellipsis even though the checkbox slot is empty: <img width="350" alt="before" src="https://i.imgur.com/placeholder-before.png" /> ### After Title uses the full row width when not hovered; the checkbox slides in on hover (or when the card is selected) and the title reflows. ### Tooltip The full title is already exposed on hover when truncated — `RecordChip` → `Chip` already wraps the label in `OverflowingTextWithTooltip`, which detects overflow (`scrollWidth > clientWidth`) and renders an `AppTooltip` with the full text. No additional wiring needed. ## Test plan - [ ] On a kanban board, verify a long record title now uses the full card width when the card is not hovered (no ellipsis if the title fits). - [ ] Hover the card: the checkbox slides in smoothly (animated), and the title reflows (may now truncate if it doesn't fit). - [ ] Hover the (now-truncated) title: tooltip with the full title appears. - [ ] Select the card via the checkbox: checkbox stays visible (and title stays in its hover-state width) without hovering. - [ ] Compact view (eye icon) still renders correctly. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
565995e715 |
security: harden CI against supply-chain attacks (#20476)
- Pin all third-party actions to SHA - Gate claude.yml triggers to internal authors with Harden-Runner egress audit - Ignore fork-PR lifecycle scripts - Narrow cross-repo dispatch payloads - Add 7d npm release-age gate - Add CODEOWNERS on .github/** and .yarnrc.yml --------- Co-authored-by: prastoin <paul@twenty.com> |
||
|
|
79b612ee12 | Billing - Add default ff (#20480) | ||
|
|
5003fcbbf2 |
chore: remove dead feature flags (#20460)
## Summary Two related cleanups, following the same pattern as #19916 and #19074. ### Dead feature flags Drops four feature flags whose only references are the enum entry and the generated GraphQL/SDK files: - `IS_COMMAND_MENU_ITEM_ENABLED` — never read anywhere. - `IS_DATASOURCE_MIGRATED` — already commented `@deprecated`. Zero non-generated consumers. - `IS_RICH_TEXT_V1_MIGRATED` — the 1-19 migration that gated it was removed in #19074; the flag became dead at that point. - `IS_CONNECTED_ACCOUNT_MIGRATED` — only read by the 1-21 `migrate-messaging-infrastructure-to-metadata` command as an early-return guard, but the flag was never written anywhere in the codebase, so the guard never fired (and that workspace command is now removed entirely — see below). Generated GraphQL/SDK schemas and the `workspace-entity-manager` test mock are trimmed to match. ### 1-21 workspace commands Same pattern as #19074 (which removed workspace commands ≤ 1.18). Twenty is now on 2-5; the 1-21 workspace commands have long since run on every active workspace and are dead code. Removes: - All 14 workspace commands under `upgrade-version-command/1-21/` (compose-email menu item, key-value-pair index, datasource backfill, message-thread backfill, dedup engine commands, select-all fixes, AI response format migration, edit-layout label, drop messaging FKs, folder parent-id migration, messaging-infra-to-metadata, navigation refactor, message-thread label fix, search-menu-item label). - The `1-21-upgrade-version-command.module.ts` registration and the `V1_21_UpgradeVersionCommandModule` import from `WorkspaceCommandProviderModule`. **Kept** (intentionally): the 3 `1-21-instance-command-fast-*` files. Unlike workspace commands (which mutate data), instance commands carry **schema deltas** still required by current entity definitions (`AddViewFieldGroupIdIndex`, `MigrateMessagingCalendarToCore`, `AddEmailThreadWidgetType`). They remain registered in `INSTANCE_COMMANDS` and `'1.21.0'` stays in `TWENTY_PREVIOUS_VERSIONS`. They will fold away naturally on a future version bump when a `CoreMigrationCheck`-style snapshot picks them up. ## Test plan - [x] `npx nx typecheck twenty-shared` - [x] `npx nx typecheck twenty-server` - [x] `npx nx typecheck twenty-front` - [x] `npx prettier --check` on the changed files - [x] `npx oxlint` on the changed server files - [x] `npx jest feature-flag` (4 suites, 20 tests pass) - [x] `npx jest workspace-entity-manager` (1 suite, 5 tests pass) |