Commit graph

11678 commits

Author SHA1 Message Date
Charles Bochet
41ee6eac7a
chore(server): bump current version to 2.0.0 and add 2.1.0 as next (#19907)
## Summary

We are releasing Twenty v2.0. This PR sets up the
upgrade-version-command machinery for the new release line:

- Move `1.23.0` into `TWENTY_PREVIOUS_VERSIONS` (it just shipped)
- Set `TWENTY_CURRENT_VERSION` to `2.0.0` (no specific upgrade commands
— this is just the major version cut)
- Set `TWENTY_NEXT_VERSIONS` to `['2.1.0']` so future PRs that
previously would have targeted `1.24.0` now target `2.1.0`
- Add empty `V2_0_UpgradeVersionCommandModule` and
`V2_1_UpgradeVersionCommandModule` and wire them into
`WorkspaceCommandProviderModule`
- Refresh the `InstanceCommandGenerationService` snapshots to reflect
the new current version (`2.0.0` / `2-0-` slug)

The `2-0/` directory is intentionally empty — there are no specific
upgrade commands for the v2.0 cut. New upgrade commands authored after
this merges should land in `2-1/` (or be generated against `--version
2.1.0`).

## Test plan

- [x] `npx jest` on the impacted upgrade test files
(`upgrade-sequence-reader`, `upgrade-command-registry`,
`instance-command-generation`) passes (41 tests, 8 snapshots)
- [x] `prettier --check` and `oxlint` clean on touched files
- [ ] Manual: open `nx run twenty-server:command -- upgrade --dry-run`
against a local stack with workspaces still on `1.23.0` and confirm the
sequence is computed without errors

Made with [Cursor](https://cursor.com)
2026-04-21 02:05:27 +02:00
Charles Bochet
b4f996e0c4
Release v1.23.0 for twenty-sdk, twenty-client-sdk, and create-twenty-app (#19906)
## Summary
- Bump `twenty-sdk` from `1.23.0-canary.9` to `1.23.0`
- Bump `twenty-client-sdk` from `1.23.0-canary.9` to `1.23.0`
- Bump `create-twenty-app` from `1.23.0-canary.9` to `1.23.0`

Made with [Cursor](https://cursor.com)
2026-04-21 01:34:10 +02:00
Weiko
5c58254eb4
Fix activity relation picker (#19898)
## Context
ActivityTargetsInlineCell passed editModeContent via
RecordInlineCellContext, but that context key was no longer read.
Fix aligns the code with the rest of the codebase.

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
2026-04-21 01:30:23 +02:00
Charles Bochet
192a842f57
fix(website-new): pre-resolve wyw-in-js babel presets to absolute paths (#19905)
## Problem

Building `twenty-website-new` in any environment that does **not** also
include `twenty-website` (e.g. the Docker image used by the deployment
workflow) fails with:

```
Error: Turbopack build failed with 99 errors:
Error evaluating Node.js code
Error: Cannot find module 'next/babel'
Require stack:
- /app/node_modules/@babel/core/lib/config/files/plugins.js
- ...
- /app/node_modules/babel-merge/src/index.js
- /app/packages/twenty-website-new/node_modules/@wyw-in-js/transform/lib/plugins/babel-transform.js
- /app/packages/twenty-website-new/node_modules/next-with-linaria/lib/loaders/turbopack-transform-loader.js
```

## Root cause

`packages/twenty-website-new/wyw-in-js.config.cjs` references presets by
bare name:

```js
presets: ['next/babel', '@wyw-in-js'],
```

These options flow through
[`babel-merge`](https://github.com/cellog/babel-merge/blob/master/src/index.js#L11),
which calls `@babel/core`'s `resolvePreset(name)` **without** a
`dirname` argument. With no `dirname`, `@babel/core` falls back to
`require.resolve(id)` from its own file location — so resolution starts
at `node_modules/@babel/core/...` and only walks parent `node_modules`
directories from there, never down into individual workspace packages.

In a normal local install both presets happen to be hoisted to the
workspace root (because `twenty-website` pins `next@^14` and wins the
hoist), so resolution succeeds by accident. In the single-workspace
Docker build only `twenty-website-new` is present, so `next` (16.1.7)
and `@wyw-in-js/babel-preset` are nested in
`packages/twenty-website-new/node_modules` and Babel cannot reach them —
hence the failure.

## Fix

Pre-resolve both presets with `require.resolve(...)` in the wyw-in-js
config so Babel receives absolute paths and resolution becomes
independent of hoisting layout.

## Verification

- `yarn nx build twenty-website-new` — passes locally with the full
workspace
- Reproduced the original failure with a simulated single-workspace
install (only `twenty-website-new` and `twenty-oxlint-rules` present),
confirmed it fails on `main` and passes with this patch
- This unblocks the `twenty-infra` `Deploy Website New` workflow
([related infra PR](https://github.com/twentyhq/twenty-infra/pull/586))


Made with [Cursor](https://cursor.com)
2026-04-21 01:03:26 +02:00
Charles Bochet
1b469168c8
chore(workflow): temporarily lift credit-cap gate on workflow steps (#19904)
## Summary

- Removes the per-step `canBillMeteredProduct(WORKFLOW_NODE_EXECUTION)`
gate in `WorkflowExecutorWorkspaceService.executeStep` so workflows keep
running when a workspace reaches `hasReachedCurrentPeriodCap`.
Previously every step failed with
`BILLING_WORKFLOW_EXECUTION_ERROR_MESSAGE` (\"No remaining credits to
execute workflow…\").
- Drops the now-unused `BillingService` injection, related imports, and
the helper `canBillWorkflowNodeExecution`. Updates the spec to drop the
corresponding billing-validation case and mock.
- Leaves the constant file and `BillingService` itself in place, plus a
TODO at the previous gate site, so the behavior can be re-enabled with a
small, reviewable revert.

## Notes

- Usage events are still emitted (`USAGE_RECORDED` /
`UsageResourceType.WORKFLOW`), and `EnforceUsageCapJob` keeps computing
the cap and flipping `hasReachedCurrentPeriodCap` — only the executor
stops consulting that flag.
- The runner-level `canFeatureBeUsed` check in
`WorkflowRunnerWorkspaceService.run` was already log-only (subscription
presence, not credits), so no change there.
- AI chat (`agent-chat.resolver.ts`) keeps its own
`BILLING_CREDITS_EXHAUSTED` gate; this PR does not touch it.

## Test plan

- [x] `npx jest workflow-executor.workspace-service.spec.ts` (17/17
pass)
- [ ] Manual: with billing enabled and the metered subscription item
flagged `hasReachedCurrentPeriodCap = true`, trigger a workflow run and
verify steps execute end-to-end instead of failing with the billing
error.

Made with [Cursor](https://cursor.com)
2026-04-21 01:00:28 +02:00
github-actions[bot]
57de05ea74
i18n - translations (#19903)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-21 00:50:40 +02:00
Charles Bochet
a174aff5c8
fix(infra): copy nx.json and tsconfig.base.json into website-new image (#19902)
## Summary

Fix the website-new Docker build which currently fails with:

\`\`\`
NX   \"production\" is an invalid fileset.
All filesets have to start with either {workspaceRoot} or {projectRoot}.
\`\`\`

\`packages/twenty-website-new/project.json\` declares \`\"inputs\":
[\"production\", \"^production\"]\` — a named input defined in the root
\`nx.json\`. Without copying \`nx.json\` into the image, nx can't
resolve it and the build fails.

Mirrors what the main twenty Dockerfile already does (line 9 of
\`packages/twenty-docker/twenty/Dockerfile\` copies both
\`tsconfig.base.json\` and \`nx.json\`).

## Test plan

- [ ] Re-run twenty-infra's \`Deploy Website New\` workflow (dev) —
build step should now pass

Made with [Cursor](https://cursor.com)
2026-04-21 00:45:17 +02:00
Charles Bochet
96fc98e710
Fix Apps UI: replace 'Managed' label with actual app name and unify app icons (#19897)
## Summary

- The Data Model table was labeling core Twenty objects (e.g. Person,
Company) as **Managed** even though they are part of the standard
application. This PR teaches the frontend to resolve an `applicationId`
back to its real application name (`Standard`, `Custom`, or any
installed app), and removes the misleading **Managed** label entirely.
- Introduces a single, consistent way to render an "app badge" across
the settings UI:
- new `Avatar` variant `type="app"` (rounded 4px corners + 1px
deterministic border derived from `placeholderColorSeed`)
- new `AppChip` component (icon + name) backed by a new
`useApplicationChipData` hook
- new `useApplicationsByIdMap` hook + `CurrentApplicationContext` so the
chip can render **This app** when shown inside the matching app's detail
page
- Reuses these primitives on:
- the application detail page header (`SettingsApplicationDetailTitle`)
  - the Installed / My apps tables (`SettingsApplicationTableRow`)
  - the NPM packages list (`SettingsApplicationsDeveloperTab`)
- Backend: exposes a minimal `installedApplications { id name
universalIdentifier }` field on `Workspace` (resolved from the workspace
cache, soft-deleted entries filtered out) so the frontend can resolve
`applicationId` -> name without N+1 fetches.
- Cleanup: deletes `getItemTagInfo` and inlines its tiny
responsibilities into the components that need them, matching the
`RecordChip` pattern.
2026-04-21 00:44:14 +02:00
Charles Bochet
9a963ddeca
feat(infra): add Dockerfile for twenty-website-new (#19901)
## Summary

Adds the Docker build for the new marketing website at
`packages/twenty-website-new`, mirroring the existing
`packages/twenty-docker/twenty-website/Dockerfile`.

Differences from the existing `twenty-website` Dockerfile:

- Uses `nx build twenty-website-new` / `nx start twenty-website-new`
- Drops the `KEYSTATIC_*` build-time fake env (the new website doesn't
use Keystatic)
- Doesn't copy `twenty-ui` source (the new website has no workspace
dependency on it)

The image will be built by the new `deploy-website-new.yaml` workflow in
[`twentyhq/twenty-infra`](https://github.com/twentyhq/twenty-infra) and
pushed to ECR repos `dev-website-new` / `staging-website-new`.

Companion PRs:
- twentyhq/twenty-infra: Helm chart + ArgoCD app + deploy workflow
- twentyhq/twenty-infra-releases: bootstrap tags.yaml

## Test plan

- [ ] Local build: \`docker build -f
packages/twenty-docker/twenty-website-new/Dockerfile .\`
- [ ] First run of \`Deploy Website New\` workflow on dev succeeds
(build + push to ECR)
- [ ] ArgoCD \`website-new\` application becomes Healthy on dev
- [ ] https://website-new.twenty-main.com serves the new website

Made with [Cursor](https://cursor.com)
2026-04-21 00:27:11 +02:00
Félix Malfait
13afef5d1d
fix(server): scope loadingMessage wrap/strip to AI-chat callers (#19896)
Some checks are pending
CD deploy main / deploy-main (push) Waiting to run
CI Create App E2E minimal / changed-files-check (push) Waiting to run
CI Create App E2E minimal / create-app-e2e-minimal (push) Blocked by required conditions
CI Create App E2E minimal / ci-create-app-e2e-minimal-status-check (push) Blocked by required conditions
CI Create App / changed-files-check (push) Waiting to run
CI Create App / create-app-test (lint) (push) Blocked by required conditions
CI Create App / create-app-test (test) (push) Blocked by required conditions
CI Create App / create-app-test (typecheck) (push) Blocked by required conditions
CI Create App / ci-create-app-status-check (push) Blocked by required conditions
CI Docs / changed-files-check (push) Waiting to run
CI Docs / docs-lint (push) Blocked by required conditions
CI Emails / changed-files-check (push) Waiting to run
CI Emails / emails-test (push) Blocked by required conditions
CI Emails / ci-emails-status-check (push) Blocked by required conditions
CI Example App Hello World / changed-files-check (push) Waiting to run
CI Example App Hello World / example-app-hello-world (push) Blocked by required conditions
CI Example App Hello World / ci-example-app-hello-world-status-check (push) Blocked by required conditions
CI Example App Postcard / example-app-postcard (push) Blocked by required conditions
CI Example App Postcard / changed-files-check (push) Waiting to run
CI Example App Postcard / ci-example-app-postcard-status-check (push) Blocked by required conditions
Push translations to Crowdin / Extract and upload translations (push) Waiting to run
## Summary

MCP tool execution crashed with \`Cannot destructure property
'loadingMessage' of 'parameters' as it is undefined\` whenever
\`execute_tool\` was called without an inner \`arguments\` field. Root
cause: \`loadingMessage\` is an AI-chat UX affordance (lets the LLM
narrate progress so the chat UI can show "Sending email…") but it was
being wrapped into **every** tool schema — including those advertised to
external MCP clients — and \`dispatch\` unconditionally stripped it,
crashing on \`undefined\` args.

The fix scopes the wrap/strip pair to AI-chat callers only:

- Pair wrap and strip inside \`hydrateToolSet\` (they belong together).
- New \`includeLoadingMessage\` option on \`hydrateToolSet\` /
\`getToolsByName\` / \`getToolsByCategories\` (default \`true\` so
AI-chat behavior is unchanged).
- MCP opts out → external clients see clean inputSchemas without a
required \`loadingMessage\` field.
- \`dispatch\` no longer strips; args default to \`{}\` defensively.
- \`execute_tool\` defaults \`arguments\` to \`{}\` at the LLM boundary.

## Test plan

- [x] \`npx nx typecheck twenty-server\` passes
- [x] \`npx oxlint\` clean on changed files
- [x] \`npx jest mcp-protocol mcp-tool-executor\` — 23/23 tests pass
- [ ] Manually: call \`execute_tool\` via MCP with and without inner
\`arguments\` — verify no crash, endpoints execute
- [ ] Manually: inspect MCP \`tools/list\` response — verify
\`search_help_center\` schema no longer contains \`loadingMessage\`
- [ ] Regression: AI chat still streams loading messages as the LLM
calls tools

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

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 21:43:16 +02:00
Abdullah.
83bc6d1a1b
[Website] Self-host billing migration and some responsiveness fixes. (#19894)
Closes the following issues.

https://github.com/twentyhq/core-team-issues/issues/2371
https://github.com/twentyhq/core-team-issues/issues/2379
https://github.com/twentyhq/core-team-issues/issues/2383
2026-04-20 21:23:54 +02:00
github-actions[bot]
755f1c92d1
i18n - translations (#19893)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-20 19:45:24 +02:00
Weiko
6d3ff4c9ce
Translate standard page layouts (#19890)
## Context
Standard page layout tabs, page layout widgets, and view field group
titles were hardcoded English in the backend. This PR brings them under
the same translation pipeline as views.

Notes: Once a standard widget/tab/section title is overriden, the
backend returns its value without translation
2026-04-20 17:29:33 +00:00
Charles Bochet
9a95cd02ed
Fix applications query cartesian product causing read timeouts (#19892)
## Summary

Same fix pattern as #19511 (`rolesPermissions` cartesian product).

The `Settings > Applications` page was hitting query read timeouts in
production. The offending SQL came from
`ApplicationService.findManyApplications` / `findOneApplication`, which
loaded **5 `OneToMany` children** in a single query via TypeORM
`relations`:

```
logicFunctions × agents × frontComponents × objects × applicationVariables
```

Postgres returns the Cartesian product of all five — e.g. 20 logic
functions × 5 agents × 30 front components × 100 objects × 10 variables
= **3M rows for ~165 distinct records**, which trivially exceeds the
read timeout.

## Changes

- **`findManyApplications`** — dropped all `OneToMany` relations. The
frontend `FIND_MANY_APPLICATIONS` query only selects scalar fields and
the `applicationRegistration` ManyToOne, so joining the children was
pure waste at the list level.
- **`findOneApplication`** — kept the cheap `ManyToOne` / `OneToOne`
joins (`packageJsonFile`, `yarnLockFile`, `applicationRegistration`) on
the main query and fetched the 5 `OneToMany` children in parallel via
`Promise.all`, reattaching them on the entity. Same shape as
`WorkspaceRolesPermissionsCacheService.computeForCache` after #19511.
- **`application.module.ts`** — registered the 5 child entity
repositories via `TypeOrmModule.forFeature`.

The other internal caller (`front-component.service.ts →
findOneApplicationOrThrow`) only reads
`application.universalIdentifier`, so the extra parallel single-key
lookups remain far cheaper than the previous 8-way join with row
explosion.
2026-04-20 17:04:10 +00:00
Abdullah.
27e1caf9cc
Cleanup files that were committed with website PR, but should not be there. (#19891)
Some files went through with the last PR from Thomas I merged - some
screenshots at root, others inside output folder. This PR removes them.
2026-04-20 17:46:10 +02:00
Etienne
b140f70260
Website - Plan pricing update (#19887) 2026-04-20 14:58:00 +00:00
Charles Bochet
13c4a71594
fix(ui): make CardPicker hover cover the whole card and align content left (#19884)
## Summary

Two small visual issues with the shared `CardPicker` (used in the
Enterprise plan modal and the onboarding plan picker):

- Labels like \`Monthly\` / \`Yearly\` were center-aligned inside their
cards while the subtitle (\`\$25 / seat / month\`) stayed left-aligned,
because the underlying \`<button>\` element's default \`text-align:
center\` was leaking into the children.
- The hover background was painted on the same element that owned the
inner padding, so the hover surface didn't visually feel like the whole
card.

This PR:
- Moves the content padding into a new \`StyledCardInner\` so the outer
\`<button>\` is just the card chrome (border + radius + background +
hover).
- Adds \`text-align: left\` so titles align with their subtitles.
- Hoists \`cursor: pointer\` out of \`:hover\` (it should be on by
default for the card).

Affects:
- \`EnterprisePlanModal\` (Settings → Enterprise)
- \`ChooseYourPlanContent\` (onboarding trial picker)
2026-04-20 17:00:12 +02:00
github-actions[bot]
4ecde0e161
i18n - translations (#19888)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-20 16:45:06 +02:00
Etienne
117909e10a
Billing - Adapt to new unit (#19886) 2026-04-20 14:28:07 +00:00
Félix Malfait
9f5688ab13
Redesign why-twenty page with three-section narrative (#19882)
## Summary
- Restructures the why-twenty page into a clearer three-act story (the
shift / what this means / the opportunity), with new copy across hero
subtitle, all editorials, marquee, quote and signoff.
- Adds visual rhythm via left/right section anchoring (sections 1 and 3
left-aligned, section 2 right-aligned) and per-section `GuideCrosshair`
markers at the top edge of each editorial.
- Adds a CTA `Signoff` section ("Get started") at the end of the page.
- Bumps the 3D quotation marks (`Quotes` illustration) so the Quote can
serve as a visual section break.

## Changes
- **Editorial section**
([Editorial.Heading](packages/twenty-website-new/src/sections/Editorial/components/Heading/Heading.tsx),
[Editorial.Body](packages/twenty-website-new/src/sections/Editorial/components/Body/Body.tsx),
[Editorial.Root](packages/twenty-website-new/src/sections/Editorial/components/Root/Root.tsx)):
  - Default heading size `xl` → `lg`
- New `two-column-left` and `two-column-right` body layouts via
`data-align` on `TwoColumnGrid`
- New optional `crosshair` prop on `Editorial.Root` that anchors a
`GuideCrosshair` to the section
- **Why-twenty constants** — fresh copy in `hero.ts`, `editorial-one`,
`editorial-three`, `editorial-four`, `marquee.ts`, `quote.ts`,
`signoff.ts`
- **Page layout**
([why-twenty/page.tsx](packages/twenty-website-new/src/app/why-twenty/page.tsx)):
  - Section 1 → left content + crosshair on right
  - Section 2 → right content + crosshair on left
  - Section 3 → left content + crosshair on right
  - Adds `Signoff` block with `LinkButton` "Get started" CTA
- **Quote 3D illustration** — `previewDistance` 6 → 4 and bigger
`StyledVisualMount` (added a one-line `oxlint-disable` for the
pre-existing `@ts-nocheck` that the diff surfaced)
- **Signoff** — keeps the GuideCrosshair behavior limited to Partners
(per-page config map remains in place)

Editorial is only consumed on the why-twenty page so the heading/body
changes don't affect any other page.

## Test plan
- [ ] Visit `/why-twenty` on desktop — verify the three editorials read
as left/right/left with crosshairs at section top edges
- [ ] Verify the Signoff CTA renders white "Get started" pill button on
dark background and links to `app.twenty.com/welcome`
- [ ] Verify mobile layout — crosshairs hidden, content left-aligned,
body stacks single column
- [ ] Lighthouse / no console errors

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-04-20 15:32:02 +02:00
Charles Bochet
c959998111
Bump twenty-sdk, twenty-client-sdk, create-twenty-app to 1.23.0-canary.9 (#19883)
## Summary
- Bumps `twenty-sdk`, `twenty-client-sdk`, and `create-twenty-app` from
`1.23.0-canary.2` to `1.23.0-canary.9`.

## Test plan
- [ ] Canary publish workflow succeeds for the three packages.

Made with [Cursor](https://cursor.com)
2026-04-20 13:21:00 +00:00
neo773
ade55e293f
fix 1.22 upgrade command add-workspace-id-to-indirect-entities (#19868)
/closes #19863
2026-04-20 13:19:10 +00:00
Charles Bochet
10c49a49c4
feat(sdk): support viewSorts in app manifests (#19881)
Some checks are pending
CD deploy main / deploy-main (push) Waiting to run
CI Create App E2E minimal / changed-files-check (push) Waiting to run
CI Create App E2E minimal / create-app-e2e-minimal (push) Blocked by required conditions
CI Create App E2E minimal / ci-create-app-e2e-minimal-status-check (push) Blocked by required conditions
CI Create App / changed-files-check (push) Waiting to run
CI Create App / create-app-test (lint) (push) Blocked by required conditions
CI Create App / create-app-test (test) (push) Blocked by required conditions
CI Create App / create-app-test (typecheck) (push) Blocked by required conditions
CI Create App / ci-create-app-status-check (push) Blocked by required conditions
CI Docs / changed-files-check (push) Waiting to run
CI Docs / docs-lint (push) Blocked by required conditions
CI Emails / changed-files-check (push) Waiting to run
CI Emails / emails-test (push) Blocked by required conditions
CI Emails / ci-emails-status-check (push) Blocked by required conditions
CI Example App Hello World / changed-files-check (push) Waiting to run
CI Example App Hello World / example-app-hello-world (push) Blocked by required conditions
CI Example App Hello World / ci-example-app-hello-world-status-check (push) Blocked by required conditions
CI Example App Postcard / changed-files-check (push) Waiting to run
CI Example App Postcard / example-app-postcard (push) Blocked by required conditions
CI Example App Postcard / ci-example-app-postcard-status-check (push) Blocked by required conditions
Push docs to Crowdin / Push documentation to Crowdin (push) Waiting to run
Push translations to Crowdin / Extract and upload translations (push) Waiting to run
## Summary

Today the SDK lets apps declare `filters` on a view but not `sorts`, so
any view installed via an app manifest can never have a default
ordering. This PR adds declarative view sorts end-to-end: SDK manifest
type, `defineView` validation, CLI scaffold, and the application
install/sync pipeline that converts the manifest into the universal flat
entity used by workspace migrations. The persistence layer
(`ViewSortEntity`, resolvers, action handlers, builders…) already
existed server-side; the missing piece was the manifest → universal-flat
converter and the relation wiring on `view`.

## Changes

**`twenty-shared`**
- Add `ViewSortDirection` enum (`ASC` | `DESC`) and re-export it from
`twenty-shared/types`.
- Add `ViewSortManifest` type and an optional `sorts?:
ViewSortManifest[]` on `ViewManifest`, exported from
`twenty-shared/application`.

**`twenty-sdk`**
- Validate `sorts` entries in `defineView` (`universalIdentifier`,
`fieldMetadataUniversalIdentifier`, `direction` ∈ `ASC`/`DESC`).
- Add a commented `// sorts: [ ... ]` example to the CLI view scaffold
template + matching snapshot assertion.

**`twenty-server`**
- Re-export `ViewSortDirection` from `twenty-shared/types` in
`view-sort/enums/view-sort-direction.ts` (single source of truth,
backward compatible for existing imports).
- New converter `fromViewSortManifestToUniversalFlatViewSort` (+ unit
tests for `ASC` and `DESC`).
- Wire the converter into
`computeApplicationManifestAllUniversalFlatEntityMaps` so
`viewManifest.sorts` are added to `flatViewSortMaps`, mirroring how
filters are processed.
- Replace the `// @ts-expect-error TODO migrate viewSort to v2 /
viewSorts: null` placeholder in `ALL_ONE_TO_MANY_METADATA_RELATIONS`
with the proper relation (`viewSortIds` /
`viewSortUniversalIdentifiers`).
- Update affected snapshots (`get-metadata-related-metadata-names`,
`all-universal-flat-entity-foreign-key-aggregator-properties`).

## Example usage

\`\`\`ts
defineView({
  name: 'All issues',
  objectUniversalIdentifier: 'issue',
  sorts: [
    {
      universalIdentifier: 'all-issues__sort-created-at',
      fieldMetadataUniversalIdentifier: 'createdAt',
      direction: 'DESC',
    },
  ],
});
\`\`\`
2026-04-20 14:31:06 +02:00
Charles Bochet
5c2a0cf115
fix(front): gate renewToken Apollo logger on IS_DEBUG_MODE (#19878)
## Summary

The standalone Apollo client used by `AuthService.renewToken` attached
`loggerLink` unconditionally, while the main `apollo.factory` client
correctly gates it on `isDebugMode`. As a result, **every token refresh
in production printed the `renewToken` response — including the new
access and refresh JWTs — to the browser console** via the `loggerLink`
`RESULT` group.

Reproduced in production: opening devtools shows a
`Twenty-Refresh::Generic` collapsed group on every token renewal,
containing `HEADERS`, `VARIABLES`, `QUERY` and a `RESULT` payload with
the full token strings.

The fix mirrors the gating already used in `apollo.factory.ts`
(`...(isDebugMode ? [logger] : [])`), so the logger is only attached
when `IS_DEBUG_MODE=true`. Local debug behavior is unchanged.
2026-04-20 11:49:28 +00:00
github-actions[bot]
f9768d057e
i18n - docs translations (#19880)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-20 12:51:54 +02:00
Abdullah.
fd2288bfff
fix: prototype pollution via parse in nodejs flatted (#19870)
Resolves [Dependabot Alert
686](https://github.com/twentyhq/twenty/security/dependabot/686).
2026-04-20 10:18:51 +00:00
Charles Bochet
de1e592cd3
fix(front): suppress full-page skeleton inside auth modal (#19875)
## Summary

- Lazy auth-flow routes (`SignInUp`, `Invite`, `ResetPassword`,
`CreateWorkspace`, `CreateProfile`, `SyncEmails`, `InviteTeam`,
`PlanRequired`, `PlanRequiredSuccess`, `BookCallDecision`, `BookCall`)
render through `<Outlet/>` inside `<AuthModal>`. Their `LazyRoute`
`<Suspense>` fallback was the page-level `PageContentSkeletonLoader`, so
the two grey shimmer bars painted **inside the modal box** for a few
hundred ms while each chunk downloaded.
- `LazyRoute` now accepts an optional `fallback` prop (default
unchanged: the existing page skeleton). Every auth-modal route passes
`fallback={null}` so the modal stays empty until the lazy chunk resolves
instead of flashing the shimmer.
- `AuthModal`'s inner `StyledContent` gets a `min-height: 320px` so the
framer-motion `layout` animation doesn't rapidly resize the modal as
inner steps (loader → form → password → 2FA / workspace selection) swap.
The modal can still grow for taller steps; only the rapid jump is
removed.

## Why default-parameter syntax for `fallback`

`fallback ?? <LazyRouteFallback/>` would treat an explicit `null` as "no
value" and still render the default skeleton. Using a default parameter
(`fallback = <LazyRouteFallback/>`) preserves an explicit `null` because
defaults only kick in for `undefined`.
2026-04-20 09:39:09 +00:00
github-actions[bot]
01928fc786
i18n - docs translations (#19874)
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-20 11:06:06 +02:00
github-actions[bot]
e4d8cbdb39
i18n - translations (#19873)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-20 10:57:21 +02:00
Etienne
e68842c268
Billing - fixes (#19867)
- Uniformize credit formating : In UI, 1$=1credit. In BE 1 UI credit =
1_000_000 BE "crédits"
- Add crédit rollover information + Link to documentation +
Documentation update
<img width="291" height="317" alt="Screenshot 2026-04-17 at 18 22 59"
src="https://github.com/user-attachments/assets/2519fb9f-159d-4c85-95f4-a6e005a8a1a3"
/>
<img width="848" height="763" alt="Screenshot 2026-04-17 at 14 12 20"
src="https://github.com/user-attachments/assets/a3cc0874-f275-49ea-819f-305ec314bdfe"
/>
<img width="797" height="757" alt="Screenshot 2026-04-17 at 14 12 13"
src="https://github.com/user-attachments/assets/9048409b-d5a2-435a-b735-70370705e668"
/>

- Enable direct top-up (or subscription if in trial) from AI chat
<img width="333" height="215" alt="Screenshot 2026-04-17 at 22 52 00"
src="https://github.com/user-attachments/assets/7a20c627-2806-4bcf-a037-b45752232be9"
/>
<img width="457" height="769" alt="Screenshot 2026-04-17 at 22 51 41"
src="https://github.com/user-attachments/assets/d2a90c1b-271f-4fe9-8891-baeb2fabb86d"
/>

- Inform users if credit limit is reached - Banner
<img width="1130" height="127" alt="Screenshot 2026-04-17 at 19 15 11"
src="https://github.com/user-attachments/assets/30723e5e-c07e-462f-8eb8-e08f52bbab1c"
/>
2026-04-20 08:43:02 +00:00
github-actions[bot]
903ae5cb09
i18n - translations (#19869)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-20 09:35:54 +02:00
martmull
5dd7eba911
Fix app design 6 (#19827)
Unify application display page and isntalled page

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
2026-04-20 09:29:25 +02:00
Félix Malfait
42f57db005
fix(server): add registration_client_uri to DCR response for Claude.ai connector (#19858)
## Summary

Claude.ai's custom remote MCP connector fails with "Couldn't reach the
MCP server" after successfully completing OAuth dynamic client
registration. Driving the flow through Chrome DevTools showed Claude's
backend creates our DCR client (many hundreds of orphan rows visible in
the admin panel), then never returns the user to `/authorize` — it gives
up silently.

**Empirical comparison against known-working MCP servers Claude.ai
connects to identified one concrete difference**: every server that
works returns `registration_client_uri` in the DCR response. We didn't.

| Server | DCR `registration_client_uri` | Claude.ai web connector |
|---|---|---|
| Linear (`mcp.linear.app`) | `/register/<client_id>` |  works |
| Sentry (`mcp.sentry.dev`) | `/oauth/register/<client_id>` |  works |
| Atlassian (`mcp.atlassian.com`) | yes |  works |
| **Twenty** (before this PR) | **missing** |  "Couldn't reach" |

## What this PR changes

### 1. Add `registration_client_uri` to the DCR response

```
{
  "client_id": "…",
  …existing fields…,
+ "registration_client_uri": "<issuer>/oauth/register/<client_id>"
}
```

Pointer at the registration's management endpoint per RFC 7591 §3.2.1.
Marked OPTIONAL in the spec but empirically required by Claude.ai.

### 2. New `GET /oauth/register/:clientId` endpoint (RFC 7592 read-back)

Returns public registration metadata (`client_name`, `redirect_uris`,
`grant_types`, `scope`, etc.). 404 for unknown clients.

No `registration_access_token` is issued (and none required to hit this
endpoint): the `client_id` is an unguessable UUID and the fields
returned are already public-readable via
`findApplicationRegistrationByClientId` GraphQL. This matches Linear's
behaviour — they return a `registration_client_uri` but issue no access
token.

### 3. Advertise `response_modes_supported: ["query"]` in AS metadata

RFC 8414 default, but explicitly listed by Linear / Sentry / Atlassian
and absent from ours. Some clients treat its absence as a capability
gap.

## Why I'm confident this is the root cause

- The failure mode exactly matches an orphaned-DCR retry loop (hundreds
of registrations, none `installed` on a workspace).
- #19847 reporter confirmed Claude Desktop + VS Code work — those
clients use the MCP Python SDK which doesn't require
`registration_client_uri`. **Claude.ai web** uses Anthropic's
proprietary backend client (`User-Agent: Claude-User`), which
empirically does.
- All 3 working reference servers return the field; we were the odd one
out.

## Test plan

- [x] `tsc --noEmit` clean on touched files
- [x] `yarn jest
--testPathPatterns="oauth-discovery.controller|mcp-auth.guard"` → 4/4
pass
- [ ] After deploy:
  ```bash
curl -s -X POST https://<host>/oauth/register -H 'Content-Type:
application/json' \
-d
'{"client_name":"probe","redirect_uris":["https://claude.ai/api/mcp/auth_callback"],"token_endpoint_auth_method":"none"}'
\
    | jq .registration_client_uri
  # expect: "https://<host>/oauth/register/<uuid>"
  ```
- [ ] After deploy: add the MCP connector in Claude.ai — user should now
reach the Twenty `/authorize` page

## Honesty

This is the nth fix in a long debugging chain. Unlike the earlier round
of fixes (which were real spec-compliance bugs but not Claude's
blocker), this one is backed by empirical evidence across 3
known-working implementations. If Claude.ai still fails after this
deploys, the remaining delta is `cli_client_id` in AS metadata
(non-standard field, could confuse strict parsers) or a field we
advertise that others don't (e.g. `client_credentials` grant) — both
small, removable, not disruptive.

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

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-20 09:28:36 +02:00
Thomas des Francs
0729ad27b7
Partners, customers and more (#19862)
## Summary
- Refresh the Twenty website with updated homepage, product, pricing,
partner, customer, case study, and release content
- Add and replace supporting imagery, illustrations, and Lottie assets
used across the site
- Adjust layout constants, navigation/footer content, and page-level
copy for the updated marketing experience
- Update Next.js config and ignore rules to support the new assets and
build output

## Testing
- Not run (not requested)

---------

Co-authored-by: Abdullah <125115953+mabdullahabaid@users.noreply.github.com>
2026-04-20 07:13:56 +00:00
github-actions[bot]
46aedcf133
chore: sync AI model catalog from models.dev (#19866)
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>
2026-04-20 08:38:46 +02:00
Félix Malfait
75848ff8ea
feat: move admin panel to dedicated /admin-panel GraphQL endpoint (#19852)
Some checks are pending
CD deploy main / deploy-main (push) Waiting to run
CI Create App E2E minimal / changed-files-check (push) Waiting to run
CI Create App E2E minimal / create-app-e2e-minimal (push) Blocked by required conditions
CI Create App E2E minimal / ci-create-app-e2e-minimal-status-check (push) Blocked by required conditions
CI Emails / emails-test (push) Blocked by required conditions
CI Example App Hello World / ci-example-app-hello-world-status-check (push) Blocked by required conditions
CI Example App Postcard / changed-files-check (push) Waiting to run
CI Example App Postcard / example-app-postcard (push) Blocked by required conditions
CI Example App Postcard / ci-example-app-postcard-status-check (push) Blocked by required conditions
Push translations to Crowdin / Extract and upload translations (push) Waiting to run
CI Create App / changed-files-check (push) Waiting to run
CI Create App / create-app-test (lint) (push) Blocked by required conditions
CI Create App / create-app-test (test) (push) Blocked by required conditions
CI Create App / create-app-test (typecheck) (push) Blocked by required conditions
CI Create App / ci-create-app-status-check (push) Blocked by required conditions
CI Docs / changed-files-check (push) Waiting to run
CI Docs / docs-lint (push) Blocked by required conditions
CI Emails / changed-files-check (push) Waiting to run
CI Emails / ci-emails-status-check (push) Blocked by required conditions
CI Example App Hello World / changed-files-check (push) Waiting to run
CI Example App Hello World / example-app-hello-world (push) Blocked by required conditions
## Summary

Splits admin-panel resolvers off the shared `/metadata` GraphQL endpoint
onto a dedicated `/admin-panel` endpoint. The backend plumbing mirrors
the existing `metadata` / `core` pattern (new scope, decorator, module,
factory), and admin types now live in their own
`generated-admin/graphql.ts` on the frontend — dropping 877 lines of
admin noise from `generated-metadata`.

## Why

- **Smaller attack surface on `/metadata`** — every authenticated user
hits that endpoint; admin ops don't belong there.
- **Independent complexity limits and monitoring** per endpoint.
- **Cleaner module boundaries** — admin is a cross-cutting concern that
doesn't match the "shared-schema configuration" meaning of `/metadata`.
- **Deploy / blast-radius isolation** — a broken admin query can't
affect `/metadata`.

Runtime behavior, auth, and authorization are unchanged — this is a
relocation, not a re-permissioning. All existing guards
(`WorkspaceAuthGuard`, `UserAuthGuard`,
`SettingsPermissionGuard(SECURITY)` at class level; `AdminPanelGuard` /
`ServerLevelImpersonateGuard` at method level) remain on
`AdminPanelResolver`.

## What changed

### Backend
- `@AdminResolver()` decorator with scope `'admin'`, naming parallels
`CoreResolver` / `MetadataResolver`.
- `AdminPanelGraphQLApiModule` + `adminPanelModuleFactory` registered at
`/admin-panel`, same Yoga hook set as the metadata factory (Sentry
tracing, error handler, introspection-disabling in prod, complexity
validation).
- Middleware chain on `/admin-panel` is identical to `/metadata`.
- `@nestjs/graphql` patch extended: `resolverSchemaScope?: 'core' |
'metadata' | 'admin'`.
- `AdminPanelResolver` class decorator swapped from
`@MetadataResolver()` to `@AdminResolver()` — no other changes.

### Frontend
- `codegen-admin.cjs` → `src/generated-admin/graphql.ts` (982 lines).
- `codegen-metadata.cjs` excludes admin paths; metadata file shrinks by
877 lines.
- `ApolloAdminProvider` / `useApolloAdminClient` follow the existing
`ApolloCoreProvider` / `useApolloCoreClient` pattern, wired inside
`AppRouterProviders` alongside the core provider.
- 37 admin consumer files migrated: imports switched to
`~/generated-admin/graphql` and `client: useApolloAdminClient()` is
passed to `useQuery` / `useMutation`.
- Three files intentionally kept on `generated-metadata` because they
consume non-admin Documents: `useHandleImpersonate.ts`,
`SettingsAdminApplicationRegistrationDangerZone.tsx`,
`SettingsAdminApplicationRegistrationGeneralToggles.tsx`.

### CI
- `ci-server.yaml` runs all three `graphql:generate` configurations and
diff-checks all three generated dirs.

## Authorization (unchanged, but audited while reviewing)

Every one of the 38 methods on `AdminPanelResolver` has a method-level
guard:
- `AdminPanelGuard` (32 methods) — requires `canAccessFullAdminPanel ===
true`
- `ServerLevelImpersonateGuard` (6 methods: user/workspace lookup + chat
thread views) — requires `canImpersonate === true`

On top of the class-level guards above. No resolver method is accessible
without these flags + `SECURITY` permission in the workspace.

## Test plan

- [ ] Dev server boots; `/graphql`, `/metadata`, `/admin-panel` all
mapped as separate GraphQL routes (confirmed locally during
development).
- [ ] `nx typecheck twenty-server` passes.
- [ ] `nx typecheck twenty-front` passes.
- [ ] `nx lint:diff-with-main twenty-server` and `twenty-front` both
clean.
- [ ] Manual smoke test: log in with a user who has
`canAccessFullAdminPanel=true`, open the admin panel at
`/settings/admin-panel`, verify each tab loads (General, Health, Config
variables, AI, Apps, Workspace details, User details, chat threads).
- [ ] Manual smoke test: log in with a user who has
`canImpersonate=false` and `canAccessFullAdminPanel=false`, hit
`/admin-panel` directly with a raw GraphQL request, confirm permission
error on every operation.
- [ ] Production deploy note: reverse proxy / ingress must route the new
`/admin-panel` path to the Nest server. If the proxy has an explicit
allowlist, infra change required before cutover.

## Follow-ups (out of scope here)

- Consider cutting over the three
`SettingsAdminApplicationRegistration*` components to admin-scope
versions of the app-registration operations so the admin page is fully
on the admin endpoint.
- The `renderGraphiQL` double-assignment in
`admin-panel.module-factory.ts` is copied from
`metadata.module-factory.ts` — worth cleaning up in both.
2026-04-19 20:55:10 +02:00
github-actions[bot]
90661cc821
i18n - translations (#19851)
Some checks are pending
CD deploy main / deploy-main (push) Waiting to run
CI Create App E2E minimal / changed-files-check (push) Waiting to run
CI Create App E2E minimal / create-app-e2e-minimal (push) Blocked by required conditions
CI Create App E2E minimal / ci-create-app-e2e-minimal-status-check (push) Blocked by required conditions
CI Create App / changed-files-check (push) Waiting to run
CI Create App / create-app-test (lint) (push) Blocked by required conditions
CI Create App / create-app-test (test) (push) Blocked by required conditions
CI Create App / create-app-test (typecheck) (push) Blocked by required conditions
CI Create App / ci-create-app-status-check (push) Blocked by required conditions
CI Docs / changed-files-check (push) Waiting to run
CI Docs / docs-lint (push) Blocked by required conditions
CI Emails / changed-files-check (push) Waiting to run
CI Emails / emails-test (push) Blocked by required conditions
CI Emails / ci-emails-status-check (push) Blocked by required conditions
CI Example App Hello World / changed-files-check (push) Waiting to run
CI Example App Hello World / example-app-hello-world (push) Blocked by required conditions
CI Example App Hello World / ci-example-app-hello-world-status-check (push) Blocked by required conditions
CI Example App Postcard / changed-files-check (push) Waiting to run
CI Example App Postcard / example-app-postcard (push) Blocked by required conditions
CI Example App Postcard / ci-example-app-postcard-status-check (push) Blocked by required conditions
Push translations to Crowdin / Extract and upload translations (push) Waiting to run
Created by Github action

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-19 13:35:47 +02:00
Félix Malfait
6117a1d6c0
refactor: standardize AI acronym to Ai (PascalCase) across internal identifiers (#19837)
## Summary

The "AI" acronym was rendered inconsistently across the codebase. The
backend AI module had settled on PascalCase `Ai` (`AiAgentModule`,
`AiBillingService`, `AiChatModule`, `AiModelRegistryService`, etc.),
while frontend components, several DTOs, a few types, and shared
identifiers still used all-caps `AI` (`AIChatTab`,
`AISystemPromptPreviewDTO`, `SettingsPath.AIPrompts`, ...). CLAUDE.md
specifies PascalCase for classes; this PR normalizes everything internal
to `Ai`.

**This is a pure internal rename.** The GraphQL schema is untouched —
`@ObjectType` decorator string arguments, resolver method names (which
become Query/Mutation field names), gql template contents, and the
`generated-metadata/graphql.ts` file are preserved verbatim. The only
visible change is TypeScript identifiers and file names.

## Also folded in (adjacent cleanups)

- **`AgentModelConfigService` → `AiModelConfigService`**. Lives in
`ai-models/` and is used by multiple AI code paths, not just the Agent
entity. The "Agent" prefix was misleading.
- **`generate-text-input.dto.ts` → `generate-text.input.ts`**. The
`ai-agent/dtos/` folder already uses `<entity>.input.ts` convention for
Input classes (`create-agent.input.ts` etc.); the old path mixed
`.dto.ts` file extension with a class that has no DTO suffix. File
rename only; class stays `GenerateTextInput`.
- **Removed stale TODO** in `ai-model-config.type.ts` that asked for the
`AiModelConfig` rename that this PR performs.

## Rename methodology

Bulk rename via perl with anchored regex
`(?<!['"])(?<![A-Z.])AI([A-Z])(?=[a-z])/Ai$1/g`:

- **Lookbehind for non-uppercase** skips adjacent acronyms (`MOSAIC`,
`OIDCSSO`) and leaves `AIRBNB_ID` alone.
- **Lookbehind for non-quote** protects most string literals.
- **Lookahead for lowercase** restricts matches to PascalCase
identifiers (`AIChatTab`), leaving SCREAMING_SNAKE constants untouched.

Strict file-scope exclusions: `generated-metadata/**`, `generated/**`,
`locales/**`, `migrations/**`, `illustrations/**`, `halftone/**`, and
the two gql template files (`queries/getAISystemPromptPreview.ts`,
`mutations/uploadAIChatFile.ts`).

Post-rename reverts for identifiers where the regex was too eager:
- Backend resolver method names kept: `getAISystemPromptPreview`,
`uploadAIChatFile` (they are GraphQL field names).
- `@ObjectType('AdminAIModels')` / `('AISystemPromptPreview')` /
`('AISystemPromptSection')` kept as-is.
- Backend classes `ClientAIModelConfig` / `AdminAIModelConfig` kept
as-is (they use `@ObjectType()` with no argument, so the class name IS
the schema name).
- External-library symbols restored: `OpenAIProvider`,
`createOpenAICompatible`, `vercelAIIntegration`.

File renames use a two-step rename to work on macOS case-insensitive
filesystems: `git mv X.tsx X.tsx.tmp && git mv X.tsx.tmp renamed.tsx`.

## Diff audit

- 0 changes to migrations
- 0 changes to locale `.po` / `.ts` files
- 0 changes to `generated-metadata/graphql.ts`
- 0 changes to website illustration files (base64 blobs preserved)
- 0 renames inside user-facing translation strings (`t\`…\``,
`msg\`…\``, `<Trans>…</Trans>`)

## Test plan

- [x] `npx nx typecheck twenty-server` — PASS
- [x] `npx nx typecheck twenty-front` — PASS
- [x] `npx jest ai-model admin agent-role` — 79/79 PASS
- [x] `npx oxlint --type-aware` on 118 changed files — 0 errors
- [x] `npx prettier --check` on 118 changed files — clean
- [ ] CI
2026-04-19 13:29:35 +02:00
Avasis AI
1e27c3b621
fix: correct sSOService → ssoService camelCase typo (#19845)
## Summary

Fixes the pre-existing camelCase typo mentioned in #19839.

The injected `SSOService` property was named `sSOService` instead of the
correct camelCase `ssoService` across the auth module. This is a
straightforward mechanical rename of the property/variable name — no
logic changes.

> **Bonus: pre-existing typo to fix** — `private sSOService: SSOService`
— The variable name is a camelCase typo (`sSOService` instead of
`ssoService`). — #19839

## Changes

Renamed `sSOService` → `ssoService` in 7 files:
- `auth/guards/oidc-auth.guard.ts`
- `auth/guards/saml-auth.guard.ts`
- `auth/guards/oidc-auth.spec.ts`
- `auth/auth.resolver.ts`
- `auth/controllers/sso-auth.controller.ts`
- `auth/strategies/saml.auth.strategy.ts`
- `sso/sso.resolver.ts`

Note: The type `SSOService` (PascalCase class name) is intentionally
left unchanged — it will be addressed in the broader SSO acronym PR from
#19839.

## Test plan

- [ ] Verify `typecheck twenty-server` passes
- [ ] Verify existing auth/SSO tests pass

Co-authored-by: Abhay <abhayjnayakpro@gmail.com>
2026-04-19 13:29:06 +02:00
Abdullah.
dbf43d792c
[Website] Fix testimonials shape, diamond direction, and integrate partner application form. (#19835)
Closes the following issues.

https://github.com/twentyhq/core-team-issues/issues/2368
https://github.com/twentyhq/core-team-issues/issues/2369
https://github.com/twentyhq/core-team-issues/issues/2374
https://github.com/twentyhq/core-team-issues/issues/2375
2026-04-19 09:12:59 +00:00
Thomas des Francs
066003cb04
Hero 2.0 (#19846)
and a few fixes

---------

Co-authored-by: Abdullah. <125115953+mabdullahabaid@users.noreply.github.com>
2026-04-19 09:01:30 +00:00
Félix Malfait
1f3defa7b3
fix(server): expose WWW-Authenticate header for browser-based MCP clients (#19836)
Some checks are pending
CI Example App Postcard / changed-files-check (push) Waiting to run
CI Example App Postcard / example-app-postcard (push) Blocked by required conditions
CI Example App Hello World / example-app-hello-world (push) Blocked by required conditions
CI Example App Hello World / ci-example-app-hello-world-status-check (push) Blocked by required conditions
CI Example App Postcard / ci-example-app-postcard-status-check (push) Blocked by required conditions
Push translations to Crowdin / Extract and upload translations (push) Waiting to run
CD deploy main / deploy-main (push) Waiting to run
CI Create App E2E minimal / changed-files-check (push) Waiting to run
CI Emails / changed-files-check (push) Waiting to run
CI Emails / emails-test (push) Blocked by required conditions
CI Emails / ci-emails-status-check (push) Blocked by required conditions
CI Example App Hello World / changed-files-check (push) Waiting to run
CI Create App E2E minimal / create-app-e2e-minimal (push) Blocked by required conditions
CI Create App E2E minimal / ci-create-app-e2e-minimal-status-check (push) Blocked by required conditions
CI Create App / changed-files-check (push) Waiting to run
CI Create App / create-app-test (lint) (push) Blocked by required conditions
CI Create App / create-app-test (test) (push) Blocked by required conditions
CI Create App / create-app-test (typecheck) (push) Blocked by required conditions
CI Create App / ci-create-app-status-check (push) Blocked by required conditions
CI Docs / changed-files-check (push) Waiting to run
CI Docs / docs-lint (push) Blocked by required conditions
## The bug

Claude's MCP connector fails with \"Couldn't reach the MCP server\" on
every URL (\`api.twenty.com/mcp\`, \`app.twenty.com/mcp\`,
\`<workspace>.twenty.com/mcp\`, custom domains). The failure happens
**before** any OAuth flow starts — the client never even reaches the
consent screen.

## Root cause

\`POST /mcp\` unauthenticated returns:

\`\`\`
HTTP/2 401
access-control-allow-origin: *
www-authenticate: Bearer
resource_metadata=\"https://…/.well-known/oauth-protected-resource\"
content-type: application/json; charset=utf-8
(no access-control-expose-headers)
\`\`\`

The [Fetch/CORS
spec](https://fetch.spec.whatwg.org/#cors-safelisted-response-header-name)
defines only six response headers as safelisted — \`Cache-Control\`,
\`Content-Language\`, \`Content-Type\`, \`Expires\`, \`Last-Modified\`,
\`Pragma\`. Every other header is withheld from cross-origin JS unless
the server opts it in via \`Access-Control-Expose-Headers\`.

Result: Claude's browser-side MCP client receives the 401 but
\`response.headers.get('WWW-Authenticate')\` returns \`null\`. No
\`resource_metadata\` URL, no discovery, no OAuth — the client gives up
with the generic \"can't reach server\" error.

The [MCP authorization
spec](https://modelcontextprotocol.io/specification/draft/basic/authorization)
explicitly requires this header to be exposed.

## Fix

One config change in \`main.ts\`:

\`\`\`ts
-    cors: true,
+ // Expose WWW-Authenticate so browser-based MCP clients can read the
+ // resource_metadata pointer on 401. Required by MCP authorization
spec.
+    cors: { exposedHeaders: ['WWW-Authenticate'] },
\`\`\`

NestJS's default \`cors: true\` uses the \`cors\` package defaults,
which don't set \`exposedHeaders\`. Moving to an explicit config keeps
all other defaults (origin \`*\`, standard methods) and adds the single
required expose.

## Why it's safe and generally beneficial

- \`Access-Control-Expose-Headers: WWW-Authenticate\` is sent on every
response but only has an effect when \`WWW-Authenticate\` is actually
present (i.e. 401s). It's an opt-in permission, not a header-setter.
- \`WWW-Authenticate\` itself is still only set by \`McpAuthGuard\` on
401 — this PR doesn't change where or when the header is emitted.
- Covers the entire app, not just \`/mcp\` — any future 401-returning
endpoint will behave correctly for browser clients automatically.
- No change to origin handling, methods, or credentials. All existing
API / GraphQL / REST traffic is unaffected.

## Verification

After deploy:
\`\`\`bash
curl -sI -X POST -H \"Origin: https://claude.ai\"
https://api.twenty.com/mcp \\
  | grep -iE 'access-control-expose|www-authenticate'
# Expect:
#   access-control-expose-headers: WWW-Authenticate
#   www-authenticate: Bearer resource_metadata=\"…\"
\`\`\`

Then re-try adding the MCP connector in Claude — if this was the only
blocker, OAuth should now complete.

## Related

- #19755, #19766, #19824 — prior fixes in the MCP/OAuth discovery chain
(host-aware metadata, path-aware well-known, \`TRUST_PROXY\` for
\`request.protocol\`). This PR completes the CORS side of that work.

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

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-18 21:28:59 +02:00
Félix Malfait
5223c4771d
fix(server): align OAuth discovery metadata with MCP / RFC 9728 spec (#19838)
## Summary

Three small spec-compliance fixes called out in an audit against the
[MCP authorization spec
(draft)](https://modelcontextprotocol.io/specification/draft/basic/authorization)
and RFC 9728 / RFC 9207.

### 1. Split Protected Resource Metadata by path (RFC 9728 §3.2)

> The `resource` value returned MUST be identical to the protected
resource's resource identifier value into which the well-known URI path
suffix was inserted.

Today a single handler serves both
\`/.well-known/oauth-protected-resource\` and
\`/.well-known/oauth-protected-resource/mcp\` and returns \`resource:
<origin>/mcp\` from both. That's wrong for the root form — per RFC 9728
the root URL corresponds to the **origin as resource**, and only the
\`/mcp\`-suffixed URL corresponds to \`<origin>/mcp\`.

After this PR:

| Request | `resource` field |
|---|---|
| `GET /.well-known/oauth-protected-resource` | `https://<host>` |
| `GET /.well-known/oauth-protected-resource/mcp` | `https://<host>/mcp`
|

Both still return the same `authorization_servers`, `scopes_supported`,
and `bearer_methods_supported`.

Claude's current flow happens to work because our WWW-Authenticate
points at the root form and Claude compares `resource` against what it
connected to. Strict clients probing the path-aware URL first were
rejecting us.

### 2. Advertise `authorization_response_iss_parameter_supported: true`
(RFC 9207)

Defense against OAuth mix-up attacks. Required by the [OAuth 2.1
security
BCP](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1).
Signals that clients receiving an authorization response will find the
issuer in the `iss` parameter and can validate it.

### 3. Fix `WWW-Authenticate` challenge: point at path-aware PRM URL,
add `scope` param

- Was: `Bearer
resource_metadata=\"https://<host>/.well-known/oauth-protected-resource\"`
- Now: `Bearer
resource_metadata=\"https://<host>/.well-known/oauth-protected-resource/mcp\",
scope=\"api profile\"`

After change (1), only the path-aware URL returns a PRM document whose
`resource` matches what the MCP client connected to (\`<host>/mcp\`).
Pointing clients at the right URL keeps discovery consistent.

The `scope` parameter is a SHOULD in RFC 6750 and lets clients ask for
least-privilege scopes on first authorization.

## Not in this PR (queued separately)

From the same audit:

- **Audit JWT `aud` (audience) validation** — the spec requires the
server to reject tokens whose audience doesn't match this resource. Need
a read-only code review to confirm; filing as a follow-up.
- **Audit PKCE enforcement** — we advertise
`code_challenge_methods_supported: [\"S256\"]`; need to confirm the
\`/authorize\` flow actually rejects requests missing `code_challenge`.
- **403 `insufficient_scope` challenge format** for step-up auth.
- **CIMD (Client ID Metadata Documents)** support — newer spec
alternative to DCR.

## Test plan

- [x] \`yarn jest
--testPathPatterns=\"mcp-auth.guard|oauth-discovery.controller\"\` → 4/4
passing
- [x] \`tsc --noEmit\` clean on touched files
- [ ] After deploy:
  \`\`\`bash
curl -s https://<host>/.well-known/oauth-protected-resource | jq
.resource
  # expect: \"https://<host>\"
curl -s https://<host>/.well-known/oauth-protected-resource/mcp | jq
.resource
  # expect: \"https://<host>/mcp\"
  curl -sI -X POST https://<host>/mcp | grep -i www-authenticate
# expect: Bearer resource_metadata=\"…/oauth-protected-resource/mcp\",
scope=\"api profile\"
  \`\`\`

## Related

- #19836 — CORS exposes `WWW-Authenticate` + `MCP-Protocol-Version` so
browser clients can read them. Pairs with this PR.
- #19755 / #19766 / #19824 — the earlier chain that got host-aware
discovery and \`TRUST_PROXY\` working.

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

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-18 21:28:44 +02:00
github-actions[bot]
3292f1758e
i18n - translations (#19844)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-18 21:18:30 +02:00
Félix Malfait
53d22a3b70
fix(server): require PKCE code_challenge for public OAuth clients (#19840)
## Summary

OAuth 2.1 and the MCP authorization spec mandate PKCE (S256) for public
clients — clients registered with \`token_endpoint_auth_method=none\`
(no client secret). We advertise \`code_challenge_methods_supported:
[\"S256\"]\` in \`/.well-known/oauth-authorization-server\` but our
\`/authorize\` flow accepted requests from public clients without
\`code_challenge\`.

## Why this was a soft failure today

\`oauth.service.ts:178\` already rejects token exchange when a client
presents neither \`client_secret\` nor \`code_verifier\`:

\`\`\`ts
if (!clientSecret && !storedCodeChallenge) {
return this.errorResponse('invalid_request', 'Either client_secret or
code_verifier (PKCE) is required');
}
\`\`\`

So a public client attempting to bypass PKCE would **eventually** fail —
but only after:
1. Getting a valid authorization code issued at \`/authorize\`
2. Round-tripping the user through consent
3. Trying to exchange the code at \`/token\` and finally getting
rejected

That's a wasted user interaction and a fuzzy spec boundary. This PR
rejects at \`/authorize\` instead, matching the spec's \"MUST require
PKCE for public clients\" expectation.

## Fix

Single check in \`AuthService.generateAuthorizationCode\`:

\`\`\`ts
const isPublicClient = !applicationRegistration.oAuthClientSecretHash;

if (isPublicClient && !codeChallenge) {
  throw new AuthException(
\`code_challenge is required for public clients (PKCE S256, per OAuth
2.1)\`,
    AuthExceptionCode.FORBIDDEN_EXCEPTION,
  );
}
\`\`\`

### Why \`!oAuthClientSecretHash\` is the right \"public\" predicate

- Dynamic registration (\`POST /oauth/register\`) hardcodes
\`oAuthClientSecretHash: null\` and rejects any
\`token_endpoint_auth_method != \"none\"\`
(oauth-registration.controller.ts:120-130).
- Confidential clients registered via the workspace settings UI have a
non-null bcrypt hash.
- The same field is already used as the public/confidential gate in
\`validateClient\` and \`validateClientSecret\`.

## Scope

-  Dynamic-registration clients (Claude, other MCP connectors) — MUST
now supply code_challenge. They already do; no behavior change for
conformant clients.
-  The seeded twenty-cli registration — public client, already uses
PKCE. No change.
-  Confidential clients (workspace-admin-registered OAuth apps with a
client_secret) — unaffected, they authenticate at the token endpoint.

## Related

- #19836 — CORS exposes \`WWW-Authenticate\` / \`MCP-Protocol-Version\`
- #19838 — RFC 9728 PRM split + RFC 9207 iss param + \`scope\` in
WWW-Authenticate challenge

## Test plan

- [x] \`tsc --noEmit\` clean on modified file (pre-existing
\`twenty-shared\` dist errors unrelated)
- [ ] Integration-level smoke test after deploy:
  \`\`\`bash
  # Register a dynamic client (public)
  CLIENT_ID=$(curl -s -X POST -H 'Content-Type: application/json' \\
-d
'{\"client_name\":\"pkce-test\",\"redirect_uris\":[\"http://localhost/cb\"]}'
\\
    https://<host>/oauth/register | jq -r .client_id)

# Without code_challenge → should now 4xx at /authorize (cannot easily
test outside the React UI,
  # but the GraphQL authorizeApp mutation will throw AuthException)
  \`\`\`
- [ ] Claude MCP connector still completes OAuth end-to-end (it always
sends code_challenge, so no-op)

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

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-18 21:17:22 +02:00
github-actions[bot]
1c54e79d6c
i18n - translations (#19843)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-04-18 21:14:49 +02:00
neo773
be9616db60
chore: remove draft email feature flag (#19842) 2026-04-18 21:12:01 +02:00
Félix Malfait
c28c20143b
refactor(server): rename Agent exception to Ai; add THREAD_NOT_FOUND / MESSAGE_NOT_FOUND codes (fixes 500s) (#19831)
## Summary

- The exception class under `ai-agent/` was serving every AI surface
(agent, chat, role, models, generate-text), so `Agent` was a misnomer.
Promoted to the `ai/` namespace; renamed `AgentException` →
`AiException`, `AgentExceptionCode` → `AiExceptionCode`, and related
interceptor / filter / handler / file names accordingly.
- Split the single `AGENT_NOT_FOUND` code into entity-specific codes.
Chat-thread lookups no longer reuse the agent identifier.
- **Fixes Sentry 500s on `GetChatMessages` / `chatThread`.** Every
"Thread not found" and "Queued message not found" throw site in ai-chat
was previously wired to `AGENT_EXECUTION_FAILED`, which maps to
`InternalServerError` (HTTP 500). They now use `THREAD_NOT_FOUND` /
`MESSAGE_NOT_FOUND`, both of which map to `NotFoundError` (HTTP 404) in
the GraphQL and REST handlers.

The underlying cause of *why* clients are asking for threads that no
longer resolve for them — per-user chat-thread create events being
broadcast workspace-wide — is addressed separately in a follow-up PR.

### Code map

- Added: `ai/ai.exception.ts`,
`ai/utils/ai-graphql-api-exception-handler.util.ts` (+ spec with new
THREAD/MESSAGE cases),
`ai/interceptors/ai-graphql-api-exception.interceptor.ts`,
`ai/filters/ai-api-exception.filter.ts`
- Deleted: `ai/ai-agent/agent.exception.ts`,
`ai/ai-agent/utils/agent-graphql-api-exception-handler.util.ts` (+
spec),
`ai/ai-agent/interceptors/agent-graphql-api-exception.interceptor.ts`,
`ai/ai-agent/filters/agent-api-exception.filter.ts`
- Updated: 21 call sites across ai-agent, ai-agent-execution,
ai-agent-role, ai-chat, ai-generate-text, ai-models, role, and
workspace-migration validators.

## Test plan

- [x] `npx nx typecheck twenty-server`
- [x] `npx jest ai-graphql-api-exception-handler` (3/3 including new
THREAD_NOT_FOUND and MESSAGE_NOT_FOUND cases)
- [x] `npx jest agent-role.service` (9/9)
- [x] `npx oxlint --type-aware` on all changed files (0 warnings/errors)
- [x] `npx prettier --check` on all changed files
- [ ] CI
2026-04-18 21:08:33 +02:00
Charles Bochet
4c94699376
Bump twenty-sdk, twenty-client-sdk, create-twenty-app to 1.23.0-canary.1 (#19841)
## Summary
- Bumps `twenty-sdk`, `twenty-client-sdk`, and `create-twenty-app` from
`1.22.0` to `1.23.0-canary.1`.

## Test plan
- [ ] CI green

Made with [Cursor](https://cursor.com)
2026-04-18 19:41:01 +02:00
Charles Bochet
eb1ca1b9ec
perf(sdk): split twenty-sdk barrel into per-purpose subpaths to cut logic-function bundle ~700x (#19834)
## Summary

Logic-function bundles produced by the twenty-sdk CLI were ~1.18 MB even
for a one-line handler. Root cause: the SDK shipped as a single bundled
barrel (`twenty-sdk` → `dist/index.mjs`) that co-mingled server-side
definition factories with the front-component runtime, validation (zod),
and React. With no `\"sideEffects\"` declaration on the SDK package,
esbuild had to assume every module-level statement could have side
effects and refused to drop unused code.

This PR restructures the SDK so consumers' bundlers can tree-shake at
the leaf level:

- **Reorganized SDK source.** All server-side definition factories now
  live under `src/sdk/define/` (agents, application, fields,
  logic-functions, objects, page-layouts, roles, skills, views,
  navigation-menu-items, etc.). All front-component runtime
  (components, hooks, host APIs, command primitives) lives under
  `src/sdk/front-component/`. The legacy bare `src/sdk/index.ts` is
  removed; the bare `twenty-sdk` entry no longer exists.

- **Split the build configs by purpose / runtime env.** Replaced
  `vite.config.sdk.ts` with two purpose-specific configs:
  - `vite.config.define.ts` — node target, externals from package
    `dependencies`, emits to `dist/define/**`
  - `vite.config.front-component.ts` — browser/React target, emits to
    `dist/front-component/**`
  Both use `preserveModules: true` so each leaf ships as its own `.mjs`.

- **\`\"sideEffects\": false\`** on `twenty-sdk` so esbuild can drop
  unreferenced re-exports.

- **\`package.json\` exports + \`typesVersions\`** updated: dropped the
bare \`.\` entry, added \`./front-component\`, and pointed \`./define\`
  at the new per-module dist layout.

- **Migrated every internal/example/community app** to the new subpath
  imports (`twenty-sdk/define`, `twenty-sdk/front-component`,
  `twenty-sdk/ui`).

- **Added \`bundle-investigation\` internal app** that reproduces the
  bundle bloat and demonstrates the fix.

- Cleaned up dead \`twenty-sdk/dist/sdk/...\` references in the
  front-component story builder, the call-recording app, and the SDK
  tsconfig.

## Bundle size impact

Measured with esbuild using the same options as the SDK CLI
(\`packages/twenty-apps/internal/bundle-investigation\`):

| Variant | Imports | Before | After |
| ----------------------- |
------------------------------------------------------- | ---------- |
--------- |
| \`01-bare\` | \`defineLogicFunction\` from \`twenty-sdk/define\` |
1177 KB | **1.6 KB** |
| \`02-with-sdk-client\` | + \`CoreApiClient\` from
\`twenty-client-sdk/core\` | 1177 KB | **1.9 KB** |
| \`03-fetch-issues\` | + GitHub GraphQL fetch + JWT signing + 2
mutations | 1181 KB | **5.8 KB** |
| \`05-via-define-subpath\` | same as \`01\`, via the public subpath |
1177 KB | **1.7 KB** |

That's a ~735× reduction on the bare baseline. Knock-on benefits for
Lambda warm + cold starts, S3 upload size, and \`/tmp\` disk usage in
warm containers.

## Test plan

- [x] \`npx nx run twenty-sdk:build\` succeeds
- [x] \`npx nx run twenty-sdk:typecheck\` passes
- [x] \`npx nx run twenty-sdk:test:unit\` passes (31 files / 257 tests)
- [x] \`npx nx run-many -t typecheck
--projects=twenty-front,twenty-server,twenty-front-component-renderer,twenty-sdk,twenty-shared,bundle-investigation\`
passes
- [x] \`node
packages/twenty-apps/internal/bundle-investigation/scripts/build-variants.mjs\`
produces the sizes above
- [ ] CI green

Made with [Cursor](https://cursor.com)
2026-04-18 19:38:34 +02:00