69a7e614ffa1f8a5f8d8ea791dfdd57dbcebe959
4956 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
69a7e614ff |
fix: restore isCustom gate in metadata label resolvers (#21432)
## Context #21228 removed the stored `isCustom` column and, with it, the `isCustom` early-return in `resolveObjectMetadataStandardOverride` / `resolveFieldMetadataStandardOverride`, on the assumption that falling through the `standardOverrides` checks was equivalent. It isn't: custom object/field labels now reach the Lingui lookup. A custom label that collides with a standard catalog string (e.g. a custom field labeled "Status") gets translated for non-English locales against the user's intent, and every other custom label pays a hash + catalog miss — and, in production, an "Uncompiled message detected" warning (#21415) — on each metadata resolution. ## Fix Restore the gate. `isCustom` is no longer stored, so call sites that build the resolver input from flat entities (dataloader, minimal-metadata, view controller, command-menu-item navigation context) derive it via `belongsToTwentyStandardApp`; GraphQL resolvers keep passing DTOs, which already carry the derived value. ## Testing - Unit tests for both resolvers, including a new regression test: a custom label matching a standard catalog entry is returned verbatim, Lingui never called. --------- Co-authored-by: Weiko <corentin@twenty.com> |
||
|
|
cfb9772179 |
feat(server): convert view to overridable entity (#21436)
## Context Every entity created as a side effect of object creation must support the overridable pattern (`isActive` + `overrides` + override routing) before we can re-own side effects to their true application. Starting with View. viewField, viewFieldGroup, pageLayoutTab and pageLayoutWidget already extend `OverridableEntity`. This PR brings `view` to the same pattern. ## What this does - `ViewEntity` now extends `OverridableEntity<ViewOverrides>` (adds `isActive` boolean + `overrides` jsonb). All editable view properties are overridable; the 3 fieldMetadata foreign keys are converted to/from universal identifiers like viewField's `viewFieldGroupId`. - **Update**: mutations on a view not owned by the caller (e.g. standard views like "All Companies") are written into `overrides` instead of mutating the row. Reads merge overrides in the DTO. - **Delete/destroy**: views not owned by the caller are deactivated (`isActive = false`) instead of deleted. ~~- **INDEX invariant**: `key = INDEX` views can only be created via object-creation side effect. The API now rejects creating, deleting or destroying INDEX views (object-deletion cascade is unaffected). This was not really needed for this migration but was flagged during implementation.~~ - **Front**: views with `isActive = false` are filtered out of the views selector. - Fast instance command adds the two columns (`2-12-instance-command-fast-...-view-overridable-entity.ts`). ## Notes - Custom (caller-owned) views behave exactly as before: direct updates, soft delete. - View-group side effects (kanban groups) are computed on the override-merged view so overridden `mainGroupByFieldMetadataId` works. |
||
|
|
41cdd83367 |
fix(ai): correct RICH_TEXT and MORPH_RELATION record filter operators (#21106)
## Problem
The AI find-records tool generates filter schemas via
`generateFieldFilterZodSchema`. `RICH_TEXT` currently shares the `TEXT`
case, so the agent is told it can use scalar text operators
(`like`/`ilike`/`startsWith`/`endsWith`/`eq`/…) directly on a rich-text
field.
But `RICH_TEXT` is a **composite** type (`markdown` + `blocknote`
sub-fields, see `rich-text.composite-type.ts`). Applying a scalar
operator to the composite root throws at query time:
```
ERROR [FindRecordsService] Failed to find records: Object person doesn't have any "ilike" field.
ERROR [FindRecordsService] Failed to find records: Sub field "ilike" not found for composite type: RICH_TEXT
```
`FindRecordsService` catches and returns `success: false`, so the agent
retries mid-turn — burning latency/tokens — and can **never** search
rich-text body content (note bodies, `about`, etc.).
## Fix
Give `RICH_TEXT` its own case in the filter-schema generator that
exposes the `markdown` and `blocknote` sub-fields, each carrying the
text operators — mirroring the existing composite patterns for `EMAILS`
(`primaryEmail`), `PHONES` (`primaryPhoneNumber`), `LINKS`
(`primaryLinkUrl`), `FULL_NAME`, and `ADDRESS`.
So the agent now emits:
```jsonc
{ "noteBody": { "markdown": { "ilike": "%onboarding%" } } } // valid composite sub-field filter
```
instead of:
```jsonc
{ "noteBody": { "ilike": "%onboarding%" } } // throws on composite root
```
This both **stops the throw** and **makes rich-text content actually
searchable** (the original intent). `TEXT` keeps its existing root-level
scalar operators unchanged.
## Test
Added `__tests__/field-filters.zod-schema.spec.ts`:
- `RICH_TEXT` routes pattern operators onto `markdown` / `blocknote`
- root-level scalar operators on `RICH_TEXT` are no longer accepted
- `TEXT` root-level operators unchanged
## Notes
- No DB/schema migration; render/tool-schema layer only.
- Reproduced against `twentycrm/twenty:latest`; the faulting code is
unchanged on `main` as of this PR.
---------
Co-authored-by: Rich Roberts <rich.roberts@talentpipe.ai>
Co-authored-by: Etienne <45695613+etiennejouan@users.noreply.github.com>
|
||
|
|
503c689f37 |
security: upgrade typeorm to 0.3.26 (CVE-2025-60542) (#21456)
## Context Retry of the typeorm upgrade that was pulled out of #21448 after CI showed "intermittently lossy metadata sync". **The investigation exonerated typeorm**: the postcard/seed failures were a pre-existing bug in `@ptc-org/nestjs-query-typeorm`'s batched relation paging (global LIMIT across parents) that scan-order luck had been hiding — reproduced byte-for-byte on typeorm **0.3.20** against a frozen repro DB. That bug is fixed in #21455, which this PR is stacked on (base branch = `charles/fix-nestjs-query-batch-relation-paging`; will retarget to main when it merges). ## Changes - typeorm `0.3.20` → `0.3.26` ([CVE-2025-60542](https://github.com/advisories/GHSA-q2pj-6v73-8rgj), MEDIUM). The CVE lives in TypeORM's MySQL path (`sqlstring`/`stringifyObjects`); Postgres-only Twenty never exercises it — this is scanner hygiene + staying current. - The local yarn patch (`PickKeysByType` + `DeleteResult.generatedMaps`) applies **verbatim** to 0.3.26 (verified against the pristine tarball) — renamed to `typeorm+0.3.26.patch`. - `WorkspaceRepository.query` restricted override adapted to the generic `query<T = any>()` base signature introduced in 0.3.24 (one-line change, still throws `RAW_SQL_NOT_ALLOWED`). - 0.3.26 ships `uuid ^11` natively → the scoped `typeorm/uuid` resolution from #21441 and its `//resolutions` comment clause (including the now-disproven "lossy sync" warning) are removed. ## Why we're confident this time The original failure signature was fully understood, not just retried: - On a frozen failing DB, **all fieldMetadata rows + workspace columns were intact** — only the batched metadata API read was truncated (`LIMIT 501` over 558 rows, no ORDER BY). - Same DB, typeorm 0.3.20: identical truncation, identical SQL → not a typeorm regression. - With #21455 applied: postcard install/uninstall stress loop **12/12 green on typeorm 0.3.26** (previously failed within 1–2 iterations), API returns 558/558 fields. ## Verification - `npx nx typecheck twenty-server` — clean - Full `twenty-server` unit suite — green (5651 passed) - `group-by-resolver` integration suite — 19/19 on a fresh 0.3.26-seeded test DB - Postcard app-sync stress loop — 12/12 on this exact stack - Lockfile: typeorm 0.3.26 + new `sql-highlight` dep, `esbuild`/uuid entries untouched |
||
|
|
d75685b8dc |
fix(metadata): nestjs-query batched relation queries truncate results across parents (#21455)
## TL;DR
The metadata API silently drops relation rows whenever a batched
relation query exceeds the requested page size. A dev-seeded workspace
already has **558 fieldMetadata rows across 31 objects**, so
`objects(paging:{first:50}) { fields(paging:{first:500}) }` executes:
```sql
SELECT DISTINCT ... FROM core."fieldMetadata" fields
WHERE workspaceId = $1 AND objectMetadataId IN (...31 ids...)
LIMIT 501 OFFSET 0 -- no ORDER BY
```
…and returns exactly **501 of 558** fields — ~57 rows dropped, and
*which object loses which field is scan-order-dependent*. This is what
made `example-app-postcard` CI flap with "PostCard object missing field
X" (different X per run).
## Root cause
`@ptc-org/nestjs-query-typeorm`'s `batchQueryRelations` (the DataLoader
batch path behind every `@CursorConnection`) applies the **per-parent**
page size as a **single global LIMIT** on the batched query, then groups
rows per parent in memory. Any batch whose combined relation rows exceed
`first + 1` truncates arbitrary parents. This affects production
metadata reads, not just CI — any workspace with enough fields/objects
loses rows in `objects.fields`-style connections.
## Fix
Yarn patch on `@ptc-org/nestjs-query-typeorm@9.4.0` (same vehicle as the
existing `nestjs-query-graphql` patch):
- `RelationQueryBuilder.batchSelect`: only apply LIMIT/OFFSET when the
batch has a **single parent**; multi-parent batches stay **bounded**
with `parents × (offset + limit)` — the upper bound a correct per-parent
pager can ever need, so it cannot wrongly truncate while still guarding
against unbounded fetches on high-cardinality relations;
- `batchQueryRelations`: enforce paging **per parent** by slicing after
`mapRelations` (preserves the `first + 1` hasNextPage probe semantics).
## Verification
- On a frozen repro DB (postcard installed, 558 fields): unpatched
returns 501 fields with `postCard` missing `deliveredAt`; patched
returns **558/558** with the full `postCard` field set. Reproduced
identically on typeorm 0.3.20 and 0.3.26 — pre-existing bug, **not** a
typeorm regression (this unblocks the typeorm upgrade that was reverted
from #21448).
- Postcard install/uninstall stress loop: unpatched fails within 1–2
iterations; patched **12/12 green**.
- `npx nx typecheck twenty-server` clean, full `twenty-server` unit
suite green (5651 passed).
## Related
#21435 chases the **same CI symptom** (postcard randomly missing a
freshly synced field) at a different layer — a workspace-cache write
racing invalidation. The two are complementary: the repro behind this PR
survives a **cold server restart + `redis-cli FLUSHALL`** with all rows
intact in Postgres, which no cache race can explain — the truncation
happens on the DB read itself (`LIMIT 501` over 558 matching rows,
captured via `log_statement=all`). Both fixes are likely needed for the
postcard job to be fully reliable.
## Notes
Worth upstreaming to `@ptc-org/nestjs-query` eventually; the proper
upstream fix is per-parent windowed pagination (`ROW_NUMBER() OVER
(PARTITION BY parentId)`), but the in-memory per-parent slice is correct
and proportionate at metadata-API scale.
|
||
|
|
d0884bd708 |
Fix missing datetime filter type (#21451)
Currently datetime fields are only typed to be filtered by string Add a proper typing to match gql filters ## Before <img width="750" height="492" alt="image" src="https://github.com/user-attachments/assets/ff3a5423-3bb0-4295-84c9-e404489354f6" /> ## After <img width="537" height="511" alt="image" src="https://github.com/user-attachments/assets/d8c8219f-b7de-41b0-96cb-5adbfda7a91d" /> |
||
|
|
0ac4f237c0 |
fix(server): stop redundant lambda rebuilds causing build-lock acquisition failures (#21442)
## Context `Lambda invocation failed for function '<id>' during build: Failed to acquire lock for key: lambda-build:<id>` fires ~1000 times/day in production. ## Root cause `LambdaExecutorManagerService.buildExecutor` re-checks `canSkip` inside the `lambda-build:<functionId>` lock, but the re-check reuses the `flatApplication` snapshot captured when the request started. `canSkip` depends on `!flatApplication.isSdkLayerStale`, so: 1. An app sync/install regenerates the SDK client and sets `isSdkLayerStale = true` 2. All in-flight executions of the function fail `canSkip` and queue on the lock 3. The first holder rebuilds and `markSdkLayerFresh` clears the flag in DB + workspace cache 4. Queued waiters can't see that fix — their in-memory snapshot still says stale — so **each waiter redoes the full rebuild serially** (download SDK archive, delete + republish layer, update function config, wait for update) 5. The lock is held back-to-back for minutes; everyone deeper in the queue exhausts the 120s retry budget and throws The local driver already handles this correctly (`LocalLayerManagerService` refreshes the flat application from the workspace cache inside its lock); the lambda driver missed it. ## Fix - Refresh `flatApplication` from the workspace cache inside the lock before re-checking `canSkip`, so waiters skip in ~100ms once the first holder finishes - Degrade gracefully on lock-acquisition timeout: re-check build status with fresh data and proceed with the invocation if the executor is already usable, instead of failing the run (introduces a typed `CacheLockAcquisitionError` so only that case is caught) ## Test plan - [x] `cache-lock.service.spec.ts` passes - [x] `lint:diff-with-main` + typecheck pass - [ ] Monitor `Failed to acquire lock for key: lambda-build:*` error rate in production after deploy |
||
|
|
184c4948d6 |
security: strip Node dev headers from images + lingui 5.9.5 (drops vulnerable esbuild) (#21448)
## Context
AWS Inspector flags the `prod-twenty` image (built from current main)
with 16 findings, and Dependabot alert 174 flags esbuild. This PR fixes
the OpenSSL scanner findings and the esbuild CVE. The typeorm bump
(CVE-2025-60542) was **pulled out of this PR** — see "typeorm status"
below.
## Changes
### Strip `/usr/local/include/node` from runtime stages
(`twenty-server`, `twenty-app-dev`)
15 OpenSSL CVEs (June 9 advisory, incl. CRITICAL CVE-2026-34182) are all
detected via **Node's bundled OpenSSL dev headers**: 3 GENERIC
`openssl/openssl` 3.5.6 detections per CVE at
`/usr/local/include/node/openssl/archs/linux-x86_64/{asm,asm_avx2,no-asm}/include/openssl/opensslv.h`.
The headers are only needed by node-gyp and native addons are compiled
in the build stages — nothing compiles at runtime. Dropping them clears
all 45 detection instances and permanently ends this class of finding
(third occurrence: 3.5.5 → 3.5.6 → 3.5.7). None of these CVEs are
reachable through Node (no CMS/PKCS#7 API, `pfx` is operator-supplied,
Node's QUIC uses ngtcp2, ASN.1 issues need ~2GB inputs).
**Follow-up (~June 17, 2026):** the `node` binary itself still
statically links OpenSSL 3.5.6 — invisible to the scanner after this PR
and unreachable in practice, but the real fix is bumping the pinned
`node:24-alpine` digest once the [announced June 17 Node.js security
releases](https://nodejs.org/en/blog/vulnerability/june-2026-security-releases)
ship a 24.x linking OpenSSL ≥ 3.5.7 (verify via
`deps/openssl/openssl/VERSION.dat` on the release tag — 24.16.0 is still
on 3.5.6). A dated TODO sits next to the cleanup in the Dockerfile.
### esbuild dev-server CORS CVE (Dependabot alert 174,
GHSA-67mh-4wv8-2f99)
`@lingui/cli@5.1.2` (pins `esbuild ^0.21.5`) was the last parent
resolving a vulnerable esbuild (≤ 0.24.2 lets any website send requests
to the dev server and read responses). Instead of a resolution override,
this bumps the lockstepped **lingui suite 5.1.2 → 5.9.5** (within-major;
lingui adopted `esbuild ^0.25.1` in 5.4.1), which:
- removes `esbuild@0.21.5` and all its platform packages from the
lockfile with no forced ranges;
- drops the `@lingui/core` lockstep resolution (its comment marked it
droppable on the next coordinated lingui bump — the tree now resolves a
single `@lingui/core@5.9.5`);
- `@lingui/swc-plugin` stays at `^5.11.0` (peers on `@lingui/core: 5`;
its 6.x line targets lingui 6).
**lingui 5.9.5 behavioral fallout handled here:**
- Translation functions now **throw without an active locale** (5.1.2
fell back silently). The global `i18n` singleton that backs server-side
`` t`…` `` calls only had a messages compiler set, never an activated
locale → activate the source locale in `I18nService.loadTranslations()`,
mirrored in the server jest setup (unit tests bypass Nest bootstrap).
- `msg`/`t` placeholders are now strictly typed (reject
`null`/`undefined`/`unknown`) → one server call site and 16 twenty-front
files adapted with minimal nullish-coalescing fixes that preserve
rendering.
- `.po`/compiled-catalog churn from the new extractor/compiler
(reference reordering, sorted keys — verified content-identical on
unchanged `.po` inputs) is intentionally not committed: the scheduled
i18n workflows regenerate those.
## typeorm status (pulled out)
typeorm 0.3.20 → 0.3.26 was originally in this PR but **made workspace
metadata sync intermittently lossy**: `example-app-postcard` failed
twice with a *different* field missing from the synced PostCard object
each run, and one integration shard's `DataSeedWorkspaceCommand` died
with "Could not find flat entity with universal identifier …" — versus
zero such failures on recent main. Local runs (db reset + seed, group-by
integration suite 19/19) pass, so it is a nondeterministic
CI-load-sensitive regression that needs dedicated debugging (typeorm
changed LIMIT/OFFSET 0 semantics, lazy count for `getManyAndCount`,
upsert WHERE construction, and topological-sort internals in that
range). The resolutions comment documents this as the blocker;
CVE-2025-60542 is MySQL-driver-only (`sqlstring`), so Postgres-only
Twenty is not exposed in the meantime.
## Verification
- `npx nx typecheck twenty-server` / `twenty-front` — clean (no cache)
- `npx nx test twenty-server` — full suite green
- `lingui:extract` + `lingui:compile` — clean for twenty-server /
twenty-emails / twenty-front
- `oxfmt --check` — clean for both packages
- Lockfile diff: lingui 5.9.5 entries, `esbuild@0.21.5` +
`@esbuild/*@0.21.5` platform packages removed, no typeorm changes
|
||
|
|
303c415dd1 |
fix(ai) - add logs + remove dashboard building (#21440)
- add logs for thread finishing without agent message - add logs to monitor toolCall token usage - remove dashboard building via AI (before fixing it) - fix Anthropic compute |
||
|
|
a6fcbf58e4 |
fix(billing) - enable upgrade if invoice already paid (#21450)
Had an issue concerning a user with credits, then invoice automatically paid. Upgrade failed |
||
|
|
166f7ee0d2 |
chore(deps): prune yarn resolutions down to load-bearing entries (#21446)
## Context Audit of all 28 `resolutions` entries in the root package.json against yarn.lock dependency graphs and the npm registry, to remove every entry that is no longer forcing anything a normal resolution wouldn't do — resolutions are hard to maintain and silently freeze versions. Net result: **28 → 22 entries**, two small dependency bumps replace pins, and every remaining entry now has its blocker + removal condition documented in `//resolutions`. ## Removed — dead weight (re-resolution lands on the same safe versions) | Entry | Why it was dead | |---|---| | `type-fest: 4.10.1` | Stale 2024 dedup pin that semver-overrode ~16 of 19 declared ranges (forced `^0.13`/`^0.20`/`^0.21` consumers up four majors, `^5.x` consumers down one). Types-only; each parent now resolves its own compatible copy. | | `typescript: 5.9.3` | No-op: every range (`^5.9.3`, `5.9.3`, `~5.9.2`) resolves to 5.9.3 naturally. Only the electron-forge scaffolding template regains its own nested `~5.4.5` (never builds this repo). | | `node-gyp: ^12.4.0` | All requesters are Yarn-injected `node-gyp: latest` = 12.4.0 today. The tar-6-era node-gyp versions it evicted have no requesting parent left. | | `cacache: ^20.0.0` | All four parents (arborist, metavuln-calculator, make-fetch-happen 15, pacote 21) already declare `^20`. Guarded by the kept `make-fetch-happen: ^15` resolution. | | `pacote/tar: ^7.5.16` | The original target (pacote 11/15 via zapier) is gone; the only pacote left is 21.5.0 which declares `tar ^7.4.3` natively. | ## Removed — replaced by a parent upgrade - **`nodemailer: 8.0.10`** → `imapflow` 1.2.1 → **1.3.6** (ships patched nodemailer 8.0.10 exact; 1.4.0 is blocked by the 3-day npm age gate). twenty-server's own `^8.0.5` range was already safe. - **`node-ical/uuid: 11.1.1`** → `node-ical` ^0.20.1 → **^0.21.0**, which drops uuid (and axios) entirely. The uuid removal happened at 0.21.0 — not in the 0.26 rrule-temporal type overhaul that #21441 flagged as the blocker. ## Narrowed — `qs: 6.15.2` global → two scoped entries Only three lockfile entries actually request vulnerable qs ranges: `express@4.22.0` (pinned by `@mintlify/previewing`), `express@4.22.1` + `@cypress/request@3.0.10` (pinned by verdaccio 6.7.2, latest). Replaced the global pin with `express/qs` + `@cypress/request/qs`, so the 12+ healthy parents (express 4.22.2/5.x, body-parser, stripe, …) are no longer frozen and will pick up future qs releases naturally. ## Re-pinned — `graphql-redis-subscriptions/ioredis` Changed `^5.6.0` → exact `5.10.1` and documented why: this must equal the exact ioredis version pinned by twenty-server and bullmq. Without it, graphql-redis-subscriptions' `^5.3.2` resolves to a second ioredis copy and `RedisPubSub`'s publisher/subscriber types reject the server's client (caught by twenty-server typecheck during this work — bump it in lockstep with the ioredis pin). ## Kept (all load-bearing, now documented inline) graphql (singleton below msw's `^16.12.0`), @lingui/core (suite lockstep), @types/qs (6.9.17 typing-break holdback), @opentelemetry/api (NoopMeterProvider singleton, #20231), chokidar v3 (NestJS CLI fsevents, #20316), tmp (zapier-platform-cli pins 0.2.5), make-fetch-happen + the two @electron tar entries (blocked on electron-forge adopting @electron/rebuild 4), @angular-devkit/core (blocked on a fixed @nestjs/cli > 11.0.23), yeoman-environment, webpack-dev-server, next/postcss (fix only in next 16.3.0 canaries), the remaining uuid pins, and react-doc-viewer/ajv. ## Follow-ups (separate PRs) - `typeorm` 0.3.20 → 0.3.30: re-roll the 46-line patch; clears the `typeorm/uuid` resolution **and** the open high-severity GHSA-q2pj-6v73-8rgj (SQL injection in `repository.save/update`, fixed in 0.3.26). - googleapis 105 → ≥152 migration clears `googleapis-common/uuid`. ## Verification - `yarn install` clean; lockfile contains **no** vulnerable qs (≤6.15.1)/tar 6/uuid <11/nodemailer <8.0.4/postcss 8.4.31/tmp <0.2.6 entries - `npx nx typecheck twenty-server` ✓ and `npx nx typecheck twenty-front` ✓ - CalDAV + IMAP unit tests (node-ical/imapflow consumers): 9 suites, 121 tests ✓ - `yarn npm audit --all`: only pre-existing typeorm finding remains (see follow-up) |
||
|
|
462dd3b0e9 |
security: uuid CVE — bump bullmq/msal/blocknote + scoped resolutions for the rest (Dependabot alert 1289) (#21441)
Closes the uuid Dependabot alert — [1289](https://github.com/twentyhq/twenty/security/dependabot/1289) — by **upgrading the parents that bump cleanly** and **scope-resolving only the ones that genuinely can't**. `uuid < 11.1.1` (buffer-bounds check in v3/v5/v6) is pulled by ~9 transitives. ### Bumped (parent upgrade — drops uuid<11, no behavior change; typecheck verified) - **bullmq** 5.40.0 → 5.78.0 — also aligned **ioredis** 5.6.0 → 5.10.1 (bullmq pins it) and fixed the renamed `Job.returnValue→returnvalue` / `stackTrace→stacktrace` (now `string[]|null`) in `admin-panel-queue.service.ts`. - **@azure/msal-node** ^3.8.4 → ^5.2.3 (5.2.4 was age-gate-quarantined). - **@blocknote/** ×5 ^0.47.3 → ^0.51.4. ### Scope-resolved to uuid 11.1.1 (no clean bump exists) - **sockjs** (latest; pinned by webpack-dev-server) and **@ptc-org/nestjs-query-typeorm** (9.4.0 *is* latest, pins `^10`) — no version drops uuid. - **typeorm** — a `patch:` dep / ORM core, too risky to bump. - **node-ical** 0.26 (type-model overhaul → caldav-parser rewrite) and **googleapis** 173 (Gmail/OAuth, 105→173) — large breaking migrations; **deferred to dedicated PRs**. - **@cypress/request** — transitive (cypress isn't a direct dep). Resolutions are **per-package** and preserve the intentional **uuid 13.x** (twenty-sdk / create-twenty-app). ### Verification - `twenty-server` typecheck ✓ (0 errors), `twenty-front` typecheck ✓ (0 errors). - `yarn install --immutable` ✓; every uuid resolves to **11.1.1** or **13.0.2**. - bullmq/msal/typeorm runtime exercised by the **server integration tests**; @blocknote by the **storybook tests** in CI. |
||
|
|
2da28cb03e |
security: upgrade express 4.22.2 + qs 6.15.2 resolution for dev-tool holdouts (Dependabot alert 1305) (#21434)
Closes the qs Dependabot alert — [1305](https://github.com/twentyhq/twenty/security/dependabot/1305) — by **upgrading express where possible** and using a **documented qs resolution** only for the irreducible dev-only holdouts. ### What's vulnerable `qs 6.14.x` (the `qs.stringify` DoS) is pulled by `express 4.22.1` / `body-parser 1.x`. The fix is `qs 6.15.2`, and there's **no backport to the 6.14 line**. ### Upgrade what we can (no resolution) `express 4.22.2` / `body-parser 1.20.5` moved to the patched `qs ~6.15.1`. So this PR bumps the app + in-range tooling to **express 4.22.2**: - twenty-server's stale direct pin `4.22.1 → 4.22.2` (its runtime HTTP is already express 5.2.1 via `@nestjs/platform-express`; this just patches the redundant direct dep — typecheck passes), - nx / electron-forge / webpack-dev-server / companion follow via `yarn up -R express body-parser`. ### Resolution only for the two holdouts Two **dev-only** tools pin express *exactly* with no patched release on a line we can use, so they still drag in `qs 6.14.2`: - **verdaccio** `4.22.1` — express 5 only landed in the **v7 beta** ([verdaccio#5680](https://github.com/verdaccio/verdaccio/issues/5680), [#2479](https://github.com/verdaccio/verdaccio/issues/2479)), **not** backported to the 6.x line we use. - **@mintlify/previewing** `4.22.0` — closed-source, latest still pins 4.22.0, no movement. A `qs: 6.15.2` resolution covers those, documented with a top-level `//resolutions` note and a removal trigger. ### Verification - `yarn install --immutable` ✓; every `qs` resolves to `6.15.2`; express app/tooling on `4.22.2` (only verdaccio/mintlify remain on old express, neutralized by the resolution). - `twenty-server` typecheck ✓ (90 files import express types; 4.22.1→4.22.2 is a patch). - Non-exploitable in prod regardless (express/body-parser use `qs.parse`, not the vulnerable `stringify`). |
||
|
|
233a6f9fb1 |
fix(server): register Lingui message compiler to stop "Uncompiled message detected" log flood (#21416)
## Summary Fixes #21415. Server-side `` t`…` `` macro calls (e.g. `FlatEntityMapsException`) resolve against Lingui's global `i18n` singleton, which `I18nService` never loads a catalog into and never registers a messages compiler on. With no `_messageCompiler` set, every such lookup logs `Uncompiled message detected!` and falls back to the raw string — flooding server logs (enough to hit hosting log rate limits) and disabling ICU interpolation on those messages. The warning is emitted unconditionally by `@lingui/core` (not `NODE_ENV`-gated). ## Changes Register `compileMessage` via `setMessagesCompiler` in `I18nService.loadTranslations()`: - on the global `i18n` singleton (used by the `t` macro), and - on each per-locale instance. `@lingui/message-utils` is already a transitive **runtime** dependency of `@lingui/core` (`@lingui/core@5.1.2 → @lingui/message-utils@^5.1.2`), so no new dependency is added. ## Testing Deployed on a real instance: before, the server emitted hundreds of `Uncompiled message detected` lines/sec (saturating the log rate limit); after, steady-state shows `0`, and the worker (which shares the code path) likewise shows `0`. App behaviour is unchanged — messages already rendered via fallback; this silences the warning and enables ICU on the affected messages. --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: Félix Malfait <felix.malfait@gmail.com> Co-authored-by: Félix Malfait <felix@twenty.com> |
||
|
|
20c83e1f86 |
fix(kanban): preserve scroll on board re-init + propagate same-column reorders via SSE (#20637)
closes https://discord.com/channels/1130383047699738754/1504130730840821860 https://github.com/user-attachments/assets/d5833031-01c6-4e46-b699-c29c42435a53 ## Summary Fixes two related issues with the kanban (board view) collaboration experience: 1. **Scroll-to-top on every data change** — `triggerRecordBoardInitialQuery` always scrolled the board to the top, even when re-initializing for a single-record data change (SSE echo of your own mutation, a collaborator's update). Scroll reset only makes sense when the dataset itself changes (filter / sort / group). 2. **Same-column reorders by other users did not propagate** — the server's diff function stripped `FieldMetadataType.POSITION`, so position-only updates produced empty `updatedFields` and short-circuited event emission entirely. SSE clients never received them. ## What's in here - **Frontend** — `useTriggerRecordBoardInitialQuery` now exposes a `triggerRecordBoardInitialQueryWithoutScrollReset` variant; data-driven re-inits in `RecordBoardDataChangedEffect` use it, while genuine filter / sort / group changes keep the scroll-resetting `triggerRecordBoardInitialQuery`. `getRecordBoardEffectsForUpdateInputs` classifies each update as `trigger-initial-query` / `reposition-records` / `none`. For position- or group-only changes we skip the re-query and reposition records in place in the store (`useRepositionRecordsOnBoard`), which avoids the flicker and preserves scroll. - **Server** — removes `POSITION` from `objectRecordChangedValues`' strip list, so position-only updates emit a non-empty diff and flow through SSE. Position is now treated as a field like any other across all event consumers (SSE, webhooks, workflows, logic functions); a trigger with an explicit field filter still excludes it. |
||
|
|
941c9e7586 |
fix: match relation field filters in optimistic & RLS record matchers (#21301)
Closes #21345. ## What It should be caused by the GraphQL optimistic query. `isRecordMatchingFilter` (front, Apollo optimistic cache) and `isRecordMatchingRLSRowLevelPermissionPredicate` (server, RLS) now handle a view filter that targets a **relation field object** (e.g. an "is (not) empty" filter on a relation) by matching against the related record id, instead of throwing. <img width="3436" height="2250" alt="CleanShot 2026-06-08 at 06 44 01@2x" src="https://github.com/user-attachments/assets/1dccbd1e-133c-4f4a-a0a9-7ccd02a9a0ae" /> <img width="1496" height="380" alt="CleanShot 2026-06-08 at 06 45 34@2x" src="https://github.com/user-attachments/assets/e5c2071e-69df-4d99-bb7d-66d503a175b6" /> ## Why Both matchers only implemented the relation **join column** branch (`fooId`) and threw `Not implemented yet, use UUID filter instead on the corresponding "fooId" field` for the relation field itself (`foo`). In practice the UI still stores relation filters keyed on the relation object, so any view with such a filter made every create/update/delete on that object throw: the optimistic effect re-evaluates all active view filters against the changed record and hits the unimplemented branch. Repro: add a self-relation field on People (e.g. "Referred By"), put it in a view filter as "is not empty", then edit any Person. The optimistic update throws. ## Behaviour change | Scenario | Before | After | |---|---|---| | View filter on relation object (`referredBy is not empty`), then edit a record | Throws `Not implemented yet...` | Record matched by related id; update succeeds | | Filter on relation join column (`referredById`) | Worked | Unchanged | ## Test plan ```bash cd packages/twenty-front && npx jest isRecordMatchingFilter cd packages/twenty-server && npx jest is-record-matching-rls-row-level-permission-predicate ``` - [x] Front: relation `is empty` / `is not empty` / `in` match by related id; join-column path still passes (20/20) - [x] Server: relation `is empty` / `is not empty` match by related id (9/9) - [x] `lint:diff-with-main` clean on both packages Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: Félix Malfait <felix.malfait@gmail.com> |
||
|
|
615c3d8dbe |
security: drop end-of-life apollo-server-core (#735, #736) (#21418)
Closes the `apollo-server-core` alerts (**#735**, **#736**) by
**removing the dependency** — no Apollo migration, no resolution.
### Why these were flagged "no patch available"
`apollo-server-core` is **Apollo Server v3, which is end-of-life** (per
its npm deprecation notice). No patched release of this package will
ever exist — the CVE fix lives only in the renamed `@apollo/server` v4
package.
### Why we can just drop it
twenty-server **doesn't use Apollo Server** — its GraphQL runtime is
**GraphQL Yoga** (`YogaDriver`). `apollo-server-core` was imported for
one thing only: the `gql` template tag in **6 integration test files**.
`gql` from `graphql-tag` is identical (apollo-server-core merely
re-exports it), `graphql-tag` is **already a direct dependency**, and
**15 other twenty-server tests already import `gql` from it**.
### Change
- Swapped `import { gql } from 'apollo-server-core'` → `import { gql }
from 'graphql-tag'` in the 6 test files.
- Removed `apollo-server-core` from
`packages/twenty-server/package.json`.
- Result: `apollo-server-core` (and its transitive surface) is gone from
`yarn.lock` entirely.
### Verification
- `yarn install --immutable` ✓
- No `apollo-server-core` references remain in source or lockfile
- Integration tests (which exercise the swapped `gql` imports) run in CI
|
||
|
|
1b26061992 |
security: close ws & file-type alerts via parent upgrades (no resolution) (#21417)
Closes 3 more Dependabot alerts via **parent upgrades only — no `resolutions`**. ### Closes - **ws #1238** (`>= 8.0.0, < 8.20.1`) - **file-type #622** (`>= 13.0.0, < 21.3.1`) and **#635** (`>= 20.0.0, <= 21.3.1`) ### How - **ws** — two parents pinned vulnerable copies; both have safe releases: - `wrangler`/`miniflare`: `ws 8.18.0 → 8.20.1` - `socket.io`/`engine.io`/`socket.io-adapter`: `ws ~8.17.1 → ~8.20.1` - Every `ws` now resolves `>= 8.20.1` (or non-vulnerable 6.x/7.x). - **file-type** — bumped `@swc/cli ^0.7.10 → ^0.8.1`, which pulls the newer `@xhmikosr` bin-wrapper → downloader → decompress/archive-type chain (`file-type ^20.5.0 → ^21.3.x`). `@swc/cli` is a devDependency that isn't directly invoked (nx's swc compiler uses `@swc/core`), so this is dev-tooling-only with no runtime impact. ### Verification - `yarn install --immutable` ✓ (passes the hardened 3-day age gate) - No vulnerable `ws` (`< 8.20.1`) or `file-type` (`20.x`) remains in `yarn.lock` ### Remaining open alerts (not closeable without a `resolution` or a major migration) - `apollo-server-core` (#735/#736) — needs Apollo Server 3 → 4 - `webpack-dev-server` (#1237/#691/#692) — `@electron-forge/plugin-webpack` only pins `^4` (no stable v5 consumer); dev-tooling - `qs` (#1305) — `express`/`body-parser` pin `~6.14`, which has no patched release - `uuid` (#1289) — spread across many `^8`/`^9`/`^10` transitives; v11 is a breaking jump - `postcss` (#1061) — bundled exact by `next` and `styled-components` - `ajv` (#481) — `@cyntler/react-doc-viewer` pins `^7` (latest still does) |
||
|
|
6e147a548b |
security: clear twenty-apps & seed-dependencies CVE alerts (#21410)
Clears the Oneleet/dependency CVE alerts from the `twenty-apps` example/internal app lockfiles and the application-package `seed-dependencies` template — all via parent/direct dependency upgrades, **no `resolutions` overrides**. ## Lock refresh (non-breaking, within existing ranges) - **postcss** 8.5.8/8.5.9 → 8.5.15 — CVE-2026-41305 — postcard, hello-world, self-hosting - **ip-address** 10.1.0 → 10.2.0 — CVE-2026-42338 — postcard, hello-world, self-hosting, twenty-for-twenty - **yaml** 1.10.2 → 1.10.3 — CVE-2026-33532 — call-recording ## seed-dependencies (direct/parent bumps) - **uuid** `^10.0.0 → ^11.1.1` (direct) — CVE-2026-41907 - **body-parser** `^1.20.4 → ^1.20.5`, which pulls **qs** 6.15.2 — CVE-2026-8723 - **socks** 2.8.3 → 2.8.9 (refresh), which pulls **ip-address** 10.2.0 — CVE-2026-42338 ## twenty-for-twenty - **resend** 6.12.0 → 6.12.4 (refresh): 6.12.4 drops the `svix` dep that pulled the vulnerable **uuid** 10.0.0, leaving only uuid 13.0.2 — CVE-2026-41907 All flagged packages were transitive (except the direct seed-deps `uuid`); no app source changes. |
||
|
|
a825dcf2cc |
security: clear 8 Dependabot alerts via transitive/parent bumps (no resolutions) (#21409)
Clears 8 Dependabot alerts via in-range transitive/parent bumps and one dead-dependency removal. **No `resolutions` overrides** were used — every fix is a real version bump within existing semver ranges or a parent upgrade. ### Root `yarn.lock` - **react-router** 6.30.3 → 6.30.4 (open redirect via protocol-relative URL) — pulled through react-router-dom, ranges unchanged — alert #1382 - **yaml** 2.8.1 → 2.9.0 (stack overflow on deeply nested collections) — alert #734 - **uuid** `^13.0.0` → 13.0.2 in twenty-sdk + create-twenty-app (buffer bounds check) — alert #1164 - **ip-address** `^9.0.5` dropped by bumping **socks** 2.8.3 → 2.8.9 (now depends on `ip-address ^10.1.1`, which is unaffected) — alert #1171 ### `seed-dependencies` lockfile - **uuid** `^10.0.0` → `^11.1.1` (direct dep; removed now-redundant `@types/uuid` since uuid v11 ships its own types) — alert #1287 - **ip-address** `^9.0.5` dropped via the same socks bump — alert #1170 ### `twenty-for-twenty` lockfile - **resend** bumped to 6.12.4 (`^6.12.0` range kept), which drops its `svix@1.90.0 → uuid@^10` transitive chain — alert #1278 ### `twenty-companion` - Removed the unused **simplemde** dependency. The note editor loads SimpleMDE from a CDN `<script>` tag and never imports the npm package; `easymde` (its maintained fork) is already a dependency — alert #690 ### Not addressed here The remaining alerts can't be closed without `resolutions` overrides (deliberately avoided in this PR) or a larger migration: - **qs** (#1305, #1304), **lodash** (#824 high / #823 / #385), **ws** (#1238), **postcss** (#1061) — vulnerable copies are pinned exact / bundled by parents (express, body-parser, @nestjs/*, next, styled-components, zapier) with no in-range patch. - **webpack-dev-server** (#1237/#692/#691) — pinned by `@electron-forge/plugin-webpack` (still on v4); dev-tooling only. - **uuid <11.1.1** (#1289) — spread across `^3`/`^8`/`^9` transitive ranges; reaching v11 is a breaking jump. - **apollo-server-core** (#735/#736) — requires an Apollo Server 3 → 4 migration. |
||
|
|
2514cab860 |
fix(server): include relation join column names in updatedFields of update events (#21405)
## Context Since #21052, update-event diffs are keyed by the relation field name (e.g. `company`) instead of the join column name (e.g. `companyId`). `updatedFields` is derived from the diff keys, so any **workflow database-event trigger** (or webhook) configured with a field filter on a relation join column **silently stopped firing** — no run is created at all. We hit this in production: a `cloudWorkspace.updated` trigger filtered on `twentyContactId` stopped creating runs the same day #21052 was deployed. Updating the record's relation produced `updatedFields: ["twentyContact"]`, which no longer matches the stored settings `fields: ["twentyContactId"]` in `WorkflowDatabaseEventTriggerListener.shouldTriggerJob`. ## Solution Keep the diff keyed by relation field name (the timeline rendering from #21052 relies on it — adding both keys to the diff would display relation changes twice), but expose **both** the relation field name and its join column name in `updatedFields`: - New `computeUpdatedFieldsFromDiff()` in `object-record-changed-values.ts`: expands MANY_TO_ONE relation diff keys with their join column name. - Used in `formatTwentyOrmEventToDatabaseBatchEvent` for UPDATED/DELETED/RESTORED and UPSERTED events instead of `Object.keys(diff)`. This restores matching for pre-existing trigger/webhook configurations (join column names) while keeping configurations using relation field names working. ## Test plan - [x] Unit tests: relation diff keyed by relation name; `updatedFields` contains both `company` and `companyId` - [x] End-to-end util test on UPDATED event: `updatedFields: ['company', 'companyId']`, diff keyed by `company` - [x] Downstream consumer specs pass (workflow trigger listener, webhooks, subscriptions, logic-function triggers) - [x] Verified against the production workspace that a `twentyContactId` update currently produces no workflow run with the old behavior |
||
|
|
7258722754 |
security: upgrade @nestjs/graphql 12→13 + @ptc-org/nestjs-query 4→9 (+ @nestjs/config 4) (#21402)
## What Upgrades the NestJS GraphQL stack to clear the High **`ws`** alert (GHSA-3h5v-q93c-6h6q) and modernize off two heavily-patched majors. `@nestjs/graphql@13` pulls `ws@8.20.1` (was 8.16.0). This had to be a **coordinated** upgrade: `@ptc-org/nestjs-query@4.2.0` doesn't support `@nestjs/graphql@13`, so all three move together. | Package | From → To | |---|---| | `@nestjs/config` | 3.3.0 → ^4.0.4 | | `@nestjs/graphql` | 12.1.1 → ^13.4.2 | | `@ptc-org/nestjs-query-{core,graphql,typeorm}` | 4.x → ^9.4.0 | ## The tricky bits - **Re-ported the custom `@nestjs/graphql` patch onto v13.** v13 rewrote the schema builder and added its *own* native multi-schema support (`includeModules`, native `clear()`). Twenty's patch (`resolverSchemaScope` + `computeReachableTypes` — the core/metadata/admin split) is re-merged into v13's new `generate(options, includeModules, reachableTypes)` flow, with a link-preserving `storage.clear()` so cross-schema `resolveType` closures keep working. - **Re-ported the `@ptc-org` patch onto 9.4.0**: removes the `@shareable` federation directive from built-in connection/response types, **and** adds a `.js` extension to its extensionless deep import of `@nestjs/graphql` internals — which v13's new `"exports"` map otherwise rejects at runtime (this was the boot blocker). - **`AppTokenService`**: nestjs-query 9 requires custom services to inject their repo and `super(repo)` it (added an `@InjectRepository` constructor). - **`gridPosition` input fields**: dropped the `deprecationReason` (a *required* input field can't be `@deprecated` under the upgraded graphql) — fields keep their original nullability, so the **schema is unchanged**. - **Service specs**: nestjs-query 9's `TypeOrmQueryService` reads the repo's driver/metadata at construction, so the mocked repos now include `manager`/`metadata`. ## Verification - `nx typecheck twenty-server`: **0 errors**; lint clean - Server boots; **all 3 GraphQL schemas** (`/graphql`, `/metadata`, `/admin-panel`) generate and respond `200` - `graphql:generate` for all 3 schemas is **byte-identical** to before the upgrade (the reachable-types re-port is faithful) - **108 service unit tests pass** (incl. all 6 `TypeOrmQueryService` services) - `ws@8.16.0` gone (now 8.17.1 + 8.18.0); `yarn install --immutable` clean ## Note on lodash `lodash@4.17.21` still remains via `zapier-platform-core` (runtime) and `@stoplight/spectral`, so the lodash alert is **reduced but not fully cleared** by this PR — it needs those separate sources addressed (or a resolution). |
||
|
|
1c3ae92c04 |
fix(server): prevent SSE stream teardown errors from crashing all pods (#21395)
## Context
In prod-eu, **all `twenty-server` API pods crash simultaneously**
several times per hour (then restart in lockstep) since ~Jun 2. Each
crash is an **unhandled promise rejection** in SSE event-stream teardown
— `exitCode=1`, identical stack on every one of the 7 pods:
```
Error: Failed to acquire lock for key: workspace:<id>:activeStreams
at CacheLockService.withLock (cache-lock.service.ts:53)
at async EventStreamService.destroyEventStream (event-stream.service.ts:75)
at async cleanup (wrap-async-iterator-with-lifecycle.ts)
at async Object.return (wrap-async-iterator-with-lifecycle.ts)
at async Object.cancel (graphql-yoga/.../result-processor/sse.js:68)
```
### Mechanism
1. A busy workspace's SSE clients reconnect (no client backoff), so
connect/disconnect contend on a **single per-workspace Redis lock**
`workspace:<id>:activeStreams`.
2. Under contention `CacheLockService.withLock` exhausts its retries and
**throws**.
3. In the `destroyEventStream` teardown path the throw escapes
`wrapAsyncIteratorWithLifecycle`'s `cleanup()` — `return()` does `try {
await cleanup() } finally { … }` and does **not** catch a cleanup throw.
4. graphql-yoga invokes this from `cancel()` as a **fire-and-forget**
`Promise.all` on connection abort. With **no global `unhandledRejection`
handler**, Node's default policy terminates the process with **exit code
1**.
5. The crash drops all that pod's SSE clients → they reconnect to
surviving pods → contention moves there → the whole fleet crashes
together → restarts → reconnect storm → repeats (~14 min period,
matching the metrics).
## Changes
Crash-stopping hotfix (defense in depth). Does **not** change the
locking design or client reconnect behavior — see follow-ups.
- **`wrapAsyncIteratorWithLifecycle`**: `onCleanup()` is now best-effort
— wrapped in try/catch so teardown can never reject out of
`next()`/`return()`/`throw()`. The original iterator error is still
rethrown unchanged. Adds an `onCleanupError` hook so the swallowed error
is still reported.
- **`EventStreamResolver`**: wires `onCleanupError` to
`ExceptionHandlerService.captureExceptions` (→ Sentry) with workspace +
channel context, so these failures stay visible.
- **`main.ts`**: registers a global `process.on('unhandledRejection')`
that reports via `ExceptionHandlerService` (Sentry) instead of letting
Node terminate. Registering the listener also suppresses Node's default
process-termination. Non-`Error` reasons are formatted with
`util.inspect` (per Copilot review) so Sentry gets a readable message
rather than `[object Object]`.
## Verification
The fix was checked against the exact crash path — a wrapped iterator
whose `onCleanup` rejects:
- `return()` (graphql-yoga's `cancel()` path) **resolves** instead of
rejecting → no unhandled rejection.
- `next()` on a completed stream **resolves** despite a rejecting
cleanup.
- A genuine iterator error still surfaces on `next()` (cleanup failure
doesn't mask it).
- `onCleanupError` receives the original `Error`.
All four pass. `nx lint twenty-server` + `oxfmt` clean; no new `tsc`
errors in the changed files.
## Follow-ups (not in this PR)
- Remove the unnecessary `withLock` around the already-atomic Redis
`SADD`/`SREM` in `event-stream.service.ts` (the contention source).
- Restore exponential backoff + jitter on the frontend SSE reconnect
(regressed in #21061) to stop the thundering herd.
- Infra: make `/healthz` a meaningful liveness signal and add
`maxUnavailable` + a PodDisruptionBudget so pods can't all die together.
|
||
|
|
3655942fa4 |
fix: reload stale clients on any older app version, not just major (#21011)
## Context
The GraphQL error handler emits an `APP_VERSION_MISMATCH` error
(surfaced on the client as a "your app version is out of date, please
refresh" prompt) when a client's app version is behind the server's.
Today that comparison only fires when the **major** version differs:
```ts
const frontEndMajor = semver.parse(frontEndAppVersion)?.major;
const backendMajor = semver.parse(backendAppVersion)?.major;
if (isDefined(frontEndMajor) && isDefined(backendMajor) && frontEndMajor < backendMajor) { ... }
```
Twenty ships schema changes in minor and patch releases too. A tab left
open across a minor/patch deploy keeps sending GraphQL documents built
against the previous schema. If a field was removed or renamed, those
operations fail with opaque field-level errors and the user never gets
the refresh prompt — a hard reload is the only recovery.
## What this does
- **Server:** fires the mismatch whenever the client version is strictly
older than the server (`semver.lt`), regardless of which version
component changed.
- **Client:** `onAppVersionMismatch` now reloads the page once (in
addition to the existing snackbar) to pull the current `index.html` and
hashed assets, guarded by a short `sessionStorage` window so a
still-stale reload can't cause a refresh loop.
## Notes
- No behavior change for clients on the same or newer version.
- Relies on the existing `x-app-version` header and `APP_VERSION` config
that already drive this check.
---------
Co-authored-by: Charles Bochet <charles@twenty.com>
|
||
|
|
71480c3888 |
fix: gracefully handle missing logic functions during workflow destroy (#21362)
## Summary - Wraps `deleteOneWithSource` calls in `.catch()` during workflow/step destruction so that a missing logic function (valid UUID but already deleted) no longer crashes the entire destroy operation - Adds a `Logger` to `WorkflowVersionStepOperationsWorkspaceService` for the warning - Fixes test mock to return a resolved Promise and use a valid UUID ## Context When a CODE step references a `logicFunctionId` that is a valid UUID but the logic function no longer exists (e.g. deleted by a previous operation or orphaned), the destroy fails with "Logic function with id X not found". This blocks users from cleaning up workflows. ## Test plan - [x] Destroy a workflow with CODE steps whose logic functions already exist → succeeds as before - [ ] Destroy a workflow with CODE steps referencing a deleted/non-existent logic function → succeeds with a warning log instead of crashing |
||
|
|
3c81566d65 |
chore: sync AI model catalog from models.dev (#21392)
Automated daily sync of `ai-providers.json` from [models.dev](https://models.dev). This PR updates pricing, context windows, and model availability based on the latest data. New models meeting inclusion criteria (tool calling, pricing data, context limits) are added automatically. Deprecated models are detected based on cost-efficiency within the same model family. **Please review before merging** — verify no critical models were incorrectly deprecated. Co-authored-by: FelixMalfait <6399865+FelixMalfait@users.noreply.github.com> |
||
|
|
ce2d77be2a |
feat(server): in-app server-level admin management (#19785) (#21321)
## Closes #19785 In-app management of **server-level admin rights** (`canAccessFullAdminPanel`, `canImpersonate`) so self-hosters no longer need raw SQL + a Redis flush + restart to grant access. > **Draft** — feature complete; `/code-review` + `/security-review` run and addressed. ### Background `AdminPanelGuard` / `ServerLevelImpersonateGuard` read `request.user.{canAccessFullAdminPanel,canImpersonate}`, hydrated each request from `CoreEntityCacheService.get('user', …)` (local 30-min + Redis no-TTL). The cache was only invalidated on soft-delete, so a raw `UPDATE core."user"` never took effect. The **first** signup auto-gets both flags; every subsequent admin previously needed raw SQL. ### UX - **Admin Panel → General → Administrators**: a read-only overview of every user with server-level access; each row links to that user's admin page. - **Find anyone** via the user search (Recent Users) — available to full admins and impersonators — then open their **admin user page**. - On the user page, an **"Administrator access"** card (gated on `canAccessFullAdminPanel`) has two toggles — *Full admin panel access* and *Impersonation* — that work for **any** user (a user with no access shows both off). Mirrors how **Impersonate** already works (find user → user page → act). Each change opens a confirm dialog with a **2FA code** field; the last full admin's toggle is disabled. ### Backend / security - **Cache fix** — invalidate the user entity cache on committed user updates (not just soft-delete) so privilege changes propagate (~100 ms, cluster-wide) with no restart. - `getServerAdmins` query + `updateServerAdminAccess` mutation (any `targetUserId`), gated on `canAccessFullAdminPanel`. - `NoImpersonationGuard` on both — an impersonated full-admin session can't be used to escalate an impersonator. - Fresh **2FA TOTP step-up** (enrolled+verified method **and** a fresh code; genuine 2FA errors surface; dev-skip on trusted `NODE_ENV`). - **Last-admin lockout** in a transaction with a pessimistic row lock (no TOCTOU). - **Email-to-all-admins + affected user** (rendered once per locale), structured log, audit event-log emit. - **Authorization**: the read-only `userLookupAdminPanel` + `adminPanelRecentUsers` lookups now accept `canAccessFullAdminPanel OR canImpersonate` (new `AdminPanelOrImpersonateGuard`), so a full admin without impersonate can still find users to manage. Workspace/impersonation queries stay impersonate-gated. ### Reviews - `/code-review` (max effort): 3 security findings (impersonation-escalation sink, lockout TOCTOU, step-up accepting PENDING 2FA) — **all fixed**. `/simplify`: applied. `/security-review`: **no high/medium vulnerabilities**. ### Follow-ups (not in this PR) - Unit tests for `AdminPanelServerAdminService` + a frontend test. - Point the self-host troubleshooting docs at the new UI. - OTP retry UX: `ConfirmationModal` closes on confirm, so a wrong code needs a reopen (kept to reuse the existing modal; no new pattern). ### Notes for reviewers - `generated-admin/graphql.ts` entries were hand-added to match codegen output (admin codegen needs a running server); re-run `nx graphql:generate twenty-front --configuration=admin` to confirm parity. - First-admin bootstrap (first signup) is unchanged. --------- Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> |
||
|
|
06d68f665d |
fix(auth): additional workspace and identity validation in auth flows (#21347)
## What Adds validation across three auth flows so that a session or reset link is consistently scoped to a workspace the authenticated principal belongs to, and to a verified identity. - **Access token** (`jwt.auth.strategy.ts`): when resolving the request's user context, the token's `userWorkspaceId` must belong to the token's `workspaceId` — the same check the application-token path already performs. - **OIDC** (`oidc.auth.strategy.ts`): reject sign-in when the identity provider explicitly reports `email_verified: false`. - **Password reset** (`reset-password.service.ts`): a supplied `workspaceId` is only used when the user is a member of it; otherwise it falls back to the user's own first password-auth-enabled workspace. ## Tests - `jwt.auth.strategy.spec.ts`: rejects an access token whose user workspace belongs to a different workspace than the token; existing mocks updated to carry the cached `workspaceId`. - `oidc.auth.strategy.spec.ts` (new): rejects unverified email; accepts verified and absent-claim cases. - `reset-password.service.spec.ts`: falls back when the supplied `workspaceId` is not one the user belongs to. `tsgo`, `oxlint` and `oxfmt` all clean on the changed files. |
||
|
|
24839d044a |
fix(server): repair server typecheck broken by isCustom deprecation (#21376)
## What Repairs `server-lint-typecheck`, which is **currently red on `main`**. After #21228 retyped `FlatObjectMetadata.isCustom` as `WasRemovedInUpgrade<boolean> | undefined`, the spec added in #21311 still passed `flatObjectMetadata.isCustom` to `computeTableName(nameSingular, isCustom: boolean)`: ``` graphql-query-order-group-by.parser.spec.ts(83,5): error TS2345: Argument of type 'WasRemovedInUpgrade<boolean> | undefined' is not assignable to parameter of type 'boolean'. ``` Both PRs merged via stale bases, and `server-lint-typecheck` only runs on PRs (not `main` pushes), so the regression landed undetected — the next PR to touch anything server-wide surfaces it. ## Fix Compute the expected physical table name with **`computeObjectTargetTable`** — the production helper that derives custom-ness from the application (`applicationUniversalIdentifier !== TWENTY_STANDARD_APPLICATION`), which is exactly the pattern the `isCustom` deprecation steers callers toward. This stops reading the deprecated field and won't break again when it's removed. One-line change in a single test file; behaviour is unchanged (custom object → `_`-prefixed physical table). ## Verification - `nx typecheck twenty-server` ✅ (was failing on `main`, now passes) - The spec runs green (3/3) - `nx lint:diff-with-main twenty-server` ✅ (lint + format) |
||
|
|
cf70565976 |
feat(twenty-server): allow shouldHideEmptyGroups in app view manifest (#21370)
## Context The view **Hide empty groups** setting (`shouldHideEmptyGroups`) can be toggled in the UI, is persisted on the `View` entity, exposed in the `CreateView`/`UpdateView` GraphQL inputs, and tracked by the flat-view sync machinery — but it could **not** be set from an app's view manifest. Root cause: the field postdates the manifest plumbing (added in #16385, Dec 2025). Two spots were never updated to thread it through: - `ViewManifest` didn't declare the field. - `fromViewManifestToUniversalFlatView` hardcoded `shouldHideEmptyGroups: false`. Ref: twentyhq/core-team-issues#414 ## Changes - Add optional `shouldHideEmptyGroups?: boolean` to `ViewManifest`. - Read it in the converter (`?? false`), mirroring the existing `isCompact` handling. - Cover it in the converter unit test (default + explicit value). No migration or schema change — the column already exists, and downstream sync (`FLAT_VIEW_EDITABLE_PROPERTIES` + the universal-flat compare type) already handles it. ## Test - `npx jest from-view-manifest-to-universal-flat-view` → 5 passed - `tsgo -p tsconfig.json` (twenty-server) → no new errors - oxlint + oxfmt clean |
||
|
|
9c66975520 |
isCustom deprecation for Objects and Fields (#21228)
## Context
`isCustom` was a legacy denormalized boolean on `ObjectMetadataEntity`
and `FieldMetadataEntity`.
Now that every metadata row carries `applicationId` (via
`SyncableEntity`), "is this custom" is fully derivable, and the stored
boolean was a redundant second source of truth that could drift.
The real meaning of `isCustom` is **"the owning application is not the
twenty-standard application"** — i.e. `!belongsToTwentyStandardApp`.
Note this is *not* "belongs to the workspace custom app" as I initially
thought: third-party-application
objects/fields are custom too.
The standard application has a globally stable `universalIdentifier`, so
the value derives with no per-workspace lookup.
## Changed
## `isCustom` checks — before → after
`isCustom` is no longer a stored column. The table below lists every
site that branched on it and how it resolves now. The unifying rule:
`isCustom ≡
!isTwentyStandardApplicationUniversalIdentifier(applicationUniversalIdentifier)`.
### Server — behavioural checks
| Location | Purpose | Before | Now |
|---|---|---|---|
| `utils/compute-object-target-table.util.ts` | Physical table name `_`
prefix | `computeTableName(nameSingular, objectMetadata.isCustom)` |
derives from `applicationUniversalIdentifier` (single source for all
table-name callers) |
| `twenty-orm/factories/entity-schema.factory.ts` +
`…/entity-schema-metadata.type.ts` | ORM table name (hot path) |
`object.isCustom` | `object.applicationId !== standardApplicationId`
(computed in `buildEntitySchemaMetadataMaps`) |
|
`twenty-orm/repository/workspace-{delete,soft-delete,update}-query-builder.ts`
| Table name for mutations | `computeTableName(nameSingular,
objectMetadata.isCustom)` | `computeObjectTargetTable(objectMetadata)` |
| `index-metadata/utils/generate-deterministic-index-name-v2.ts` | Index
name hash (must stay bit-identical) | `flatObjectMetadata.isCustom` |
derives from `applicationUniversalIdentifier` |
| `object-metadata/object-record-count.service.ts` | Table name for
record count | `computeTableName(nameSingular, isCustom)` |
`computeObjectTargetTable(flatObjectMetadata)` |
|
`workspace-manager/dev-seeder/data/services/dev-seeder-data.service.ts`
| Match seed config by table name | `computeTableName(item.nameSingular,
item.isCustom)` | `computeObjectTargetTable(item)` |
| `commands/workspace-export/workspace-export.service.ts` +
`…/utils/generate-workspace-schema-ddl.util.ts` | Export table name (raw
entity) | `objectMetadata.isCustom` |
`!isTwentyStandard…(objectMetadata.application?.universalIdentifier)` |
|
`flat-field-metadata/services/flat-field-metadata-type-validator.service.ts`
| Block users creating reserved field types |
`args.flatEntityToValidate.isCustom` |
`!args.flatEntityToValidate.isSystem` |
| `api/common/.../common-create-many-query-runner.service.ts` | Don't
let client overwrite system `createdBy` |
`createdByFieldMetadata.isCustom === false` |
`createdByFieldMetadata.isSystem === true` |
|
`field-metadata/utils/resolve-field-metadata-standard-override.util.ts`
| Skip i18n/overrides for custom fields | `if (fieldMetadata.isCustom)
return raw` | **removed** — falls through on
`isDefined(standardOverrides)` |
|
`object-metadata/utils/resolve-object-metadata-standard-override.util.ts`
| Skip i18n/overrides for custom objects | `if (objectMetadata.isCustom)
return raw` | **removed** — same fall-through |
|
`command-menu-item/utils/build-navigation-interpolation-context.util.ts`
| Override context for nav labels | passed `isCustom` into resolver |
dropped (resolver no longer needs it) |
| `api/common/.../data-arg-processor.service.ts` | `isCustom` for
record-position table name | `flatObjectMetadata.isCustom` | derives
from `applicationUniversalIdentifier` |
| `metadata-modules/minimal-metadata/minimal-metadata.service.ts` |
Minimal DTO + override context | `flatObjectMetadata.isCustom` | derives
from `applicationUniversalIdentifier` |
|
`commands/upgrade-version-command/1-23/…backfill-record-page-layouts.command.ts`
| Filter to custom objects | `objectMetadata.isCustom` |
`!isTwentyStandard…(applicationUniversalIdentifier)` |
### Server — DTO / API population
| Location | Before | Now |
|---|---|---|
|
`flat-object-metadata/utils/from-flat-object-metadata-to-object-metadata-dto.util.ts`
| passthrough `isCustom` | derives from `applicationUniversalIdentifier`
|
|
`flat-field-metadata/utils/from-flat-field-metadata-to-field-metadata-dto.util.ts`
| passthrough `isCustom` | derives from `applicationUniversalIdentifier`
|
|
`object-metadata/utils/from-object-metadata-entity-to-object-metadata-dto.util.ts`
(REST) | `entity.isCustom` | `entity.applicationId !==
standardApplicationId` |
|
`field-metadata/utils/from-field-metadata-entity-to-field-metadata-dto.util.ts`
(REST) | `entity.isCustom` | `entity.applicationId !==
standardApplicationId` |
| `dataloaders/dataloader.service.ts` | passed
`flatFieldMetadata.isCustom` into override resolver | dropped (resolver
no longer needs it) |
> REST controllers (`object-metadata.controller.ts`,
`field-metadata.controller.ts`) resolve `standardApplicationId` once per
request from the cached `flatApplicationMaps`.
### Frontend
| Location | Purpose | Before | Now |
|---|---|---|---|
| `settings/.../SettingsObjectFieldDisabledActionDropdown.tsx` | Whether
an inactive field is deletable | `isDeletable = isCustomField` |
`isDeletable = isCustomField && !isSystemField` |
### Unchanged (out of scope)
`isCustom` on `IndexMetadata` / `View` / `Skill` / `Agent` and their
guards still read the persisted column.
Breaking change is on the isCustom filter on field and object APIs, this
is never used in the FE and unlikely used by external consumers
|
||
|
|
7530775dc1 |
fix(billing) - Suspend workspace at trial period end if cancelation is planned (#21363)
https://twenty-v7.sentry.io/issues/6900026484/events/e5a9e11fe0464df296d8beeabcf0f538/?end=2026-06-09T05%3A24%3A00&project=4507072499810304&query=%21twenty.workspace.id%3A%EF%80%8DContains%EF%80%8Dad6668da-9f4c-4618-9424-9092ea87db26%20%21twenty.workspace.id%3A%EF%80%8DContains%EF%80%8D86b352c5-ffff-4a4c-8660-b1b2bfb7f57d&referrer=next-event&start=2026-05-23T06%3A02%3A00 |
||
|
|
c27c8c88b0 |
Fix various graphs bugs (#21311)
Some bugs fixed in this PR
1. From UI any field could be chosen to group the query by it, while for
instance, RAW_JSON type (eg workflowRun.state) is not supported by
PostgreSQL to group a query by. Fix: removed it from the "group by"
fields options in FE + in BE -->
2. The BE check existed (isFlatFieldMetadataSupportedInGroupBy) but the
signature was malformed: it expected`{ fieldMetadataType,
fieldMetadataName, fieldMetadataIsSystem }` while every caller passes a
flat field metadata object with type/name/isSystem. So the check is
mis-wired — at runtime the destructured props are undefined, making it
always return true (validation bypassed). Fixed this.
3. Group by does not work with Morph relations if their direction is
ONE_TO_MANY. Added that constraint.
4. Group by with morph relations were broken even for MANY_TO_ONE,
because a morph is stored as one field per target
(polymorphicOwnerRocket, polymorphicOwnerSurveyResult…), each with its
own join column, but the frontend collapsed them into a single
polymorphicOwner field — so the backend tried to resolve a non-existent
polymorphicOwnerId. Fix: Frontend: added a target picker so you choose
the specific morph target (then its sub-field), storing the real
per-target field id. Backend: fixed validate-relation-subfield to use
the per-target field's own relationTargetObjectMetadataId instead of the
multi-target resolver that returned null.
5. (improvement) When an error occured in the query, the graph showed
"No data". Updated it to "error". (screenshot 1)
6. When a field used as a filter on a graph is deleted, it is not
deleted as a graph filter (which is ok because it would involve parsing
all the graph's configuration json to find whether a field is
referenced; there is no foreign key), which prevented from further
modifying the graph's filters. Fixed this + add an indicator that the
filter is can/should be removed (see screenshot 2)
7. "Ambiguous column name" PG error occurs when ordering by "creation
date" of a related field, because both objects have createdAt field.
Fixed it by adding table alias as prefix.
8. (improvement) While working on #5 I did not understand why we could
directly do `"objectMetadataNameSingular"."columnName" `while I expected
that for custom objects it would have to be
`_objectMetadataNameSingular`. that's simply because we use an alias
from the beginning. To add clarity, within groupBy code I replaced
`objectMetadataNameSingular` with `objectAlias` everywhere it is indeed
inherited from us using objectAlias.
<img width="685" height="391" alt="Screenshot 2026-06-08 at 12 01 45"
src="https://github.com/user-attachments/assets/f2b15ca5-da39-4114-8188-69f58f3c4cbf"
/>
<img width="598" height="341" alt="Screenshot 2026-06-08 at 11 53 55"
src="https://github.com/user-attachments/assets/66372811-4a37-40d9-b43a-4af51f89b6e6"
/>
|
||
|
|
137fe45cf6 |
Deprecate dummy enterprise key 2/2 (#21328)
Following [1/2](https://github.com/twentyhq/twenty/pull/20890) Now that all usages of hasValidEnterpriseKey has been removed in prod and deployed, we can safely remove it altogether. --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> |
||
|
|
fcaf2b4d9b |
chore(twenty-server): temporary instrumentation for app-install 504 (#21365)
## Why App installs on cloud intermittently fail with a 504, surfacing in Sentry as `Migration action 'update' for 'logicFunction' failed` + `Failed to rollback transaction: Query runner already released`. This is **temporary instrumentation** to pin down where the time goes — to be reverted once the bottleneck is fixed. Everything is greppable via `[install-perf]` and marked `// TODO(install-perf)`. ## What the local repro already told us I instrumented the manifest-sync/migration path and ran a local harness (new skipped spec) installing **1 / 8 / 30 logic functions**, for both create and the checksum-bump **update** (the incident path): | stage (N=30, update) | ms | |---|---| | flat-maps recompute | ~1 | | build migration | ~11 | | transaction (all actions + commit) | ~79 | | post-commit cache invalidate | ~6 | | **full sync** | **~135** | Nothing approached 1s, let alone 10s; no slow queries logged. So the migration/cache code is **not** the algorithmic cause. Given the in-transaction `UPDATE ... WHERE id=?` is intrinsically fast, a >10s in prod almost certainly means it was **blocked on a lock**, and the 10s node-pg `query_timeout` (`core.datasource.ts`) then killed the connection → the observed errors + 504. Local can't reproduce prod lock contention / table sizes, hence this instrumentation. ## What this adds (all `TODO`-marked) - **hrtime per-stage timing** — flat-maps recompute, build vs run, per-action (`>50ms`), transaction summary, post-commit cache invalidation. Uses `process.hrtime` because the integration harness enables fake timers (so `Date.now()` is useless there). - **`maxQueryExecutionTime`** slow-query logging on the core datasource (logs the offending SQL). - **Scoped `SET LOCAL lock_timeout = '8s'`** on the migration transaction (below the 10s `query_timeout`) → a blocked action fails fast with a clear *"canceling statement due to lock timeout"* instead of the opaque connection kill. - **Best-effort `pg_stat_activity` snapshot on failure** (on a fresh pooled connection) to identify the blocking session, plus a **guarded rollback** so a released connection stops masking the real error. - **Skipped local perf harness** (`logic-function-install-performance.integration-spec.ts`) — run manually with `nx test:integration:with-db-reset -- --testPathPattern "logic-function-install-performance"`. ## How we'll use it Deploy, reproduce the failing install, and read the `[install-perf]` logs: the per-action timing names the action, the `lock_timeout` message + `pg_stat_activity` snapshot name the **blocking** query/PID. Then revert this PR and fix the actual contention. Typecheck (`nx typecheck twenty-server`) is clean. |
||
|
|
e9086b49f6 |
Increase logicFunctionQueue worker concurrency to 10 (#21364)
## Summary - Increase BullMQ worker concurrency for `logicFunctionQueue` from 1 (default) to 10 - Logic function executions are I/O-bound Lambda calls — the worker just holds an HTTP connection open, so higher concurrency doesn't add CPU/memory pressure - With 13 worker pods, this goes from 13 to ~130 concurrent slots, which should resolve the ~3h average queue latency observed in Grafana ## Test plan - [ ] Monitor `avg_latency_ms` for `logic-function-queue` in the Grafana job queue dashboard after deploy - [ ] Verify worker pod CPU/memory remains stable |
||
|
|
ed7ff4a84b |
chore: bump version to 2.12.0 (#21358)
## Summary - Moves current version to previous versions array - Sets TWENTY_CURRENT_VERSION to the new version - Updates TWENTY_NEXT_VERSIONS with the next minor version - Bumps twenty-client-sdk, twenty-sdk, and create-twenty-app to the same version ## Checklist - [ ] Verify version constants are correct - [ ] Verify npm package versions match Co-authored-by: Github Action Deploy <github-action-deploy@twenty.com> |
||
|
|
20ccc52424 |
fix(auth): align workspace resolution across SSO and OTP sign-in flows (#21346)
## What
Makes workspace resolution consistent across the SSO, OTP and
login-token sign-in flows — the target workspace is derived from the
authenticated principal rather than from separate request-supplied
values.
- **SSO callback** (`sso-auth.controller.ts`): validates the resolved
workspace against the authenticating identity provider's own workspace,
so every SSO session is scoped to the provider that issued it. The
`workspaceInviteHash` is request-controlled and shouldn't select a
different workspace than the provider.
- **`getAuthTokensFromOTP`** (`auth.resolver.ts`): reuses the shared
`validateWorkspaceAccess` helper already used by
`getAuthTokensFromLoginToken`, so the login token and the
origin-resolved workspace are checked the same way in both flows.
- **SSO enablement** (`auth.service.ts`, `workspace.validate.ts`): SSO
sign-in now checks the workspace operates an active SSO identity
provider, matching how the other providers gate on their per-workspace
settings, instead of treating SSO as unconditionally enabled.
## Tests
- `auth.service.spec.ts`: SSO sign-in throws when the workspace has no
active SSO identity provider; proceeds when it does.
The controller and resolver spec additions were dropped from this PR;
the behaviours below cover them via manual testing on `main`.
`tsgo`, `oxlint` and `oxfmt` all clean on the changed files.
## How to test on main
Check out this branch on top of `main` and exercise each flow against a
multi-workspace setup (workspace **A** and workspace **B**, each on its
own domain).
**1. SSO callback is scoped to the issuing provider**
(`sso-auth.controller.ts`)
- Configure an SSO identity provider (SAML or OIDC) on workspace **A**
and set it to **Active**.
- Start an SSO sign-in for workspace **A**, but tamper with the callback
so the resolved workspace points at **B** (e.g. supply a
`workspaceInviteHash` belonging to **B**).
- Expected: the callback is rejected with `OAUTH_ACCESS_DENIED`
("Identity provider does not belong to this workspace"). A clean
callback that resolves to **A** still completes sign-in.
**2. Inactive SSO provider blocks sign-in** (`auth.service.ts` /
`workspace.validate.ts`)
- Take workspace **A**'s SSO identity provider and set its status to
something other than `Active` (e.g. inactive/draft).
- Attempt SSO sign-in for **A**.
- Expected: sign-in is denied with `OAUTH_ACCESS_DENIED` ("Identity
provider not found"). Flipping the provider back to `Active` lets
sign-in proceed.
**3. OTP login token must match the origin workspace**
(`getAuthTokensFromOTP` in `auth.resolver.ts`)
- Enable two-factor authentication for a user who belongs to workspace
**A**.
- Sign in to obtain a login token scoped to **A**, then call
`getAuthTokensFromOTP` (submit the OTP) from workspace **B**'s
origin/domain.
- Expected: the request is rejected with `FORBIDDEN_EXCEPTION` ("Token
is not valid for this workspace") and no tokens are issued. Submitting
the OTP from **A**'s origin issues tokens as before.
---------
Co-authored-by: Charles Bochet <charles@twenty.com>
|
||
|
|
441fe73be5 |
chore: sync AI model catalog from models.dev (#21353)
Automated daily sync of `ai-providers.json` from [models.dev](https://models.dev). This PR updates pricing, context windows, and model availability based on the latest data. New models meeting inclusion criteria (tool calling, pricing data, context limits) are added automatically. Deprecated models are detected based on cost-efficiency within the same model family. **Please review before merging** — verify no critical models were incorrectly deprecated. Co-authored-by: FelixMalfait <6399865+FelixMalfait@users.noreply.github.com> |
||
|
|
434f5cbcd2 |
chore(server): bump @nestjs to 11.1.24 + serve-static 5.0.5 to clear CVEs (#21333)
Bumps `@nestjs` packages to clear the scanner findings they pin on the prod image. All within-major bumps, past the repo's `npmMinimalAgeGate: 3d`. ## Changes | Package | From → To | Clears | |---|---|---| | `@nestjs/common` | 11.1.16 → **11.1.24** | `file-type@21.3.0` → 21.3.4 | | `@nestjs/core` | ^11.1.18 → **^11.1.24** | (path-to-regexp 8.4.2) | | `@nestjs/platform-express` | 11.1.16 → **11.1.24** | `path-to-regexp@8.3.0` → 8.4.2 | | `@nestjs/serve-static` | 5.0.4 → **5.0.5** | `path-to-regexp@8.3.0` → 8.4.2 | | `@nestjs/testing` | 11.1.16 → **11.1.24** | — | Verified in the regenerated lockfile: **`file-type@21.3.0` and `path-to-regexp@8.3.0` are gone**. `twenty-server:typecheck` passes locally. ## Not in scope - **`lodash@4.17.21`** and **`ws@8.16.0`** are pinned by **`@nestjs/graphql@12.1.1`** (and lodash also by `@nestjs/config@3.3.0`). Bumping graphql 12→13 would clear them, but it's blocked by a **316-line custom patch** implementing Twenty's multi-schema scoping (`resolverSchemaScope`, `computeReachableTypes`) welded to 12.1.1's compiled internals — a dedicated effort, not a routine bump. (Twenty uses the Yoga driver, so it's *not* an Apollo migration.) - `@nestjs/config` 3→4 alone wouldn't clear `lodash` (graphql still pins it), so deferred with the graphql work. - `path-to-regexp@0.1.12` is express 4.x's own — separate from @nestjs. |
||
|
|
296c202be4 |
messaging: Microsoft driver migrate p-limit to native batching (#21132)
This PR migrates the p-limit library to Native graph SDK batching fixing the concurrency and rate limit issues in production seen for some larger accounts |
||
|
|
772b807490 |
fix: prevent deletion of standard fields via API (#21319)
## Summary - Adds a server-side guard that rejects deletion of standard fields (`isCustom: false`) in the `deleteOneField` path - The UI already prevents this, but the API had no enforcement, allowing standard fields to be deleted via direct GraphQL calls Without this guard, deleting a standard field like `jobTitle` cascades to drop dependent generated columns (e.g. `searchVector`), leaving the object in a broken state where all subsequent queries fail with "Data validation error." ## Test plan - [x] Call `deleteOneField` with a standard field ID → should return `FIELD_MUTATION_NOT_ALLOWED` error - [x] Call `deleteOneField` with a custom field ID → should succeed as before - [x] UI deactivation of standard fields still works (deactivate != delete) |
||
|
|
16db47fb70 |
fix: add queue attribute to jobs waiting gauge metric (#21324)
## Summary
- The `twenty_queue_jobs_waiting_total` gauge was summing all queues
into a single value without a `queue` label, making the Grafana "Jobs
Waiting by Queue" panel show a single aggregated line instead of
per-queue breakdown.
- Uses `getMeter()` directly to call `observableResult.observe(count, {
queue: queueName })` per queue, matching the `by (queue)` grouping the
dashboard already expects.
## Test plan
- [x] Deploy and verify the Grafana "Jobs Waiting by Queue" panel
displays separate series per queue
- [x] Confirm Prometheus scrape returns
`twenty_queue_jobs_waiting_total{queue="..."}` with distinct queue
labels
|
||
|
|
77d1e8ced6 |
feat(app-dev): sync error hints, flatEntity labels, dev-mode summary UI, and docs (#21252)
Split out of #21240 — all remaining app-dev improvements. Stacked on #21251 (review/merge that first). - Actionable recovery hints on failed syncs; unified diff renderer; `--dry-run` guard. - Return `flatEntity` on update/delete sync actions and unify the diff label. - Summarize the dev-mode entity list unless `--verbose`. - Docs: syncing & recovery guide + dry-run + open-an-issue prompt. - Live execution mode for synced logic functions; clearer manifest warnings. <img width="1018" height="700" alt="image" src="https://github.com/user-attachments/assets/5e9ce19e-0f1d-4f99-8524-4e118bde932b" /> |
||
|
|
13e8e26d1c |
security: bump uuid 9 → 11 (server, shared, front) (#21326)
Clears the `uuid` "missing buffer bounds check in v3/v5/v6" advisory — patched in **11.1.1**. Bumps `twenty-server`, `twenty-shared`, `twenty-front` from 9 → `^11.1.1`. ### Why 11 and not 13 uuid **11.1.x still ships a CommonJS build**, so jest loads it with **no config changes**. uuid went **ESM-only at v12+**, which would otherwise force `transformIgnorePatterns` workarounds across the jest projects (and broke server/integration/storybook CI on the earlier 13 attempt). 11.1.1 is the actual patched version, so this is the minimal fix. ### Changes - `uuid` → `^11.1.1` in the three workspaces (lockfile regenerated under hardened mode) - one test (`useCreateManyRecords.test.tsx`): pin the mocked `v4` to its string-returning overload — uuid's types declare a `Uint8Array` overload that `jest.mocked` resolves to (present in v11 too, unrelated to ESM). All usages are named imports, so no source migration. typecheck passes (server/shared/front); affected specs pass. **No jest config changes.** |
||
|
|
2151a414f5 | Remove IS_WORKFLOW_RUN_STEP_LOGS_ENABLED feature flag (#21323) | ||
|
|
45ceb5d74c |
security: bump vulnerable deps in logic-function runtime lockfiles (#21317)
## What Two standalone dep-set lockfiles bundled for the logic-function / application-package sandbox carried vulnerable transitive versions. Within-range bumps only (these dep sets are handed to user logic functions, so no breaking majors): **`seed-dependencies`** - `nodemailer` `^8.0.4 → ^8.0.5` (resolves 8.0.10) — SMTP CRLF command injection (direct dep) - `brace-expansion`, `picomatch`, `lodash` → patched **`common-layer-dependencies`** - `lodash`, `brace-expansion` → patched ## Left for follow-up (can't be fixed within-range) - `qs` — pinned transitively by `body-parser`, stays at 6.14.2 (needs body-parser bump) - `ip-address` 9.x → 10.x and `uuid` 10.x → 11.x — major bumps; left out since these are exposed to user functions and would be breaking `axios` here is already on the patched `^1.16.1`. |
||
|
|
d2e7dc0e74 |
security: bump vulnerable direct dependencies (axios, next, vitest, qs, dompurify, …) (#21309)
## What Within-major version bumps of **direct** dependencies to clear a large batch of Dependabot alerts that are breaching (or near) their SLA. No major-version changes — all stay within the current major, so risk is low. | Package | From → To | Clears | |---|---|---| | `axios` | ^1.13.5 → ^1.16.0 | ReDoS, Proxy-Auth leak, proto-pollution gadgets, NO_PROXY bypass, resource DoS (56 alerts) | | `next` | 16.1.7 → ^16.2.6 | DoS, middleware/proxy bypass, SSRF, cache poisoning, XSS (32 alerts) | | `vitest` | 4.0.18 → ^4.1.0 | **CRITICAL** — UI server arbitrary file read/exec (#1421) | | `qs` | ^6.11.2 → ^6.15.2 | `qs.stringify` DoS | | `dompurify` | 3.3.3 → ^3.4.0 | proto-pollution XSS + FORBID_TAGS / SAFE_FOR_TEMPLATES bypasses | | `@nestjs/core` | 11.1.16 → ^11.1.18 | improper output neutralization / injection | | `nodemailer` | 8.0.4 → 8.0.10 | SMTP command injection via CRLF (bumped via root `resolutions`) | | `path-to-regexp` | ^8.2.0 → ^8.4.0 | ReDoS via multiple wildcards | | `file-type` | ^21.3.1 → ^21.3.2 | ZIP decompression-bomb DoS | | `@opentelemetry/exporter-prometheus` | ^0.211.0 → ^0.217.0 | exporter process crash via malformed HTTP request (#1183/#1184) | ## Notes - Added a `next` root **resolution** so the dev-only `@react-email/preview-server` copy (hard-pinned at `16.0.10`) is also pulled up to the patched `16.2.x` line — otherwise that copy keeps the Next.js alerts open. - `@opentelemetry/exporter-prometheus` 0.217 pulled `@opentelemetry/sdk-metrics` to 2.7.1 (compatible); `@opentelemetry/api` stays pinned at 1.9.1. - **Transitive-only** vulnerable packages (undici, tmp, ws, brace-expansion, …) are handled in a **separate PR** per the split-by-group plan. - Breaking major bumps (electron, uuid, serialize-javascript) and migrations (Apollo Server 3→4, simplemde) are intentionally **out of scope** here. |
||
|
|
dfb3da1f8d |
feat(emailing-domain): add LOG driver for local development (#21286)
## What Adds a `LOG` driver to the emailing-domain feature, selected via a new `EMAILING_DOMAIN_DRIVER` config variable (defaults to `AWS_SES`, so production behavior is unchanged). The LOG driver: - resolves domains to `VERIFIED` instantly (no DNS / SES setup) - logs each `sendEmail` and returns a synthetic `messageId` instead of calling SES It also dev-seeds a pre-verified domain per workspace (`<workspaceId>.dev.twenty.local`) so the feature works out of the box. ## Why The emailing-domain feature currently ships only the AWS SES driver, so the verify → send flow can't be exercised locally (or in CI) without real AWS credentials. This unblocks local development and review of anything built on emailing domains. ## Usage ``` EMAILING_DOMAIN_DRIVER=LOG ``` The seeded `*.dev.twenty.local` domain is already verified; sends are logged (`[log-driver] sendEmail ...`). --------- Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> |
||
|
|
36567e96ab |
Remove twenty-sdk from package.json before build (#21305)
Follow up of https://github.com/twentyhq/twenty/pull/21179 Removes twenty-sdk from dependencies when building lambda layer |