f834b020b668121ea36139effd855f8b3aa25cb2
13913 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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> |
||
|
|
88f20a3731 |
i18n - translations (#23352)
Created by Github action --------- Co-authored-by: github-actions <github-actions@twenty.com> |
||
|
|
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. |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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.
|
||
|
|
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.
|
||
|
|
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. --> |
||
|
|
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. --> |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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> |
||
|
|
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> |
||
|
|
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. --> |
||
|
|
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> |
||
|
|
a94f2443b3 |
i18n - translations (#23328)
Created by Github action --------- Co-authored-by: github-actions <github-actions@twenty.com> |
||
|
|
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> |
||
|
|
8326fd186f |
i18n - docs translations (#23324)
Created by Github action Co-authored-by: github-actions <github-actions@twenty.com> |
||
|
|
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> |
||
|
|
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. --> |
||
|
|
0b44864f5f |
i18n - translations (#23315)
Created by Github action --------- Co-authored-by: github-actions <github-actions@twenty.com> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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. |
||
|
|
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> |
||
|
|
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. -->
|
||
|
|
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. -->
|
||
|
|
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 |
||
|
|
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. --> |
||
|
|
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. --> |
||
|
|
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. |
||
|
|
7067f6ef88 |
fix: honor agent rolePermissionConfig in record CRUD (#23248)
## Summary - Agent tools were built with the agent’s `rolePermissionConfig`, but record CRUD ignored it and re-resolved permissions from `authContext` (app `defaultRoleId`) - CRUD services now pass `rolePermissionConfig` through `CommonApiContextBuilder` and the common query runner, so repository access matches the agent role - Workflow/chat paths already use the same role for auth and `rolePermissionConfig`, so their behavior should be unchanged <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23248?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. --> |
||
|
|
70e1e94e55 |
i18n - translations (#23288)
Created by Github action <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23288?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> |
||
|
|
6cc7ed7570 |
Make solo tabs first-class: derived presentation, native editing, unified widget header (#23109)
## Why
Full-page record tabs (Timeline, Tasks, Notes, Files, Emails, Calendar,
Flow) were encoded by storing a `CANVAS` layout mode. That made them a
separate species: editing one didn't feel native (no drag handles, no
way to add a second widget, the tab couldn't adapt), and the widget
pipeline was full of `layoutMode === CANVAS` branches.
This PR replaces the stored mode with two derived rules and one unified
header grammar:
> **Presentation is derived from content, never stored.**
> A list tab with exactly **one widget** renders it **solo**
(full-bleed, it owns the tab). Anything else is a **stack** of boxed
cards. **Edit mode always shows the stack structure.**
No widget taxonomy, no per-type branches: any lone widget owns its tab.
## What
**Presentation model**
- `getTabPresentation({ widgets, layoutMode, isInEditMode })`: solo iff
a list tab has exactly one widget in view mode; grid tabs (dashboards)
and edit mode are always stacks. The pinned left panel is always a
column (a surface rule, not a widget rule).
- Solo view rendering is identical to the old CANVAS rendering
(container height, internal scroll).
- Stacked widgets in the main tab area get one bounded slot rule
(`max-height` + own scroll) so no widget swallows the tab;
pinned/side-column stacks keep their flowing behavior. This only binds
on user-composed mixed tabs, which could not exist before.
**Native editing (the point of the PR)**
- Every record-page tab is edited through the same vertical-list editor:
drag handle, reorder, remove, add widget. Add a second widget to a
Timeline tab and it becomes a stack; remove back down to one and it's
solo again. Nothing is stored, nothing to migrate.
- Fixes the stuck-drag bug found while testing the preview: widgets
publishing header info republished a fresh object on every render
(activity cards build their action from non-memoized hook returns), and
since the widget chrome reads that state above the widget content, any
tab with an activity card sat in an infinite render loop. The loop
starved React's transition lane, which dnd-kit's drop teardown waits on,
so the drag clone and drop outlines froze on screen after a drop. The
header hook now republishes only on real value changes and routes
onClick through a stable wrapper, so callers need no memoization. The
page-layout drag provider also disables the Feedback drop animation so
clone cleanup is synchronous at drop time.
**Unified widget header API**
- A widget's content can publish header info to its chrome via
`usePublishWidgetHeaderInfo({ count, primaryAction })`: a count rendered
in grey next to the title, and a primary action (icon button with
accessible name) on the right in view mode. Instance-scoped state keyed
by widget id, so third-party widgets (front components) can use the same
seam later; the hook no-ops outside a page layout (stories, previews)
and is safe to call with inline, non-memoized values.
- A solo widget's header only appears when the widget published
something: the tab label already names it, so a bare title row adds
nothing. Timeline/Flow tabs stay exactly as today.
- Emails, Tasks, Notes, Files, Calendar publish their count (query
totals, not loaded-page lengths) and action (Compose, New task, New
note, Add file) and stop rendering internal title rows ("Inbox 12", "All
5"): exactly one header per widget everywhere, same grammar.
`ComposeEmailButton`, `AddTaskButton` and the title/button plumbing in
`NoteList`/`AttachmentList`/`TaskList` are deleted.
**Object-aware tabs**
- The hardcoded `SYSTEM_OBJECT_TABS` title allowlist is gone. A tab
renders based on whether the target object supports its widgets: widgets
that read through a relation (Tasks, Notes, Files, Timeline) require the
relation field to exist and be active, while Emails and Calendar
aggregate through the messaging timeline, so a missing participants
relation is fine (Company) and a deactivated one is an explicit opt-out.
System objects on the shared default layout keep exactly Home +
Timeline, now by derivation instead of hardcoded titles.
**Data cleanup**
- Seeds (frontend defaults, server standard template, `twenty app`
scaffolder, docs) write `VERTICAL_LIST`;
`PageLayoutTabLayoutMode.CANVAS` is `@deprecated`, kept read-only for
layouts persisted before this change (they render correctly through the
derivation; no data migration, by design: an in-place flip can't pass
the widget-position/tab-layoutMode validator atomically, and it isn't
needed).
- Locale catalogs are intentionally untouched: the i18n pipeline
extracts and translates the new header labels on main; they fall back to
their English source until then.
## Deliberate view-mode changes (approved)
- A lone widget of any type now owns its tab full-bleed: lone Fields tab
(mobile/side panel), lone rich-text Note tab, lone chart, and the
message-thread page lose their card box.
- Activity tabs show the unified header (title, grey count, + action)
instead of their internal "Inbox 12"-style rows.
Everything else is pixel-parity, including solo scroll behavior and
dashboards.
## Test plan
- `nx typecheck twenty-front` / `twenty-server`: clean; oxlint/oxfmt on
the changeset: clean
- 239 suites / 1474 tests across page-layout, activities, side-panel
pass, including new tests for `getTabPresentation` (count-based,
edit-mode override) and `usePublishWidgetHeaderInfo` (publish, cleanup
on unmount, no-op outside a widget, referential stability across
re-renders with inline actions, latest-onClick wrapper)
- `getTabsRenderableForTargetObject` tests covering missing vs
deactivated relations, Emails/Calendar without a participants relation,
and non-relation widgets
- Stuck-drag repro verified fixed end to end against a local stack with
an instrumented dnd-kit: before the fix the affected tab committed ~65
renders/second at idle and drops never tore down; after it, idle commits
are flat and every drop cleans up
|
||
|
|
0bba75dd18 |
fix(ai-chat): stop duplicate tool_use ids from bricking threads (#23277)
## Issue Some AI chat threads become permanently broken. Every turn fails with an Anthropic 400: messages.5.content.1: tool_use ids must be unique The error is on the message *history*, so once a thread is in this state every subsequent turn fails too, not just the one that triggered it. It surfaces most visibly when aborting a thread and continuing it, but the abort is incidental: it just replays the already-corrupted history. ## Root cause A single tool call gets persisted as **two message parts sharing one `toolCallId`**. Confirmed in the DB for the affected thread, one assistant message held: - `tool-extract_json_paths` and `dynamic-tool`, both `toolu_01FXxxfBYP27NZEvA6AZ3AJc` - `tool-search_output` and `dynamic-tool`, both `toolu_01VqnFrYdGCERAc2dbG3zAyx` On the next turn `convertToModelMessages` turns each pair into two `tool_use` blocks with the same id, which Anthropic rejects. ## Why it happens It is not two concurrent calls. It is one call the AI SDK classifies inconsistently across its own stream chunks. The chat only exposes a small set of directly-callable tools (`execute_tool`, `learn_tools`, `load_skills`, `ask_questions`, plus native/preloaded ones). Registry tools like `extract_json_paths` and `search_output` are reachable only through `execute_tool`. When the model shortcuts that and calls one directly, the name is not in the active `ToolSet`, and the SDK does this: 1. On `tool-input-start`, `dynamic` is derived from the tool set: `tools[name]?.type === "dynamic"`. The tool is absent, so `dynamic: false`, and a **static** `tool-<name>` part is created. 2. On finalization the unknown tool throws `NoSuchToolError`. `repairToolCall` intentionally skips name errors (`return null`), so the SDK re-emits the call with a hardcoded `dynamic: true`. That error routes to the **dynamic** path and creates a second `dynamic-tool` part with the same id. The UI-message builder keeps static and dynamic tool parts in separate buckets, each searched by `toolCallId` independently, so the mid-call static-to-dynamic flip produces two parts for one call. Both persist and break the next turn. ## Fix Two independent read/convert-path passes, both in `sanitizeMessagePartsForModel` in `chat-execution.service.ts`, running before `convertToModelMessages`: 1. **`finalizeDanglingToolParts`** now dedupes tool parts by `toolCallId` (first-wins), keeping the `input-streaming` filter ahead of the dedup so a leading streaming duplicate can't strand the call. Because it runs before conversion and on the write paths too, already-corrupted threads are un-bricked on their next turn with no migration. 2. **`guideUncallableToolCallsToMetaTool`** addresses the behavior that caused it: when the model calls a tool that is not directly callable, it appends the `learn_tools` -> `execute_tool` flow to that failed tool result, so the model reads how to reach the tool. Detection is structural (a failed tool part whose name is not in the active tool set), not string-matched against the SDK's error wording. Unit tests added for both. Typecheck, lint, and the suite pass. Note: the stale duplicate rows already in the DB are harmless (deduped on every read); no migration is required. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23277?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. --> |
||
|
|
9aaa5b7778 |
i18n - translations (#23284)
Created by Github action --------- Co-authored-by: github-actions <github-actions@twenty.com> |
||
|
|
482b88bcda |
i18n - translations (#23283)
Created by Github action --------- Co-authored-by: github-actions <github-actions@twenty.com> |
||
|
|
308e4de7a6 |
TDD: twenty-ui render coverage in the front-component sandbox — golden tests cataloging every sandbox gap (fully green) (#23203)
## What — TDD coverage layer, no fixes Renders every twenty-ui component export inside the real front-component sandbox (opaque-origin iframe + remote-dom worker, real SDK esbuild pipeline) through the existing Playwright-backed storybook vitest suite, and expresses **every sandbox gap as a golden known-failure test**. This PR deliberately ships **zero fixes** and is **fully green (267/267)**: each known issue has a test asserting the exact current broken behavior. A golden test fails on regression (an unexpected component starts failing) AND on fix (the documented failure disappears) — so every future fix must flip its golden assertion to the strict one, whose acceptance criteria are spelled out in each story's comment. - One gallery fixture per submodule under `src/__stories__/twenty-ui-gallery/` (~150 components) - `component-gallery.tsx`: each component renders inside its own error boundary; failures are aggregated with component name and error message (`data-failed-messages`) so one crash cannot mask the rest - Behavior-level stories that a fake fix cannot pass (MutationObserver must actually fire; a link click must reach `hostApi.navigate`) - `vitest.config.ts` re-export so the storybook in-UI "Run tests" button works ## Current state: 267/267 green — mergeable; 18 golden tests encode the catalog below ## Failure catalog (each entry = golden tests asserting today's broken behavior) ### 1. `MutationObserver.observe is not a function` — 6 failing tests `@remote-dom/polyfill` ships `MutationObserver` as an empty class: construction succeeds, the first `.observe()` call crashes. | Failing test | twenty-ui export(s) | Call site | |---|---|---| | Input React + Preact | `Radio`, `RadioGroup`, `CardPicker` | `@base-ui/react` field internals observe form state | | Surfaces React + Preact | `AppTooltip` | twenty-ui observes `document.body` to track anchors | | Worker Platform APIs: Mutation Observer React + Preact | (behavior test) | asserts the observer actually fires on a React-driven insertion — a no-op stub cannot pass it | Validated fix (branch history, commit 94b96d01e79e): implement real mutation semantics **locally in the worker** by tapping the same `@remote-dom/polyfill` hooks (`insertChild`/`removeChild`/`setAttribute`/`removeAttribute`/`setText`) remote-dom already uses to mirror mutations to the host. The worker tree is the source of truth — no host bridge needed. ### 2. `getComputedStyle is not a function` — 4 failing tests The remote-dom `Window` polyfill does not implement `getComputedStyle`; `@base-ui/react` Collapsible calls it on open. | Failing test | twenty-ui export(s) | |---|---| | Layout React + Preact | `AnimatedEaseInOut`, `AnimatedExpandableContainer` | | Json Visualizer React + Preact | `JsonTree`, `JsonArrayNode`, `JsonObjectNode`, `JsonNestedNode` (all via `JsonNestedNode`'s Collapsible) | Validated fix (commit 7af577054fd9): `getComputedStyle` is synchronous and layout only exists host-side, so a bridge is impossible — an inert-but-valid stub (`0s` durations, `none` animation names) is the honest terminal state. ### 3. No router context in the sandbox — 5 failing tests react-router's `Link` reads `NavigationContext`, which has no provider inside the worker: `Cannot destructure property 'basename' of 'useContext(...)' as it is null`. | Failing test | twenty-ui export(s) | |---|---| | Navigation React + Preact | `RawLink`, `UndecoratedLink` | | Data Display React + Preact | `LinkChip` | | HostApi: Router Link | acceptance test — the link must render AND its click must reach `hostApi.navigate` | Validated fix (commit 221c9c353dc8): SDK build plugin wraps the component tree in a low-level react-router `<Router>` whose custom navigator forwards `push`/`replace` to the SDK `navigate` host API (NOT a MemoryRouter, which would render links but swallow clicks into in-memory history). Falls back to a pass-through provider when the app has no react-router-dom dependency. **Security finding bundled in that commit:** clicking a mirrored `<a>` performs a native host-page navigation before the async worker round-trip can `preventDefault` — a front component can escape the sandbox by rendering a link. Fix: apply the renderer's existing preventDefault-then-forward guard (already used for form submits) to anchor clicks, keeping `target="_blank"` native. The Router Link story's flip-to acceptance assertions (click must reach `hostApi.navigate`) will hard-crash the vitest browser the moment links render without this guard — by design. ### 4. Open `Modal` hangs the React runtime — 1 failing test base-ui Dialog portal with `isOpen` never commits under the React runtime (no error thrown); works under Preact. | Failing test | twenty-ui export | |---|---| | Modal Open React (Modal Open Preact passes) | `Modal` | Isolated in its own fixture so the hang cannot mask the rest of the surfaces gallery (which keeps a closed Modal for mount coverage). Root cause not yet identified — first experiment: portal into a worker-owned container instead of the polyfilled `document.body`. ### 5. monaco cannot load in the sandbox — 2 failing tests `@monaco-editor/react` lazy-loads monaco via script injection, impossible in the polyfilled worker DOM (opaque-origin CSP, no script loading). The CodeEditor wrapper mounts; monaco's `onMount` never fires. | Failing test | twenty-ui export | |---|---| | Code Editor React + Preact | `CodeEditor` | Probably wont-fix in the worker: if front components need a code editor, the path is a host-rendered privileged component. The failing test is the documentation. ### Also worth knowing (no failing test possible yet) - `ResizeObserver` / `IntersectionObserver` / `matchMedia` are absent in the worker: components mount without them today only because nothing crashes at mount — behavior like popup auto-resize and `useIsMobile` is silently wrong. Real fix is a host bridge (async observers on the component's own mirrored elements only — never the host document). Behavior tests should land with that fix. - `Icon`/`IconsProvider`/`useIcons` are excluded from galleries by choice: they dynamic-import the multi-MB Tabler catalog; direct icon imports are the supported pattern in bundled front components. ## Iteration plan Each fix is one commit + one set of stories flipping green, in suggested order: 1. Worker-local MutationObserver (6 tests) — cherry-pick base: 94b96d01e79e 2. `getComputedStyle` + observer/matchMedia stubs (4 tests) — cherry-pick base: 7af577054fd9 3. Router provider + anchor click guard (5 tests) — cherry-pick base: 221c9c353dc8 4. Open-Modal React hang investigation (1 test) 5. CodeEditor: decide wont-fix + keep the failing story or convert to a documented skip (2 tests) ## How to run ``` npx nx run twenty-front-component-renderer:storybook:prebuild cd packages/twenty-front-component-renderer npx vitest run --config vitest.storybook.config.ts --project storybook ``` |
||
|
|
cbcfba0de2 |
feat(workflow): dedicated updateWorkflowVersionTrigger mutation + close version CRUD holes (#23207)
Prerequisite for the workflow-core soft-ref migration: every
`workflowVersion` content write must go through a dedicated,
draft-guarded server mutation so it can later be wrapped in a
transactional core mirror. This closes the generic-CRUD holes that let
writes bypass that path.
## Part A - dedicated `updateWorkflowVersionTrigger` mutation
The builder saved a version's trigger through generic
`updateOneWorkflowVersion` - the only content write not going through a
dedicated mutation. Added:
- Server: `updateWorkflowVersionTrigger(input: { workflowVersionId,
trigger })` resolver +
`WorkflowVersionStepWorkspaceService.updateWorkflowVersionTrigger`,
draft-guarded via `getValidatedDraftWorkflowVersion` then
`updateWorkflowVersionStepsAndTrigger` (reuses existing write logic).
- Front: `useUpdateWorkflowVersionTrigger` now calls the dedicated
mutation instead of `useUpdateOneRecord`.
## Part B - restrict generic `updateOneWorkflowVersion`
`validateWorkflowVersionForUpdateOne` previously allowed writing
`trigger`, `position`, `workflowId` (re-parenting),
`coreWorkflowVersionId`, and let `steps: null` slip through on a draft.
It now rejects any update that sets `steps`, `trigger`, `status`,
`workflowId`, or `coreWorkflowVersionId`, or that clears the `name`,
while still allowing a plain rename. (A name-only allowlist was tried
first but blocked legitimate renames - at the pre-hook the generic
update payload is not single-key - so it was replaced by this denylist,
verified live.)
## Part C - close the destroy/restore hole
`workflowVersion` had no `destroyOne/destroyMany/restoreOne/restoreMany`
query hooks, so a caller with object permission could hard-destroy any
version (including active) or resurrect one with no validation. Added
pre-hooks that forbid all four via the API ("Method not allowed"),
matching the existing forbidden generic mutations (`createOne`,
`deleteMany`, ...). Rationale: there is no legitimate API use for
standalone version destroy/restore - retention purging happens through
the trash-cleanup cron (internal, not hook-gated) and restore happens
through the workflow-restore cascade or create-draft-from-version.
## Tests
Integration specs that set a trigger through the generic mutation were
migrated to the new `updateWorkflowVersionTrigger` mutation (new
`update-workflow-version-trigger.util.ts`). Unit test for the front hook
updated.
## Verification
- `twenty-server` + `twenty-front` typecheck: clean.
- oxlint + oxfmt on all changed files: clean.
- `graphql.ts` regenerated for the new mutation; its types match the
server DTOs exactly. Local `graphql:generate` introspects a running
server, so it only succeeds against a server built from this branch - CI
regenerates against the PR server and verifies.
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23207?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>
|
||
|
|
25b0b2601f |
Replace admin app rollout buttons with upgrade-application CLI command (#23212)
## What Removes the two rollout buttons from the admin application detail page and replaces the upgrade flow with a CLI command that can be run directly from a server or worker pod. Also restructures application stop into its own module with a kill switch CLI command, and surfaces stopped apps in workspace settings. ### Removed - "Install on all workspaces" button (General tab, `SettingsAdminApplicationRegistrationGeneralToggles`), its confirmation modal and tooltip - "Upgrade existing installations" button (`SettingsApplicationRegistrationGeneralStats`), its confirmation modal and batch size input - `backfillApplicationInstallation` and `upgradeRegistrationApplications` admin GraphQL mutations and their frontend documents / generated types - `BackfillApplicationInstallationJob` (its only trigger was the removed mutation); `UpgradeApplicationsJob` is kept since the auto-upgrade flow still enqueues it Per review, the "install on all workspaces" flow is dropped without a CLI replacement for now; a dedicated command will be added when needed. ### application:upgrade command Located in `application-upgrade/commands`, registered in `ApplicationUpgradeModule`: ``` yarn command:prod application:upgrade \ --application-registration-universal-identifier <universalIdentifier> \ [--batch-size 5] \ [--workspace-id <id> --workspace-id <id2>] \ [--workspace-count-limit 10] \ [--dry-run] [--yes] ``` - `--workspace-id` (repeatable) restricts the upgrade to specific workspaces; `--workspace-count-limit` caps how many installations are upgraded (max 50, for canary rollouts) - `--batch-size` and `--workspace-count-limit` are validated as positive integers, max 50 - `--dry-run` reports how many (and which) workspaces would be upgraded, without upgrading - Without `--dry-run`, a confirmation prompt shows the app, target version and impacted workspaces; the run then executes exactly the confirmed set; `--yes` skips the prompt for non-interactive usage The upgrade plan is computed by a new `ApplicationUpgradeService.findApplicationsToUpgrade`, and batches run through a new `upgradeApplications` method — both reused by `upgradeAllApplications`, so the auto-upgrade job path is unchanged. ### Application kill switch (per review) Global mechanism only — a per-workspace stop had no demonstrated operational need and added a Redis key format, execution branching, CLI options and tests; an isolated workspace issue can be handled directly in the DB or Redis with the same effort. - `ApplicationStopService` moved to a dedicated `application-stop/` folder with its own `ApplicationStopModule` (imported and re-exported by `ApplicationModule`) - `stop` / `remove` methods that enable or clear the Redis-backed global kill switch; the logic function executor checks it before executing - `application:kill-switch` command with a positional action, confirmation prompt (shows the installation count) and `--yes` bypass: ``` # Enable the kill switch (stop is the default action) yarn command:prod application:kill-switch stop -u <universalIdentifier> [-y] yarn command:prod application:kill-switch -u <universalIdentifier> # Remove the kill switch yarn command:prod application:kill-switch remove -u <universalIdentifier> [-y] ``` ### Stopped apps surfaced in workspace settings (per review) - Dedicated `isApplicationStopped(applicationUniversalIdentifier)` query backed by the kill switch, fetched with `network-only` policy solely by the application detail page — listing applications triggers no extra Redis reads - Application detail page shows a danger banner when the app is stopped: "We are currently encountering issues with this app, its behavior may be degraded while we work on a fix." ## Test - `npx nx typecheck twenty-server` / `npx nx typecheck twenty-front` pass - `npx nx lint:diff-with-main` passes for both packages - `application-stop.service.spec.ts` covers stop, remove, caching and fail-open behavior - Verified end to end locally: ran the kill switch command on a seeded workspace and confirmed the banner renders on the app detail page (screenshot shared separately) |
||
|
|
940d150775 |
chore(self-hosting): upgrade to latest twenty CLI tooling (#23279)
## What Upgrades the internal `self-hosting` app to the latest Twenty CLI tooling, bringing it in line with the other actively-maintained apps in the monorepo. - `twenty-sdk`: `2.19.0-alpha.1` → `2.23.0-alpha.2` (dependency + devDependency) - `twenty-client-sdk`: `2.19.0-alpha.1` → `2.23.0-alpha.2` (dependency + devDependency) - `engines.twenty`: `>=2.19.0` → `>=2.23.0` - Regenerated `yarn.lock` to match. The `twenty` CLI ships inside `twenty-sdk`, so this pulls the app onto the same CLI version every other recently-updated app (call-recorder, people-data-labs, twenty-partners, real-estate, last-contact, postcard) already uses. ## Verification - `yarn typecheck` passes - `yarn lint` passes (0 warnings, 0 errors) - `yarn test:unit` passes (3/3) Integration tests (`yarn test`) require a running Twenty server and were not run in this environment. ## Notes Scope is limited to the CLI/SDK tooling. Framework deps (React 18) were left untouched since they are not tied to the CLI version and vary across apps. --- _Generated by [Claude Code](https://claude.ai/code/session_01HUPkxerLhethaP9Lw6qDyd)_ <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23279?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. --> |
||
|
|
86d0e15a6a |
i18n - translations (#23281)
Created by Github action --------- Co-authored-by: github-actions <github-actions@twenty.com> |
||
|
|
d1c6b8ee72 |
Show relation record labels instead of UUIDs in dashboard charts (#23163)
https://github.com/user-attachments/assets/d012a013-2c90-49a1-a27e-b8e4b684a84f Charts grouped by a relation without a sub-field rendered raw FK UUIDs on axis ticks, legends and tooltips. The server now batch-resolves the grouped record ids to their label identifier through a permission-scoped query and formats every bucket with the record's display name. Unresolvable records (deleted or not readable) render as Unknown and their ids are stripped from the response payload. Same-named records get an ordinal suffix so their buckets don't merge. Covers bar, line and pie, plain and morph relations. ```mermaid flowchart TD A["Dashboard widget load"] --> B["Chart data service<br/>(bar / line / pie)"] B --> C["executeGroupByQuery:<br/>group by relation FK id,<br/>ORDER BY target label identifier,<br/>scoped to source object permissions"] C --> D["filterOutEmptyChartBuckets"] D --> E{"Bare relation axis?<br/>(no sub-field)"} subgraph RL["ChartRelationLabelService.resolveRelationLabels"] direction TB G1["Collect distinct record ids<br/>per target object"] --> G2["Batch SELECT label identifier columns,<br/>scoped to TARGET object permissions"] G2 --> G3["buildRawLabelByRecordId:<br/>display name per record"] G3 --> G4["buildUniqueRelationLabels:<br/>suffix duplicates, Unknown for unresolved"] end E -- No --> H["formatDimensionValue per bucket"] E -- Yes --> G1 G4 --> H H --> I["Strip unresolved ids from<br/>formattedToRawLookup"] I --> J["Chart DTO to frontend"] ``` The chart settings sub-field dropdown gains a Record option to group by the related record itself, and now only offers sub-fields the backend accepts (system fields like a workspace member's updatedBy were selectable but rejected at query time). Chart-data errors are now logged server-side. Also fixes two latent bugs on this path: sorting a bare-relation chart by field threw `Cannot orderBy unknown field: agentId`, and the pie chart truncated slices before sorting. The AI dashboard tool guidance and the seeded dashboards no longer force the sub-field workaround. The group-by query orders buckets by the related record's label identifier at the database level (the engine now accepts ordering by a target field when grouping by its id), so with more than 100 distinct related records the surviving buckets match the label order. |
||
|
|
b036d67ec9 |
Configure async ClickHouse inserts for pageview events (#23274)
## Context Pageview tracking goes through the `trackAnalytics` mutation on the metadata API and is persisted through the unified event pipeline before the mutation resolves. ClickHouse inserts already use: ```text async_insert = 1 wait_for_async_insert = 1 ``` `async_insert` lets ClickHouse buffer and batch small inserts, but `wait_for_async_insert = 1` still keeps the API request open until that buffer is flushed successfully. For sparse pageview inserts, the buffer timeout can therefore account for most of the request duration and contribute to metadata API tail latency. ## What this changes - Adds a named `ClickHouseService.insert` option for overriding `async_insert_busy_timeout_max_ms`. - Caps the pageview buffer wait at 100 ms. - Keeps `wait_for_async_insert = 1`. - Leaves workspace, object, usage, application-log, and other event inserts on the existing default timeout. ## Why this approach This removes the avoidable buffer wait from the pageview request path without changing the delivery guarantees of the event pipeline. In particular, this does **not** use `wait_for_async_insert = 0` or fire-and-forget writes. The API still receives an acknowledgement only after ClickHouse flushes the pageview successfully, and insert/schema errors still propagate through the existing handling. The 100 ms value caps only the batching wait. It does not impose a 100 ms deadline on the complete ClickHouse request. ## Expected impact - Lower ClickHouse span duration for pageview tracking. - Lower tail latency for metadata API requests that emit pageviews. - No behavior or durability change for other event types. The trade-off is that pageviews may be flushed in smaller batches. The setting remains scoped to the pageview table so higher-value event streams keep their current batching behavior. ## Testing - Added coverage for the optional ClickHouse busy-timeout setting. - Added coverage verifying that only pageview inserts receive the 100 ms override. - Existing insert failure/retry behavior remains covered. - `twenty-server` typecheck passes. - Focused test result: 23 tests passed. ## Post-deploy verification - Compare pageview ClickHouse span p95/p99 before and after deployment. - Compare metadata API p95/p99. - Check ClickHouse asynchronous-insert failures. - Watch ClickHouse part creation and merge pressure for unexpected growth. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23274?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. --> |