2da28cb03e02f368b21ee2c4303a3f3e5b2ed821
10384 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
2da28cb03e |
security: upgrade express 4.22.2 + qs 6.15.2 resolution for dev-tool holdouts (Dependabot alert 1305) (#21434)
Closes the qs Dependabot alert — [1305](https://github.com/twentyhq/twenty/security/dependabot/1305) — by **upgrading express where possible** and using a **documented qs resolution** only for the irreducible dev-only holdouts. ### What's vulnerable `qs 6.14.x` (the `qs.stringify` DoS) is pulled by `express 4.22.1` / `body-parser 1.x`. The fix is `qs 6.15.2`, and there's **no backport to the 6.14 line**. ### Upgrade what we can (no resolution) `express 4.22.2` / `body-parser 1.20.5` moved to the patched `qs ~6.15.1`. So this PR bumps the app + in-range tooling to **express 4.22.2**: - twenty-server's stale direct pin `4.22.1 → 4.22.2` (its runtime HTTP is already express 5.2.1 via `@nestjs/platform-express`; this just patches the redundant direct dep — typecheck passes), - nx / electron-forge / webpack-dev-server / companion follow via `yarn up -R express body-parser`. ### Resolution only for the two holdouts Two **dev-only** tools pin express *exactly* with no patched release on a line we can use, so they still drag in `qs 6.14.2`: - **verdaccio** `4.22.1` — express 5 only landed in the **v7 beta** ([verdaccio#5680](https://github.com/verdaccio/verdaccio/issues/5680), [#2479](https://github.com/verdaccio/verdaccio/issues/2479)), **not** backported to the 6.x line we use. - **@mintlify/previewing** `4.22.0` — closed-source, latest still pins 4.22.0, no movement. A `qs: 6.15.2` resolution covers those, documented with a top-level `//resolutions` note and a removal trigger. ### Verification - `yarn install --immutable` ✓; every `qs` resolves to `6.15.2`; express app/tooling on `4.22.2` (only verdaccio/mintlify remain on old express, neutralized by the resolution). - `twenty-server` typecheck ✓ (90 files import express types; 4.22.1→4.22.2 is a patch). - Non-exploitable in prod regardless (express/body-parser use `qs.parse`, not the vulnerable `stringify`). |
||
|
|
233a6f9fb1 |
fix(server): register Lingui message compiler to stop "Uncompiled message detected" log flood (#21416)
## Summary Fixes #21415. Server-side `` t`…` `` macro calls (e.g. `FlatEntityMapsException`) resolve against Lingui's global `i18n` singleton, which `I18nService` never loads a catalog into and never registers a messages compiler on. With no `_messageCompiler` set, every such lookup logs `Uncompiled message detected!` and falls back to the raw string — flooding server logs (enough to hit hosting log rate limits) and disabling ICU interpolation on those messages. The warning is emitted unconditionally by `@lingui/core` (not `NODE_ENV`-gated). ## Changes Register `compileMessage` via `setMessagesCompiler` in `I18nService.loadTranslations()`: - on the global `i18n` singleton (used by the `t` macro), and - on each per-locale instance. `@lingui/message-utils` is already a transitive **runtime** dependency of `@lingui/core` (`@lingui/core@5.1.2 → @lingui/message-utils@^5.1.2`), so no new dependency is added. ## Testing Deployed on a real instance: before, the server emitted hundreds of `Uncompiled message detected` lines/sec (saturating the log rate limit); after, steady-state shows `0`, and the worker (which shares the code path) likewise shows `0`. App behaviour is unchanged — messages already rendered via fallback; this silences the warning and enables ICU on the affected messages. --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: Félix Malfait <felix.malfait@gmail.com> Co-authored-by: Félix Malfait <felix@twenty.com> |
||
|
|
20c83e1f86 |
fix(kanban): preserve scroll on board re-init + propagate same-column reorders via SSE (#20637)
closes https://discord.com/channels/1130383047699738754/1504130730840821860 https://github.com/user-attachments/assets/d5833031-01c6-4e46-b699-c29c42435a53 ## Summary Fixes two related issues with the kanban (board view) collaboration experience: 1. **Scroll-to-top on every data change** — `triggerRecordBoardInitialQuery` always scrolled the board to the top, even when re-initializing for a single-record data change (SSE echo of your own mutation, a collaborator's update). Scroll reset only makes sense when the dataset itself changes (filter / sort / group). 2. **Same-column reorders by other users did not propagate** — the server's diff function stripped `FieldMetadataType.POSITION`, so position-only updates produced empty `updatedFields` and short-circuited event emission entirely. SSE clients never received them. ## What's in here - **Frontend** — `useTriggerRecordBoardInitialQuery` now exposes a `triggerRecordBoardInitialQueryWithoutScrollReset` variant; data-driven re-inits in `RecordBoardDataChangedEffect` use it, while genuine filter / sort / group changes keep the scroll-resetting `triggerRecordBoardInitialQuery`. `getRecordBoardEffectsForUpdateInputs` classifies each update as `trigger-initial-query` / `reposition-records` / `none`. For position- or group-only changes we skip the re-query and reposition records in place in the store (`useRepositionRecordsOnBoard`), which avoids the flicker and preserves scroll. - **Server** — removes `POSITION` from `objectRecordChangedValues`' strip list, so position-only updates emit a non-empty diff and flow through SSE. Position is now treated as a field like any other across all event consumers (SSE, webhooks, workflows, logic functions); a trigger with an explicit field filter still excludes it. |
||
|
|
9d7f0c405f |
fix(twenty-front): new layout fast-follows — command menu, field options & logs (#21429)
Fast-follows for the new layout — the remaining open sub-issues of
twentyhq/core-team-issues#2478.
## Changes
- **Command menu items should not have extra right padding**
(twentyhq/core-team-issues#2500)
`SidePanelList` set `width: calc(100% - spacing[4])` on top of its own
8px left/right padding. Under the global `box-sizing: border-box`, that
extra `-16px` shrinks the list and, because it's left-aligned, dumps the
whole gap on the right. Switched to `width: 100%` so item highlights
inset 8px symmetrically. This is shared by every side-panel list — they
all had the same right-only gutter, so they're all corrected the same
way.
- **Field options should not be cropped and should keep row gaps**
(twentyhq/core-team-issues#2503)
The option row used a fixed `height: spacing[6]`, so under border-box
the `6px` vertical padding was absorbed and consecutive rows sat flush.
Changed `height` → `min-height` so the padding separates the rows again.
- **Logs table with filters should use Background secondary**
(twentyhq/core-team-issues#2505)
The Logs filter card and the upgrade card defaulted to a transparent
background, showing the white page through. Passed
`backgroundColor={themeCssVariables.background.secondary}`, matching
`SettingsTableCard`. The results table stays on the primary surface, per
the Figma reference.
- **Command menu back chevron** (twentyhq/core-team-issues#2504)
`SidePanelTopBar` showed a back chevron whenever the nav stack had more
than one entry. A command-menu page is the root of a fresh command-menu
session, so it now only shows the chevron when it was opened from
another command-menu page. Every other side-panel page keeps standard
history-based back navigation, so workflow / page-layout / record stacks
are unaffected.
## Verification
- oxlint (`--type-aware`, full `src/`): 0 errors
- oxfmt: clean
- tsgo typecheck: no errors in the changed files (the one reported error
is pre-existing in `RestPlayground.tsx`, which this PR does not touch)
- Verified live at apple.localhost:
- #2500 — command menu item highlight insets measured 8px left / 8px
right (was 8 / 24)
- #2503 — option rows render at 38px tall with ~14px gaps, text no
longer cropped
- #2505 — filter + upgrade cards compute to background-secondary;
results table stays on primary
- #2504 — direct command menu shows the close-X with no chevron; pages
opened from the command menu still show the chevron
## Open question for review (#2504)
The issue also describes the page-header three-dots toggle: *"the three
dots icon button should remain visible if the side panel is on a page
that is not a child of the command menu (AI chat, or a page opened
directly)."* Today that toggle morphs to an X for any non-command-menu
side-panel page (e.g. Ask AI). Honoring that touches the shared
`SidePanelToggleButton`, and there's a related decision: search / Ask AI
opened from the command menu currently reset the nav stack rather than
push, so they don't get a "back to command menu" chevron. I left those
out here since they're a behavior change to a shared control with a
product call attached — happy to follow up once you confirm the intended
toggle behavior.
Closes twentyhq/core-team-issues#2500
Closes twentyhq/core-team-issues#2503
Closes twentyhq/core-team-issues#2505
Refs twentyhq/core-team-issues#2504
Refs twentyhq/core-team-issues#2478
|
||
|
|
941c9e7586 |
fix: match relation field filters in optimistic & RLS record matchers (#21301)
Closes #21345. ## What It should be caused by the GraphQL optimistic query. `isRecordMatchingFilter` (front, Apollo optimistic cache) and `isRecordMatchingRLSRowLevelPermissionPredicate` (server, RLS) now handle a view filter that targets a **relation field object** (e.g. an "is (not) empty" filter on a relation) by matching against the related record id, instead of throwing. <img width="3436" height="2250" alt="CleanShot 2026-06-08 at 06 44 01@2x" src="https://github.com/user-attachments/assets/1dccbd1e-133c-4f4a-a0a9-7ccd02a9a0ae" /> <img width="1496" height="380" alt="CleanShot 2026-06-08 at 06 45 34@2x" src="https://github.com/user-attachments/assets/e5c2071e-69df-4d99-bb7d-66d503a175b6" /> ## Why Both matchers only implemented the relation **join column** branch (`fooId`) and threw `Not implemented yet, use UUID filter instead on the corresponding "fooId" field` for the relation field itself (`foo`). In practice the UI still stores relation filters keyed on the relation object, so any view with such a filter made every create/update/delete on that object throw: the optimistic effect re-evaluates all active view filters against the changed record and hits the unimplemented branch. Repro: add a self-relation field on People (e.g. "Referred By"), put it in a view filter as "is not empty", then edit any Person. The optimistic update throws. ## Behaviour change | Scenario | Before | After | |---|---|---| | View filter on relation object (`referredBy is not empty`), then edit a record | Throws `Not implemented yet...` | Record matched by related id; update succeeds | | Filter on relation join column (`referredById`) | Worked | Unchanged | ## Test plan ```bash cd packages/twenty-front && npx jest isRecordMatchingFilter cd packages/twenty-server && npx jest is-record-matching-rls-row-level-permission-predicate ``` - [x] Front: relation `is empty` / `is not empty` / `in` match by related id; join-column path still passes (20/20) - [x] Server: relation `is empty` / `is not empty` match by related id (9/9) - [x] `lint:diff-with-main` clean on both packages Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: Félix Malfait <felix.malfait@gmail.com> |
||
|
|
61b76b681e |
fix: i18n missing hardcoded strings in settings (#21424)
## What Two user-visible strings in the Settings area were never wrapped with Lingui, so they were excluded from i18n extraction and shipped untranslated regardless of the selected language: - **"Remote"** — the type chip shown for remote objects in **Settings → Data Model** (`SettingsItemTypeTag`) - **"Done"** — the confirm button of the fields configuration group rename input (`FieldsConfigurationGroupRenameInput`) ## Changes - Wrap the `Chip` `label` with the `t` macro in `SettingsItemTypeTag.tsx` (the `placeholder` / `placeholderColorSeed` props are intentionally left as-is — they drive the avatar initial and color hash, not display text). - Wrap the `Button` `title` with the existing `t` from `useLingui()` in `FieldsConfigurationGroupRenameInput.tsx`. - Add the corresponding source entries to `en.po` so Crowdin can propagate the translations to all supported locales. Both follow i18n patterns already used throughout the codebase — these two were simply missed. ## Screenshots Both components rendered via Storybook (source `en` locale) after the change — the strings now resolve through Lingui's `t` macro without breaking rendering:  ## How to test 1. Switch the workspace language to a non-English locale. 2. Go to **Settings → Data Model** with a remote object present → the type chip reads "Remote" translated. 3. Rename a fields configuration group → the confirm button reads "Done" translated. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
615c3d8dbe |
security: drop end-of-life apollo-server-core (#735, #736) (#21418)
Closes the `apollo-server-core` alerts (**#735**, **#736**) by
**removing the dependency** — no Apollo migration, no resolution.
### Why these were flagged "no patch available"
`apollo-server-core` is **Apollo Server v3, which is end-of-life** (per
its npm deprecation notice). No patched release of this package will
ever exist — the CVE fix lives only in the renamed `@apollo/server` v4
package.
### Why we can just drop it
twenty-server **doesn't use Apollo Server** — its GraphQL runtime is
**GraphQL Yoga** (`YogaDriver`). `apollo-server-core` was imported for
one thing only: the `gql` template tag in **6 integration test files**.
`gql` from `graphql-tag` is identical (apollo-server-core merely
re-exports it), `graphql-tag` is **already a direct dependency**, and
**15 other twenty-server tests already import `gql` from it**.
### Change
- Swapped `import { gql } from 'apollo-server-core'` → `import { gql }
from 'graphql-tag'` in the 6 test files.
- Removed `apollo-server-core` from
`packages/twenty-server/package.json`.
- Result: `apollo-server-core` (and its transitive surface) is gone from
`yarn.lock` entirely.
### Verification
- `yarn install --immutable` ✓
- No `apollo-server-core` references remain in source or lockfile
- Integration tests (which exercise the swapped `gql` imports) run in CI
|
||
|
|
1b26061992 |
security: close ws & file-type alerts via parent upgrades (no resolution) (#21417)
Closes 3 more Dependabot alerts via **parent upgrades only — no `resolutions`**. ### Closes - **ws #1238** (`>= 8.0.0, < 8.20.1`) - **file-type #622** (`>= 13.0.0, < 21.3.1`) and **#635** (`>= 20.0.0, <= 21.3.1`) ### How - **ws** — two parents pinned vulnerable copies; both have safe releases: - `wrangler`/`miniflare`: `ws 8.18.0 → 8.20.1` - `socket.io`/`engine.io`/`socket.io-adapter`: `ws ~8.17.1 → ~8.20.1` - Every `ws` now resolves `>= 8.20.1` (or non-vulnerable 6.x/7.x). - **file-type** — bumped `@swc/cli ^0.7.10 → ^0.8.1`, which pulls the newer `@xhmikosr` bin-wrapper → downloader → decompress/archive-type chain (`file-type ^20.5.0 → ^21.3.x`). `@swc/cli` is a devDependency that isn't directly invoked (nx's swc compiler uses `@swc/core`), so this is dev-tooling-only with no runtime impact. ### Verification - `yarn install --immutable` ✓ (passes the hardened 3-day age gate) - No vulnerable `ws` (`< 8.20.1`) or `file-type` (`20.x`) remains in `yarn.lock` ### Remaining open alerts (not closeable without a `resolution` or a major migration) - `apollo-server-core` (#735/#736) — needs Apollo Server 3 → 4 - `webpack-dev-server` (#1237/#691/#692) — `@electron-forge/plugin-webpack` only pins `^4` (no stable v5 consumer); dev-tooling - `qs` (#1305) — `express`/`body-parser` pin `~6.14`, which has no patched release - `uuid` (#1289) — spread across many `^8`/`^9`/`^10` transitives; v11 is a breaking jump - `postcss` (#1061) — bundled exact by `next` and `styled-components` - `ajv` (#481) — `@cyntler/react-doc-viewer` pins `^7` (latest still does) |
||
|
|
868cb02e45 |
security: close lodash CVEs (#824/#823/#385) via parent upgrades, no resolution (#21414)
Closes the remaining lodash Dependabot alerts **without any `resolutions` override** — by upgrading the parent packages that pinned the vulnerable lodash. Every `lodash` in the tree now resolves to **4.18.1**. ### Closes - **#824 — `_.template` code injection (HIGH)** - #823 / #385 — prototype pollution in `_.unset` / `_.omit` ### What changed (4 parents pinned vulnerable lodash 4.17.x; all upgraded, no override) - **`@stoplight/spectral-functions`** → 1.10.2 (in-range; now uses `lodash ^4.18.1`) - **`zapier-platform-core`** 15.5.1 → 19.0.0 — aligns with the already-present `zapier-platform-cli ^19` (they were mismatched). v19 tightened the `Bundle` types, so 3 call sites now type their bundle as `Bundle<InputData>` and the test bundle includes the new `meta` fields. - **`@graphql-codegen`** → `cli 6.3.1`, `typescript 5.0.10`, `typescript-operations 5.1.0`, `typed-document-node 6.1.8`. These depend on `@graphql-codegen/plugin-helpers ^6.3.0`, the release that dropped lodash. (Stayed on the 6.x/5.x line on purpose — 7.x changes generated output far more.) ### About the generated-file changes — they are cosmetic, not real changes The codegen bump touches one generated file. **Verified there is zero semantic change:** - Only `src/generated-metadata/graphql.ts` changes. `src/generated/graphql.ts` (data) and `src/generated-admin/graphql.ts` (admin) are **byte-identical**. - Same 1,638 type declarations before and after — none added, none removed. - After stripping whitespace and union pipes, the file is **byte-for-byte identical** — no type, field, or union member changed. The entire diff is one formatting change from `typescript-operations@5.x`: multi-member union types are now printed multi-line with a leading `|` instead of on one line — which TypeScript treats identically: ```ts // before payload?: { …ObjectMetadata… } | { …Path… } | null // after payload?: | { …ObjectMetadata… } | { …Path… } | null ``` Only metadata is affected because only its operations select GraphQL union types. To keep generated types otherwise behavior-identical, `defaultScalarType: 'any'` was added to the three codegen configs (codegen 6 would otherwise default unmapped scalars to `unknown`). ### Verification - `twenty-front` typecheck ✓, `twenty-zapier` typecheck ✓ - `yarn install --immutable` ✓ (passes the hardened 3-day age gate) - CI green — including the `graphql:generate` freshness check, which regenerates against the canonical schema and confirms the committed output is exactly what codegen produces - No `lodash@4.17.x` remains anywhere in `yarn.lock` Supersedes #21411 (which closed these via a one-line resolution). |
||
|
|
6e147a548b |
security: clear twenty-apps & seed-dependencies CVE alerts (#21410)
Clears the Oneleet/dependency CVE alerts from the `twenty-apps` example/internal app lockfiles and the application-package `seed-dependencies` template — all via parent/direct dependency upgrades, **no `resolutions` overrides**. ## Lock refresh (non-breaking, within existing ranges) - **postcss** 8.5.8/8.5.9 → 8.5.15 — CVE-2026-41305 — postcard, hello-world, self-hosting - **ip-address** 10.1.0 → 10.2.0 — CVE-2026-42338 — postcard, hello-world, self-hosting, twenty-for-twenty - **yaml** 1.10.2 → 1.10.3 — CVE-2026-33532 — call-recording ## seed-dependencies (direct/parent bumps) - **uuid** `^10.0.0 → ^11.1.1` (direct) — CVE-2026-41907 - **body-parser** `^1.20.4 → ^1.20.5`, which pulls **qs** 6.15.2 — CVE-2026-8723 - **socks** 2.8.3 → 2.8.9 (refresh), which pulls **ip-address** 10.2.0 — CVE-2026-42338 ## twenty-for-twenty - **resend** 6.12.0 → 6.12.4 (refresh): 6.12.4 drops the `svix` dep that pulled the vulnerable **uuid** 10.0.0, leaving only uuid 13.0.2 — CVE-2026-41907 All flagged packages were transitive (except the direct seed-deps `uuid`); no app source changes. |
||
|
|
a825dcf2cc |
security: clear 8 Dependabot alerts via transitive/parent bumps (no resolutions) (#21409)
Clears 8 Dependabot alerts via in-range transitive/parent bumps and one dead-dependency removal. **No `resolutions` overrides** were used — every fix is a real version bump within existing semver ranges or a parent upgrade. ### Root `yarn.lock` - **react-router** 6.30.3 → 6.30.4 (open redirect via protocol-relative URL) — pulled through react-router-dom, ranges unchanged — alert #1382 - **yaml** 2.8.1 → 2.9.0 (stack overflow on deeply nested collections) — alert #734 - **uuid** `^13.0.0` → 13.0.2 in twenty-sdk + create-twenty-app (buffer bounds check) — alert #1164 - **ip-address** `^9.0.5` dropped by bumping **socks** 2.8.3 → 2.8.9 (now depends on `ip-address ^10.1.1`, which is unaffected) — alert #1171 ### `seed-dependencies` lockfile - **uuid** `^10.0.0` → `^11.1.1` (direct dep; removed now-redundant `@types/uuid` since uuid v11 ships its own types) — alert #1287 - **ip-address** `^9.0.5` dropped via the same socks bump — alert #1170 ### `twenty-for-twenty` lockfile - **resend** bumped to 6.12.4 (`^6.12.0` range kept), which drops its `svix@1.90.0 → uuid@^10` transitive chain — alert #1278 ### `twenty-companion` - Removed the unused **simplemde** dependency. The note editor loads SimpleMDE from a CDN `<script>` tag and never imports the npm package; `easymde` (its maintained fork) is already a dependency — alert #690 ### Not addressed here The remaining alerts can't be closed without `resolutions` overrides (deliberately avoided in this PR) or a larger migration: - **qs** (#1305, #1304), **lodash** (#824 high / #823 / #385), **ws** (#1238), **postcss** (#1061) — vulnerable copies are pinned exact / bundled by parents (express, body-parser, @nestjs/*, next, styled-components, zapier) with no in-range patch. - **webpack-dev-server** (#1237/#692/#691) — pinned by `@electron-forge/plugin-webpack` (still on v4); dev-tooling only. - **uuid <11.1.1** (#1289) — spread across `^3`/`^8`/`^9` transitive ranges; reaching v11 is a breaking jump. - **apollo-server-core** (#735/#736) — requires an Apollo Server 3 → 4 migration. |
||
|
|
e357116024 |
fix(ui): freeze framer-motion in Argos runs to stop flaky visual diffs (#21412)
## Problem twenty-ui Argos visual tests are flaky — `Loader`, `CircularProgressBar`, `ProgressBar` get flagged as changed with no relevant code change. ## Cause Argos screenshots use Playwright's `animations: "disabled"`, which only freezes CSS animations. These components animate via framer-motion (main-thread rAF), so each run captures a different frame. ## Fix Set `MotionGlobalConfig.skipAnimations = true` in the Vitest setup of both UI packages — freezes framer-motion to a deterministic state during Argos runs only (interactive Storybook unaffected). Removes the now-redundant per-story `disableSnapshot` opt-outs. Note: removing `CircularProgressBar`'s opt-out adds new baselines (one-time Argos approval, not flakiness). |
||
|
|
2514cab860 |
fix(server): include relation join column names in updatedFields of update events (#21405)
## Context Since #21052, update-event diffs are keyed by the relation field name (e.g. `company`) instead of the join column name (e.g. `companyId`). `updatedFields` is derived from the diff keys, so any **workflow database-event trigger** (or webhook) configured with a field filter on a relation join column **silently stopped firing** — no run is created at all. We hit this in production: a `cloudWorkspace.updated` trigger filtered on `twentyContactId` stopped creating runs the same day #21052 was deployed. Updating the record's relation produced `updatedFields: ["twentyContact"]`, which no longer matches the stored settings `fields: ["twentyContactId"]` in `WorkflowDatabaseEventTriggerListener.shouldTriggerJob`. ## Solution Keep the diff keyed by relation field name (the timeline rendering from #21052 relies on it — adding both keys to the diff would display relation changes twice), but expose **both** the relation field name and its join column name in `updatedFields`: - New `computeUpdatedFieldsFromDiff()` in `object-record-changed-values.ts`: expands MANY_TO_ONE relation diff keys with their join column name. - Used in `formatTwentyOrmEventToDatabaseBatchEvent` for UPDATED/DELETED/RESTORED and UPSERTED events instead of `Object.keys(diff)`. This restores matching for pre-existing trigger/webhook configurations (join column names) while keeping configurations using relation field names working. ## Test plan - [x] Unit tests: relation diff keyed by relation name; `updatedFields` contains both `company` and `companyId` - [x] End-to-end util test on UPDATED event: `updatedFields: ['company', 'companyId']`, diff keyed by `company` - [x] Downstream consumer specs pass (workflow trigger listener, webhooks, subscriptions, logic-function triggers) - [x] Verified against the production workspace that a `twentyContactId` update currently produces no workflow run with the old behavior |
||
|
|
6971d6fa95 |
Prevent self-hosting app from re-matching/re-creating people on no-op updates (#21406)
The match-telemetry-event-with-people logic function triggers on selfHostingUser.* (both created and updated) and unconditionally wrote personId back to the record on every run. Since the handler's own write produces an updated event — and the telemetry webhook also updates unrelated fields (name, serverUrl, etc.) on returning signups — the matching logic re-ran unnecessarily, querying people and writing on each pass. This adds an early-return guard so the handler only does work when there's actually something to match: Skip when the selfHostingUser is already linked (personId set) and the primary email hasn't changed. Still (re)match on first link and on genuine email changes — the legitimate reasons for listening on updated. The guard reads before.email.primaryEmail via an 'before' in properties narrowing so it stays type-safe across the create/update event union. |
||
|
|
7258722754 |
security: upgrade @nestjs/graphql 12→13 + @ptc-org/nestjs-query 4→9 (+ @nestjs/config 4) (#21402)
## What Upgrades the NestJS GraphQL stack to clear the High **`ws`** alert (GHSA-3h5v-q93c-6h6q) and modernize off two heavily-patched majors. `@nestjs/graphql@13` pulls `ws@8.20.1` (was 8.16.0). This had to be a **coordinated** upgrade: `@ptc-org/nestjs-query@4.2.0` doesn't support `@nestjs/graphql@13`, so all three move together. | Package | From → To | |---|---| | `@nestjs/config` | 3.3.0 → ^4.0.4 | | `@nestjs/graphql` | 12.1.1 → ^13.4.2 | | `@ptc-org/nestjs-query-{core,graphql,typeorm}` | 4.x → ^9.4.0 | ## The tricky bits - **Re-ported the custom `@nestjs/graphql` patch onto v13.** v13 rewrote the schema builder and added its *own* native multi-schema support (`includeModules`, native `clear()`). Twenty's patch (`resolverSchemaScope` + `computeReachableTypes` — the core/metadata/admin split) is re-merged into v13's new `generate(options, includeModules, reachableTypes)` flow, with a link-preserving `storage.clear()` so cross-schema `resolveType` closures keep working. - **Re-ported the `@ptc-org` patch onto 9.4.0**: removes the `@shareable` federation directive from built-in connection/response types, **and** adds a `.js` extension to its extensionless deep import of `@nestjs/graphql` internals — which v13's new `"exports"` map otherwise rejects at runtime (this was the boot blocker). - **`AppTokenService`**: nestjs-query 9 requires custom services to inject their repo and `super(repo)` it (added an `@InjectRepository` constructor). - **`gridPosition` input fields**: dropped the `deprecationReason` (a *required* input field can't be `@deprecated` under the upgraded graphql) — fields keep their original nullability, so the **schema is unchanged**. - **Service specs**: nestjs-query 9's `TypeOrmQueryService` reads the repo's driver/metadata at construction, so the mocked repos now include `manager`/`metadata`. ## Verification - `nx typecheck twenty-server`: **0 errors**; lint clean - Server boots; **all 3 GraphQL schemas** (`/graphql`, `/metadata`, `/admin-panel`) generate and respond `200` - `graphql:generate` for all 3 schemas is **byte-identical** to before the upgrade (the reachable-types re-port is faithful) - **108 service unit tests pass** (incl. all 6 `TypeOrmQueryService` services) - `ws@8.16.0` gone (now 8.17.1 + 8.18.0); `yarn install --immutable` clean ## Note on lodash `lodash@4.17.21` still remains via `zapier-platform-core` (runtime) and `@stoplight/spectral`, so the lodash alert is **reduced but not fully cleared** by this PR — it needs those separate sources addressed (or a resolution). |
||
|
|
f1c7aecadb |
fix(front): sanitize optimistic input when creating a record (#21076)
## Summary Closes #15800. Clicking **+ Add New** from a relation cell to create a **Task** or **Note** (e.g. from a custom object's Tasks/Notes section in the list view) throws: ``` Uncaught (in promise) Error: Should never occur, encountered unknown fields name in objectMetadataItem task ``` ### Root cause `useCreateOneRecord` computes a **sanitized** input (with `sanitizeRecordInput`, which strips fields that don't belong to the object) and sends it to the GraphQL mutation. But it still feeds the **raw** input to the optimistic cache computation: ```ts const sanitizedInput = { ...sanitizeRecordInput({ objectMetadataItem, recordInput }), id: idForCreation }; const optimisticRecordInput = computeOptimisticRecordFromInput({ ... recordInput: { ...computeOptimisticCreateRecordBaseRecordInput(objectMetadataItem), ...recordInput, // ← raw input, may contain fields unknown to the object id: idForCreation, }, ... }); // mutation uses the sanitized input: mutate({ variables: { input: sanitizedInput } }); ``` `computeOptimisticRecordFromInput` asserts that every input key maps to a field on the object and `throw`s otherwise. So when the create input carries a field the target object doesn't have (the relation-create path passes a `name`, but Task/Note use `title`), the optimistic step throws before the mutation ever runs. `useCreateManyRecords` does **not** have this problem — it already feeds the sanitized input to `computeOptimisticRecordFromInput`. ### Fix Feed the sanitized input to the optimistic computation in `useCreateOneRecord`, exactly as `useCreateManyRecords` does: ```ts recordInput: { ...computeOptimisticCreateRecordBaseRecordInput(objectMetadataItem), ...sanitizedInput, }, ``` This is safe and behavior-preserving for valid creates: `computeOptimisticRecordFromInput` only ever reads *known* fields (it iterates the object's field metadata); unknown input keys never contribute to the optimistic record — they only trip the invariant. Relations are resolved through their join columns, which sanitization keeps. ## Test plan - [x] `npx oxlint --type-aware` — passes on the changed files - [x] `npx oxfmt --check` — passes - [x] `tsc --noEmit` — no type errors in the changed files - [x] `npx jest computeOptimisticRecordFromInput` — passes, including a new case asserting that input which has been through `sanitizeRecordInput` no longer trips the "Should never occur, encountered unknown fields" invariant (the existing test already covers the raw input throwing) - [ ] Manual: from a custom object's Notes/Tasks relation, use **+ Add New** to create a Note/Task — no error, the record is created ### Note on test scope The crash only reproduces through the full relation-create flow with live metadata; at the hook level in jsdom the create resolves regardless, so a hook-level test would not guard the regression. The added test instead locks the underlying mechanism the fix relies on — that sanitized input is safe for `computeOptimisticRecordFromInput` — alongside the existing test that proves raw unknown fields throw. --------- Co-authored-by: Charles Bochet <charles@twenty.com> |
||
|
|
402e4b6a78 |
fix(docker): upgrade Alpine system OpenSSL libs to patched 3.5.7-r0 (#21399)
## Context Image scanner flags the `prod-twenty` server image for OpenSSL CVEs. The image carries **two independent OpenSSL surfaces**, and they are fixed by different levers: | Surface | Version (before) | Used by app at runtime? | Fix lever | |---|---|---|---| | **Node-bundled** OpenSSL | `3.5.6` | ✅ yes (Node links its own) | Node base bump (separate; `24.16.0-alpine3.23` is already the latest digest) | | **Alpine system** `libcrypto3`/`libssl3` | `3.5.6-r0` | ❌ no (`ldd node` shows no link) | **this PR** — apk upgrade to `3.5.7-r0` | The pinned `node:24.16.0-alpine3.23` base bakes `libcrypto3`/`libssl3` at `3.5.6-r0`, while the Alpine v3.23 repo now ships `3.5.7-r0`. These libs ship as deps of `apk-tools`/`libapk`/`ssl_client` (so they can't be removed), and Node does not link them — but the scanner still flags them by version. ## Change Pin a patched floor (`libcrypto3>=3.5.7-r0`, `libssl3>=3.5.7-r0`) in the `apk add` lines of both runtime stages (`twenty-server` and `twenty-app-dev`) so apk upgrades them at build time. Same technique already used for `curl`/`nghttp2-libs`/`postgresql18-client` (#20805). ## Verification - Built `--target twenty-server`; image builds clean. - On the built image, `libcrypto3` and `libssl3` resolve to `3.5.7-r0`. - Node-bundled OpenSSL is unaffected by this change (separate surface, no Dockerfile-side upgrade path until a newer Node release links a patched OpenSSL). |
||
|
|
1c3ae92c04 |
fix(server): prevent SSE stream teardown errors from crashing all pods (#21395)
## Context
In prod-eu, **all `twenty-server` API pods crash simultaneously**
several times per hour (then restart in lockstep) since ~Jun 2. Each
crash is an **unhandled promise rejection** in SSE event-stream teardown
— `exitCode=1`, identical stack on every one of the 7 pods:
```
Error: Failed to acquire lock for key: workspace:<id>:activeStreams
at CacheLockService.withLock (cache-lock.service.ts:53)
at async EventStreamService.destroyEventStream (event-stream.service.ts:75)
at async cleanup (wrap-async-iterator-with-lifecycle.ts)
at async Object.return (wrap-async-iterator-with-lifecycle.ts)
at async Object.cancel (graphql-yoga/.../result-processor/sse.js:68)
```
### Mechanism
1. A busy workspace's SSE clients reconnect (no client backoff), so
connect/disconnect contend on a **single per-workspace Redis lock**
`workspace:<id>:activeStreams`.
2. Under contention `CacheLockService.withLock` exhausts its retries and
**throws**.
3. In the `destroyEventStream` teardown path the throw escapes
`wrapAsyncIteratorWithLifecycle`'s `cleanup()` — `return()` does `try {
await cleanup() } finally { … }` and does **not** catch a cleanup throw.
4. graphql-yoga invokes this from `cancel()` as a **fire-and-forget**
`Promise.all` on connection abort. With **no global `unhandledRejection`
handler**, Node's default policy terminates the process with **exit code
1**.
5. The crash drops all that pod's SSE clients → they reconnect to
surviving pods → contention moves there → the whole fleet crashes
together → restarts → reconnect storm → repeats (~14 min period,
matching the metrics).
## Changes
Crash-stopping hotfix (defense in depth). Does **not** change the
locking design or client reconnect behavior — see follow-ups.
- **`wrapAsyncIteratorWithLifecycle`**: `onCleanup()` is now best-effort
— wrapped in try/catch so teardown can never reject out of
`next()`/`return()`/`throw()`. The original iterator error is still
rethrown unchanged. Adds an `onCleanupError` hook so the swallowed error
is still reported.
- **`EventStreamResolver`**: wires `onCleanupError` to
`ExceptionHandlerService.captureExceptions` (→ Sentry) with workspace +
channel context, so these failures stay visible.
- **`main.ts`**: registers a global `process.on('unhandledRejection')`
that reports via `ExceptionHandlerService` (Sentry) instead of letting
Node terminate. Registering the listener also suppresses Node's default
process-termination. Non-`Error` reasons are formatted with
`util.inspect` (per Copilot review) so Sentry gets a readable message
rather than `[object Object]`.
## Verification
The fix was checked against the exact crash path — a wrapped iterator
whose `onCleanup` rejects:
- `return()` (graphql-yoga's `cancel()` path) **resolves** instead of
rejecting → no unhandled rejection.
- `next()` on a completed stream **resolves** despite a rejecting
cleanup.
- A genuine iterator error still surfaces on `next()` (cleanup failure
doesn't mask it).
- `onCleanupError` receives the original `Error`.
All four pass. `nx lint twenty-server` + `oxfmt` clean; no new `tsc`
errors in the changed files.
## Follow-ups (not in this PR)
- Remove the unnecessary `withLock` around the already-atomic Redis
`SADD`/`SREM` in `event-stream.service.ts` (the contention source).
- Restore exponential backoff + jitter on the frontend SSE reconnect
(regressed in #21061) to stop the thundering herd.
- Infra: make `/healthz` a meaningful liveness signal and add
`maxUnavailable` + a PodDisruptionBudget so pods can't all die together.
|
||
|
|
3655942fa4 |
fix: reload stale clients on any older app version, not just major (#21011)
## Context
The GraphQL error handler emits an `APP_VERSION_MISMATCH` error
(surfaced on the client as a "your app version is out of date, please
refresh" prompt) when a client's app version is behind the server's.
Today that comparison only fires when the **major** version differs:
```ts
const frontEndMajor = semver.parse(frontEndAppVersion)?.major;
const backendMajor = semver.parse(backendAppVersion)?.major;
if (isDefined(frontEndMajor) && isDefined(backendMajor) && frontEndMajor < backendMajor) { ... }
```
Twenty ships schema changes in minor and patch releases too. A tab left
open across a minor/patch deploy keeps sending GraphQL documents built
against the previous schema. If a field was removed or renamed, those
operations fail with opaque field-level errors and the user never gets
the refresh prompt — a hard reload is the only recovery.
## What this does
- **Server:** fires the mismatch whenever the client version is strictly
older than the server (`semver.lt`), regardless of which version
component changed.
- **Client:** `onAppVersionMismatch` now reloads the page once (in
addition to the existing snackbar) to pull the current `index.html` and
hashed assets, guarded by a short `sessionStorage` window so a
still-stale reload can't cause a refresh loop.
## Notes
- No behavior change for clients on the same or newer version.
- Relies on the existing `x-app-version` header and `APP_VERSION` config
that already drive this check.
---------
Co-authored-by: Charles Bochet <charles@twenty.com>
|
||
|
|
99bd1daaec |
feat(twenty-partners): website field + restructured partner & opportunity views (#21385)
## What Twenty Partners app — partner enrichment + a simpler, partner-centric view structure. **Partner** - New `website` (LINKS) field. - Icons for the opportunity `partner` relation and `matchStatus` fields (were the default "123"). **Partner views** (Partners folder → Applications / Validated / All) - **Applications**: grouped by validation stage, showing Application + Potential; columns Categories / Skills / Type of Team / Languages / Country / LinkedIn. - **Validated**: grouped by availability; columns harmonized with Applications, plus Partner Tier. - Wider Name / Categories columns. **Opportunity pipeline** (Pipeline folder, simplified by partner presence) - `OPP without partner` / `OPP with partner` tables (filtered on partner *is empty* / *is not empty*), `OPP all`, and a kanban **board** grouped by match status. - Sort by match status (uses the option position, i.e. pipeline order). - Removes the previous matching views (waiting-for-match, matches-overview, partner-deals). ## Version Patch bump `0.4.2 → 0.4.3` (relative to main). |
||
|
|
232ca8eec2 |
security: clear happy-dom High alerts by upgrading wyw-in-js 0.7 → 1.1 (#21394)
## What Clears the 2 High `happy-dom` alerts (GHSA-w4gp-fjgq-3q4g, GHSA-6q6h-j7hj-3r64) via a parent bump — **no resolution**. `happy-dom@15.11.7` came from **`@wyw-in-js/transform@0.7.0`** (Linaria's CSS transform), pinned by a root resolution + a local `.yarn` patch and requested by `@wyw-in-js/vite@^0.7.0` in twenty-front + twenty-ui-deprecated. - `@wyw-in-js/vite` `^0.7.0` → `^1.1.0` (twenty-front, twenty-ui-deprecated) - `@wyw-in-js/babel-preset` `^0.6.0` → `^1.1.0` (twenty-ui-deprecated) - **drop the `@wyw-in-js/transform` 0.7.0 resolutions + the `.yarn` patch** — the patch added a `visited` cycle-guard to `TransformCacheCollection.invalidateIfChanged`, which is **already upstream** in transform 1.1.0, so it's obsolete. `@wyw-in-js/transform` now resolves to **1.1.0** (→ happy-dom 20.10.2) and 0.8.1 (website, unchanged, → happy-dom 20.8.9). The vulnerable 0.7.0/15.11.7 are gone. ## Required config change wyw-in-js 1.x resolves modules in its CSS pre-build via vite's `resolve.alias` instead of `vite-tsconfig-paths`. So twenty-front's `@/` and `~/` tsconfig path aliases are mirrored into `vite.config` `resolve.alias` — otherwise the CSS evaluator throws `Cannot find module '@/...'` for aliased imports used inside `styled` definitions. ## Verification - happy-dom now **20.8.9 + 20.10.2** (both patched); no 15.x left - `nx build twenty-front` — CSS extraction works (**1018 files transformed**) + `typecheck` - `nx build twenty-ui`, `twenty-ui-deprecated` (Linaria CSS extraction) - website's Linaria transform runs fine (local build only stops on a missing `TWENTY_PARTNERS_API_URL` env var, unrelated) - `yarn install --immutable` clean |
||
|
|
71480c3888 |
fix: gracefully handle missing logic functions during workflow destroy (#21362)
## Summary - Wraps `deleteOneWithSource` calls in `.catch()` during workflow/step destruction so that a missing logic function (valid UUID but already deleted) no longer crashes the entire destroy operation - Adds a `Logger` to `WorkflowVersionStepOperationsWorkspaceService` for the warning - Fixes test mock to return a resolved Promise and use a valid UUID ## Context When a CODE step references a `logicFunctionId` that is a valid UUID but the logic function no longer exists (e.g. deleted by a previous operation or orphaned), the destroy fails with "Logic function with id X not found". This blocks users from cleaning up workflows. ## Test plan - [x] Destroy a workflow with CODE steps whose logic functions already exist → succeeds as before - [ ] Destroy a workflow with CODE steps referencing a deleted/non-existent logic function → succeeds with a warning log instead of crashing |
||
|
|
adba66caea |
fix(twenty-front): new layout fast-follows — settings drawer, loading & command menu (#21389)
Second batch of new-layout fast-follows (master: twentyhq/core-team-issues#2478). All changes verified live against a running workspace. ## Settings drawer & header - **twentyhq/core-team-issues#2489** — sidebar icons render as plain 16px icons, no background tiles. - **twentyhq/core-team-issues#2488** — Advanced toggle spans the full drawer width; yellow dot removed. - **twentyhq/core-team-issues#2497** — page title stays centered in the settings header (breadcrumb stays left). - **twentyhq/core-team-issues#2490** — Exit Settings control aligned to the workspace switcher (24px, matching padding/gap). - **twentyhq/core-team-issues#2499** — 2px vertical gap restored between collapsible drawer section items. - **twentyhq/core-team-issues#2491** — settings drawer rhythm now matches the main app (28px items, 2px gaps, 28px section headers). - **twentyhq/core-team-issues#2492** — Home/Chat tab switch no longer flickers: both tab subtrees stay mounted (a shared `NavigationDrawerTabbedContent` toggles visibility instead of remounting + flashing the chat skeleton). ## Loading states - **twentyhq/core-team-issues#2486** — metadata loading shows an empty body (no dense skeleton rows). - **twentyhq/core-team-issues#2487** — settings table keeps its layout while loading, with the shimmer localized to the first row's first cell. ## Command menu & navigation - **twentyhq/core-team-issues#2501** — navigation section header height matches the nav item rhythm (28px). - **twentyhq/core-team-issues#2502 (part 1)** — the page side-panel toggle stays as the dots glyph while the command menu is open, instead of morphing into a second close control. ## New-field flow - **twentyhq/core-team-issues#2494** — the new-field stepper moved from a breadcrumb dropdown into a centered secondary wizard bar (back chevron + Save on the configure step); breadcrumb stays clean and the object label is the centered title. ## Descoped (substantive bugs already fixed) - **twentyhq/core-team-issues#2500** — command-menu highlight right gutter: the menu-item base measures full-width, so it's likely a scrollbar gutter on the list, not the shared component. Left for a focused follow-up. - **twentyhq/core-team-issues#2502 part 2** — moving the command-menu close from left to right is cosmetic (the duplicate-control bug is fixed by part 1) and would touch the shared `SidePanelTopBar` used by search/AI panels. ## Verification typecheck (tsgo) + oxlint + oxfmt green for all changed files; each change DOM-measured / screenshotted in the running app. |
||
|
|
6525af5ba4 |
Improve log visibility in dev mode (#21393)
Small PR to improve logging in twenty sdk dev mode ## After <img width="1406" height="580" alt="image" src="https://github.com/user-attachments/assets/a14edc2d-2194-420a-8b0b-b3ca35c31ccd" /> |
||
|
|
fc764115ef |
security: clear all High next alerts by upgrading react-email 5 → 6 (#21377)
## What Clears **all 9 High `next` Dependabot alerts** (incl. GHSA-26hh-7cqf-hhc6) in twenty-emails — via a parent bump, no resolutions. All 9 traced to a stale **`next@16.0.10`** pulled by `@react-email/preview-server` 5.x. The latest preview-server 5.x still ships a vulnerable next (16.1.7 < 16.2.6), so bumping it alone wouldn't help. **react-email 6.x** is a rewrite that no longer depends on next or on a separate preview-server. - bump `react-email` `5.1.0` → `6.5.0` - remove the obsolete `@react-email/preview-server` devDependency - add `@react-email/ui` `6.5.0` devDependency ### Why `@react-email/ui` (the CI fix) react-email 6's `email dev` preview server loads its UI from `@react-email/ui`, and **prompts to install it interactively** if missing — which hangs the `emails-test` CI job (no TTY), so the server never starts and the `/preview/test.email` smoke check fails. Pinning `@react-email/ui` makes `email dev` start non-interactively. ### Net effect on `next` The vulnerable `16.0.10` is gone. `@react-email/ui@6.5.0` pulls **`next@16.2.6`** — the **patched** version (≥ every current next advisory fix), so all 9 alerts clear and **no vulnerable next remains**. ## Notes - `react-email` and `@react-email/ui` pinned to exact `6.5.0` (matching the prior react-email pin) because the `6.6.0` line was published today and is still registry-quarantined. - react-email is a dev-only preview tool; CI builds emails via `vite` + typecheck. ## Verification - No `next < 16.2.6` in `yarn.lock` - `nx build` + `nx typecheck` twenty-emails - `email dev -d src/emails -p 4001` starts non-interactively and serves `/preview/test.email` → HTTP 200 (reproduces the emails-test check, now passing) - `yarn install --immutable` clean |
||
|
|
217e1f5ab3 |
security: clear immutable High alert via @graphql-codegen typescript plugins v4 (#21380)
## What Clears the High `immutable` alert (GHSA-wf6x-7x77-mvgw) via a parent bump — **no resolution**. `immutable@3.7.6` was pulled by `@ardatan/relay-compiler@12.0.0` (→ `immutable ~3.7.6`), reached through `@graphql-tools/relay-operation-optimizer` inside the `@graphql-codegen` visitor plugins. The fix lives in `relay-operation-optimizer@7.1.4` → `relay-compiler@13.0.1` → `immutable@^5.1.5` — but the old codegen typescript plugins (v3) pinned a 6.x optimizer stuck on relay-compiler 12. **Fix chain:** - `@graphql-codegen/typescript` `^3.0.4` → `^4.1.6` - `@graphql-codegen/typescript-operations` `^3.0.4` → `^4.6.1` - refresh `@graphql-tools/relay-operation-optimizer` (within its existing `^7.0.0` range) → 7.1.4 → `relay-compiler@13.0.1` → `immutable@5.1.6` ## Heads-up: this is effectively a codegen v4 plugin upgrade The codegen typescript plugins v4 change the generated **scalar shape** (`Scalars['X']` → `Scalars['X']['input'|'output']`), so the committed `generated*/graphql.ts` are regenerated (~7.8k lines). The diff is **purely type-level** — no runtime/enum/document changes — and was regenerated against the current schema (verified: **no schema-content drift**). ## Verification - `immutable@3.7.6` gone (now 5.1.6); `relay-compiler@13.0.1` - `nx typecheck twenty-front` passes against the regenerated types (0 errors) - `yarn install --immutable` clean - Generated files regenerated against a clean origin/main schema (no drift markers) |
||
|
|
3c81566d65 |
chore: sync AI model catalog from models.dev (#21392)
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> |
||
|
|
ca63904ac5 |
fix(security): bump @scalar/api-reference-react to clear unhead XSS (#21382)
Resolves [Dependabot Alert 630](https://github.com/twentyhq/twenty/security/dependabot/630). unhead@1.11.20 was pulled in transitively via @scalar/api-reference-react@0.4.42 (@unhead/vue@^1.11.11). The useHeadSafe XSS bypass (GHSA, alert https://github.com/twentyhq/twenty/issues/630) is only patched on the unhead 2.x line; the 1.x branch was never fixed and 1.11.20 is the latest 1.x release, so the existing semver range could not reach a patched version. Rather than a resolutions override, bump the direct dependency to a Scalar release that depends on @unhead/vue@^2.x, which resolves unhead to 2.1.15. - Upgrade @scalar/api-reference-react ^0.4.36 -> ^0.9.42 (0.9.43+ blocked by the 3-day npmMinimalAgeGate; the caret adopts them once aged). - Migrate RestPlayground configuration to the new Scalar API: - spec.content -> top-level content - authentication.http.bearer -> authentication.securitySchemes.bearerAuth (with preferredSecurityScheme), matching the server's OpenAPI scheme name. - Drop the ?inline query on the style.css import. It was added in https://github.com/twentyhq/twenty/pull/12099 to stop the old Scalar's global CSS reset from leaking; the new CSS scopes every reset to :where(.scalar-app), so importing it normally restores styling without re-introducing that leak. Proof: <img width="215" height="48" alt="image" src="https://github.com/user-attachments/assets/3a738fae-63bd-4e88-82c3-5dbe72d993ec" /> Co-authored-by: Félix Malfait <felix.malfait@gmail.com> |
||
|
|
7002c7bcb2 |
i18n - website translations (#21384)
Created by Github action Co-authored-by: github-actions <github-actions@twenty.com> |
||
|
|
13a7d62c19 |
[Website] Re-introduce footer language switcher (#21387)
Reintroduce locale-switcher in the footer. <img width="1369" height="514" alt="image" src="https://github.com/user-attachments/assets/b9e4f48a-dd47-4bd1-96f2-fe146b8c8bbe" /> |
||
|
|
ce2d77be2a |
feat(server): in-app server-level admin management (#19785) (#21321)
## Closes #19785 In-app management of **server-level admin rights** (`canAccessFullAdminPanel`, `canImpersonate`) so self-hosters no longer need raw SQL + a Redis flush + restart to grant access. > **Draft** — feature complete; `/code-review` + `/security-review` run and addressed. ### Background `AdminPanelGuard` / `ServerLevelImpersonateGuard` read `request.user.{canAccessFullAdminPanel,canImpersonate}`, hydrated each request from `CoreEntityCacheService.get('user', …)` (local 30-min + Redis no-TTL). The cache was only invalidated on soft-delete, so a raw `UPDATE core."user"` never took effect. The **first** signup auto-gets both flags; every subsequent admin previously needed raw SQL. ### UX - **Admin Panel → General → Administrators**: a read-only overview of every user with server-level access; each row links to that user's admin page. - **Find anyone** via the user search (Recent Users) — available to full admins and impersonators — then open their **admin user page**. - On the user page, an **"Administrator access"** card (gated on `canAccessFullAdminPanel`) has two toggles — *Full admin panel access* and *Impersonation* — that work for **any** user (a user with no access shows both off). Mirrors how **Impersonate** already works (find user → user page → act). Each change opens a confirm dialog with a **2FA code** field; the last full admin's toggle is disabled. ### Backend / security - **Cache fix** — invalidate the user entity cache on committed user updates (not just soft-delete) so privilege changes propagate (~100 ms, cluster-wide) with no restart. - `getServerAdmins` query + `updateServerAdminAccess` mutation (any `targetUserId`), gated on `canAccessFullAdminPanel`. - `NoImpersonationGuard` on both — an impersonated full-admin session can't be used to escalate an impersonator. - Fresh **2FA TOTP step-up** (enrolled+verified method **and** a fresh code; genuine 2FA errors surface; dev-skip on trusted `NODE_ENV`). - **Last-admin lockout** in a transaction with a pessimistic row lock (no TOCTOU). - **Email-to-all-admins + affected user** (rendered once per locale), structured log, audit event-log emit. - **Authorization**: the read-only `userLookupAdminPanel` + `adminPanelRecentUsers` lookups now accept `canAccessFullAdminPanel OR canImpersonate` (new `AdminPanelOrImpersonateGuard`), so a full admin without impersonate can still find users to manage. Workspace/impersonation queries stay impersonate-gated. ### Reviews - `/code-review` (max effort): 3 security findings (impersonation-escalation sink, lockout TOCTOU, step-up accepting PENDING 2FA) — **all fixed**. `/simplify`: applied. `/security-review`: **no high/medium vulnerabilities**. ### Follow-ups (not in this PR) - Unit tests for `AdminPanelServerAdminService` + a frontend test. - Point the self-host troubleshooting docs at the new UI. - OTP retry UX: `ConfirmationModal` closes on confirm, so a wrong code needs a reopen (kept to reuse the existing modal; no new pattern). ### Notes for reviewers - `generated-admin/graphql.ts` entries were hand-added to match codegen output (admin codegen needs a running server); re-run `nx graphql:generate twenty-front --configuration=admin` to confirm parity. - First-admin bootstrap (first signup) is unchanged. --------- Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> |
||
|
|
6c65ae8257 |
perf(twenty-front): stop Sentry Replay from re-serializing record-table mutations on navigation (#21381)
## Problem
Navigating between record-index pages (e.g. People ↔ Companies) blocks
the main thread for seconds, on every navigation, for ~every user.
Profiling pointed at **Sentry Session Replay(rrweb)**, not app code.
## Root cause
Swapping one record table for another produces a large DOM mutation
batch. rrweb serializes that batch **synchronously on the main thread**
(`_isParentRemoved` / mutation processing).
The built-in `mutationLimit` safety valve doesn't help: it's a *count*
threshold (default 10000), but our cost is *per-mutation serialization*
on a wide/deep table DOM — the batch is expensive, not numerous, so it
slips under the limit.
## Fix
```ts
replayIntegration({
_experiments: {
ignoreMutations: ['[id^="row-virtual-index-"]'],
},
}),
```
- ignoreMutations tells rrweb to drop mutation batches originating from
the virtualized row containers (StyledVirtualizedRowContainer, ids
row-virtual-index-N) — the source of the
table-swap churn. The table still appears in replays (initial snapshot;
text is already masked by default), its live row updates just aren't
re-serialized.
## Test
Measured locally
```
┌────────────────────────────────────────────────────────────┬───────────┬───────────────┐
│ │ Baseline │ With fix │
├────────────────────────────────────────────────────────────┼───────────┼───────────────┤
│ Replay/rrweb total │ 4,112 ms │ 188 ms (−95%) │
├────────────────────────────────────────────────────────────┼───────────┼───────────────┤
│ _isParentRemoved │ 2,195 ms │ 6 ms │
├────────────────────────────────────────────────────────────┼───────────┼───────────────┤
```
## Tradeoff
ignoreMutations tells rrweb to skip mutation batches coming from the
virtualized record-table rows, so session replays won't reflect live
changes inside the table — rows scrolling, cells updating, inline edits
will appear "frozen" at the last full snapshot. The table still shows in
the replay (initial render), and **its text is masked by default anyway,
so in practice we lose little**: the surrounding UI, navigation, clicks,
and interactions are all still recorded. The cost we're removing
(multi-second main-thread freeze on every navigation, for ~all users)
**far outweighs not seeing table row churn in replays** imho.
(@FelixMalfait @charlesBochet)
Two caveats worth noting: _experiments.ignoreMutations is an
experimental Sentry API, and it's batch-coarse, if a mutation batch
contains any matching element, the whole batch is dropped, so an
unrelated change occasionally batched with table mutations could be
missed. During navigation these batches are almost entirely table
mutations, so collateral is minimal.
If it ever proves insufficient, the reliable fallback is
`data-sentry-block` on the record-table body (which turns the table into
a placeholder box in replays).
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
|
||
|
|
ca4fc5615f |
security: refresh lodash + picomatch in twenty-apps lockfiles (#21378)
## What Clears the 4 remaining High alerts in the standalone `twenty-apps` lockfiles (hello-world, call-recording). Both are transitive and already in-range, so a plain lockfile refresh picks up the patched releases — no resolutions. | Package | From → To | Requested by | Advisory | |---|---|---|---| | lodash | 4.17.x → 4.18.1 | `@genql/runtime` (`^4.17.20`), `twenty-client-sdk` (`^4.17.21`) | GHSA-r5fr-rjxr-66jc | | picomatch | 4.0.x → 4.0.4 | `tinyglobby` (`^4.0.3`) | GHSA-c2c7-rcm5-vvqj | Only the two app `yarn.lock` files change. These are isolated example/internal apps (not in the root workspace), in the same family as the already-merged #21371 / #21374. |
||
|
|
5f7638cdaf |
fix(front): surface widget render errors via ErrorBoundary onError (#21009)
## Summary
The page-layout widget `ErrorBoundary` in `WidgetCardShell` renders a
generic
"Invalid Configuration" fallback whenever a widget renderer throws.
Because
there is no `onError` handler, the underlying error is swallowed —
unrelated
widget types (fields, notes, front-component, etc.) all surface the same
chip
with no telemetry, which makes render failures hard to triage.
This adds an `onError` handler that forwards the caught error to
`console.error`
and to Sentry (when available) with the widget's `id`, `type`, and
`configurationType` as extra context. It reuses the same dynamic-import
Sentry
pattern already used by `AppErrorBoundary` and
`CommandMenuItemErrorBoundary`,
so it degrades gracefully to a console log when Sentry is not
configured. The
fallback UI is unchanged.
## Test plan
- [ ] `npx nx typecheck twenty-front` passes
- [ ] `npx nx lint:diff-with-main twenty-front` passes
- [ ] When a widget renderer throws, the "Invalid Configuration" chip
still
renders and the error now appears in the browser console / Sentry
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Charles Bochet <charles@twenty.com>
|
||
|
|
0d8d463a44 |
security: clear all High minimatch Dependabot alerts via parent bumps (#21373)
## What Clears **all 14 High `minimatch` ReDoS alerts** (GHSA-7r86-cg39-jmmj, GHSA-23c5-xmqv-rm74, GHSA-3ppc-4f35-3m26) in the root tree — **by bumping the actual parent dev tools, with no `resolutions`/overrides**. Each parent that pinned a vulnerable minimatch is upgraded so the patched version resolves naturally. | Vulnerable minimatch | Pinned by | Fix | |---|---|---| | 10.0.3 | `@microsoft/api-extractor` 7.55.1 | → 7.58.7 (in-range refresh) → minimatch 10.2.3 | | 3.1.2 | `@stoplight/spectral-core` 1.20.0 | → 1.23.0 (in-range refresh) → minimatch ^3.1.4 | | 3.0.8 | `vite-plugin-dts` 3.8.1 → api-extractor 7.43.0 | bump to `^4.5.4` (already used elsewhere here) → minimatch 10.2.3 | | 4.2.3 | `graphql-config` 4.5.0 via `@graphql-codegen/cli` ^3.3.1 | bump cli to `^5.0.7` → graphql-config 5.1.6 → minimatch ^10 | | 9.0.3 | `zapier-platform-cli` ^15.4.1 | bump to `^19.0.0` | | 7.4.6 | `verdaccio` 6.5.2 → `@verdaccio/core` 8.0.0-next | refresh to 6.7.2 → core 8.1.1 → minimatch 7.4.9 | All six are **build/test tooling** — the ReDoS exposure is build-time, never shipped to users. ## Verification - ✅ Every resolved `minimatch` in `yarn.lock` is now ≥ its patched floor (3.1.5 / 7.4.9 / 9.0.9 / 10.2.3+). No `resolutions` added. - ✅ `nx build`: twenty-shared, twenty-ui, twenty-ui-deprecated, twenty-emails (validates vite-plugin-dts v4) - ✅ twenty-zapier: typecheck + build + `zapier validate` (35/35 checks pass; cli 19 + core 15.5.1) - ✅ twenty-front: typecheck; `graphql:generate` with codegen cli 5 produces **byte-identical** output (no generated-file changes in this PR) - ✅ `yarn install --immutable` clean ## Notes - The large `yarn.lock` diff is expected: major bumps to codegen (3→5), zapier-cli (15→19), and vite-plugin-dts (3→4) cascade through dev-tree transitives (net −1244 lines after dedup). - `zapier-platform-core` (runtime) intentionally left at 15.5.1 — only the CLI (dev tool) carried the vulnerable minimatch; `zapier validate` flags only a non-blocking "consider upgrading core" suggestion. - codegen plugins (`typescript`/`typescript-operations`) left at v3: they run fine under cli 5 and produce identical output, so the minimal change is just the cli bump. |
||
|
|
06d68f665d |
fix(auth): additional workspace and identity validation in auth flows (#21347)
## What Adds validation across three auth flows so that a session or reset link is consistently scoped to a workspace the authenticated principal belongs to, and to a verified identity. - **Access token** (`jwt.auth.strategy.ts`): when resolving the request's user context, the token's `userWorkspaceId` must belong to the token's `workspaceId` — the same check the application-token path already performs. - **OIDC** (`oidc.auth.strategy.ts`): reject sign-in when the identity provider explicitly reports `email_verified: false`. - **Password reset** (`reset-password.service.ts`): a supplied `workspaceId` is only used when the user is a member of it; otherwise it falls back to the user's own first password-auth-enabled workspace. ## Tests - `jwt.auth.strategy.spec.ts`: rejects an access token whose user workspace belongs to a different workspace than the token; existing mocks updated to carry the cached `workspaceId`. - `oidc.auth.strategy.spec.ts` (new): rejects unverified email; accepts verified and absent-claim cases. - `reset-password.service.spec.ts`: falls back when the supplied `workspaceId` is not one the user belongs to. `tsgo`, `oxlint` and `oxfmt` all clean on the changed files. |
||
|
|
24839d044a |
fix(server): repair server typecheck broken by isCustom deprecation (#21376)
## What Repairs `server-lint-typecheck`, which is **currently red on `main`**. After #21228 retyped `FlatObjectMetadata.isCustom` as `WasRemovedInUpgrade<boolean> | undefined`, the spec added in #21311 still passed `flatObjectMetadata.isCustom` to `computeTableName(nameSingular, isCustom: boolean)`: ``` graphql-query-order-group-by.parser.spec.ts(83,5): error TS2345: Argument of type 'WasRemovedInUpgrade<boolean> | undefined' is not assignable to parameter of type 'boolean'. ``` Both PRs merged via stale bases, and `server-lint-typecheck` only runs on PRs (not `main` pushes), so the regression landed undetected — the next PR to touch anything server-wide surfaces it. ## Fix Compute the expected physical table name with **`computeObjectTargetTable`** — the production helper that derives custom-ness from the application (`applicationUniversalIdentifier !== TWENTY_STANDARD_APPLICATION`), which is exactly the pattern the `isCustom` deprecation steers callers toward. This stops reading the deprecated field and won't break again when it's removed. One-line change in a single test file; behaviour is unchanged (custom object → `_`-prefixed physical table). ## Verification - `nx typecheck twenty-server` ✅ (was failing on `main`, now passes) - The spec runs green (3/3) - `nx lint:diff-with-main twenty-server` ✅ (lint + format) |
||
|
|
bd084afc11 |
security: force shell-quote >= 1.8.4 (GHSA-w7jw-789q-3m8p, critical) (#21372)
## What `shell-quote <= 1.8.3` is affected by [GHSA-w7jw-789q-3m8p](https://github.com/advisories/GHSA-w7jw-789q-3m8p) / CVE-2026-9277 (**critical**): `quote()` backslash-escapes `.op` characters with `/(.)/g`, which doesn't match line terminators (`\n`, `\r`, U+2028/2029). A line terminator in an object token's `.op` value passes through unescaped, and POSIX shells treat a literal `\n` as a command separator — enabling shell command injection in callers that pass attacker-influenced object tokens to `quote()`. First patched in **1.8.4**. This is Dependabot alert #1434 on the root `yarn.lock`. ## How The root lockfile resolved two vulnerable versions: - `1.8.1` — via the `^1.6.1` / `^1.7.3` / `^1.8.1` ranges - `1.8.3` — **hard-pinned** by `concurrently@9.2.1` (used in `twenty-companion`) `yarn up -R shell-quote` only re-resolves the ranged dependents; the exact `1.8.3` pin from `concurrently` stays. So I added a `shell-quote: "^1.8.4"` entry to root `resolutions`, matching the existing `tmp` / `chokidar` / `tar` security overrides. Every consumer now resolves to the patched `1.8.4`. ## Scope - `package.json`: +1 resolution line. - `yarn.lock`: two vulnerable entries collapse to a single `shell-quote@1.8.4`. - `1.8.4` is a semver-compatible patch over `1.8.3`; latest `concurrently` (10.x) already depends on `1.8.4`. - Verified no `shell-quote <= 1.8.3` remains in any lockfile across the repo. |
||
|
|
e51efef7c8 |
security(apps): bump twenty-sdk to 2.10.1 for the 3 remaining pre-2.0 apps (tmp, undici) (#21374)
## Summary Completes the follow-up flagged in #21344, which deliberately deferred the **three apps pinning a pre-2.0 `twenty-sdk`** (a major jump that needed per-app validation). These were the last `twenty-apps/*` lockfiles still carrying the `tmp` + `undici` Dependabot clusters: | App | SDK before | SDK after | |---|---|---| | `examples/hello-world` | `0.9.0` | `2.10.1` | | `internal/call-recording` | `0.6.3-alpha` | `2.10.1` | | `internal/self-hosting` | `1.22.0-canary.6` | `2.10.1` | Bumping to `twenty-sdk@2.10.1` drops the two vulnerable transitive deps these apps still inherited (via `inquirer ^10 → external-editor`, and `@genql/cli`): | Vuln dep | Advisory | Source | |---|---|---| | `tmp@0.0.33` | [GHSA-ph9p-34f9-6g65](https://github.com/advisories/GHSA-ph9p-34f9-6g65) / CVE-2026-44705 (path traversal) | `inquirer ^10 → external-editor` | | `undici@5.29.0` | [GHSA-vrm6-8vpv-qv8q](https://github.com/advisories/GHSA-vrm6-8vpv-qv8q) / CVE-2026-1526 (websocket OOM) | `@genql/cli` | ## Changes - Bump `twenty-sdk` (and `twenty-client-sdk` where pinned) to `2.10.1` in all 3 apps + regenerate each lockfile. - `hello-world` and `self-hosting` migrate transparently (typecheck clean). - `internal/call-recording` needed source changes for the 2.x API: - `twenty-sdk/clients` → `twenty-client-sdk/core` + `twenty-client-sdk/metadata` (5 files); added `twenty-client-sdk` dependency. - `defineRole` `permissionFlags` → `permissionFlagUniversalIdentifiers` (`SystemPermissionFlag`) — real runtime fix (old key is silently ignored in 2.x). ## Verification Per-app after regen: **`tmp@0.0.33` = 0**, **`undici@5` = 0** in every lockfile; `oxlint` passes with **0 errors**. Root `yarn.lock` untouched; all other undici in the repo is already ≥ patched (`6.26.0` / `7.24.8`). |
||
|
|
123db9e3be |
security: bump vite to 7.3.5 in twenty-apps lockfiles (GHSA-v2wj-q39q-566r) (#21371)
## What The standalone apps under `packages/twenty-apps/*` each ship **their own `yarn.lock`** (they're not part of the root workspace). Three of them still pinned the vulnerable transitive `vite@7.3.1`: - `examples/hello-world` - `examples/postcard` - `internal/call-recording` `vite <= 7.3.1` is affected by three advisories, all first patched in **7.3.2**: | Advisory | Summary | Open Dependabot alerts | |----------|---------|------------------------| | [GHSA-v2wj-q39q-566r](https://github.com/advisories/GHSA-v2wj-q39q-566r) (CVE-2026-39364) | `server.fs.deny` bypassed with queries | #894, #892, #891 | | [GHSA-4w7w-66w2-5vf9](https://github.com/advisories/GHSA-4w7w-66w2-5vf9) | Path traversal in optimized-deps `.map` handling | #901, #899, #898 | | [GHSA-p9ff-h696-f583](https://github.com/advisories/GHSA-p9ff-h696-f583) | Arbitrary file read via dev-server WebSocket | #908, #906, #905 | The root `yarn.lock` was already remediated separately (vite 7.3.2 / 8.0.16); these three sub-package lockfiles were the only ones still flagged open. ## How Ran `yarn up -R vite` per app to re-resolve vite within the existing range; it lands on **7.3.5**. ## Scope - **Lockfile-only**, 3 apps. No `package.json` changes. - Each lockfile diff is 3 lines (version / resolution / checksum). - Verified no vite resolution below the patched thresholds remains anywhere in the repo. |
||
|
|
cf70565976 |
feat(twenty-server): allow shouldHideEmptyGroups in app view manifest (#21370)
## Context The view **Hide empty groups** setting (`shouldHideEmptyGroups`) can be toggled in the UI, is persisted on the `View` entity, exposed in the `CreateView`/`UpdateView` GraphQL inputs, and tracked by the flat-view sync machinery — but it could **not** be set from an app's view manifest. Root cause: the field postdates the manifest plumbing (added in #16385, Dec 2025). Two spots were never updated to thread it through: - `ViewManifest` didn't declare the field. - `fromViewManifestToUniversalFlatView` hardcoded `shouldHideEmptyGroups: false`. Ref: twentyhq/core-team-issues#414 ## Changes - Add optional `shouldHideEmptyGroups?: boolean` to `ViewManifest`. - Read it in the converter (`?? false`), mirroring the existing `isCompact` handling. - Cover it in the converter unit test (default + explicit value). No migration or schema change — the column already exists, and downstream sync (`FLAT_VIEW_EDITABLE_PROPERTIES` + the universal-flat compare type) already handles it. ## Test - `npx jest from-view-manifest-to-universal-flat-view` → 5 passed - `tsgo -p tsconfig.json` (twenty-server) → no new errors - oxlint + oxfmt clean |
||
|
|
7894ae39f0 |
fix(front): reject backslash paths in isValidReturnToPath (open-redirect hardening) (#21287)
## Summary
`isValidReturnToPath` validates the post-login `returnTo` path and
already rejects protocol-relative `//` paths — but not the backslash
variant. Browsers normalize `\` to `/`, so `/\evil.com` resolves like
`//evil.com` (a protocol-relative, external URL) while still passing the
existing `//` check:
```ts
isValidReturnToPath("/\\evil.com"); // returns true today; should be false
```
This hardens the open-redirect guard by rejecting any path containing a
backslash, so a `returnTo` can only ever be a same-site absolute path.
## Changes
- `isValidReturnToPath`: reject paths containing `\`.
- Added tests for backslash-tricked paths.
Framed as defense-in-depth — the validator should reject this class
regardless of how each consumer performs the redirect.
---------
Co-authored-by: Charles Bochet <charles@twenty.com>
|
||
|
|
c5e95c6649 |
Enrich app:add field relation and morph relations (#21368)
# Before <img width="833" height="358" alt="image" src="https://github.com/user-attachments/assets/e006bc5b-79e8-449f-beba-eb75393a9c60" /> # After <img width="1015" height="383" alt="image" src="https://github.com/user-attachments/assets/586f19df-e43b-45bc-9b26-16573108e750" /> <img width="1061" height="413" alt="image" src="https://github.com/user-attachments/assets/962635fa-6cdf-4aef-a1ea-cd129c5ff656" /> |
||
|
|
9c66975520 |
isCustom deprecation for Objects and Fields (#21228)
## Context
`isCustom` was a legacy denormalized boolean on `ObjectMetadataEntity`
and `FieldMetadataEntity`.
Now that every metadata row carries `applicationId` (via
`SyncableEntity`), "is this custom" is fully derivable, and the stored
boolean was a redundant second source of truth that could drift.
The real meaning of `isCustom` is **"the owning application is not the
twenty-standard application"** — i.e. `!belongsToTwentyStandardApp`.
Note this is *not* "belongs to the workspace custom app" as I initially
thought: third-party-application
objects/fields are custom too.
The standard application has a globally stable `universalIdentifier`, so
the value derives with no per-workspace lookup.
## Changed
## `isCustom` checks — before → after
`isCustom` is no longer a stored column. The table below lists every
site that branched on it and how it resolves now. The unifying rule:
`isCustom ≡
!isTwentyStandardApplicationUniversalIdentifier(applicationUniversalIdentifier)`.
### Server — behavioural checks
| Location | Purpose | Before | Now |
|---|---|---|---|
| `utils/compute-object-target-table.util.ts` | Physical table name `_`
prefix | `computeTableName(nameSingular, objectMetadata.isCustom)` |
derives from `applicationUniversalIdentifier` (single source for all
table-name callers) |
| `twenty-orm/factories/entity-schema.factory.ts` +
`…/entity-schema-metadata.type.ts` | ORM table name (hot path) |
`object.isCustom` | `object.applicationId !== standardApplicationId`
(computed in `buildEntitySchemaMetadataMaps`) |
|
`twenty-orm/repository/workspace-{delete,soft-delete,update}-query-builder.ts`
| Table name for mutations | `computeTableName(nameSingular,
objectMetadata.isCustom)` | `computeObjectTargetTable(objectMetadata)` |
| `index-metadata/utils/generate-deterministic-index-name-v2.ts` | Index
name hash (must stay bit-identical) | `flatObjectMetadata.isCustom` |
derives from `applicationUniversalIdentifier` |
| `object-metadata/object-record-count.service.ts` | Table name for
record count | `computeTableName(nameSingular, isCustom)` |
`computeObjectTargetTable(flatObjectMetadata)` |
|
`workspace-manager/dev-seeder/data/services/dev-seeder-data.service.ts`
| Match seed config by table name | `computeTableName(item.nameSingular,
item.isCustom)` | `computeObjectTargetTable(item)` |
| `commands/workspace-export/workspace-export.service.ts` +
`…/utils/generate-workspace-schema-ddl.util.ts` | Export table name (raw
entity) | `objectMetadata.isCustom` |
`!isTwentyStandard…(objectMetadata.application?.universalIdentifier)` |
|
`flat-field-metadata/services/flat-field-metadata-type-validator.service.ts`
| Block users creating reserved field types |
`args.flatEntityToValidate.isCustom` |
`!args.flatEntityToValidate.isSystem` |
| `api/common/.../common-create-many-query-runner.service.ts` | Don't
let client overwrite system `createdBy` |
`createdByFieldMetadata.isCustom === false` |
`createdByFieldMetadata.isSystem === true` |
|
`field-metadata/utils/resolve-field-metadata-standard-override.util.ts`
| Skip i18n/overrides for custom fields | `if (fieldMetadata.isCustom)
return raw` | **removed** — falls through on
`isDefined(standardOverrides)` |
|
`object-metadata/utils/resolve-object-metadata-standard-override.util.ts`
| Skip i18n/overrides for custom objects | `if (objectMetadata.isCustom)
return raw` | **removed** — same fall-through |
|
`command-menu-item/utils/build-navigation-interpolation-context.util.ts`
| Override context for nav labels | passed `isCustom` into resolver |
dropped (resolver no longer needs it) |
| `api/common/.../data-arg-processor.service.ts` | `isCustom` for
record-position table name | `flatObjectMetadata.isCustom` | derives
from `applicationUniversalIdentifier` |
| `metadata-modules/minimal-metadata/minimal-metadata.service.ts` |
Minimal DTO + override context | `flatObjectMetadata.isCustom` | derives
from `applicationUniversalIdentifier` |
|
`commands/upgrade-version-command/1-23/…backfill-record-page-layouts.command.ts`
| Filter to custom objects | `objectMetadata.isCustom` |
`!isTwentyStandard…(applicationUniversalIdentifier)` |
### Server — DTO / API population
| Location | Before | Now |
|---|---|---|
|
`flat-object-metadata/utils/from-flat-object-metadata-to-object-metadata-dto.util.ts`
| passthrough `isCustom` | derives from `applicationUniversalIdentifier`
|
|
`flat-field-metadata/utils/from-flat-field-metadata-to-field-metadata-dto.util.ts`
| passthrough `isCustom` | derives from `applicationUniversalIdentifier`
|
|
`object-metadata/utils/from-object-metadata-entity-to-object-metadata-dto.util.ts`
(REST) | `entity.isCustom` | `entity.applicationId !==
standardApplicationId` |
|
`field-metadata/utils/from-field-metadata-entity-to-field-metadata-dto.util.ts`
(REST) | `entity.isCustom` | `entity.applicationId !==
standardApplicationId` |
| `dataloaders/dataloader.service.ts` | passed
`flatFieldMetadata.isCustom` into override resolver | dropped (resolver
no longer needs it) |
> REST controllers (`object-metadata.controller.ts`,
`field-metadata.controller.ts`) resolve `standardApplicationId` once per
request from the cached `flatApplicationMaps`.
### Frontend
| Location | Purpose | Before | Now |
|---|---|---|---|
| `settings/.../SettingsObjectFieldDisabledActionDropdown.tsx` | Whether
an inactive field is deletable | `isDeletable = isCustomField` |
`isDeletable = isCustomField && !isSystemField` |
### Unchanged (out of scope)
`isCustom` on `IndexMetadata` / `View` / `Skill` / `Agent` and their
guards still read the persisted column.
Breaking change is on the isCustom filter on field and object APIs, this
is never used in the FE and unlikely used by external consumers
|
||
|
|
7530775dc1 |
fix(billing) - Suspend workspace at trial period end if cancelation is planned (#21363)
https://twenty-v7.sentry.io/issues/6900026484/events/e5a9e11fe0464df296d8beeabcf0f538/?end=2026-06-09T05%3A24%3A00&project=4507072499810304&query=%21twenty.workspace.id%3A%EF%80%8DContains%EF%80%8Dad6668da-9f4c-4618-9424-9092ea87db26%20%21twenty.workspace.id%3A%EF%80%8DContains%EF%80%8D86b352c5-ffff-4a4c-8660-b1b2bfb7f57d&referrer=next-event&start=2026-05-23T06%3A02%3A00 |
||
|
|
02aa086866 |
fix(twenty-front): new layout fast-follows (#21360)
Fast-follows for the new layout / flat redesign (master: twentyhq/core-team-issues#2478). ## Changes - **Main navbar 48px** (twentyhq/core-team-issues#2479) — `SIDE_PANEL_TOP_BAR_HEIGHT` 40 → 48, so `PageCardHeader` matches the Figma target. The side panel top bar shares this constant and stays aligned. - **Content panel 12px radius** (twentyhq/core-team-issues#2480) — `PageCardLayout` card gets a full border + 12px radius and an 8px inset (`spacing[2]`) so it floats on the shell instead of square full-bleed. - **Square three-dots button** (twentyhq/core-team-issues#2481) — added a `square` option to `AnimatedButton`; the page-header side-panel toggle now renders a 24×24 square icon button instead of a 32×24 pill. - **Table checkbox sizing** (twentyhq/core-team-issues#2482) — restored `box-sizing: content-box` on the checkbox box. Its border is declared outside the label size, so the global `border-box` reset (#21349) was shrinking it (14px → 12px). Same fix pattern as #21349. - **Tertiary navbar background** (twentyhq/core-team-issues#2483) — left navbar / app shell use `background/tertiary` instead of the noisy surface (`DefaultLayout`, `UserOrMetadataLoader`). - **Skeleton loading** (twentyhq/core-team-issues#2484) — metadata + content loading now match the new layout: tertiary shell, 12px rounded content panel, 48px navbar, sparse bars, empty body (removed the dense full-width rows). Left-panel skeleton bars use `quaternary` so they stay visible on the tertiary shell. ## Verification - typecheck (tsgo) + oxlint + oxfmt pass for all changed files. - Verified live against a running workspace: measured navbar = 48px, three-dots = 24×24, checkbox box-sizing = content-box (14px), card radius = 12px, shell background = tertiary (no noisy image). Content-loading skeleton matches the target. |
||
|
|
c27c8c88b0 |
Fix various graphs bugs (#21311)
Some bugs fixed in this PR
1. From UI any field could be chosen to group the query by it, while for
instance, RAW_JSON type (eg workflowRun.state) is not supported by
PostgreSQL to group a query by. Fix: removed it from the "group by"
fields options in FE + in BE -->
2. The BE check existed (isFlatFieldMetadataSupportedInGroupBy) but the
signature was malformed: it expected`{ fieldMetadataType,
fieldMetadataName, fieldMetadataIsSystem }` while every caller passes a
flat field metadata object with type/name/isSystem. So the check is
mis-wired — at runtime the destructured props are undefined, making it
always return true (validation bypassed). Fixed this.
3. Group by does not work with Morph relations if their direction is
ONE_TO_MANY. Added that constraint.
4. Group by with morph relations were broken even for MANY_TO_ONE,
because a morph is stored as one field per target
(polymorphicOwnerRocket, polymorphicOwnerSurveyResult…), each with its
own join column, but the frontend collapsed them into a single
polymorphicOwner field — so the backend tried to resolve a non-existent
polymorphicOwnerId. Fix: Frontend: added a target picker so you choose
the specific morph target (then its sub-field), storing the real
per-target field id. Backend: fixed validate-relation-subfield to use
the per-target field's own relationTargetObjectMetadataId instead of the
multi-target resolver that returned null.
5. (improvement) When an error occured in the query, the graph showed
"No data". Updated it to "error". (screenshot 1)
6. When a field used as a filter on a graph is deleted, it is not
deleted as a graph filter (which is ok because it would involve parsing
all the graph's configuration json to find whether a field is
referenced; there is no foreign key), which prevented from further
modifying the graph's filters. Fixed this + add an indicator that the
filter is can/should be removed (see screenshot 2)
7. "Ambiguous column name" PG error occurs when ordering by "creation
date" of a related field, because both objects have createdAt field.
Fixed it by adding table alias as prefix.
8. (improvement) While working on #5 I did not understand why we could
directly do `"objectMetadataNameSingular"."columnName" `while I expected
that for custom objects it would have to be
`_objectMetadataNameSingular`. that's simply because we use an alias
from the beginning. To add clarity, within groupBy code I replaced
`objectMetadataNameSingular` with `objectAlias` everywhere it is indeed
inherited from us using objectAlias.
<img width="685" height="391" alt="Screenshot 2026-06-08 at 12 01 45"
src="https://github.com/user-attachments/assets/f2b15ca5-da39-4114-8188-69f58f3c4cbf"
/>
<img width="598" height="341" alt="Screenshot 2026-06-08 at 11 53 55"
src="https://github.com/user-attachments/assets/66372811-4a37-40d9-b43a-4af51f89b6e6"
/>
|
||
|
|
137fe45cf6 |
Deprecate dummy enterprise key 2/2 (#21328)
Following [1/2](https://github.com/twentyhq/twenty/pull/20890) Now that all usages of hasValidEnterpriseKey has been removed in prod and deployed, we can safely remove it altogether. --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> |
||
|
|
bda6fcfec9 |
feat(website): book an intro call after partner application (#21343)
## What
After a partner submits the application wizard, the success screen now
offers an inline Cal.com booking widget so they can book an intro call
on the spot — keeping the partner process high-touch.
- Inline Cal.com embed on the application success step, prefilled with
the applicant's name/email (company in the notes)
- Wide `month_view` layout; the success modal widens to ~960px (the
4-step form and mobile are unchanged)
- Event-type-details panel hidden via `cal('ui', { hideEventTypeDetails:
true })` so it's just calendar + times, on its own `partner-intro` Cal
namespace (isolated from the ContactCal embed)
- "I'll book later" escape hatch; backend / submission path untouched
## Why
The warmest moment is right after someone opts in. Today the success
screen only shows a Close button — this turns that moment into a
scheduled conversation.
## How
- `PartnerIntroCalEmbed` — thin wrapper over `@calcom/embed-react`
(already a dependency), reusing the existing `ContactCal` embed pattern
- `buildPartnerIntroPrefill` — pure mapping of applicant fields → Cal
prefill
- `PartnerApplicationSuccess` — presentational success view (heading +
subtitle + embed + dismiss)
- The wizard reports submitted-state up (`onSubmittedChange`) so the
modal widens only on the booking step
- New Lingui copy + regenerated catalogs (en/es/fr)
## Test plan
- `npx jest PartnerApplication` — green (prefill mapping, embed
link/layout/prefill, success view)
- `npx nx typecheck twenty-website` — clean
- `npx oxlint -c .oxlintrc.json` / `npx oxfmt --check` — clean
- Manual: submit the wizard → success step shows the wide booking
calendar, prefilled, dark theme, no event-details panel; "I'll book
later" closes the modal
## Notes
- Frontend only — no backend, schema, or submission-path change
- Cal link `rashad-twenty/partner-intro` lives as a constant in
`config.ts`
- Branch is currently behind `main`; happy to rebase before review
|
||
|
|
ba4ac6b70e |
fix(twenty-front): restore top-bar-title testid to unbreak merge queue (#21367)
## Problem The merge queue is broken. Every queued PR (#21357, #21361, #21364, #21366, …) fails on the same E2E assertion in `workflow-creation.spec.ts:36`: ``` Locator: getByTestId('top-bar-title').getByPlaceholder('Name') Error: element(s) not found ``` All other E2E tests pass, which pointed to a regression already on `main` rather than any individual PR. ## Root cause #21308 ("generalize the page primary/secondary bars (flat redesign)") switched `RecordShowPageHeader` from `PageHeader` to the new `PageCardHeader`. - The old `PageHeader` wrapped its title in `<StyledTitleContainer data-testid="top-bar-title">`. - The new `PageCardHeader` renders the breadcrumb slot **without** that `data-testid`. The editable record title cell (the `Name` input the test fills in) still renders fine inside `ObjectRecordShowPageBreadcrumb` — it just lost the `top-bar-title` wrapper that the E2E suite locates it by. The testid is also used by the `blank-workflow` fixture. ## Fix Restore `data-testid="top-bar-title"` on the record-show breadcrumb container, which wraps exactly what `PageHeader` previously did (the editable `Name` input and, after save, the record name text). Minimal and behavior-preserving; record-index and standalone pages use different header slots and were unaffected (their E2E tests passed throughout). |