Commit Graph

12902 Commits

Author SHA1 Message Date
Félix Malfait 705caab2b0 fix(onboarding): refresh stale workspace in currentWorkspace field resolver (#21839)
## What & why

After sign-up, users are redirected to `/sync/emails` and the page loads
forever; a full refresh fixes it. This blocks production deploy.

**Root cause** — a GraphQL field resolver returns an unrefreshed (stale)
workspace:

- `@AuthWorkspace()` (`request.workspace`) is read from the per-instance
core entity cache and can still be `PENDING_CREATION` /
`ONGOING_CREATION` right after `activateWorkspace`.
- The `currentUser` query resolver and the `onboardingStatus` field
already guard against this by calling
`refreshWorkspaceIfPendingOrOngoingCreation(...)`.
- But the `currentWorkspace` `@ResolveField` returned the raw
`@AuthWorkspace()` workspace. Because a field resolver takes precedence
over any value the query resolver attaches to its returned object, the
client receives that stale workspace.

So right after activation the client got an inconsistent payload:
- `onboardingStatus: SYNC_EMAIL` (fresh — computed from a direct DB
read)
- `currentWorkspace.activationStatus: ONGOING/PENDING_CREATION` (stale)

On the frontend, metadata loading is gated on
`isWorkspaceActiveOrSuspended(currentWorkspace)`, and
`MinimalMetadataGater` does **not** exclude `/sync/emails`. So the
workspace looked inactive → metadata never loaded (no metadata GraphQL
request was even issued) → the gater's loader showed indefinitely. A
full refresh worked because the cache had since refreshed to `ACTIVE`.

## Why it surfaces on staging but isn't caught by tests

The stale window only opens on a real fresh sign-up followed by
immediate activation, against a workspace cache that hasn't refreshed
yet (multi-instance / cache TTL). Single-instance local dev and the
existing `successful-user-and-workspace-creation` integration test
exercise `activateWorkspace` + `getCurrentUser` against one consistent
cache, so `currentWorkspace` already looks `ACTIVE` and they pass —
which is why this reproduces on staging/production but not locally, and
why a manual refresh recovers.

## How

Refresh the workspace in the `currentWorkspace` field resolver too, so
it is consistent with `onboardingStatus`. For active workspaces this is
a no-op (no extra DB read).

```ts
async currentWorkspace(@AuthWorkspace({ allowUndefined: true }) workspace) {
  if (!isDefined(workspace)) return workspace;
  return this.userService.refreshWorkspaceIfPendingOrOngoingCreation(workspace);
}
```

This is preferred over the frontend alternative (excluding
`/sync/emails` from `MinimalMetadataGater`), which would only hide the
symptom while every other consumer still received a wrong
`activationStatus`.

## Verification
- `nx typecheck twenty-server` and `nx lint:diff-with-main
twenty-server` (oxlint + oxfmt) are green.

https://claude.ai/code/session_018c1X6CwDgttMXA5tB797yS

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-06-19 14:45:12 +02:00
Raphaël Bosi 88967a6e47 Add Update fields select to People Data Labs enrichment functions (#21801)
## What

The People Data Labs enrichment logic functions (`enrich-person`,
`enrich-company`, `enrich-people`, `enrich-companies`) now expose an
**`Update fields`** select instead of the `overrideExistingValues`
boolean, and always return the enriched data in their output.

`Update fields` options:
- **Yes and overwrite**: persist, overwriting existing standard fields
- **Yes and don't overwrite** (default): persist, filling standard
fields only when empty
- **No**: write nothing to the record (no CRM fields, no PDL metadata,
no company creation)

## Why

The functions previously only persisted data. With `No`, they can now
fetch from PDL and return the result without modifying the record, so
downstream workflow steps can consume it. Every matched result now
carries a `data` object with the mapped record fields (standard + `pdl*`
values), and the bulk functions also declare their `results[]` array in
the output schema.

Billing is unchanged: a successful PDL match is still charged in all
modes, since the API cost is incurred regardless of persistence.

## Notes

- Default behavior is preserved (unset input means fill-empty +
persist).
- Typecheck, lint, and the full unit suite (368 tests) pass.
2026-06-19 14:39:49 +02:00
Abdul Rahman d19b7f8485 Enable getting started translations (#21842)
## Summary

The Getting Started pages on the docs site (docs.twenty.com) were only
ever available in English, never translated into the other supported
languages.

**Root cause:** The Getting Started section (added in #19728) was never
added to the Crowdin source config (`crowdin-docs.yml`), so its `.mdx`
files were never uploaded for translation. Only `user-guide`,
`developers`, and `twenty-ui` were configured.

This also surfaced a related bug: because the pages had no translations,
the navigation generator fell back to the English page path for every
language, duplicating paths like `getting-started/introduction` across
all 14 language navs. Mintlify treats duplicate cross-language paths as
undefined behavior, which broke the language switcher (it always
redirected to `/getting-started/introduction`).

## Changes

- `.github/crowdin-docs.yml` — add `getting-started/**/*.mdx` as a
translation source so the pages get sent to Crowdin.
- `packages/twenty-docs/scripts/fix-translated-links.sh` — add
`getting-started` link-rewriting rules to match the other sections.
- `packages/twenty-docs/scripts/generate-docs-json.ts` — only include a
page in a non-default language when its translated file exists; drop
empty groups/tabs (removes the duplicate cross-language paths that broke
the switcher).
- `packages/twenty-docs/docs.json` — regenerated.



<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21842?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-19 14:16:17 +02:00
github-actions[bot] f61522b56a i18n - docs translations (#21841)
Created by Github action

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21841?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->

Co-authored-by: github-actions <github-actions@twenty.com>
2026-06-19 13:52:47 +02:00
Charles Bochet 0064ff6741 fix(ai): validate AI agent output field names against schema-key constraint (#21834)
## Problem

On a self-hosted instance, an AI Agent workflow action fails at run time
with an opaque model error:

```
The model returned the following errors: tools.0.custom.input_schema.properties:
Property keys should match pattern '^[a-zA-Z0-9_.-]{1,64}$'
```

This is Anthropic's validation on tool `input_schema` **property keys**.
An AI Agent's structured **Output** fields are turned into a JSON schema
and passed to the model as a tool; each output **variable name** becomes
a property key. Anthropic rejects any key that does not match
`^[a-zA-Z0-9_.-]{1,64}$` — most commonly a name containing a **space**
(e.g. `meetings brief`), but also names over 64 characters or with other
symbols.

Until now nothing validated this: `fieldsToSchema` writes
`properties[field.name]` verbatim, so a bad name only failed once the
workflow executed, with an error that gives the user no idea what to
fix. It doesn't reproduce on every instance — it depends purely on how
the workflow's output variables happen to be named.

## Fix

Introduce a single shared check,
`isValidAgentResponseSchemaPropertyKey`, and enforce it in two places:

- **Backend** — `validateAgentResponseFormat` now rejects invalid output
field names at agent **save time** with a clear `userFriendlyMessage`,
instead of letting the broken schema reach the model. This also gates
agents created via the API and re-saves of existing bad data.
- **Frontend** — the output schema builder shows an inline error on the
Variable Name field as soon as an invalid name is entered.

## Tests

- Unit test for the shared validity check (valid + invalid cases:
spaces, leading space, empty, > 64 chars, symbols, unicode).
- Unit test for `validateAgentResponseFormat` covering text/json
formats, valid names, a space in a name, an over-length name, and
reporting multiple invalid names at once.

## Notes for the reporter

The immediate unblock for an affected workflow is to rename the output
variable to remove the space (e.g. `meetings brief` → `meetings_brief`)
and retry the run. With this change the bad name is caught up front with
an explanation rather than failing mid-run.


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21834?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-19 13:50:44 +02:00
martmull 576f88b5c5 Update ci internal applications (#21837)
Fix `twenty-last-contact` ci

- add tests
- add tsgo
- update package commands

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21837?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-19 13:42:15 +02:00
Dilan Melvin T 1bd7be36e0 fix(front): recompute ExpandableList visible chips on resize (#21139)
## Summary

Relation field cells in the record table render their chips through
`ExpandableList`, which measured how many chips fit only once (during
the
children ref pass) and cached the cutoff. It recomputed on item-count
and
hover changes, but never when the cell's available width changed — so a
cell
measured while narrow stayed stuck on that count even after the column
grew
wider. This is the "only ~3 items shown even when the cell is larger"
bug.

This PR adds a `ResizeObserver` on the outer container that resets the
first
hidden child index whenever the available width changes, so the list
reveals
as many chips as fit (and re-trims when narrowed). The outer container
is
observed because its width tracks the available width independently of
how
many chips are currently rendered, which avoids a
measure → trim → shrink → re-measure feedback loop. The observer is
cleaned
up on unmount.

## Test plan

- [x] Added a Storybook interaction test
(`RecomputesVisibleChipsOnResize`)
that renders the list in a narrow container, widens it, and asserts more
      chips become visible.
- [x] Verified the test fails without the fix and passes with it.
- [x] `oxfmt` and `oxlint` pass on the changed files.
- Manual: open a record table with a to-many relation field that has
several
  linked records, widen the column, and confirm more chips appear.

Fixes #12039

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
2026-06-19 13:38:39 +02:00
Paul Rastoin 26db3f5735 Deprecate legacy encryption (#21831)
# Introduction
Still preserving the cross-upgrade flow

close https://github.com/twentyhq/core-team-issues/issues/2465


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21831?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-19 11:32:39 +00:00
Parship Chowdhury 4de9f45015 fix: Edit Layout keeping the command menu open (#21161)
## Summary
- Fixes https://github.com/twentyhq/core-team-issues/issues/2460
- Engine/headless commands always skipped closing the menu
(`closeSidePanelOnCommandMenuListExecution: false`), even when the item
was not pinned. Edit Layout is `isPinned: false`, so the menu should
close like other list-only actions.

## Approach
- Option 1 (I chose this one): Derive close behavior from
`item.isPinned` -> pinned commands keep the menu open; non-pinned ones
close it.
- Option 2 (not chosen): remove the engine command override totally and
use the default close behavior for all commands.

Option 1 is more targeted: it fixed Edit Layout without changing pinned
commands (e.g. Export progress in the menu list). Option 2 is simpler
but widens the blast radius to every engine command clicked from the
side panel list.

## Screenshots
### Before

https://github.com/user-attachments/assets/70b8dc75-af00-4917-81a1-646381f571d5

### After

https://github.com/user-attachments/assets/f733d696-9330-4e6c-993b-8a8133c53e0d

---------

Signed-off-by: Parship Chowdhury <parshipchowdhury@gmail.com>
Co-authored-by: Charles Bochet <charles@twenty.com>
2026-06-19 11:21:50 +00:00
Thomas Trompette 1ec59beb8b docs(user-guide): remove unsupported Between operator for Number filters (#21838)
## What

The [Filters & Sorting user
guide](https://docs.twenty.com/user-guide/views-pipelines/capabilities/filters-and-sorting)
lists a **Between** operator for **Number** fields, but it does not
exist in the product and we do not plan to implement it. This removes it
from the operators table.

| Field Type | Before | After |
|---|---|---|
| Number | Equals, Greater than, Less than, **Between**, Is empty |
Equals, Greater than, Less than, Is empty |

The **Date** row is intentionally left untouched.

## Why

Avoids documenting a capability that does not exist (the same gap that
prompted #20932). Closing the external PR that attempted to add it for
numbers; the docs should reflect what we actually ship.

## Notes

- Only the canonical English source is edited; localized copies under
`packages/twenty-docs/l/*` are regenerated by the existing i18n
docs-translation workflow.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21838?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-19 12:58:50 +02:00
Félix Malfait 5674f693d7 fix: prevent Create Workspace redirect from being cancelled (#21835)
## Summary

Clicking **Create Workspace** in the multi-workspace dropdown did
nothing.

The handler closed the dropdown right before redirecting:

```ts
const createWorkspace = () => {
  closeDropdown(MULTI_WORKSPACE_DROPDOWN_ID); // unmounts this component
  redirectToDefaultDomain({ ... });           // schedules window.open ~1ms later
};
```

`redirectToDefaultDomain` → `useRedirect` wraps the navigation in
`useDebouncedCallback(..., 1)`. `closeDropdown` flips the dropdown
content to `{isDropdownOpen && ...}` → `false`, unmounting
`MultiWorkspaceDropdownDefaultComponents` — the component that owns that
debounced callback. `use-debounce` drops the pending call on unmount, so
the queued `window.open` never fires. React commits the unmount before
the 1ms timer, so it loses every time. Regression from #21723, which
added the `closeDropdown` call.

## Fix

Remove the `closeDropdown` call. The redirect navigates the whole page
away, so closing the dropdown first is unnecessary — and it mirrors the
sibling "switch workspace" handler, which already redirects without
closing.

## Why not reorder, or drop the debounce?

The 1ms debounce in `useRedirect` is intentional (#9079, "sleep before
redirect"). Callers set cookie-backed state immediately before
redirecting — e.g. `redirectToDefaultDomain` clears the
`lastAuthenticateWorkspaceDomain` cookie via `useCookieStorage`.
Deferring the hard navigation by one macrotask lets that cookie write
flush before the page tears down; removing it risks dropping the write.
Reordering wouldn't help either, since the unmount still beats the
timer. So the debounce is left untouched.

## Logout is not affected

`signOut` → `clearSession` navigates with `window.location.assign(...)`
directly (synchronous, not debounced) and never calls `closeDropdown`,
so it can't hit this race.

## Testing

- Before: clicking Create Workspace → `window.open` called 0 times, page
unchanged.
- After: navigates to
`<defaultDomain>/welcome?action=create-new-workspace` and renders the
"Create your workspace" form.
- Switch-workspace and Log out both still work.


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21835?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-19 12:57:06 +02:00
Thomas Trompette 4075018834 fix(workflow): label manual trigger record output as Record/Records (#21832)
## What

The manual trigger output schema exposed the triggering record(s) under
a node labeled **Payload**. Relabels it to match what the node actually
contains:

- **Single-record** availability → **Record**
- **Bulk-records** availability → **Records**

## Why

"Payload" was a misnomer — the node holds the record(s) that triggered
the workflow. This is a display-label-only change.

## Notes for reviewers

- **No migration.** The persisted output schema key stays `payload`, so
existing variable references (`{{trigger.payload.x}}`) are unaffected.
- The front recomputes the output schema on the fly
(`computeStepOutputSchema`), so the variable picker shows the new labels
immediately, including for existing triggers.
- The backend (`workflow-schema.workspace-service`) is updated to match
for newly persisted/re-saved schemas. Previously persisted schemas keep
"Payload" until re-saved.
- Added `WORKFLOW_TRIGGER_RECORD_LABEL` /
`WORKFLOW_TRIGGER_RECORDS_LABEL` and removed the now-unused
`WORKFLOW_TRIGGER_PAYLOAD_LABEL`.
- Unit tests updated for both single and bulk cases (55/55 passing).

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21832?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-19 12:36:14 +02:00
martmull 8f7d7101ff Add application to test input in workflow (#21830)
https://github.com/twentyhq/twenty/pull/21791 follow up
2026-06-19 11:56:56 +02:00
Thomas Trompette ceecae30db fix(workflow): show fields for system objects in record-updated trigger (#21826)
## Problem

On the **Record is updated** (and **upserted**) workflow trigger,
picking a record type under the **Advanced** submenu (i.e. a *system*
object) showed an empty "Fields (Optional)" list — you couldn't select
any field to watch.

## Root cause

The trigger's field picker (`WorkflowFieldsMultiSelect`) was called with
`actionType="UPDATE_RECORD"`, which runs each field through
`shouldDisplayFormField`. For `UPDATE_RECORD` that predicate requires
`(isUIEditable ?? true)` — correct for the *Update Record action* (you
can't write to a read-only field), but wrong for a *trigger*, where
you're choosing which fields to **watch for changes** and editability is
irrelevant.

System objects define their fields with `isUIEditable: false`, so every
field failed the gate and the list rendered empty.

## Fix

Add DATABASE_EVENT trigger type to separated from action type.

The `UPDATE_RECORD` / `UPSERT_RECORD` action paths are untouched.
2026-06-19 09:48:14 +00:00
Abdullah. 8ff494e5e8 fix(security): bump tar to 7.5.16 in seed-dependencies (PAX file smuggling) (#21829)
## fix(security): bump tar to 7.5.16 in seed-dependencies (PAX file
smuggling)

Resolves [Dependabot Alert
#1500](https://github.com/twentyhq/twenty/security/dependabot/1500).

### What

`tar` (`node-tar`) `<= 7.5.15` applies a PAX size override to
intermediary GNU long-name/long-link headers, causing a tar-parser
interpretation differential (file smuggling) —
[GHSA-vmf3-w455-68vh](https://github.com/advisories/GHSA-vmf3-w455-68vh).
Patched in `7.5.16`.

This is the **seed-dependencies holdout** deferred from the main tar PR
(#21813): that lockfile + its checksum constants were also touched by
the form-data PR, so it was carved out to avoid a conflict. The
form-data PR has since merged, unblocking it.

### How

- Refreshed `tar` `7.5.13 -> 7.5.16` in `seed-dependencies/yarn.lock`
(transitive via `^7.5.4`, which already permits it) — an in-range
refresh, no override.
- Regenerated `DEFAULT_YARN_LOCK_CHECKSUM` in
`get-default-application-package-fields.util.ts` so the row-stored
checksum matches the value recomputed from file content in
`application.service.ts` (the deps-layer cache key;
`logicFunctionCreateHash` = SHA-512, first 32 hex). `package.json` is
unchanged, so `DEFAULT_PACKAGE_JSON_CHECKSUM` is unaffected.

### Verification

- No `tar <= 7.5.15` remains in the seed lockfile.
- Both checksum constants verified to match the canonical recompute of
the current seed files.
- Lint + format pass on the changed `.ts` file.
2026-06-19 11:46:28 +02:00
Félix Malfait adf6eb572b feat(billing): embed Stripe Payment Element in onboarding (#21759)
## What & why

Replaces the hosted Stripe Checkout redirect on the onboarding "Choose
your plan" step (credit-card trial) with an inline Stripe **Payment
Element**, so users never leave the app to enter card details.

## How it works

- **Frontend:** a deferred `<Elements mode="setup">` renders the Payment
Element, themed via the Appearance API. On Continue: `elements.submit()`
→ `checkoutSession` mutation creates the trialing subscription
server-side and returns its pending SetupIntent `clientSecret` →
`stripe.confirmSetup()` confirms the card (handling 3DS) → redirect to
the existing `/plan-required/payment-success`.
- **Backend:** new `BILLING_STRIPE_PUBLISHABLE_KEY` config var exposed
via `/client-config`; the card path creates the subscription with
`payment_behavior: default_incomplete` + a free trial (so Stripe
attaches a `pending_setup_intent`) and returns its client secret. The
hosted-Checkout code path is removed.
- The **no-credit-card** trial path is unchanged.
- Billing address collection is **disabled** in the Payment Element to
reduce friction; `automatic_tax` is correspondingly disabled (tax needs
an address — collect it later, e.g. at conversion / via the billing
portal).

## Required before this works
1. Set `BILLING_STRIPE_PUBLISHABLE_KEY` (`pk_…`) on the server (infra
change pending).
2. Run `nx run twenty-front:graphql:generate --configuration=metadata`
against a server exposing the updated schema (see inline note on the
hand-authored document).
3. Verify in Stripe test mode: happy path, 3DS (`4000 0025 0000 3155`),
a decline.

## Verified
typecheck (front + server), oxlint + oxfmt clean,
`client-config.service.spec` passing. Not run here: the app end-to-end /
Stripe test mode and `graphql:generate` (no server/DB in the dev
container).

I've left self-review comments inline flagging cleanup opportunities
plus a couple of architectural/tech-debt items.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_01TxCfinXq7abSrbF7aTw2cA

---
_Generated by [Claude
Code](https://claude.ai/code/session_01TxCfinXq7abSrbF7aTw2cA)_

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21759?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-06-19 11:40:55 +02:00
Harsh Singh 1e744a761d fix: reorder table columns against visible fields only (#20940) (#21084)
## Fixes #20940

### Problem
The "Move Left" / "Move Right" actions in the table column header menu
were unreliable. Clicking them often produced no visible change, or
appeared to move the column an inconsistent number of positions.

### Root cause
`useMoveRecordField` computed the swap target from **all** record fields
(`currentRecordFieldsComponentState`) sorted by position — including
hidden and non-readable columns. As a result, a move frequently swapped
positions with an *invisible* neighbor, leaving the visible column order
unchanged.

This was also inconsistent with the drag-and-drop reorder path
(`useReorderVisibleRecordFields`), which already operates only on the
visible field set, and with the dropdown's own Move enable/disable
logic, which is based on `visibleRecordFields`.

### Fix
`useMoveRecordField` now sources the neighbor from
`visibleRecordFieldsComponentSelector` — the same selector that drives
the table display and the Move menu items (`isVisible && isReadable &&
isActive`, sorted by position). The real `position` values are still
swapped, so hidden columns keep their positions and only the visible
order changes.

### Tests
Added `useMoveRecordField.test.tsx`, which seeds real object metadata
with a hidden column interleaved between visible ones (by position) and
asserts that the visible selector reorders correctly after a move. The
test fails against the previous implementation and passes with this
change.

### How to verify
1. Open any table view.
2. Open a column header menu and click "Move Right" / "Move Left".
3. The column now moves reliably by one visible position each click,
regardless of hidden columns.

---------

Co-authored-by: Harsh Singh <harsh@Harshs-MacBook-Air.local>
Co-authored-by: Charles Bochet <charles@twenty.com>
2026-06-19 11:26:52 +02:00
martmull 5a6c02a7aa Create CI workflow for internal apps (#21791)
as title

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21791?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 11:08:52 +02:00
github-actions[bot] f61ce4ce68 i18n - website translations (#21827)
Created by Github action

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21827?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-06-19 10:40:27 +02:00
Parship Chowdhury c32cb78562 fix: filtered view resetting to unfiltered list on navigation (#21080)
## Summary
- Resolves #21079 
- Object navigation links no longer force the default index view, which
had no saved filters.
- Returning to an object after “Save as new view” now opens the last
visited (filtered) view instead of the unfiltered default list.

## Test plan
1. Add a filter -> save as new view -> list is filtered
2. Navigate away and back -> filters still applied (not reset to
unfiltered)

## Screencast
### Before

https://github.com/user-attachments/assets/25326339-a3a1-4171-89cc-5149e254982e

### After

https://github.com/user-attachments/assets/b4600043-fc7a-4670-9c68-23daa6c31ec8

---------

Signed-off-by: Parship Chowdhury <parshipchowdhury@gmail.com>
Co-authored-by: Charles Bochet <charles@twenty.com>
2026-06-19 10:40:14 +02:00
Etienne 99f4084a05 fix(front): pass auth headers to GraphiQL fetcher for introspection (#21821)
## Summary
https://discord.com/channels/1130383047699738754/1517429151328374896
- The GraphiQL playground's automatic introspection query was firing
without an `Authorization` header, causing "GraphQL introspection has
been disabled" errors in production
- `defaultHeaders` only pre-fills the GraphiQL headers editor UI — it
does not inject headers into actual fetch requests
- Added `headers` to the `createGraphiQLFetcher` config so all requests
(including auto-introspection on load) are authenticated


## Test plan

- [x] Open Settings > API & Webhooks, select Core schema + GraphQL,
click Launch
- [x] Verify the introspection query in the network tab now includes the
`Authorization: Bearer ...` header
- [x] Confirm the schema explorer loads successfully in a production
environment (or with `NODE_ENV=production`)

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21821?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-19 08:25:20 +00:00
Félix Malfait a6aa03b77b fix(front): emit error instead of completing empty on failed token renewal (#21822)
handleTokenRenewal returned rxjs EMPTY when there was no token pair or
token renewal failed, so the operation's link chain completed without
emitting a value. Apollo Client v4 treats that as an invariant violation
("The link chain completed without emitting a value"), which surfaces as
an uncaught error during auth/token transitions (e.g. the
workspace-token swap in onboarding). Re-emit the original error so the
operation rejects cleanly instead.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21822?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-19 10:24:31 +02:00
Abdullah. 569d887d1e [Website] Cut over to the rebuilt site (#21825)
Renaming the package so any further PRs directed to the website are
targeted to the reworked code instead of diverging. Once merged, I will
start preparing this for deployment to dev to test before releasing to
prod. Any improvements will also be applied to this package.

I avoided making significant changes to API routes so nothing breaks,
but will test it thoroughly today to confirm. That said, everything is
ported - double checked.

Big diff PR, impossible to review, but last one! No more rebuilds.
2026-06-19 10:22:46 +02:00
Félix Malfait 40c9a11f43 feat(onboarding): always show the Create Profile step (#21823)
The Create Profile step was skipped whenever the user already had a
first or last name (e.g. provided during sign-up or via SSO), because
the create-profile-pending flag was only set when both were empty.
Always set it so the step is presented during onboarding and the user
can review/confirm their profile; submitting it still clears the flag
and advances.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21823?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-19 09:50:29 +02:00
nitin a7afff7465 fix call recording bot automatic leave activate after bug (#21820)
hardcoded activate_after -- 0 on everyone_left_timeout was getting
rejected by recall with a 400 (activate_after can't be 0). pulled it
into a named const, set to 1.

its weird -- recall doc says default is 0 -- but its erroring out for us
if we send zero. looks like the 0 default only applies when you leave
the field out, not when you pass it explicitly(did not verify). keeping
it as lowest possible value (1) for now.

what the property does -- after the meet starts, how long before the bot
starts watching the "everyone left" timeout. 1s is basically immediate,
which is what we wanted with 0 anyway.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21820?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-19 13:07:21 +05:30
Rashad Karanouh 0e97e1a908 feat(partners): add partner-application-triage and partner-meeting-recap skills (#21819)
## What

Adds two Twenty partner-pipeline skills to
`twenty-partners/src/skills/`, plus a patch version bump.

### `twenty-partner-application-triage`
Ranks the partner-application backlog by net-new value and surfaces a
short chase-list of high-value applicants who haven't booked a call.
Read-only against the live partners workspace. Ships `rank.py` as its
scoring helper.

### `twenty-partner-meeting-recap`
After partner calls, pulls Fireflies meetings, matches each to an
existing Partner by attendee email/domain, writes a recap
(transcript-first, Fireflies summary as fallback), and injects it as a
Note linked to the partner via `NoteTarget`. Skips leads/discovery calls
(no Partner match) and meetings whose content isn't ready yet. Optional
`--prune` deletes the Fireflies recording once its recap is safely in
the CRM (confirmed first).

## Version
`twenty-partners` 0.5.4 → **0.5.5** (patch: additive skill docs, no app
behaviour change).

## Notes
- Both skills read credentials from `~/.twenty/credentials.env`; no
secrets committed.
- All GraphQL queries/mutations are the proven ones used against the
live workspace.
2026-06-19 09:35:47 +02:00
Abdullah. ba7b435885 fix(security): bump webpack-dev-server resolution to 5.2.5 (HMR WS interception) (#21818)
## fix(security): bump webpack-dev-server resolution to 5.2.5 (HMR WS
interception)

Resolves [Dependabot Alert
#1514](https://github.com/twentyhq/twenty/security/dependabot/1514).

### What

`webpack-dev-server` `< 5.2.5` is affected by
[GHSA-mx8g-39q3-5c79](https://github.com/advisories/GHSA-mx8g-39q3-5c79)
(**Moderate**) — HMR WebSocket interception via permissive user proxies.
Patched in `5.2.5`.

### How

`webpack-dev-server` is already force-resolved via a scoped
`resolutions` entry: its sole consumer `@electron-forge/plugin-webpack`
(latest 7.11.2 / 8.x alphas) still declares `webpack-dev-server ^4`, so
the resolution evicts the vulnerable 4.x line up to 5.x. The
newly-disclosed CVE affects the pinned `5.2.4`, so this **bumps the
existing scoped resolution `5.2.4 -> 5.2.5`** and updates its
`//resolutions` doc line — extending an already-documented, load-bearing
entry rather than adding a new one.

**Still-required check:** `@electron-forge/plugin-webpack` still
declares `^4`, so the resolution remains necessary (removing it would
regress to vulnerable 4.x).

### Verification

- `webpack-dev-server` resolves to a single `5.2.5` bucket; no `< 5.2.5`
remains.
- Not imported in our source (electron-forge build tooling only).
- `yarn install --immutable` passes.

Co-authored-by: Charles Bochet <charles@twenty.com>
2026-06-19 07:17:23 +00:00
Raphaël Bosi 3675f264f1 Infer record pickers for record-typed logic function workflow inputs (#21494)
## Context

Logic functions can declare workflow inputs typed as records or arrays
of records (e.g. the People Data Labs enrichment functions), but the
workflow builder rendered those as a plain text input with a variable
picker, which is not usable.

## What this does

- Adds an `objectUniversalIdentifier` link on input schema properties,
so a record-typed input is tied to a workspace object.
- The SDK build infers it from a
`TwentyRecord<'objectUniversalIdentifier'>` marker type in the handler
signature, reading the object's universal identifier straight from the
source; explicit input schemas can still set the field directly.
- The workflow builder renders these inputs as a single record picker or
a record multi-select with the variable picker on the right. Selected
records are stored as record ids; `TwentyRecord<UID>` is a branded
`string`, so the handler signature reflects that it receives ids (a
bound variable resolves to whatever the referenced step produced).
- The multi-select collapses overflowing chips into a `+N` badge
(reusing `ExpandableList`) and its variable picker offers both record
objects and fields.
- Updates the People Data Labs enrichment inputs as the reference
implementation.

<img width="802" height="824" alt="CleanShot 2026-06-12 at 16 54 10@2x"
src="https://github.com/user-attachments/assets/a0896d74-0aab-49bd-a173-14c578a2e533"
/>


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21494?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-19 09:10:01 +02:00
Abdullah. 9eb90e72f9 [Website] Add internal enterprise key reissue endpoint. (#21807)
Port of #21660 into the redone's enterprise backend.

- Extract getLicenseeFromStripeCustomer into the platform/enterprise
barrel, replacing the inline licensee-from-Stripe-customer block in the
activate route; cover its branches with a unit test.
- Add POST /api/enterprise/reissue: a support endpoint that re-signs an
enterprise key from a subscriptionId, guarded by a timing-safe compare
against ENTERPRISE_ADMIN_API_SECRET. Adapted to the redone — barrel
imports, node:crypto named imports, and the sibling routes' 503
configured-check guard — while keeping the original's generic
non-leaking 500.
- Document ENTERPRISE_ADMIN_API_SECRET in .env.example.
2026-06-19 08:56:00 +02:00
Abdullah. 2169e15162 fix(security): patch yaml across both major lines (stack overflow) (#21817)
## fix(security): patch yaml across both major lines (stack overflow)

Resolves [Dependabot Alert
#734](https://github.com/twentyhq/twenty/security/dependabot/734) and
[#697](https://github.com/twentyhq/twenty/security/dependabot/697).

### What

`yaml` is affected by
[GHSA-48c2-rrv3-qjmp](https://github.com/advisories/GHSA-48c2-rrv3-qjmp)
(**Moderate**) — stack overflow via deeply nested YAML collections —
across two major lines:
- **2.x** (`>= 2.0.0, < 2.8.3`, patched `2.8.3`) — alert #734 (runtime).
- **1.x** (`>= 1.0.0, < 1.10.3`, patched `1.10.3`) — alert #697 (dev,
auto-dismissed).

### How

Both vulnerable copies are transitive, and `yaml` is not imported in our
source:
- **2.x:** the `2.8.1` bucket (`^2.0.0` / `^2.4.5`, via `vfile-matter` /
`@mintlify/openapi-parser`) is deduped into the safe `2.9.0` already in
the tree.
- **1.x:** the `1.10.2` bucket (`^1.10.0`, via `cosmiconfig@^7.0.0`) is
refreshed to `1.10.3`.

Both move within ranges the parents already declared — no `resolutions`
override.

### Verification

- No `yaml` `1.x < 1.10.3` or `2.x < 2.8.3` resolution remains.
- `yaml` is not imported in our source (transitive only).
- Lockfile-only change; `yarn install --immutable` passes.
2026-06-19 08:50:17 +02:00
Abdullah. ebdf77075a fix(security): dedupe uuid to 11.1.1 (missing buffer bounds check) (#21815)
## fix(security): dedupe uuid to 11.1.1 (missing buffer bounds check)

Resolves [Dependabot Alert
#1289](https://github.com/twentyhq/twenty/security/dependabot/1289).

### What

`uuid` `< 11.1.1` is affected by
[GHSA-w5hq-g745-h8pq](https://github.com/advisories/GHSA-w5hq-g745-h8pq)
(**Moderate**) — a missing buffer bounds check in `v3`/`v5`/`v6` when a
`buf` argument is provided. Patched in `11.1.1`.

### How

The only vulnerable copy was a transitive `11.1.0` bucket pinned via
`^11.1.0` by `typeorm@0.3.26` and `@recallai/desktop-sdk` — both ranges
already permit `11.1.1`, and a `11.1.1` bucket already existed (our own
packages resolve there). `yarn dedupe uuid` collapses the `^11.1.0`
descriptor into the existing `11.1.1` resolution, removing the
vulnerable copy. No `resolutions` override; the unrelated `13.x` bucket
is left untouched.

### Verification

- No `uuid < 11.1.1` resolution remains.
- Our packages already resolved to `11.1.1`, so compiled output is
unaffected (the change only lifts the transitive `typeorm`/`@recallai`
copy).
- Lockfile-only change (net −9 lines); `yarn install --immutable`
passes.
2026-06-19 08:49:46 +02:00
Abdullah. 6bbd070172 fix(security): bump markdown-it to 14.2.0 (smartquotes ReDoS) (#21814)
## fix(security): bump markdown-it to 14.2.0 (smartquotes ReDoS)

Resolves [Dependabot Alert
#1511](https://github.com/twentyhq/twenty/security/dependabot/1511).

### What

`markdown-it` `<= 14.1.1` is affected by
[GHSA-6v5v-wf23-fmfq](https://github.com/advisories/GHSA-6v5v-wf23-fmfq)
(**Moderate**) — quadratic-complexity DoS in the smartquotes rule via
`replaceAt` string operations. Patched in `14.2.0`.

### How

`markdown-it` is pulled transitively by `@graphiql/react` (`^14.1.0`)
and `prosemirror-markdown` (`^14.0.0`), both of which already permit
`14.2.0`. This refreshes the lockfile resolution `14.1.1 -> 14.2.0`
within the existing range — no `resolutions` override. It is **not**
imported in our source, and the fix is internal to the smartquotes rule
(no public API change).

### Verification

- `markdown-it` resolves to `14.2.0`; no `<= 14.1.1` copy remains.
- Diff limited to `markdown-it` + its own `linkify-it` dep bump.
- Lockfile-only change; `yarn install --immutable` passes.
2026-06-19 08:49:27 +02:00
Abdullah. bebe03e453 fix(security): bump tar to 7.5.16 across lockfiles (PAX file smuggling) (#21813)
## fix(security): bump tar to 7.5.16 across lockfiles (PAX file
smuggling)

Resolves [#1472 1474 1476 1479 1481 1483 1485 1487 1489 1491 1493 1496
1498 1505](https://github.com/twentyhq/twenty/security/dependabot/1472
1474 1476 1479 1481 1483 1485 1487 1489 1491 1493 1496 1498 1505).

### What

`tar` (`node-tar`) `<= 7.5.15` applies a PAX size override to
intermediary GNU long-name/long-link headers, causing a tar-parser
interpretation differential (file smuggling). Patched in `7.5.16`.

### Why these alerts

The advisory is scanned across many independent Yarn projects, so it
surfaced as one alert per lockfile: the root `yarn.lock` plus 13
`packages/twenty-apps/**` lockfiles (each pulls `tar` transitively).

### How

- Refreshed `tar` to `7.5.16` in the root and all 13 app lockfiles —
they hold `tar` via `^7.5.x` ranges that already permit it, so this is
an in-range lockfile refresh (no override) via `yarn up -R tar`.
- The root additionally had `tar@7.5.15` exact-pinned by
`@mintlify/previewing`, which has **no upstream fix** (latest `4.0.1163`
still pins `7.5.15`). Added a scoped resolution
`@mintlify/previewing/tar -> ^7.5.16`, **extending the existing scoped
tar resolutions** already used for `@electron/rebuild` and
`@electron/node-gyp`.

### Not included

`seed-dependencies/yarn.lock` (alert #1500) is intentionally excluded:
that lockfile and its checksum constants are already modified by the
open form-data PR, so its `tar` bump will follow separately to avoid a
conflict.

### Verification

- No `tar <= 7.5.15` remains across the root or any app lockfile.
- `yarn install --immutable` passes.
2026-06-19 08:49:07 +02:00
Abdullah. e2df785aec fix(security): bump http-proxy-middleware to 3.0.7 (multipart field injection) (#21812)
## fix(security): bump http-proxy-middleware to 3.0.7 (multipart field
injection)

Resolves [Dependabot Alert
#1517](https://github.com/twentyhq/twenty/security/dependabot/1517).

### What

`http-proxy-middleware` `3.0.4 – 3.0.6` is affected by
[GHSA-gcq2-9pq2-cxqm](https://github.com/advisories/GHSA-gcq2-9pq2-cxqm)
(**High**) — multipart/form-data field injection via unescaped CRLF in
`fixRequestBody`. Patched in `3.0.7` for the 3.x line.

### How

`http-proxy-middleware` is pulled transitively by
`@nx/module-federation` and `@nx/react` via `^3.0.5`, which already
permits `3.0.7`. This refreshes the stale lockfile resolution `3.0.5 →
3.0.7` within the existing range — no `resolutions` override needed. The
separate `2.0.9` bucket (from `webpack-dev-server`) is outside the
advisory's `>= 3.0.4` range and is left unchanged.

### Verification

- No `http-proxy-middleware` copy in the vulnerable `3.0.4 – 3.0.6`
range remains; the 3.x bucket resolves to `3.0.7`.
- Diff is limited to the resolved version + checksum.
- Lockfile-only change; `yarn install --immutable` passes.
2026-06-19 08:48:47 +02:00
Abdullah. 2eef2f12be fix(security): bump protobufjs to 7.6.4 (DoS + property shadowing) (#21811)
## fix(security): bump protobufjs to 7.6.4 (DoS + property shadowing)

Resolves [Dependabot Alert
#1508](https://github.com/twentyhq/twenty/security/dependabot/1508) and
[#1507](https://github.com/twentyhq/twenty/security/dependabot/1507).

### What

`protobufjs` in the 7.x line is affected by two advisories:
-
[GHSA-wcpc-wj8m-hjx6](https://github.com/advisories/GHSA-wcpc-wj8m-hjx6)
(**High**,
[#1508](https://github.com/twentyhq/twenty/security/dependabot/1508)) —
DoS through unbounded `Any` expansion during JSON conversion. Patched in
`7.6.1`.
-
[GHSA-f38q-mgvj-vph7](https://github.com/advisories/GHSA-f38q-mgvj-vph7)
(**Moderate**,
[#1507](https://github.com/twentyhq/twenty/security/dependabot/1507)) —
schema-derived names can shadow runtime-significant properties. Patched
in `7.6.3`.

### How

`protobufjs` is pulled transitively via `^7.3.0`, which already permits
the patched releases. This refreshes the stale lockfile resolution
`7.6.0 → 7.6.4` (the latest `7.x`; `>= 7.6.3` covers both advisories)
within the existing range — no `resolutions` override needed. The `8.x`
ranges in these advisories do not apply.

### Verification

- `protobufjs` resolves to a single `7.6.4` bucket (`>= 7.6.1` and `>=
7.6.3`), clearing both alerts.
- Diff is limited to the protobufjs family (`protobufjs` + its
`@protobufjs/*` utility deps).
- Lockfile-only change; `yarn install --immutable` passes.
2026-06-19 08:48:24 +02:00
Abdullah. da49ed81e4 fix(security): bump piscina to 4.9.3 (prototype pollution → RCE) (#21810)
## fix(security): bump piscina to 4.9.3 (prototype pollution → RCE)

Resolves [Dependabot Alert
#1515](https://github.com/twentyhq/twenty/security/dependabot/1515).

### What

`piscina` `<= 4.9.2` is affected by
[GHSA-x9g3-xrwr-cwfg](https://github.com/advisories/GHSA-x9g3-xrwr-cwfg)
/ CVE-2026-55388 — a **prototype-pollution gadget enabling RCE via
inherited `options.filename`** (High). For the 4.x line, the first
patched version is `4.9.3`.

### How

`piscina` is pulled transitively by `@swc/cli@0.8.1` via `^4.3.1`, which
already permits `4.9.3`. This refreshes the stale lockfile resolution
`4.9.2 → 4.9.3` within the existing range — no `resolutions` override
needed.

### Verification

- `piscina` resolves to a single `4.9.3` bucket; no `<= 4.9.2` copy
remains.
- Diff is limited to piscina's resolved version + checksum (its
dependency set is unchanged).
- Lockfile-only change; `yarn install --immutable` passes.
2026-06-19 08:47:49 +02:00
Abdullah. d205c72fa2 fix(security): remove vulnerable lodash 4.17.23 (code injection + prototype pollution) (#21809)
## fix(security): remove vulnerable lodash 4.17.23 (code injection +
prototype pollution)

Resolves [Dependabot Alert
#824](https://github.com/twentyhq/twenty/security/dependabot/824) and
[#823](https://github.com/twentyhq/twenty/security/dependabot/823).

### What

`lodash` `<= 4.17.23` is affected by:
- **Code injection via `_.template`**
([#824](https://github.com/twentyhq/twenty/security/dependabot/824),
High)
- **Prototype pollution via `_.unset`/`_.omit`**
([#823](https://github.com/twentyhq/twenty/security/dependabot/823),
Medium)

Both are patched in `4.18.0`. The repo already resolved lodash to
`4.18.1` everywhere **except** one copy held at `4.17.23` by
`@stoplight/spectral-functions@1.10.1`, whose `~4.17.21` range capped
lodash below `4.18.0`.

### How

Instead of a standing `resolutions` override, this bumps the parent that
imposed the cap: **`@stoplight/spectral-functions` 1.10.1 → 1.10.3**
(pulled transitively via `@asyncapi/parser` ← `@mintlify/common`,
accepted through `^1.7.2`). 1.10.3 widened its lodash dependency to
`^4.18.1`, so the capped bucket collapses into the existing `4.18.1`
resolution and the vulnerable copy is removed — leaving the dependency
graph honest with no lingering override.

### Also

Refreshes `@types/lodash` to the latest **4.17.24**: bumps the
`twenty-client-sdk` pin `^4.17.15 → ^4.17.24` and dedupes the stale
transitive `*` bucket (4.17.15) into a single `4.17.24` resolution.
Type-stub only.

### Verification

- The only real `lodash` resolution is now `4.18.1` (remaining `4.17.x`
entries are `@types/lodash` type stubs, not the library);
`@types/lodash` resolves to a single `4.17.24` bucket.
- Lockfile-only dependency change; `yarn install --immutable` passes;
`twenty-client-sdk` typecheck passes.
2026-06-19 08:47:11 +02:00
Abdullah. 26b4d6caed fix(security): bump form-data to 4.0.6 (CRLF injection) (#21808)
Resolves [Dependabot Alert
#1473](https://github.com/twentyhq/twenty/security/dependabot/1473),
[#1475](https://github.com/twentyhq/twenty/security/dependabot/1475),
[#1477](https://github.com/twentyhq/twenty/security/dependabot/1477),
[#1478](https://github.com/twentyhq/twenty/security/dependabot/1478),
[#1480](https://github.com/twentyhq/twenty/security/dependabot/1480),
[#1482](https://github.com/twentyhq/twenty/security/dependabot/1482),
[#1484](https://github.com/twentyhq/twenty/security/dependabot/1484),
[#1486](https://github.com/twentyhq/twenty/security/dependabot/1486),
[#1488](https://github.com/twentyhq/twenty/security/dependabot/1488),
[#1490](https://github.com/twentyhq/twenty/security/dependabot/1490),
[#1492](https://github.com/twentyhq/twenty/security/dependabot/1492),
[#1494](https://github.com/twentyhq/twenty/security/dependabot/1494),
[#1495](https://github.com/twentyhq/twenty/security/dependabot/1495),
[#1497](https://github.com/twentyhq/twenty/security/dependabot/1497),
[#1499](https://github.com/twentyhq/twenty/security/dependabot/1499),
[#1501](https://github.com/twentyhq/twenty/security/dependabot/1501) and
[#1506](https://github.com/twentyhq/twenty/security/dependabot/1506).
2026-06-19 08:46:32 +02:00
Yash Singh 505094650f fix(twenty-shared): derive short-number suffix from the rounded value (#21591)
`formatToShortNumber`
(`packages/twenty-shared/src/utils/format/formatToShortNumber.ts`)
picked the unit suffix from the **raw** value but printed the
**rounded** figure, so `999999` rendered as `"1000k"` instead of `"1m"`,
and `999999999` as `"1000m"` instead of `"1b"`. This affects
number/currency cells, column-footer aggregates, and dashboard charts.

The fix replaces the hard-coded band branches with a promotion loop that
derives the suffix from the rounded display value, so the suffix and
figure always agree at boundaries. Adds boundary, just-below-boundary,
and negative-boundary tests.

Red-green proven: the two new boundary tests fail on the original source
(`expected "1m" but got "1000k"`); the 11 pre-existing tests still pass;
all 13 pass with the fix. Verified with a standalone strict `tsc` (0
errors) and oxlint on both changed files.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21591?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-19 08:29:47 +02:00
neo773 814b43ca41 feat(server): derive email/calendar timelines from object relations (#21684)
Simplifies our existing implementation that uses three different GraphQL
endpoints to just one `getTimelineEventsFrom{Person, Company,
Opportunity}Id` to `getTimelineCalendarEventsFromObjectRecord`


/closes https://github.com/twentyhq/twenty/issues/19676

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21684?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
2026-06-19 02:22:27 +02:00
Brendan Erofeev 1b3d933900 fix(workflow): hide empty option for non-nullable select fields (#21075)
## Summary

Closes #20139.

In a workflow **Create / Update / Upsert Record** action, the field
input for a `SELECT` field always offered a **"No value"** option.
Selecting it clears the field — even when that field is **not nullable**
(for example an opportunity `Stage`). The maintainer asked in the issue
to *"not allow to put a 'No stage' if the field is not nullable"*.

### Root cause

`FormSelectFieldInput` unconditionally builds a `defaultEmptyOption` and
passes it as the `<Select>`'s `emptyOption`, regardless of the field's
nullability:

```tsx
const defaultEmptyOption = {
  label: label ? t`No ${label}` : t`No value`,
  value: '',
  icon: IconCircleOff,
};
// ...
<Select ... emptyOption={defaultEmptyOption} />
```

The nullability is already available on the field metadata
(`FieldSelectMetadata.isNullable`), and the regular (non-workflow)
record input already gates its clear affordance on it in
`SelectFieldInput.tsx` (`fieldDefinition.metadata.isNullable &&
canSelectEmpty ? handleClearField : undefined`). The workflow form input
just wasn't using it.

### Fix

- Add an optional `isNullable` prop to `FormSelectFieldInput` and only
build the empty option when the field is nullable (`isNullable !==
false`). `emptyOption` is already optional on `<Select>`, so omitting it
cleanly removes the choice.
- Pass `isNullable={field.metadata?.isNullable}` from `FormFieldInput`'s
`isFieldSelect` branch.

The prop is optional and defaults to the previous behaviour, so the
other `FormSelectFieldInput` call sites (country, calling code,
currency, cron, etc.) are unaffected.

Scope is intentionally limited to single `SELECT`:
`FieldMultiSelectMetadata` has no `isNullable` field and the
multi-select input has no equivalent empty option, so it is left
untouched.

## 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] Added a `NonNullable` Storybook interaction test asserting the "No
value" option is not rendered when `isNullable={false}`
- [ ] Manual: in a workflow Update Record step on Opportunity, the
**Stage** field no longer shows a "No value" option; a nullable select
still does

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
2026-06-19 00:09:57 +00:00
neo773 616d58bc7e messaging: gmail folder backfill (#21753)
demo


https://github.com/user-attachments/assets/a157cee1-a8fa-4050-af1b-c31a83fb75da

/closes #17095


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21753?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-19 01:59:35 +02:00
Charles Bochet d88eb6c16b fix(front): unwrap react-qr-code default export to fix 2FA crash on prod (#21804)
## Problem

2FA is broken on prod (critical, reported on Discord and in #21649):
instead of the 2FA setup screen, users hit the app-wide error page —
both at login-time provisioning and on **Settings > Profile > Two-Factor
Authentication**. The 2FA screen flashes briefly (loader) and then the
error page replaces it.

Fixes #21649.

## Root cause

The crash is a React render error — *"Element type is invalid: got
object"* — at the exact moment the QR code renders (when `qrCode` flips
from `null` to a value).

`react-qr-code` is a CommonJS package (`__esModule: true`,
`exports.default = QRCode`). The recent **Vite 8 / rolldown** bundler
migration changed how its CommonJS default export is resolved into an
ESM import: `import QRCode from 'react-qr-code'` now resolves to the
**module namespace object** `{ default, QRCode }` instead of the
component itself. Rendering that object as a React element throws and
trips the error boundary.

The import code never changed — only the bundler's module resolution
did, which is why this regressed without any 2FA code change. Reproduced
the resolution with an esbuild/rolldown-style bundle: the default import
comes back as `{ default, QRCode }`, with the real `forwardRef`
component sitting on `.default`.

## Fix

Add a small `resolveCjsModuleDefaultExport` helper that returns the
default export when a CommonJS import is handed back as a namespace
object, and a no-op otherwise. Use it in the two 2FA QR render paths:

- `SignInUpTwoFactorAuthenticationProvision.tsx` (login-time
provisioning)
- `SettingsTwoFactorAuthenticationMethod.tsx` (profile settings)

## Verification

- `npx nx lint:diff-with-main twenty-front`  (lint + format)
- `npx nx typecheck twenty-front`  (CI)
2026-06-19 01:58:08 +02:00
Thomas des Francs de9af38a67 Fix view type label casing (#21772)
## Summary

Fix the view type label shown in the object Options menu by introducing
a shared `ViewType` label map instead of formatting raw enum values at
each call site.

I chose to fix the root cause instead of only patching the symptom: the
menu was displaying the generated enum value `TABLE`, and `capitalize()`
only uppercased the first character without lowercasing the rest. The
new mapping gives each view type an explicit translated UI label, so the
parent Options menu, the Layout submenu, the view picker, and
application content rows all use the same casing source.

## Before / After

| Before | After |
| --- | --- |
| The Options menu showed `TABLE` in uppercase. | The Options menu now
shows `Table`, and the Layout submenu still shows `Table`, `Calendar`,
and `Kanban`. |
| ![Before: Options menu displayed
TABLE](https://raw.githubusercontent.com/twentyhq/twenty/7b1752e53cfa63868eb8ea46bd74fbadf89f3091/.github/pr-assets/view-type-labels/before.png)
| ![After: Options menu displays
Table](https://raw.githubusercontent.com/twentyhq/twenty/7b1752e53cfa63868eb8ea46bd74fbadf89f3091/.github/pr-assets/view-type-labels/after.jpg)
|

## Tests

- `git diff --check`
- Browser smoke test on `http://apple.localhost:3001/objects/companies`
  - default view Options menu still opens
- custom view Options menu shows `Layout` contextual text as `Table`,
not `TABLE`
  - Layout submenu still shows `Table`, `Calendar`, and `Kanban`
  - no browser console errors

Not run: package lint/test commands, because this checkout has no
`node_modules` installed.
2026-06-18 23:37:28 +00:00
Priyanshu Bartwal 40e386bfb4 Fix(record table): Drag select position (#21579)
Fixes: #21578



https://github.com/user-attachments/assets/8a1f3aa1-b436-4ce0-a9ec-c49ab61e214c



<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21579?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->

---------

Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
2026-06-19 01:36:27 +02:00
neo773 c07dd53a48 Scope empty fixture workspaces to upgrade integration tests (#21778)
The dev seeder activated Empty3/Empty4 workspaces without creating their
DB schema, so every workspace-iterating job (e.g. the workflow cron
trigger) logged 'relation does not exist' for those schemas on each run.


```
[1] query failed: SELECT * FROM workspace_4rdlooovb6mo66rdmgupv06zi."workflowAutomatedTrigger" WHERE type = 'CRON'
[1] error: error: relation "workspace_4rdlooovb6mo66rdmgupv06zi.workflowAutomatedTrigger" does not exist
[1] [Nest] 51868  - 18/06/2026, 5:07:04 pm   ERROR [WorkflowCronTriggerCronJob] Error processing workspace 506915ec-21ca-431b-a04a-257eb216865e: QueryFailedError: relation "workspace_4rdlooovb6mo66rdmgupv06zi.workflowAutomatedTrigger" does not exist
[1] Exception Captured
```

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21778?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-19 01:21:50 +02:00
Weiko 9f30915f6f fix(metadata): remove deprecated isCustom from Objects and Fields (#21799)
## Context

Follow-up to #21228, which deprecated `isCustom` on object/field
metadata but kept it exposed because the frontend still relied on it.
This removes it from the GraphQL API and the frontend entirely.

## Implementation

### Server
- Remove `isCustom` `@Field` from the `Object`, `Field`, and
`MinimalObjectMetadata` GraphQL types
- Remove the `isCustom` `@ResolveField` resolvers and the
`isCustomLoader` dataloader (+ payload/interface)
- Remove `isCustom` as an internal `@HideField()` on the Object/Field
DTOs used by the i18n standard-override gate > Use an explicit
isStandard instead (which is the correct gating)

### Frontend
- Add `getIsMetadataItemCustom` helper + `useGetIsMetadataItemCustom`
hook: an item is custom when `applicationId ===
currentWorkspace.workspaceCustomApplication.id`
- Migrate all consumers off `objectMetadataItem.isCustom` /
`fieldMetadataItem.isCustom`; `isRecordFieldReadOnly` now takes a
precomputed `isFieldCustom`
- Drop `isCustom` from the metadata fragment/mutations/minimal query, FE
types, zod schemas, and mock generators; regenerate GraphQL types

## Notes
- Breaking change on the (already-deprecated) `Object.isCustom` /
`Field.isCustom` GraphQL fields and the `isCustom` filter
- FE semantic is "belongs to the workspace custom app" (third-party-app
objects/fields are treated as non-custom)
- `isCustom` on IndexMetadata / View / Skill / Agent is a separate
column and is untouched
- Breaking changes on REST metadata API
2026-06-19 01:19:49 +02:00
Félix Malfait 7b5ee8a7bc feat(server): report enterprise instance metadata on license validation (#21793)
## What

Enriches the **enterprise-only** license-validation channel
(`/validate`, `/seats`) with best-effort instance metadata so the
licensing backend can later reconcile seats and surface signs of license
abuse (e.g. one subscription on many `serverId`s, a `serverId` on many
URLs, dev-mode-in-prod).

Reported alongside the existing `enterpriseKey` (and `seatCount` on
`/seats`), under a new `instanceMetadata` object:

| Field | Purpose |
|---|---|
| `serverId`, `serverUrl` | instance identity — sharing / clone signals
|
| `workspaceCount`, `activeUserWorkspaceCount`, `distinctUserCount` |
seat reconciliation / overage |
| `appVersion`, `nodeEnv`, `telemetryEnabled` | fleet/support;
dev-mode-in-prod signal |
| `adminContactEmail` | **single** administrative contact (oldest active
user) for license administration — explicitly *not* an abuse signal |
| `sentAt` | timestamp |

No CRM data, record contents, or member PII beyond the one admin contact
are sent.

## Why it's safe for existing instances

- **Enterprise-only.** Gathering runs only after the `ENTERPRISE_KEY`
checks, so free/community instances make no extra queries and send
nothing — unchanged behavior.
- **Never blocks a refresh.** Each lookup is isolated (`safeCount` /
try-catch); any failure degrades to `null` and the license refresh /
seat report proceeds.
- **Purely additive.** `enterpriseKey` and `seatCount` are preserved;
the `/validate` and `/seats` handlers ignore unknown fields, so this can
ship ahead of any backend consumer.
- **No schema or token-verification changes** → no migration, existing
validity tokens keep validating.

## Verification

- `nx test twenty-server` — full unit suite green (5829 passed),
including the updated `enterprise-plan.service.spec` with a new
metadata-payload test
- `nx typecheck twenty-server` — pass
- `oxlint` + `oxfmt --check` on changed files — clean

## Deliberately out of scope (follow-ups)

- **Server-side correlation/detection** and **short-TTL + instance-bound
validity tokens** live on the signing/billing side (`twenty-website`)
and need a coordinated rollout (enforcing token binding now would break
already-issued tokens).
- **`adminContactEmail`** is PII on a contractual enterprise channel —
the enterprise terms should disclose it before rollout.
- The dev-key / build-provenance hardening discussed separately is
**not** part of this PR.

Opening as **draft** for review of the field set and the cross-repo
rollout plan before wiring a consumer.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_0114DV9tctTjVo8eggBGgtKc

---
_Generated by [Claude
Code](https://claude.ai/code/session_0114DV9tctTjVo8eggBGgtKc)_

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21793?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
2026-06-18 19:16:57 +00:00
Abdullah. 5053c63f02 [Website] Straighten the footer 20 model (#21797)
Reduce the footer model's resting yaw (rotationY 0.457 -> 0.363) so the
"20" sits level instead of leaning.

<img width="1512" height="578" alt="image"
src="https://github.com/user-attachments/assets/6f3151b1-b1c4-4d6e-9cec-6881e4e33845"
/>
2026-06-18 20:43:56 +02:00
Abdullah. b14da2f9e8 [Website] Port partner application form rework (required fields, skills, fail-fast) (#21802)
Ports twenty-website PR #21710 (Rashad) into `twenty-website-redone`.
The old site's partner application form was reworked last week —
required fields, a skills rethink, and fail-fast validation — after the
redone had already ported the form, so the redone was running the
pre-rework behavior. This brings it to parity.
  
Re-derived into the redone's own conventions rather than copied: it
reuses the redone's `STEP_REQUIRED_FIELDS`/`STEP_FORMAT_CHECKS`
validator, keeps one-export-per-file, and injects the new `searchPool`
as an opt-in prop.
2026-06-18 20:43:24 +02:00