Commit Graph

13926 Commits

Author SHA1 Message Date
Raphaël Bosi 3c48e27b2e Fix stuck onboarding route on failed chunk preload (#23359)
Fixes [Sentry 7604159654](https://sentry.io/issues/7604159654/)
(v2.20.0, Mobile Safari). The onboarding router preloads 7 lazy chunks
on entry; Vite's CSS preload for SyncEmails rejected and three defects
compounded:

- `void SomePage.preload()` discarded the promise, so it became an
unhandled rejection and the user got a raw `Unable to preload CSS for
/assets/...css` snackbar.
- `lazyWithPreload` cached the *rejected* promise and rendered via
`throw preload()`. React pings on the rejection, re-renders, the
component throws the same settled rejected thenable, the ping listener
de-dupes, and the route hangs on its loader forever.
- `checkIfItsAViteStaleChunkLazyLoadingError` only matched Chrome's
message, so `AppErrorBoundary`'s reload recovery never fired for the
CSS-preload or Safari variants.

`lazyWithPreload` now records the failure in state instead of
rethrowing, so the thenable thrown into Suspense always fulfills,
`preload()` returns void and can never reject, and the render path
throws the real `Error` to the boundary, which reloads.

Two things worth knowing for review: `React.lazy` is not a substitute
here (its initializer has no synchronous fast path, so it suspends even
when the module is already loaded, reintroducing the loader flash #22392
removed), and the failure is deliberately sticky because Vite marks the
dep `seen` before attempting it, so an in-document retry loads the JS
without its CSS and silently renders an unstyled page.


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23359?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-07-27 14:47:58 +00:00
Thomas Trompette 79bf20515d feat(workflow): mirror version delete/restore/destroy to core transactionally (#23356)
Next step in the workflowVersion -> core soft-ref migration. The
transactional mirror (#23243) made **content** writes (create/update)
drift-free. This does the same for the **lifecycle** events (delete /
restore / destroy), which were still handled only by the async
best-effort listener. It's the prerequisite for dropping that listener.

## What changed
`delete` and `restore` already soft-delete / restore the workflow's
versions inside `handleWorkflowSubEntities` (twenty doesn't cascade
soft-deletes, so it does each sub-entity explicitly). So the core
delete/recreate just sits next to the existing version write:

- **delete** — after `workflowVersionRepository.softDelete({ workflowId
})`, `deleteCoreVersionsByWorkflowIds` removes the
`core.workflowVersion` rows (`workflowId IN (...)`).
(`deactivateVersionOnDelete` no longer re-mirrors the deactivated
version — that was recreating the core row it just deleted; it only
flips the workspace status to `DEACTIVATED` so a restore comes back
deactivated.)
- **restore** — after `workflowVersionRepository.restore({ workflowId
})`, `recreateCoreVersionsByWorkflowId` re-reads the restored versions
and reuses the existing `upsertToCore` (which reuses the stored
`coreWorkflowVersionId` soft-ref, so rows come back with their original
ids and current status).
- **destroy** — version destroy isn't done in
`handleWorkflowSubEntities` (it happens via the generic cascade), so
there's no existing place to hang the core delete. New
`workflow.destroyOne`/`destroyMany` **post**-hooks call
`deleteCoreVersionsByWorkflowIds` (batched `IN`) only after the destroy
commits, so a rejected destroy can't remove core rows while the
workspace versions survive.

No new transactional wrappers or raw SQL — the delete/recreate reuse the
existing `WorkflowVersionCoreSyncService` methods
(`deleteFromCore`-style delete, `upsertToCore`). The async listener
stays as an idempotent backstop until the cron soaks zero drift.

## Async listener kept as backstop
`handleRestored` / `handleDeleted` / `handleDestroyed` stay for now.
Both paths are idempotent (delete-of-deleted is a no-op; upsert
converges), so they don't conflict. Those handlers come out in a
follow-up once the consistency cron soaks zero drift - which this PR
unblocks.

## Verification
Lifecycle integration test — creates a workflow, **activates** the
version (the active path is where the delete re-mirror bug bit), then
asserts the core row: present -> gone after delete -> back after restore
(as `DEACTIVATED`, same id) -> gone after destroy. Plus the existing
`workflow-resolver` delete/restore suite (regression, since
`handleWorkflowSubEntities` is shared).

Also verified **live** on a running instance against the real DB: the
full active-version lifecycle above, plus a batched `destroyWorkflows`
on two workflows removing both core rows in one `IN` delete. `nx
typecheck` + oxlint + oxfmt clean.


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23356?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-07-27 14:43:59 +00:00
Weiko 5c23ddb1ac Dedupe common result handlers (#23364)
## Context

`CommonResultGettersService` selects field handlers by mapping every
returned record field to the handler registered for its metadata type.

Handlers are shared instances, and each handler already receives the
complete field metadata list. This means that when multiple fields have
the same handled type, the same handler is added and executed multiple
times.

For example, a record with two `FILES` fields previously produced this
execution list:

```text
[objectHandler, filesHandler, filesHandler]
```

Each `filesHandler` execution processes both `FILES` fields. As a
result, both file URLs were signed twice. With several fields of the
same type, this can make the work grow quadratically.

The same duplication can affect rich-text processing, including JSON
parsing, serialization, and embedded file URL signing.

## What changed

Field handler instances are collected in an insertion-ordered `Set`
before execution:

```text
[objectHandler, filesHandler]
```

Each distinct field handler now runs once per record and continues to
process every matching field.

## Why this is safe

- The object-specific handler still runs first.
- Different field-handler types keep their first-seen order.
- No field is skipped, handlers still receive the complete field
metadata list.
- Requests with zero or one field for a handled type are unchanged.
- No cache or cross-request state is introduced.

## Expected impact

This removes repeated synchronous file-token signing and repeated
rich-text transformations for records with multiple fields of the same
handled type. It also reduces event-loop blocking when large result sets
contain several file or rich-text fields.

## Tests

Added a regression test with two `FILES` fields. It verifies that both
fields are processed while `signFileByIdUrl` is called exactly once per
file, two calls instead of the previous four.

Validated with:

```bash
yarn nx jest twenty-server --runInBand --runTestsByPath src/engine/api/common/common-result-getters/__tests__/common-result-getters.service.spec.ts
```

Touched files also pass type-aware Oxlint and Oxfmt.
2026-07-27 14:02:55 +00:00
Raphaël Bosi 590ae069e8 Support workspace member Me filter for relation fields in dashboards (#23282)
<img width="3024" height="1484" alt="CleanShot 2026-07-27 at 15 04
22@2x"
src="https://github.com/user-attachments/assets/a57797b7-dbe9-4748-aeff-18667f1f69bb"
/>


Fixes #20225

Workspace member "Me" filters worked for standard actor fields (Created
by / Updated by) in dashboard widgets but not for relation fields
pointing to a workspace member (e.g. "Account owner"). In the
advanced-filter UI, picking such a relation forced a relation traversal
and never produced a filter you could set to "Me".

For a many-to-one relation targeting workspaceMember, the
relation-target sub-menu now offers a "filter by record" entry that
creates a direct relation filter with the same "Me" multi-select picker
used by view filters. Traversal (e.g. "Account owner -> Name") is
preserved. No backend change is needed: the stored value matches view
filters and is already resolved server-side.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23282?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-07-27 13:51:05 +00:00
Weiko 0c57d7c108 perf: reuse Google webhook OAuth client (#23361)
## Context
A new client currently downloads signing certificates for every webhook

## Fix
reuse same OAuth client instance

## Impact
Probably small but not really risky to merge imho
2026-07-27 13:42:59 +00:00
github-actions[bot] dbbad1bffa i18n - translations (#23367)
Created by Github action

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23367?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-07-27 15:30:31 +02:00
Raphaël Bosi 2899058b5f Warn users before front components navigate to an external site (#23270)
https://github.com/user-attachments/assets/af3fb042-d066-4e0c-9348-f86ea92a6fcd



Front component anchors render a real host `<a>`, so clicking a link to
another domain performed an uncontrolled full-page navigation. This adds
a phishing-resistant "you're leaving Twenty" confirmation modal before
navigating to an external origin (Fixes
[#23260](https://github.com/twentyhq/twenty/issues/23260)).

The renderer intercepts external anchor clicks in
`createHtmlHostWrapper` and hands the destination to a host callback via
context; twenty-front owns the modal (reuses `ConfirmationModal`) and a
per-application list of trusted origins persisted in localStorage. A
"Don't ask again for this site" checkbox (checked by default) skips the
modal next time for that app.

Scope is external cross-origin http(s) links only; same-origin links
keep native behavior. External links always open in a new tab, so a
component can never navigate the Twenty tab away, even once its origin
is trusted. The modal is rendered by the trusted host, so components
cannot style or suppress it.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23270?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-07-27 13:21:38 +00:00
github-actions[bot] 7804111e6c i18n - translations (#23360)
Created by Github action

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23360?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-07-27 15:07:33 +02:00
Raphaël Bosi 56245a35af Stop leaking the refresh token in the social SSO redirect URL (#23061)
The Google/Microsoft callback for a sign-in with no target workspace
redirected to `/sign-in-up?tokenPair={...}`, putting a 60-day refresh
token in a query string. Those persist in browser history, `Referer`
headers and access logs.

It now carries a single-use, 5-minute opaque token in the URL fragment,
which the frontend exchanges over POST. Browsers never send the fragment
on the wire, so the token stays out of access logs, proxies and
`Referer` headers entirely. Redemption claims the row with a `DELETE`
guarded on `revokedAt`/`deletedAt` being null, so concurrent requests
cannot each mint a refresh token and a revoked token cannot redeem.
Enterprise SSO (OIDC/SAML) already used a POST exchange and is
unchanged.

```mermaid
sequenceDiagram
  participant Browser
  participant Server
  participant DB

  Note over Browser,Server: before, the redirect carried access + 60-day refresh in ?tokenPair
  Browser->>Server: GET /auth/google/redirect
  Server->>DB: store sha256(token), expires in 5 min
  Server-->>Browser: 302 /sign-in-up#ssoExchangeToken=opaque
  Note over Browser: fragment never sent back to any server
  Browser->>Server: POST getAuthTokensFromSSOExchangeToken
  Server->>DB: guarded DELETE, single-use claim
  Server-->>Browser: access + refresh token, in the response body
```

Since the token is single-use, the refresh token is minted at redemption
instead of at callback, so an abandoned redirect leaves an inert expired
hash rather than a live credential.

Redemption lives in its own `SignInUpSSOExchangeTokenEffect` +
`useRedeemSSOExchangeToken`, mirroring the existing
`VerifyLoginTokenEffect` + `useVerifyLogin` pair, so
`SignInUpGlobalScopeFormEffect` only loses the vulnerable branch. Like
`useVerifyLogin`, the hook clears any stale token pair before
exchanging. The effect reads `window.location.hash` live and strips it
synchronously, which doubles as the StrictMode double-invocation latch.

Remaining exposure is the browser itself (history until the synchronous
strip, client-side scripts), same as any fragment-based OAuth response.
`loginToken` on the workspace-targeted branch still travels as
`/verify?loginToken=` and is replayable for 15 minutes; moving it to the
fragment too is a separate change.

A fast instance command adds a unique partial index on `("type",
"value")` for live SSO exchange tokens, so redemption is an index lookup
instead of a full scan of the shared token table and at most one row can
ever match.
2026-07-27 12:58:42 +00:00
Shinu Cherian b81ca99162 fix(twenty-front): hide layout editor UI when SystemPermissionFlag.LAYOUTS is missing (#23303) (#23343)
## Description
Fixes #23303.

This PR ensures that the layout editor UI and customization entry points
are hidden and protected when a user lacks the
`SystemPermissionFlag.LAYOUTS` permission flag.

### Changes Made:
1. **`useEnterLayoutCustomizationMode.ts`**: Added
`useHasPermissionFlag(PermissionFlagType.LAYOUTS)` check inside
`enterLayoutCustomizationMode` to return `false` and prevent entering
customization mode if the user lacks permission.
2. **`WorkspaceSection.tsx`**: Updated the sidebar `WorkspaceSection`
component to render the layout edit button (`IconTool`) only when
`hasLayoutsPermission` is `true`.
3. **`ObjectLayout.tsx`**: Disabled customize and reset layout controls
in the Data Model Object Details settings page if the user lacks
`LAYOUTS` permission.
4. **`useEnterLayoutCustomizationMode.test.tsx`**: Added unit tests to
verify that `useEnterLayoutCustomizationMode` correctly guards layout
customization initialization based on permission.

## Testing
- Added unit tests for `useEnterLayoutCustomizationMode` permission
checks.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23343?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-07-27 12:57:24 +00:00
Thomas Trompette 22a01dc1c6 Fix: password reset link returns FORBIDDEN for logged-in users (#21248) (#23335)
## Problem

Fixes #21248.

After upgrading, workspace members who open a password reset link while
a token pair still exists in local storage get a generic `You do not
have permission to perform this action.` (FORBIDDEN) error, blocking
account recovery.

## Root cause

Every GraphQL request passes through
`GraphQLHydrateRequestFromTokenMiddleware` before any resolver. If a
token is present it validates it; if no token is present it
short-circuits and lets the request through unauthenticated.

The reset flow was only ever designed for the unauthenticated case (the
user is logged out, so no token exists). Two intended changes broke that
assumption:

- A token pair now persists in local storage at reset time (unified
`accessOrWorkspaceAgnosticToken` + tokenPair moved off session cookies
into local storage).
- The Apollo auth link attaches `authorization: Bearer <token>` whenever
any token pair exists, regardless of the operation.

So the public `validatePasswordResetToken` /
`updatePasswordViaResetToken` operations now arrive with a token that
the middleware rejects, producing FORBIDDEN before the resolver runs.
Note: the `PublicEndpointGuard` / `NoPermissionGuard` on these resolvers
both just `return true` — they do not inspect headers and are not the
gate. The middleware is.

## Fix

Add a generic `skipAuthToken` operation-context flag. The auth link
omits the `Authorization` header when a request sets it, staying
agnostic of any specific operation or endpoint. The two public reset
operations opt in at their call site in `PasswordReset.tsx`.

This restores the exact unauthenticated path the flow was designed for,
regardless of whether a token pair happens to sit in local storage.
Nothing is reverted; all authenticated traffic is unaffected.

## Testing

Ran the built frontend against a local backend, logged in so a
`tokenPairState` was present in local storage, then opened a reset link
and inspected the outgoing `ValidatePasswordResetToken` request:

- Request headers: `accept`, `content-type`, `x-locale` only. No
`authorization` header, despite a token pair being present.
- With an invalid token the response is the resolver-level `Token is
invalid` error (it reaches the resolver) instead of the middleware's
FORBIDDEN.
- With a valid token the query succeeds (`validatePasswordResetToken`
returns the email + `hasPassword`) and the Set/Change Password form
renders, so the recovery flow completes.

Lint and typecheck pass on the changed files.
2026-07-27 12:32:51 +00:00
Rashad Karanouh e631c986a1 v1.4.0 — partners: auto-link partner user on workspaceMember.created (#23295)
**App version:** `1.4.0` (partners app —
`packages/twenty-apps/internal/twenty-partners`)

## What

Adds partner onboarding auto-linking: when a `workspaceMember` is
created (invite signup), a DB-event-triggered logic function resolves
the partner by the member's email and stamps `partnerUser` across the
partner and its cascade (person, company, links, services, content,
applications).

## Key design decision — data-linking only, no role assignment

The trigger **does not** assign the Partner role. A logic function runs
as an app **agent**, with no user session; `updateWorkspaceMemberRole`
is guarded by `UserAuthGuard` + `AuthWorkspaceMemberId` and is
unreachable from an agent, so the mutation silently no-ops regardless of
permission flags. The dead role code (`ensure-partner-role` service, its
role query/mutation, and the role mocks) is removed so the trigger's
responsibility is unambiguous: resolve partner by email → link
`partnerUser` cascade with retry-on-partial-failure. Role assignment, if
wanted, belongs on the invite path (`sendInvitations` accepts a
`roleId`), not the trigger.

## Changes

- `on-workspace-member-created.logic-function.ts` — DB-event trigger on
`workspaceMember.created`; skips internal (`@twenty.com`) and unmatched
emails
- `resolve-partner-by-email` / `link-partner-user` services + typed
`graphql/` operations for the cascade
- `normalize-invite-email` util
- `partnerUserLinkedAt` field on Partner
- Seed: one contact `Person` (with `partnerId` + email) and one
`Company` per partner so onboarding is testable via a seeded invite
email; drops the `person.city` write removed in SDK 2.25 that broke
`yarn seed`

## Verification

- Unit: **173/173 pass** (27 files) · `tsc --noEmit` clean · `oxlint` 0
warnings/0 errors
- End-to-end: invited + signed in a seeded partner
(`lena@act-education.example`) on the workspace subdomain; the trigger
linked the member to the **Act Education** partner and the self-service
**My Profile** page rendered the linked profile (`POST
/s/my-partner-profile → 200`)

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23295?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-07-27 11:57:23 +00:00
Abdullah. 302f46f0ea fix: bump brace-expansion to 5.0.8 in app lockfiles (Dependabot) (#23346)
## Summary

Bumps **brace-expansion -> 5.0.8** in the three app lockfiles whose copy
sits on the 5.x line, clearing **GHSA-mh99-v99m-4gvg** (high, vulnerable
`<= 5.0.7`) on those manifests:

- `examples/hello-world` (`^5.0.2`)
- `examples/postcard` (`^5.0.5`)
- `internal/self-hosting` (`^5.0.5`)

All three are caret ranges, so a recursive `yarn up -R brace-expansion`
lifts them with **no resolution and no `package.json` change**.

## Why the fixtures are not included

This advisory declares a single vulnerable range, `<= 5.0.7`, which
spans **every** major line - so the `brace-expansion@2.1.2` copies in
`seed-dependencies` and `common-layer-dependencies` are flagged as well.
But **2.1.2 is the last 2.x release** (1.x likewise ends at 1.1.16), and
the only patched version is **5.0.8**. Those consumers declare `^2.0.1`
/ `^2.0.2`, which caps below 3.0.0, so there is no in-range fix:
clearing them would mean forcing a cross-major jump from 2.x to 5.x via
a resolution, which is a behavior risk rather than a mechanical lift.

Same situation for the root alert
([1765](https://github.com/twentyhq/twenty/security/dependabot/1765)),
where `nx` pins `brace-expansion` 5.0.6 exact.

## Verification

- brace-expansion resolves to **5.0.8** in all three lockfiles.
- `yarn install --immutable` passes in each.
- 5.0.8 published 2026-07-23, clears the 3-day npm age gate.
2026-07-27 11:33:38 +00:00
github-actions[bot] f834b020b6 i18n - website translations (#23348)
Created by Github action

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23348?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-07-27 11:30:09 +00:00
github-actions[bot] 88f20a3731 i18n - translations (#23352)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-07-27 12:34:28 +02:00
nitin a6b36422f9 Fireflies: upgrade to twenty-sdk 2.23 (#23349)
Fireflies was skipped by both SDK bump sweeps (#23124, #23165) and sat
on `twenty-sdk ^2.18.0` with no `engines.twenty` floor, while the rest
of the published set moved to `2.23.0-alpha.2`.

- `twenty-sdk` / `twenty-client-sdk` `^2.18.0` -> `2.23.0-alpha.2`
- adds `engines.twenty: ">=2.23.0"`

No source changes needed: the 2.19 identifier migration (#22601) only
affected apps referencing a standard object's system-field identifier,
defining a relation into a standard object, or calling the field-UID
derivation helper. Fireflies does none of those.

The `engines.twenty` floor means the app integration job needs a server
image at 2.23+, so it may fail on version mismatch rather than an app
defect, as in #22601.
2026-07-27 10:31:20 +00:00
Weiko cdd78462b9 fix: block route triggers for suspended workspaces (#23347)
## Problem

Logic function route triggers (app HTTP endpoints served under `/s/*`
and public domains) kept serving traffic for suspended workspaces. A
workspace suspended for non-payment (`activationStatus = SUSPENDED`)
still served its route triggers for the entire suspension window until
soft-deletion removed it from domain lookup. Every other trigger path
gates on activation status — cron triggers only process `ACTIVE`
workspaces — but the route trigger path had no check at all.

## Fix

`RouteTriggerService.getLogicFunctionWithPathParamsOrFail` now rejects
requests when the resolved workspace has `activationStatus = SUSPENDED`,
throwing a `RouteTriggerException` with a new `WORKSPACE_SUSPENDED` code
mapped to `403 Forbidden` in the REST exception filter. Scope is
intentionally limited to `SUSPENDED`; other non-active statuses and the
DB event trigger path are untouched.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23347?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: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2026-07-27 10:25:34 +00:00
github-actions[bot] 5948c167a0 i18n - website translations (#23196)
Created by Github action

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23196?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-07-27 09:55:17 +00:00
Guillaume Flambard 710d4da4b1 fix(emails): bump @react-email/render to ^2.0.6 to fix empty transactional email bodies (#23323)
## Problem

Fixes #23307. Every transactional email (workspace invite, password
reset,
email verification, etc.) is delivered with an **empty body** — no
title, text,
or CTA.

## Root cause

`twenty-server` pins `@react-email/render` directly at `^1.2.3`:

```jsonc
// packages/twenty-server/package.json
"@react-email/render": "^1.2.3",
```

In 1.2.3, `render()` reads `renderToReadableStream` **before** the email
template's async Suspense boundary (i18n/locale load) has resolved. The
result
is the Suspense fallback marker instead of the real markup:

```html
<!DOCTYPE html ...><!--$!--><template></template><!--/$-->
```

This was fixed upstream in `@react-email/render@2.0.6`
(*"await stream.allReady before reading renderToReadableStream
output"*).
`twenty-emails` already resolves a 2.x render via `react-email@6.5.0`,
so the
server's direct pin was simply stale — the two were out of sync.

## Fix

Bump the direct pin to `^2.0.6` (resolves to `2.1.0`) and regenerate the
lockfile. The server's `render()` imports now use the fixed 2.x.

> Note: a `1.2.3` entry remains in `yarn.lock` — it is an internal
transitive
> pin of `@react-email/components@0.5.3`, not the server render path, so
it is
> expected and harmless.

## Verification

Rendering `SendInviteLinkEmail` through the real `render()` (Node 24)
now
returns full markup (5.7 kB) with no Suspense marker and the resolved
invite
link + workspace content, instead of the empty fallback.

A jest unit test was intentionally not added: `@react-email/render` 2.x
uses a
dynamic import that jest's CJS runtime rejects ("A dynamic import
callback was
invoked without --experimental-vm-modules") — which is exactly why the
existing
email specs mock `render`. The fix was verified with a standalone Node
script.


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23323?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-07-27 11:37:09 +02:00
Thomas Trompette 44ed0d5498 Fix phantom targetId field in workflow triggers for morph relations (#23290)
## Problem

In a workflow **record-change trigger** on `noteTarget`, the output
variables offered a `targetId` field that doesn't exist. A morph
relation reaches the frontend as a single field named `target` (the
per-target morph fields are grouped by `morphId`), so the output-schema
generators synthesized its foreign-key column as `` `${field.name}Id` ``
→ `targetId`. But `noteTarget` has no `targetId` column; its FKs are one
per target type: `targetCompanyId`, `targetPersonId`,
`targetOpportunityId`, etc. The phantom `targetId` never matched
anything in the event payload.

## Fix

New helper `getRelationIdFieldNames` returns the actual FK id column(s)
for a relation field:
- normal relation → `[`${name}Id`]`
- morph relation → one column per `morphRelations` target, via the
existing `computeMorphRelationGqlFieldJoinColumnName`
(`targetCompanyId`, `targetPersonId`, ...).

Used by the two output-schema generators:
- `generateRecordEventOutputSchema` — the record-change trigger output
variables (the reported symptom).
- `generateRecordOutputSchema` — record output for
form/find/update-record output schemas.

Scoped strictly to the output schema; no workflow component changes.

## Testing

- Unit tests updated to assert per-target columns instead of the phantom
`targetId` (both generators). 28 passing.
2026-07-27 09:35:06 +00:00
Thomas Trompette 46a3a83866 feat(workflow): mirror all workflowVersion writes to core in-transaction, drop async create/update dual-write (#23243)
Switches the workflowVersion -> core dual-write from the async,
best-effort listener to a **transactional mirror**, wires every
content-write funnel through it, and drops the async create/update
handlers. Core can no longer drift from the workspace on any covered
path: the core copy commits or rolls back atomically with the workspace
write.

## Helper (`WorkflowVersionCoreSyncService`)
Two entry points, both writing `core.workflowVersion` and stamping the
`coreWorkflowVersionId` soft-ref on the **caller's transaction
manager**:

- `writeWorkflowVersionAndMirror(workspaceId, write)` - for funnels that
don't own a transaction. Opens a workspace queryRunner, runs the
caller's workspace write on that manager, re-reads the row, mirrors to
core in the same tx, commits, then invalidates the trigger-map cache
post-commit.
- `mirrorWorkflowVersionWrite({ workspaceId, entityManager,
workflowVersion })` - for funnels that already own a queryRunner tx
(activation / deactivation / delete-cascade); they just gain this one
call on their existing manager before commit.

`invalidateAutomatedTriggerMaps` is public so tx-owning callers run it
post-commit.

## Funnels wired (all content writes now mirror in-transaction)
- `updateWorkflowVersionStepsAndTrigger` (central builder step/trigger
edit)
- edge create/delete (4 trigger/step writes)
- `createDraftFromWorkflowVersion` (update + insert),
`duplicateWorkflow` (content update), `updateWorkflowVersionPositions`
- iterator / if-else empty-node step writes
- `workflow.createOne` / `createMany` post-hooks (v1 draft insert)
- AI `create_complete_workflow` tool (v1 insert)
- activation / deactivation status writes (ACTIVE / ARCHIVED /
DEACTIVATED) on their existing tx
- `deactivateVersionOnDelete` cascade (ACTIVE -> DEACTIVATED) on its
existing tx

Direct `workflowVersion.createOne/createMany` is forbidden by a
pre-hook, so there is no un-funneled create path.

## Async listener trimmed
`handleCreated` and `handleUpdated` are removed: every create/update now
mirrors in-transaction, so the post-commit handlers were redundant and
were the source of the rollback-drift (an edit that rolls back still
emitted an `UPDATED` event carrying the uncommitted payload, which the
async listener wrote to core).

`handleRestored`, `handleDeleted`, `handleDestroyed` are **kept**.
Soft-delete/restore go through the generic ORM (not a funnel):
`handleDeleted` drops the core row on soft-delete, and `handleRestored`
recreates it on restore (the restore path just calls
`workflowVersionRepository.restore()`). Removing the restore handler
would leave restored versions with no core row, so the delete/restore
pair stays async.

## Why the core write is raw SQL
`core.workflowVersion` is on the core DataSource, not the workspace
DataSource, so a repository can't be pointed at it from the workspace
queryRunner's manager. But both schemas are one Postgres DB and a
queryRunner is a single connection, so a schema-qualified `INSERT INTO
core."workflowVersion" ... ON CONFLICT` on that manager participates in
the workspace transaction (the prefill util's pattern). The workspace
write and soft-ref write-back go through the ORM's
`repository.update(criteria, data, undefined, queryRunner.manager)`, so
the source-of-truth workspace write keeps its ORM machinery (actor
stamping, search vector, events); only the dumb core mirror is raw,
confined to the helper.

**jsonb:** the raw insert has no entity transformer, so
`triggers`/`steps` are passed as JSON strings (Postgres parses them) -
the inverse of the prefill core insert (the #23204 double-encode trap),
covered by the test. `universalIdentifier` is minted only for a new
link; on conflict only `triggers`/`steps`/`status` are updated.

## Test
In-process integration test: opens a workspace queryRunner, calls the
helper, asserts the core row is visible inside the tx with correct
native jsonb, rolls back, asserts the core row is gone - empirically
confirming one workspace queryRunner writes `core.*` in the same tx.

## Review feedback addressed
- **Duplicate atomicity:** `duplicateWorkflow` now inserts the draft
version with its final steps/trigger inside
`writeWorkflowVersionAndMirror`, so a mirror failure rolls back the
whole version instead of leaving an unmirrored empty draft.
- **Delete cascade:** `deactivateVersionOnDelete` moved its command-menu
cleanup after commit, so a rolled-back deactivation can no longer strip
the menu item from a still-active version.
- **Rollback drift / unlinked rows:** resolved structurally by the
in-transaction mirror + removal of the async CREATED/UPDATED handlers,
and by the soft-ref-field guard that skips mirroring (returns null) on
workspaces without `coreWorkflowVersionId`.
- **Unit suites:** the step-helpers, step-operations and edge suites now
provide a `WorkflowVersionCoreSyncService` mock whose
`writeWorkflowVersionAndMirror` runs the write callback against the test
repo; all three pass (35 tests).

## Verification
Rebased onto `origin/main`. `nx typecheck twenty-server` is green, the
three affected unit suites pass (35 tests), and every changed file
passes type-aware oxlint + oxfmt. Integration suites were failing only
on behind-main DB-init drift (`core.keyValuePair` / data-migration),
which the rebase resolves. Live end-to-end test on a running instance
still pending.
2026-07-27 09:17:17 +00:00
Weiko a96dc335ab fix: apply configured pool size to core database (#23322)
## Context

`PG_POOL_MAX_CONNECTIONS` is the server setting for the maximum number
of PostgreSQL clients in a connection pool. The workspace primary and
replica data sources already apply this setting, but the core TypeORM
data source did not.

Without an explicit `poolSize`, `node-postgres` uses its default limit
of 10. As a result, deployments configured with a larger pool still kept
the core pool at 10 connections per server process.

During bursts of core database work, requests could therefore wait for a
local pool connection even when PostgreSQL itself still had available
capacity. That acquisition queue adds latency before the query starts,
so database-level utilization alone does not reveal the bottleneck.

## What changes

The core data source now applies:

```ts
poolSize: Number(process.env.PG_POOL_MAX_CONNECTIONS ?? 10)
```

This makes the core data source consistent with the workspace data
sources and with the documented meaning of `PG_POOL_MAX_CONNECTIONS`.

## Expected impact

Deployments that configure a value above 10 can use that capacity for
core database operations instead of queueing behind the driver's default
limit. This targets short acquisition spikes affecting operations backed
by the core database.

The pool remains lazy, so this changes the maximum number of connections
available to each process, it does not eagerly open every configured
connection.

## Safety

- Deployments without `PG_POOL_MAX_CONNECTIONS` keep the previous limit
of 10.
- Query behavior, transaction behavior, and timeouts are unchanged.
- Workspace pool configuration is unchanged.
- Operators remain responsible for choosing a value compatible with
their total PostgreSQL connection budget and maximum server replica
count.

## Scope

This removes an unintended local connection-pool bottleneck. It does not
address the source of synchronized database bursts, which should be
handled separately by reducing unnecessary work.

## Testing

Added a regression test that loads the core data source with
`PG_POOL_MAX_CONNECTIONS=40` and verifies that TypeORM receives
`poolSize: 40`.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23322?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-07-27 08:58:41 +00:00
Weiko 42c598a33a Filter stale sync workspaces by active channels (#23321)
## Context

The hourly calendar and messaging stale-sync crons currently load every
active workspace and enqueue one recovery job per workspace. Each
recovery job then enters the workspace context and queries its channels,
even when that workspace has no stale sync.

As the number of workspaces grows, the cost of this check grows with
every workspace rather than with the number of syncs that actually need
recovery. Because both crons run on the hour, this also creates a
synchronized burst of mostly unnecessary queue, database, and
workspace-context work.

## What changes

The crons now use TypeORM repository `find` operations on
`calendarChannel` and `messageChannel` before enqueueing recovery jobs:

- Select only `workspaceId` from matching channels.
- Keep only active, non-deleted workspaces.
- Keep only the scheduled and ongoing stages handled by the existing
recovery jobs.
- Treat a sync as stale when `syncStageStartedAt` is null or older than
the existing 30-minute timeout.
- Deduplicate workspace IDs in memory before enqueueing.
- Enqueue the existing recovery job only for matching workspaces.
- Share the stage definitions between candidate selection and recovery
so they cannot drift independently.

## Before and after

Before:

1. Load every active workspace.
2. Enqueue a calendar job and a messaging job for each workspace.
3. Enter every workspace context.
4. Query its channels.
5. Usually find nothing to recover.

After:

1. Run one candidate lookup for calendar channels and one for message
channels.
2. Return only the workspace ID for each potentially stale channel.
3. Deduplicate those IDs and enqueue one job per affected workspace.
4. Let the existing recovery jobs recheck and recover those channels.

Database reads now scale with stale channel candidates, while queue and
workspace-context work scale with affected workspaces rather than the
total workspace count.

## Why deduplicate in memory

TypeORM repository find options do not provide `DISTINCT`, so these
lookups can return the same workspace ID once per stale channel. A `Set`
removes those duplicates before jobs are created.

This is a deliberate tradeoff:

- The database returns only UUIDs, not full channel records.
- The scans run hourly against channel tables, and stale candidates
should remain sparse.
- It keeps the query expressed through the typed repository API.
- It avoids adding a database sort or hash aggregation for `DISTINCT`.

If candidate volume becomes large enough for result transfer or
in-process deduplication to matter, database-side deduplication can be
moved into a dedicated repository query. A practical signal would be
tens of thousands of candidates per run, a high duplicate ratio, or
measurable lookup and event-loop latency.

## Safety

- The recovery jobs and sync-state transitions are unchanged.
- The candidate lookup uses the same stage lists and timeout constants
as the recovery jobs.
- Recovery jobs still recheck staleness after dequeueing, which protects
against a channel recovering between candidate selection and execution.
- Jobs are still enqueued sequentially with per-workspace exception
handling.
- Disabled and group channels are not newly excluded, preserving the
previous recovery behavior.

## Scope

This only changes hourly stale-sync detection. It does not change normal
calendar or messaging scheduling, imports, retry policies, or recovery
state transitions.

## Testing

Added unit coverage for:

- active and non-deleted workspace filtering,
- selecting only workspace IDs,
- scheduled and ongoing stage filtering,
- null or expired `syncStageStartedAt`,
- the configured stale timeout,
- application-side workspace deduplication,
- enqueueing one recovery job per affected workspace.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23321?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-07-27 08:56:28 +00:00
Abdullah. ed95b8cfde fix: bump postcss to 8.5.22 across app lockfiles (Dependabot) (#23340)
## Summary

Bumps **postcss -> 8.5.22** in the 13 twenty-apps lockfiles that carry
it transitively, clearing **GHSA-r28c-9q8g-f849** (high) on those
manifests: path traversal in previous source map auto-loading
(`sourceMappingURL`) leading to arbitrary `.map` file disclosure,
vulnerable `<= 8.5.17`.

Apps covered: document-generator, hello-world, postcard, self-hosting,
twenty-partners, call-recorder, people-data-labs, twenty-discord,
twenty-exa, twenty-fireflies, twenty-last-contact, twenty-linear,
twenty-slack.

Every app reaches postcss through a caret range (`^8.5.15`), so a
recursive `yarn up -R postcss` lifts it in each project with **no
resolution and no `package.json` change** - the diff is 13 `yarn.lock`
files and nothing else. Yarn resolves to **8.5.22**, the latest in range
(above the 8.5.18 fix floor).

## Not included

The **root lockfile** carries the same advisory but its postcss copies
are held by exact pins - `next` (8.4.31 in every stable release,
including 16.2.11) and `@mintlify/common` (8.5.14, unchanged in its
latest) - so no `yarn up` reaches it. That one needs a scoped resolution
and is handled separately.

## Verification

- postcss resolves to **8.5.22** in all 13 lockfiles; nothing at or
below 8.5.17 remains.
- `yarn install --immutable` passes in each of the 13 projects.
- 8.5.22 published 2026-07-22, clears the 3-day npm age gate.
2026-07-27 08:50:00 +00:00
Abdullah. 6060d88c54 fix: bump tar 7.5.20 -> 7.5.21 in the root lockfile (Dependabot) (#23330)
## Summary

Bumps **tar 7.5.20 -> 7.5.21** in the root `yarn.lock`, clearing
Dependabot alert
[1852](https://github.com/twentyhq/twenty/security/dependabot/1852):
**GHSA-r292-9mhp-454m** (medium) - uncontrolled recursion in
`mapHas`/`filesFilter` allows an uncatchable stack-overflow DoS via a
crafted long-path tar with member selection, vulnerable `<= 7.5.20`.

Every root tar consumer declares a caret range (`^7.4.3`, `^7.5.4`,
`^7.5.9`, `^7.5.11`, `^7.5.16`) and the existing scoped tar resolutions
for the @electron/rebuild toolchain and @mintlify/previewing are carets
as well (`npm:^7.5.16`), so a recursive `yarn up -R tar` lifts the
single tar entry with **no resolution change and no `package.json`
change**.

## Verification

- `yarn install --immutable` passes.
- Diff is `yarn.lock` only; the single tar entry resolves to 7.5.21,
nothing below remains.
- 7.5.21 published 2026-07-21, clears the 3-day npm age gate.

The same advisory affects the twenty-apps and server fixture lockfiles;
those follow in separate PRs.
2026-07-27 07:55:25 +00:00
Abdullah. 97bb56d471 fix: bump tar and brace-expansion in seed-dependencies (Dependabot) (#23333)
## Summary

Bumps **tar 7.5.20 -> 7.5.21** and **brace-expansion 5.0.7 -> 5.0.8** in
the `application-package/constants/seed-dependencies` fixture:

| Severity | Advisory | Package | Alert |
|---|---|---|---|
| medium | GHSA-r292-9mhp-454m | tar (`<= 7.5.20`) |
[1850](https://github.com/twentyhq/twenty/security/dependabot/1850) |
| high | GHSA-mh99-v99m-4gvg | brace-expansion |
[1856](https://github.com/twentyhq/twenty/security/dependabot/1856) |

Both are reached through caret ranges (`^7.5.4`, `^5.0.2`), so the
lockfile diff comes from a plain recursive `yarn up` - no resolution, no
`package.json` change.

## Checksum coupling

This fixture is read at runtime by `getDefaultApplicationPackageFields`
and pinned by stored constants (first 32 hex chars of SHA512).
**`DEFAULT_YARN_LOCK_CHECKSUM`** is regenerated to match the new
lockfile. `package.json` is byte-untouched so
`DEFAULT_PACKAGE_JSON_CHECKSUM` stays as-is.

Verified in order: the hash formula reproduces **both** current
constants before regenerating; the new constant matches the new content;
`yarn install --immutable` passes in the fixture.

## sharp deliberately excluded

The third open alert here (GHSA-f88m-g3jw-g9cj, high - inherited libvips
CVEs) is **not** a lockfile lift: sharp is a *direct* dependency of this
fixture at `^0.34.5`, so clearing it means moving the declared range to
`^0.35.0`. That changes the package set exposed to user logic functions,
shifts `DEFAULT_PACKAGE_JSON_CHECKSUM` too, and sharp 0.35 raises its
engines floor from Node 18 to `>=20.9.0` while Lambda layers are still
advertised as NODE18-compatible. The same `^0.34.5` ceiling gates
twenty-sdk and 15 app manifests, so it deserves one coordinated decision
rather than a drive-by change here.
2026-07-27 07:55:17 +00:00
Abdullah. 0efc92b3f3 fix: bump shell-quote 1.8.4 -> 1.10.0 (Dependabot) (#23331)
## Summary

Bumps **shell-quote 1.8.4 -> 1.10.0**, clearing Dependabot alert
[1769](https://github.com/twentyhq/twenty/security/dependabot/1769):
**GHSA-395f-4hp3-45gv / CVE-2026-13311** (high) - quadratic-complexity
Denial of Service in `parse()` (CWE-407), vulnerable `<= 1.8.4`, fixed
1.9.0.

Both consumers declare caret ranges - `@graphql-codegen/cli` (`^1.7.3`)
and `concurrently` (`^1.8.1`) - so a recursive `yarn up -R shell-quote`
lifts the single entry with **no resolution and no `package.json`
change**. Yarn resolves to 1.10.0, the latest in range (above the 1.9.0
fix floor).

## Verification

- `yarn install --immutable` passes.
- Diff is `yarn.lock` only; shell-quote resolves to 1.10.0, no 1.8.4
remains.
- 1.10.0 published 2026-07-10, clears the 3-day npm age gate.
2026-07-27 07:55:09 +00:00
Abdullah. 155636d7d9 fix: bump tar to 7.5.21 across app lockfiles (Dependabot) (#23332)
## Summary

Bumps **tar -> 7.5.21** in the 13 twenty-apps lockfiles that carry it
transitively, clearing **GHSA-r292-9mhp-454m** (medium) on those
manifests: uncontrolled recursion in `mapHas`/`filesFilter` allows an
uncatchable stack-overflow DoS via a crafted long-path tar with member
selection, vulnerable `<= 7.5.20`.

Apps covered: document-generator, hello-world, postcard, self-hosting,
twenty-partners, call-recorder, people-data-labs, twenty-discord,
twenty-exa, twenty-fireflies, twenty-last-contact, twenty-linear,
twenty-slack.

Every app reaches tar through a caret range (`^7.5.4`), so a recursive
`yarn up -R tar` lifts it in each project with **no resolution and no
`package.json` change** - the diff is 13 `yarn.lock` files and nothing
else.

## Not included

- **Root lockfile**: same advisory, shipped separately in #23330.
- **`application-package/constants/seed-dependencies`**: the 14th
manifest with this advisory. Its `yarn.lock` is checksum-coupled to
`DEFAULT_YARN_LOCK_CHECKSUM`, so it moves in its own PR with the
constant regenerated alongside.

## Verification

- tar resolves to **7.5.21** in all 13 lockfiles; nothing below remains.
- `yarn install --immutable` passes in each of the 13 projects.
- 7.5.21 published 2026-07-21, clears the 3-day npm age gate.
2026-07-27 07:55:04 +00:00
github-actions[bot] ad3291f4b4 i18n - docs translations (#23338)
Created by Github action

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23338?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-07-27 09:46:37 +02:00
github-actions[bot] 32a031ac0b i18n - translations (#23336)
Created by Github action

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23336?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-07-27 09:19:30 +02:00
Guillaume Flambard 1e5b8e6db4 fix(front): add accessible labels to icon-only options dropdown triggers (#23325)
## Problem

Refs #23127. Icon-only `LightIconButton` "more options" triggers
(`IconDotsVertical`) render without an accessible name, failing WCAG
4.1.2 (button-name) — screen readers announce nothing for them.

## Fix

Add `aria-label={t`More options`}` to the affected dropdown triggers.
`LightIconButton` already forwards `aria-label` and sets `aria-hidden`
on the icon when a label is present, so this is purely additive — no
behavioral or visual change.

Scoped to a coherent set of options-menu triggers (attachments, public
domains, SSO, connected accounts, field group config). Other unlabeled
icon buttons can follow in separate PRs.

## Verification

`oxlint --type-aware` and `oxfmt` pass on all changed files. The label
is i18n-wrapped via the existing `useLingui` macro already imported in
each component.


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23325?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-07-27 07:10:49 +00:00
martmull 4f9fd6f674 feat(applications): restore the application custom settings tab (#23256)
## Summary

Restores the application **custom settings tab** feature that was
removed in #22156. This reverts that removal so applications can again
expose a custom settings tab via a front component.

## Changes

- Restore the `SettingsApplicationCustomTab` component and its tab
entry/rendering in `SettingsApplicationDetails`.
- `ApplicationManifestMigrationService` syncs
`settingsCustomTabFrontComponent` from application manifests again
(`syncDefaultRoleAndSettingsCustomTab`), resolving the front component
from `settingsCustomTabFrontComponentUniversalIdentifier`.
- Remove the deprecation annotations added by #22156:
- `ApplicationDTO.settingsCustomTabFrontComponentId` (drop GraphQL
`@deprecated`)
-
`ApplicationManifest.settingsCustomTabFrontComponentUniversalIdentifier`
- the `settingsCustomTabFrontComponentId` column comment on
`ApplicationEntity`
- Regenerate the corresponding GraphQL schema/types to drop the
`@deprecated` reason.

The DB column was never dropped, so no schema migration is required.


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

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23256?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-07-27 06:52:04 +00:00
github-actions[bot] a94f2443b3 i18n - translations (#23328)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-07-26 22:55:56 +02:00
neo773 3fb29db28a Feat/email settings v2 (#23180)
Settings pages changes

- Add `displayName`
- Unsubscribers Page

<img width="1496" height="844" alt="Screenshot 2026-07-22 at 8 52 15 PM"
src="https://github.com/user-attachments/assets/69bc1993-4547-4a64-83a6-b47fef1a4e40"
/>


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23180?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-07-26 22:48:30 +02:00
github-actions[bot] 8326fd186f i18n - docs translations (#23324)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-07-26 20:51:07 +02:00
martmull 1ee08ff92b docs: set correct credit cost for workflow steps and app logic functions (#23297)
The Credits page listed the app logic function row as "A small fraction
of a credit / Thousands per credit", which understates the rate.

A workflow step and a logic function run each cost a flat 100
micro-credits (`workflow-executor.workspace-service.ts:353`,
`logic-function-executor.service.ts:536`), i.e. $0.0001, or 10,000 runs
per credit. Since the two rows carried the same rate stated twice,
they're merged into one.

Also adds a note that Call Recorder and Last contact don't consume
credits for their logic function runs. They're in
`MARKETPLACE_BILLING_EXEMPT_UNIVERSAL_IDENTIFIERS`, which is 2 of the 3
apps currently in `MARKETPLACE_VETTED_APPLICATIONS`. The note is
explicit that the exemption covers only the per-run charge, since those
apps still bill metered work (recorded call minutes) through
`chargeCredits`.

## Test plan

Docs-only change. Rates cross-checked against
`workflow-executor.workspace-service.ts` and
`logic-function-executor.service.ts`; the exempt list against
`marketplace-billing-exempt-applications.constant.ts` and
`marketplace-vetted-applications.constant.ts`.

Co-authored-by: Martin <martin@twenty.com>
2026-07-26 16:53:49 +00:00
Paul Rastoin 24067ec87a chore: remove twenty-companion dead code (#23310)
## What

Removes `packages/twenty-companion` (package name `twenty-desktop`), the
Electron "Twenty Desktop" proof of concept that landed with the
Recall.ai call-recording work in #18281.

## Why it's dead code

- **Not in the nx graph** — no `project.json`, so no target ever runs
against it.
- **Not in CI** — no workflow references it. #21327 said as much when
bumping its Electron: "there's no CI job that builds/tests
twenty-companion, so this isn't exercised by CI".
- **No code references** — nothing imports it, and the only path
references were the root `workspaces` array, `yarn.lock`, and
`.vscode/twenty.code-workspace`. It talks to Twenty over the public REST
API from a separate process, so there is no coupling to remove.
- **Self-declared POC** — its README opens with "This application is a
Proof of Concept (POC) and must NOT be used in production. [...]
Security, stability, and performance have not been validated for
production use."
- **No feature work since it landed** (March 2026). Every commit
touching it since has been a dependency or tooling sweep: React 19
migration, ESLint→OxLint, npm→yarn workspaces, and four CVE bumps.
- **Docs already stale** — its README points at
`packages/twenty-apps/internal/call-recording`, which no longer exists.
The shipped app lives at `packages/twenty-apps/public/call-recorder` and
does not reference the desktop companion.

Meanwhile it pulled a full Electron + electron-forge toolchain into
every root install, and kept generating Dependabot noise against a tree
nothing builds.

## Changes

- Delete `packages/twenty-companion`.
- Drop its entry from root `workspaces` and from
`.vscode/twenty.code-workspace`.
- Drop four root `resolutions` that existed only to evict CVEs from the
Electron tree, along with their entries in the `//resolutions` rationale
doc:
  - `@electron/rebuild/tar`, `@electron/node-gyp/tar`
  - `@electron-forge/plugin-webpack/webpack-dev-server`
- `make-fetch-happen` — its only sub-`^15` consumer was the Electron
`node-gyp` fork; the remaining consumers (`@sigstore/sign`,
`npm-registry-fetch`, `tuf-js`) already declare `^15.x`
- Regenerate `yarn.lock`.

## Lockfile impact

469 descriptors removed, **zero version changes for any surviving
descriptor** (verified with a descriptor-level diff of old vs new
resolutions). Two descriptors show up as new —
`make-fetch-happen@npm:^15.0.1` and `@npm:^15.0.4` — only because the
global resolution was previously rewriting them; both still resolve to
`15.0.6`. Re-running resolution produces a byte-identical lockfile.

## Test plan

- [x] Repo-wide grep confirms no remaining references to
`twenty-companion` / `twenty-desktop` / the Electron toolchain.
- [x] `yarn install --mode=update-lockfile` is stable and idempotent
under hardened mode.
- [x] Descriptor-level lockfile diff shows no resolution changes outside
the removed tree.
- [ ] CI green (nothing targets the removed package, so the risk surface
is the lockfile).


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

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23310?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-07-26 16:52:52 +00:00
github-actions[bot] 0b44864f5f i18n - translations (#23315)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-07-26 12:32:26 +02:00
eeshsaxena 472c7c1edc fix(workspace): open edit panel for PAGE_LAYOUT sidebar items (#23293)
Fixes #22649.

Custom page-layout links in the sidebar (like "Star History") couldn't
be removed. Clicking them in edit mode did nothing.

**Root cause**

`handleNavigationMenuItemClick` in `WorkspaceSection.tsx` switches on
`item.type`. `FOLDER` and `LINK` have explicit cases that call
`openNavigationMenuItemInSidePanel`. `PAGE_LAYOUT` fell through to
`default`, which calls `openViewOrRecordEditPanelAndNavigate`. That
function only opens the side panel when `objectMetadataItem` is defined
- PAGE_LAYOUT items don't have one - so the panel never opened.

**Fix**

Add a `PAGE_LAYOUT` case that calls `openNavigationMenuItemInSidePanel`
directly, using the item's own label and icon. Same pattern as `LINK`.

**How to test**

1. Create a custom page link in the sidebar (Settings > Workspace > Add
menu item > Page layout).
2. Click the wrench icon to enter edit mode.
3. Click the custom page item - the edit side panel should now open.
4. Verify you can remove it from the sidebar.


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23293?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: prastoin <paul@twenty.com>
2026-07-26 10:24:07 +00:00
github-actions[bot] 4eb5ad9e32 i18n - translations (#23313)
Created by Github action

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23313?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-07-26 10:40:10 +02:00
Gueye Papa Djadji 93066ae800 fix(a11y): add aria-label to navigation drawer collapse button (WCAG … (#23287)
…4.1.2)
Fixes #23131

Added `aria-label` to the navigation drawer collapse/expand button 
(LightIconButton with IconLayoutSidebarLeftCollapse/RightCollapse), 
which previously had no accessible name for screen readers.

Verified with axe DevTools scan on localhost — the button-name 
violation for this element no longer appears.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23287?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: Thomas Trompette <thomas.trompette@sfr.fr>
2026-07-26 08:32:02 +00:00
github-actions[bot] 763d31a859 chore: sync AI model catalog from models.dev (#23298)
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.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23298?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: FelixMalfait <6399865+FelixMalfait@users.noreply.github.com>
2026-07-25 08:43:55 +02:00
Abdullah. c102a22375 fix: lift axios/tar/brace-expansion/body-parser in server fixture lockfiles (Dependabot) (#23291)
## Summary

Follow-up to #23267: lifts **axios, tar, brace-expansion, body-parser**
in the two **twenty-server fixture projects** that were deliberately
excluded from the apps sweep because of checksum coupling:

- `application-package/constants/seed-dependencies`: axios 1.16.1 ->
1.18.1, body-parser 1.20.5 -> 1.20.6, brace-expansion 2.1.1 -> 2.1.2 and
5.0.6 -> 5.0.7, tar 7.5.16 -> 7.5.20
- `logic-function/.../common-layer-dependencies`: brace-expansion 2.1.1
-> 2.1.2

All moves fit the declared ranges (recursive `yarn up`), so both diffs
are lockfile-only.

## Checksum coupling

`seed-dependencies` is read at runtime by
`getDefaultApplicationPackageFields` and its content is pinned by stored
constants (first 32 hex chars of SHA512; package.json hashes the
re-serialized JSON). The lockfile change therefore regenerates
**`DEFAULT_YARN_LOCK_CHECKSUM`** in
`get-default-application-package-fields.util.ts`. `package.json` is
byte-untouched, so `DEFAULT_PACKAGE_JSON_CHECKSUM` stays.

Verified in order: the hash formula reproduces both *current* constants
before regenerating; the new constant matches the new lockfile content;
`yarn install --immutable` passes in both fixture projects.
`common-layer-dependencies` has no checksum coupling (copied + installed
at Lambda layer build time).

## Deliberately not covered

**sharp** stays at 0.34.5 in seed-dependencies: every path is
minor-locked at `^0.34.5` (including twenty-sdk latest); pending the
twenty-sdk range decision.

## Alerts

Clears the axios (10), tar (4), brace-expansion (3) and body-parser (1)
Dependabot alerts on these two manifests.
2026-07-24 18:11:29 +00:00
github-actions[bot] ab3d921218 i18n - docs translations (#23292)
Created by Github action

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23292?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-07-24 19:05:04 +02:00
Etienne 4851489ebc fix(ai-chat) fix AI chat tool-output spill leaks: spill learn_tools, cap navigation tools, truncate on spill failure (#23286)
## Context

AI chat spills tool outputs larger than `MAX_INLINE_TOOL_OUTPUT_BYTES`
(16 kB) to a file and lets
the model page them back with `search_output` / `extract_json_paths`.
Three paths bypass this and
let unbounded payloads into conversation history:

1. **`learn_tools` is never spilled.** Tool schemas go inline whatever
their size.
2. **Navigation tools have no inline cap.** They are exempt from
spilling by design (they page
   spilled files), but nothing bounds their own output.
3. **Spill failure falls back to full inline.** On any spill error the
service returns the complete
   payload with only a warning appended.

## What changed

- **`learn_tools` now spills.** `createLearnToolsTool` takes `{
excludeTools?, spillLargeOutput? }`
(same shape as `createExecuteToolTool`); chat execution enables it. Only
the bulky `tools`
schemas are spilled; `message` / `notFound` / `suggestions` stay inline
and the response carries
a `spilledTools` envelope (fileId, preview, hint) pageable via
`extract_json_paths`. MCP is
  unchanged.
- **Navigation tools get a hard inline cap.** Still never spilled, but
output above 16 kB is
head+tail truncated with a marker telling the model to narrow the query
or page with `offset`.
- **Spill failure truncates instead of inlining.** The fallback returns
head+tail within the 16 kB
  budget with the original byte size in the marker, keeping the warning.
- New `truncateHeadTail` util: byte-budgeted, marker-aware, UTF-8
codepoint-safe.

## Test plan

- `tool-output-spill.service.spec.ts`: spill envelope unchanged,
under-budget passthrough,
navigation cap for both tools (budget respected, marker mentions
`offset`, no file written),
  truncated fallback on spill failure with warnings preserved.
- `learn-tools.tool.spec.ts`: no spill without the option, inline under
budget,
`message`/`notFound`/`suggestions` intact when spilled, spill-failure
warnings surfaced.
- `truncate-head-tail.util.spec.ts`: budget, head+tail+marker,
multibyte-safe cuts.
- 63 tests across 6 suites; `lint:diff-with-main` and `typecheck` green.

## Post-deploy

Watch the `AiChatToolOutputTokens` histogram (p95 should collapse to ~4k
tokens) and the
`AiChatInputTokens` / `AiChatCacheReadTokens` ratio on GPT-5-class
models.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23286?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-07-24 16:57:35 +00:00
Etienne 3a1067ec6d fix(server): index page-layout FKs to fix workspace cleanup timeout (#23289)
The cleanSuspendedWorkspacesJob cron timed out every run (Sentry monitor
"a timeout check-in was detected"): hard-deleting soft-deleted
workspaces hung on `DELETE FROM core.pageLayout`, hit the 10s query
timeout, rolled back, so those workspaces were never destroyed and got
retried hourly.

Root cause: the FKs in the pageLayout -> pageLayoutTab ->
pageLayoutWidget tree had no usable index on the referencing column. The
existing indexes lead with workspaceId and are partial ("deletedAt" IS
NULL), so ON DELETE CASCADE / SET NULL fell back to full sequential
scans of the shared core tables per deleted row; on layout-heavy
workspaces this exceeded 10s.

- Add non-partial FK-column indexes on pageLayoutTab(pageLayoutId) and
pageLayoutWidget(pageLayoutTabId)

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23289?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-07-24 16:50:57 +00:00
martmull 45f92b6763 feat(billing): make logic function executions free for exempt apps (#23255)
## Problem

Workspaces get 5 free credits/month to run logic functions, AI and
workflows. When a user imports their mailbox with the
onboarding-suggested **Call Recorder** and **Last contact** apps, each
imported message/calendar event fires those apps'
database-event-triggered logic functions, and each execution bills a
flat 100 micro-credits. A single import can fire tens of thousands of
executions and drain the entire monthly allowance before the user has
done anything else.

The trigger pipeline has no notion of "this came from sync", and logic
function executions are metered per record (one job per imported
record), so the burn is unavoidable today.

## Approach

Keep a static list of billing-exempt app identifiers
(`MARKETPLACE_BILLING_EXEMPT_UNIVERSAL_IDENTIFIERS` — Call Recorder and
Last contact) and check it in the logic-function executor's billing step
via a small `isBillingExemptApplication(universalIdentifier)` utility.
When the running app is exempt, the per-invocation meter records
`creditsUsedMicro: 0` and skips the credit decrement.

Scope is deliberately narrow: only the automatic per-invocation meter is
exempted. Anything the function itself charges via `chargeCredits` (the
separate `/app/billing/charge` endpoint) and any AI token usage keep
billing and keep their enforcement, so a free app can still charge for
real paid work (e.g. Call Recorder's per-recording charge, People Data
Labs enrichment) and AI usage still throws on credit exhaustion.

There is no DB column, migration, cache, admin UI, or per-registration
state — the exemption is derived entirely from the app's
`universalIdentifier` against the in-memory list, so it applies
uniformly to fresh and existing installations.

## Changes

- `isBillingExemptApplication` utility over the exempt-apps constant,
with a unit test.
- Logic-function executor consults the utility to decide
`creditsUsedMicro` (0 for exempt apps, 100 otherwise) and only
decrements credits for non-exempt invocations.

## Notes / follow-ups

- This fixes the billing drain but not the execution burst: an import
still fires the real isolate executions for zero user-visible benefit
over the apps' existing batch backfill. Suppressing database-event
triggers during historical import is a complementary follow-up worth
doing for infra cost and rate-limit reasons.

## Test plan

- [x] `nx typecheck twenty-server` / `nx typecheck twenty-front`
- [x] Server unit tests (`isBillingExemptApplication`) pass
- [ ] Manual: install Call Recorder / Last contact, import a mailbox,
confirm credits are not consumed by their logic function executions
while AI usage and in-app charges still bill
2026-07-24 18:23:17 +02:00
Thomas Trompette d6c186a71e Fix system objects bypassing role object permission overrides (#23280)
## Bug

Fixes #23062 (security). A workspace member whose role denies all object
access could still read/mutate system-object records (messages, calendar
events, and related system objects). System objects bypassed explicit
role-level object permissions.

## Root cause

In `workspace-roles-permissions-cache.service.ts`, the per-object
permission helper resolved values as:

```ts
(isSystem ? true : (overrideValue ?? defaultValue))
```

For every non-workflow, non-workspace-member system object this forced
`read`/`update`/`softDelete`/`destroy` to `true`, so an explicit deny
override on the role was never consulted.

## Fix

Flip the precedence so an explicit role override wins, and the
`isSystem` default only applies when the role provides no override:

```ts
overrideValue ?? (isSystem ? true : defaultValue)
```

Because the override fields are `boolean | undefined`, `??` correctly
honors an explicit `false` while still falling back to the system
default (`true`) when the role has no override row for that object.

Workflow objects (settings-gated via the `WORKFLOWS` flag) and
workspace-member objects (settings-gated, always readable) are handled
in separate branches and are unchanged, so their intended defaults do
not regress.

## Testing

- `nx lint:diff-with-main twenty-server` passes.
- Typecheck: no new errors from this change (pre-existing unrelated
failures in `twenty-shared` date-filter utils only).
- Manually verified on a local instance that a deny-all role no longer
has read access to system objects.


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23280?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-07-24 15:49:52 +00:00
Paul Rastoin bc7922da7d fix: move add-agent-foreign-key-to-role-target instance command to 2.25 (#23285)
## What

Moves the `add-agent-foreign-key-to-role-target` fast instance command
from `2.24.0` to `2.25.0`.

Introduced in #23206, the command was registered under version `2.24.0`.
Since `TWENTY_CURRENT_VERSION` is now `2.25.0`, `2.24.0` is an
already-released version, so its instance commands do not re-run on
upgrade and the foreign-key migration would never execute.

This is the same issue #23271 fixed for the message-list-members
backfill workspace command.

## Changes

- Moved the command file from `upgrade-version-command/2-24/` to `2-25/`
(renamed the file prefix).
- Updated the decorator from `@RegisteredInstanceCommand('2.24.0', ...)`
to `('2.25.0', ...)`.
- Updated the import in `instance-commands.constant.ts` to the new
relative path and reordered both the import and the array entry to sit
after the 2-24 commands.

The timestamp (`1784820332810`) and command logic (`up`/`down`) are
unchanged.

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

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23285?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-07-24 15:32:33 +00:00
Abdullah. 0d876eb714 fix: lift axios/tar/brace-expansion/body-parser across app lockfiles (Dependabot) (#23267)
## Summary

Sweeps the **twenty-apps lockfiles** for this week's advisory wave:
recursive `yarn up` for **axios, tar, brace-expansion, body-parser** in
each of the 12 apps with open Dependabot alerts (hello-world, postcard,
self-hosting, twenty-partners, call-recorder, people-data-labs,
twenty-discord, twenty-exa, twenty-fireflies, twenty-last-contact,
twenty-linear, twenty-slack).

All moves fit the declared ranges (apps carry these transitively via
`twenty-sdk`, whose `axios ^1.16.0` and deep tar/brace chains are
carets), so the diff is **lockfile-only** across all 12 manifests - no
resolutions, no `package.json` changes. axios -> 1.18.x, tar -> 7.5.20
(critical GHSA-23hp-3jrh-7fpw chain), brace-expansion -> 1.1.16 / 2.1.2
/ 5.0.7, body-parser -> 1.20.6 / 2.3.0.

The second commit narrows scope to apps only: the twenty-server fixture
projects (seed-dependencies, common-layer-dependencies) move to a
dedicated PR because seed-dependencies' yarn.lock is checksum-coupled to
`DEFAULT_YARN_LOCK_CHECKSUM` in
`get-default-application-package-fields.util.ts`; it also drops
accidentally committed `.yarn/install-state.gz` artifacts.

## Deliberately not covered

- **sharp**: every path is minor-locked at `^0.34.5` (including
twenty-sdk latest) - separate PR bumping twenty-sdk's range.
- **react-router / react-router-dom**: no fixed release on the 6.x line
(fix is the v7 major); tracked separately.

## Verification

- Vulnerable-version scan across all 12 lockfiles: no axios <1.18, tar
<7.5.19, brace-expansion below 1.1.16/2.1.2/5.0.7, or body-parser below
1.20.6/2.3.0 remains.
- `yarn install --immutable` passes in each app.
- All fix versions clear the 3-day npm age gate.
2026-07-24 15:21:32 +00:00