071980d511d7c24d29fc2548528ddef2791ef7e2
4318 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
071980d511 |
Revert "fix compute folders to update util (#19749)" (#19921)
This reverts commit
|
||
|
|
30b8663a74 |
chore: remove IS_AI_ENABLED feature flag (#19916)
## Summary - AI is now GA, so the public/lab `IS_AI_ENABLED` flag is removed from `FeatureFlagKey`, the public flag catalog, and the dev seeder. - Drops every backend `@RequireFeatureFlag(IS_AI_ENABLED)` guard (agent, agent chat, chat subscription, role-to-agent assignment, workflow AI step creation) and the now-unused `FeatureFlagModule`/`FeatureFlagGuard` wiring in the AI and workflow modules. - Removes frontend gating from settings nav, role permissions/assignment/applicability, command menu hotkeys, side panel, mobile/drawer nav, and the agent chat provider so AI UI is always on. Tests and generated GraphQL/SDK schemas updated accordingly. ## Test plan - [x] `npx nx typecheck twenty-shared` - [x] `npx nx typecheck twenty-server` - [x] `npx nx typecheck twenty-front` - [x] `npx nx lint:diff-with-main twenty-server` - [x] `npx nx lint:diff-with-main twenty-front` - [x] `npx jest --config=packages/twenty-server/jest.config.mjs feature-flag` - [x] `npx jest --config=packages/twenty-server/jest.config.mjs workspace-entity-manager` - [ ] Manual smoke test: AI features still accessible without any flag row in `featureFlag` 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
5d438bb70c |
Docs: restructure navigation, add halftone illustrations, clean up hero images (#19728)
## Summary - **New Getting Started section** with quickstart guide and restructured navigation - **Halftone-style illustrations** for User Guide and Developer introduction cards using a Canvas 2D filter script - **Removed hero images** (`image:` frontmatter + `<Frame><img>` blocks) from all user-guide article pages - **Cleaned up translations** (13 languages): removed hero images and updated introduction cards to use halftone style - **Cleaned up twenty-ui pages**: removed outdated hero images from component docs - **Deleted orphaned images**: `table.png`, `kanban.png` - **Developer page**: fixed duplicate icon, switched to 3-column layout ## Test plan - [ ] Verify docs site builds without errors - [ ] Check User Guide introduction page renders halftone card images in both light and dark mode - [ ] Check Developer introduction page renders 3-column layout with distinct icons - [ ] Confirm article pages no longer show hero images at the top - [ ] Spot-check a few translated pages to ensure hero images are removed 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: github-actions <github-actions@twenty.com> |
||
|
|
a1de37e424 | Fix Email composer rich text to HTML conversion (#19872) | ||
|
|
a8d4039629 |
chore: sync AI model catalog from models.dev (#19914)
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> |
||
|
|
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) |
||
|
|
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) |
||
|
|
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.
|
||
|
|
13afef5d1d |
fix(server): scope loadingMessage wrap/strip to AI-chat callers (#19896)
## 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>
|
||
|
|
755f1c92d1 |
i18n - translations (#19893)
Created by Github action --------- Co-authored-by: github-actions <github-actions@twenty.com> |
||
|
|
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 |
||
|
|
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. |
||
|
|
117909e10a | Billing - Adapt to new unit (#19886) | ||
|
|
ade55e293f |
fix 1.22 upgrade command add-workspace-id-to-indirect-entities (#19868)
/closes #19863 |
||
|
|
10c49a49c4 |
feat(sdk): support viewSorts in app manifests (#19881)
## 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',
},
],
});
\`\`\`
|
||
|
|
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" /> |
||
|
|
5dd7eba911 |
Fix app design 6 (#19827)
Unify application display page and isntalled page --------- Co-authored-by: Charles Bochet <charles@twenty.com> |
||
|
|
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> |
||
|
|
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> |
||
|
|
75848ff8ea |
feat: move admin panel to dedicated /admin-panel GraphQL endpoint (#19852)
## 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. |
||
|
|
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
|
||
|
|
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> |
||
|
|
1f3defa7b3 |
fix(server): expose WWW-Authenticate header for browser-based MCP clients (#19836)
## 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> |
||
|
|
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> |
||
|
|
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>
|
||
|
|
1c54e79d6c |
i18n - translations (#19843)
Created by Github action --------- Co-authored-by: github-actions <github-actions@twenty.com> |
||
|
|
be9616db60 | chore: remove draft email feature flag (#19842) | ||
|
|
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 |
||
|
|
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)
|
||
|
|
fd495ee61b |
fix(server): deliver user-scoped metadata events only to the owning user (#19832)
## Summary Fixes cross-user cache contamination for AI chat threads in multi-user workspaces. `agentChatThread` is the only user-scoped (`userWorkspaceId`-filtered) entry in `METADATA_NAME_TO_ENTITY_KEY`, but its create/update events were going through `WorkspaceEventBroadcaster`, which fans out to every active SSE stream in the workspace. Every client therefore received other users' threads into their local `agentChatThreads` metadata store (which is persisted to localStorage). On a subsequent session, `AgentChatThreadInitializationEffect` would pick the most-recently-updated thread — potentially another user's — and fire `GetChatMessages` against it; the server's `userWorkspaceId` filter didn't match, producing "Thread not found" errors (now 404 thanks to twentyhq/twenty#19831). ### The fix mirrors the RLS pattern already used for object records `ObjectRecordEventPublisher` already reads `streamData.authContext.userWorkspaceId` to filter per subscriber. Metadata events had no equivalent. This PR closes that gap with a minimal, opt-in change: - Add optional `recipientUserWorkspaceIds?: string[]` to `WorkspaceBroadcastEvent`. Omit → workspace-wide (unchanged for views, objects, fields, etc.). Set → delivered only to streams whose `authContext.userWorkspaceId` is in the list. - `WorkspaceEventBroadcaster.broadcast` builds the payload per stream and skips events whose recipient doesn't match. - Both `agentChatThread` broadcast call sites in `AgentChatService` now pass `recipientUserWorkspaceIds: [userWorkspaceId]`. ### Scope 3 files, +40/-11 LOC: - `subscriptions/workspace-event-broadcaster/types/workspace-broadcast-event.type.ts` - `subscriptions/workspace-event-broadcaster/workspace-event-broadcaster.service.ts` - `metadata-modules/ai/ai-chat/services/agent-chat.service.ts` ### Stale cache note Existing clients still carry poisoned localStorage from before this lands. They self-heal on sign-out/in because `clearAllSessionLocalStorageKeys` already drops `agentChatThreads`. If we want to actively flush on deploy, a frontend cache-version bump can follow in a separate PR. ### Related - twentyhq/twenty#19831 turns the resulting "Thread not found" from 500 to 404. This PR addresses the root cause; #19831 stops the Sentry noise. ## Test plan - [x] `npx nx typecheck twenty-server` - [x] `npx oxlint --type-aware` on all 3 files (0 warnings/errors) - [x] `npx prettier --check` on all 3 files - [ ] Manual: two users in the same workspace, user A creates/sends a chat thread — confirm user B's session does not receive the event and their `chatThreads` sidebar is unaffected - [ ] CI |
||
|
|
70a73534c0 |
perf(server): reuse ESM module cache across warm Lambda invocations of logic functions (#19830)
## Summary Lambda warm-invocations of logic functions were spending **~440 ms** re-parsing and re-evaluating the user bundle on every call. The executor wrote the user code to a **randomly-named** temp file and `import()`-ed it, so each warm call resolved to a new URL and Node's ESM cache could never reuse the previous module record. This PR makes the executor write to a **content-hash filename**, skip the write when the file already exists, and stop deleting it. Identical code now reuses the same module record across warm calls in the same container, dropping warm-invocation overhead by **~30–40%**. ## What changed - `executor/index.mjs`: temp filename derived from `sha256(code)`, write skipped when file exists, no `fs.rm` on cleanup. - `lambda.driver.ts`: single structured `[lambda-timing]` log per invocation with `totalMs / buildExecutorMs / getBuiltCodeMs / payloadBytes / invokeSendMs / reportDurationMs / billedMs / initDurationMs / coldStart`. Goes through the standard NestJS `Logger`. No behavioural change for callers: same input → same output, same error semantics. ### Caveat: module-scope state now persists across warm calls With a stable filename, the user bundle is evaluated **once per warm container**. Any module-scoped state or top-level side-effects in user code are now shared across invocations of the same container, instead of re-running on every call. This is documented in the executor and is the intended trade-off — module scope should be treated as a per-container cache, not as per-call isolation. ## Findings — measured impact Same logic function (`fetch-prs`, ~12k PRs to page through), same workspace, same Lambda config (eu-west-3, 512 MB), token cache primed. ### Warm invocations | Phase | Before fix | After fix | Δ | | -------------------------------------- | ------------- | -------------- | ------------ | | Executor `import(userBundle)` | ~440 ms | **~0 ms** | **-440 ms** | | Lambda billed duration | ~1.5–1.7 s | **~1.0–1.1 s** | **~30–40%** | | Server-perceived round-trip | ~1.7–2.0 s | **~1.0–1.2 s** | **~30–40%** | ### Cold starts Unchanged — the cache helps subsequent warm calls in the same container, not the first one. Init Duration stays ~130–170 ms; total cold call ~2.5–3.0 s. ### Stress Could not reproduce the previously-reported \"every ~10th call times out\" behaviour after the fix: - 30 sequential calls: max 1.7 s, median ~1.1 s, 0 timeouts - 50 concurrent calls: max 9.4 s (clear cold-start cluster), median ~1.5 s, 0 timeouts Hypothesis: the warm-import overhead was eating into the headroom against the function timeout under bursty load; removing it pushed everything well below the limit. ## Observability One structured log line per invocation, sent through the standard NestJS logger: \`\`\` [lambda-timing] fnId=abc123 totalMs=1187 buildExecutorMs=2 getBuiltCodeMs=3 payloadBytes=1466321 invokeSendMs=1180 reportDurationMs=992 billedMs=1000 initDurationMs=n/a coldStart=false \`\`\` \`coldStart=true\` whenever Lambda spun up a fresh container; on warm calls \`buildExecutorMs\` and \`getBuiltCodeMs\` collapse to single-digit ms, confirming the cache fix is working. ## Test plan - [ ] CI green. - [ ] Deploy to a Lambda-backed env, trigger a logic function several times in a row. - [ ] Confirm \`[lambda-timing]\` warm invocations show \`totalMs\` ~30–40% lower than before, and \`coldStart=false\` after the first call in a container. - [ ] Push a new version of an app; confirm the next call shows higher \`buildExecutorMs\` (new hash, new file written) followed by warm calls again. - [ ] Smoke test: errors thrown by the user handler are still surfaced correctly. Made with [Cursor](https://cursor.com) |
||
|
|
1d575f0496 |
fix oauth permission check (#19829)
was regressed due to https://github.com/twentyhq/twenty/pull/19441 |
||
|
|
768469f2bd |
chore: sync AI model catalog from models.dev (#19828)
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> |
||
|
|
b292a93376 |
fix(server): honor X-Forwarded-* via configurable trust proxy (#19824)
## The bug Pasting `https://<workspace>.twenty.com/mcp` into an MCP client (Claude connector, etc.) fails discovery. Curl shows why: ```bash $ curl -si https://twentyfortwenty.twenty.com/.well-known/oauth-protected-resource HTTP/2 200 ... { \"resource\": \"http://{workspace}.twenty.com/mcp\", \"authorization_servers\": [\"http://twentyfortwenty.twenty.com\"], ... } ``` The response advertises `http://` even though the request came in on `https://`. RFC 9728 / RFC 8707 require the client to validate that the advertised `resource` matches the URL it connected to, so strict MCP clients reject the mismatch and OAuth never starts. ## Why request.protocol returns \"http\" Per [Express docs](https://expressjs.com/en/guide/behind-proxies.html), `request.protocol` returns the socket-level protocol unless `app.set('trust proxy', ...)` is configured. In our deployment: ``` client -- https --> Cloudflare -- https --> ingress-nginx -- http --> NestJS pod ``` TLS is terminated at the edge. The upstream TCP connection into the pod is plain HTTP, and nginx sets `X-Forwarded-Proto: https` for the pod to read. Without a `trust proxy` setting, Express ignores `X-Forwarded-Proto` and `request.protocol === 'http'`. `main.ts` currently has no `app.set('trust proxy', ...)` call anywhere. ## Why this only surfaced now `grep -rn request.protocol` finds three pre-existing call sites — `RestApiMetadataService`, `OpenApiService`, `RouteTriggerService`. All three wrap it in `getServerUrl({ serverUrlEnv: SERVER_URL, serverUrlFallback: \`${request.protocol}://${request.get('host')}\` })`, which returns `SERVER_URL` whenever it's non-empty. In production `SERVER_URL` is always set (e.g. \`api.twenty.com\`), so the \`request.protocol\` branch is effectively dead code there. #19755 introduced the first call site that uses `request.protocol` unconditionally — the OAuth discovery controller has to echo the request host, because the whole point is supporting multiple paste-able origins (workspace subdomains, custom domains, etc.). That's why this is the first \"wrong protocol\" bug anyone has seen in our app. ## The fix One line in `main.ts`: ```ts app.set('trust proxy', twentyConfigService.get('TRUST_PROXY')); ``` Backed by a new `TRUST_PROXY` env var with a default. `request.protocol` then honors `X-Forwarded-Proto`, `request.ip` honors `X-Forwarded-For`, etc. OAuth discovery URLs come out on the right scheme, and any future `request.protocol` callers Just Work. ## Why this needs to be configurable (not hardcoded) Twenty is open-source and deployed in at least three distinct topologies: 1. **Kubernetes with ingress** (us, enterprise self-hosters) — TLS terminated upstream, needs `trust proxy` **on**. 2. **Self-host behind a user-supplied reverse proxy** (Caddy, Traefik, nginx — our [recommended setup](https://twenty.com/developers/section/self-hosting)) — same as above, needs `trust proxy` **on**. 3. **Self-host with NestJS exposed directly to the internet** — no upstream proxy, needs `trust proxy` **off** (otherwise any curl with `X-Forwarded-For: 1.2.3.4` spoofs `request.ip`, poisoning rate-limiters and audit logs). There is no single static value that's correct for all three. Express makes this a setting for exactly this reason — we follow suit. ## Why the default is `'loopback, linklocal, uniquelocal'` Shorthand for loopback (127/8, ::1), link-local (169.254/16, fe80::/10), and unique-local (10/8, 172.16/12, 192.168/16, fc00::/7). In practical terms: **trust peers coming from private networks; don't trust the public internet**. This default is correct for shapes 1 and 2 (cloud, proxied self-host) because the ingress/proxy peer is always a private-network IP in every sane deployment. For shape 3 (directly exposed), the default is still safe because public clients have public IPs, which are not in any of those ranges — so `X-Forwarded-For` from an attacker on the internet is ignored. The only way to be bitten is the exotic case where a public client reaches NestJS through a private-network hop that isn't a proxy (e.g. a NAT appliance that forwards to the pod on a private IP and blindly appends headers). Narrow attack surface, and an operator running that kind of setup is expected to configure `TRUST_PROXY=false` explicitly. \"Safer than the naïve `true`, more useful than `false`\" — this matches what Rails, Django, and many other frameworks recommend for Kubernetes-style deployments. ## Why an env var instead of hardcoded - Rejecting hardcoded `true`: would expose shape-3 self-hosters to IP spoofing without a way to opt out. - Rejecting hardcoded `false`: would leave cloud + shape-2 self-hosters broken, same bug as today. - Accepting string-typed env (not boolean): Express's `trust proxy` accepts booleans, hop counts (`1`, `2`), IP ranges (`'10.0.0.0/8'`), and named CIDRs (`'loopback'`). A boolean would hide that flexibility; operators occasionally need the richer values. The string maps 1:1 onto what Express accepts. ## Deployment matrix | Deployment | Default works? | Override needed? | |---|---|---| | Cloud (us, K8s + nginx ingress + Cloudflare) | ✓ | — | | Self-host behind reverse proxy (recommended) | ✓ | — | | Self-host exposed directly on public IP | ✓ (public IPs not in private ranges) | Optional: `TRUST_PROXY=false` for strictness | | Local dev (direct, no proxy) | ✓ (no `X-Forwarded-*` headers arrive) | — | | Exotic: multi-hop through non-sanitizing private-network middlebox | Risky | `TRUST_PROXY=false` | ## Related - Blocks MCP connector OAuth on `<ws>.twenty.com` / custom domains. After deploy: `curl -s https://<ws>.twenty.com/.well-known/oauth-protected-resource | jq .resource` should return `https://...` (not `http://...`). - Fixes latent issue in `RestApiMetadataService`, `OpenApiService`, `RouteTriggerService` fallback paths (pre-existing but dead in production because `SERVER_URL` is always set — no behavior change there). ## Test plan - [x] `tsc --noEmit` clean - [ ] After deploy: `curl -s https://twentyfortwenty.twenty.com/.well-known/oauth-protected-resource` returns `https://` URLs - [ ] After deploy: MCP connector in Claude successfully completes OAuth against `https://<ws>.twenty.com/mcp` - [ ] No change in `request.ip` logging behavior on cloud (nginx-ingress peer is already private-network, was already being trusted implicitly by every framework layer that wasn't `request.protocol`) 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> |
||
|
|
4fa2c400c0 |
fix(server): skip standard page layout widgets referencing missing field metadatas during 1.23 backfill (#19825)
## Summary The 1.23 backfill command (`upgrade:1-23:backfill-record-page-layouts`) creates standard page layout widgets from `STANDARD_PAGE_LAYOUTS`. Some widgets reference field metadatas via `universalConfiguration` (e.g. the `opportunity.owner` FIELD widget pointing at universal identifier `20202020-be7e-4d1e-8e19-3d5c7c4b9f2a`). If a workspace's matching field metadata does not exist or has a different universal identifier (e.g. older workspaces created before standard universal identifiers were backfilled), the runner throws ``` Field metadata not found for universal identifier: 20202020-be7e-4d1e-8e19-3d5c7c4b9f2a ``` and the entire migration for that workspace aborts. This was the underlying cause behind the `Migration action 'create' for 'pageLayoutWidget' failed` error surfaced by #19823. |
||
|
|
e878d646ed |
chore(server): bump logic-function executor lambda memory to 512MB (#19826)
## Summary The executor lambda for user logic functions is created in `LambdaDriver` without a `MemorySize` parameter, so AWS Lambda falls back to its 128 MB default. That cap is too tight for non-trivial logic functions — large upstream GraphQL responses, JSON parsing of paginated batches, and chained Twenty Core API mutations push the process over the limit and trigger an OOM SIGKILL surfaced to the user as: ``` Runtime exited with error: signal: killed ``` This bumps the executor lambda memory to **512 MB** (matching the existing `BUILDER_LAMBDA_MEMORY_MB`). The change is applied on both: - The `CreateFunctionCommand` path used when a logic function is first deployed. - The `UpdateFunctionConfigurationCommand` path used when the deps/SDK layer wiring is refreshed — so existing functions get reconfigured on next deploy without any additional manual action. ## Why 512 MB Lambda compute is allocated proportionally to memory. 512 MB: - Matches the builder lambda already in this file (`BUILDER_LAMBDA_MEMORY_MB`). - Is comfortably above the 128 MB default that the existing OOMs are hitting. - Stays well below the higher tiers, keeping the per-invocation cost increase modest. Made with [Cursor](https://cursor.com) |
||
|
|
fb5a1988b1 |
fix(server): log inner errors of WorkspaceMigrationRunnerException in workspace iterator (#19823)
## Summary
When a workspace migration action fails during workspace iteration (e.g.
during upgrade commands), only the wrapper message was logged:
```
[WorkspaceIteratorService] Error in workspace 7914ba64-...: Migration action 'create' for 'pageLayoutWidget' failed
```
The underlying error (transpilation/metadata/workspace schema) and its
stack were swallowed, making production debugging painful.
This PR adds a follow-up log entry for each inner error attached to a
`WorkspaceMigrationRunnerException`, including its message and stack
trace. The runner exception itself is untouched — it already exposes
structured `errors` (`actionTranspilation`, `metadata`,
`workspaceSchema`).
After this change, logs look like:
```
[WorkspaceIteratorService] Error in workspace 7914ba64-...: Migration action 'create' for 'pageLayoutWidget' failed
[WorkspaceIteratorService] Caused by actionTranspilation in workspace 7914ba64-...: <real reason>
at ...
```
## Test plan
- [ ] Trigger a failing workspace migration (e.g. backfill record page
layouts) on a workspace and confirm the underlying cause + stack now
appear in logs.
Made with [Cursor](https://cursor.com)
|
||
|
|
3eeaebb0cc |
fix(server): make workspace:seed:dev --light actually seed only one workspace (#19822)
## Summary The `--light` flag of `workspace:seed:dev` was supposed to seed a single workspace for thin dev containers, but it was only filtering the rich workspaces (Apple, YCombinator) — the `Empty3`/`Empty4` fixtures introduced in #19559 for upgrade-sequence integration tests were always seeded. So `--light` actually produced **3** workspaces: - Apple - Empty3 - Empty4 In single-workspace mode (`IS_MULTIWORKSPACE_ENABLED=false`, the default for the `twenty-app-dev` container), [`WorkspaceDomainsService.getDefaultWorkspace`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-server/src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service.ts) returns the most recently created workspace — Empty4 — which has no users. The prefilled `tim@apple.dev` therefore cannot sign in, which breaks flows that depend on the default workspace such as `yarn twenty remote add --local`'s OAuth handshake against the dev container. This PR makes `--light` actually skip the empty fixtures so the dev container ends up with a single workspace (Apple). The default (no flag) invocation, used by `database:reset` for integration tests, still seeds all four workspaces, so `upgrade-sequence-runner-integration-test.util.ts` keeps working unchanged. |
||
|
|
59e4ed715a |
fix(server): normalize empty composite phone sub-fields to NULL (#19775)
Fixed using Opus 4.7, I wanted to test this model out and in this repo I know you guys care about quality, pls let me know if this is good code. It looks good to me Fixes #19740. ## Summary PostgreSQL UNIQUE indexes treat two `''` values as duplicates but two `NULL`s as distinct. `validateAndInferPhoneInput` was persisting blank `primaryPhoneNumber` as `''` instead of `NULL`, so a second record with an empty unique phone failed with a constraint violation. The sibling composite transforms (`transformEmailsValue`, `removeEmptyLinks`, `transformTextField`) already canonicalize null-equivalent values; phones was the outlier. - Empty-string phone sub-fields now normalize to `null`. `undefined` is preserved so partial updates leave columns the user did not touch alone. - `PhonesFieldGraphQLInput` drops the aspirational `CountryCode` brand on input. GraphQL delivers raw strings at the boundary; branding happens during validation. --------- Co-authored-by: Charles Bochet <charles@twenty.com> |
||
|
|
df9c4e26b5 |
Disable reset to default when custom tab or widget (#19814)
## Context "Reset to default" action is rejected by the backend for custom entities because there is no "default" concept for them ## Implementation Grey out the Reset to default action on record page-layout tabs and widgets when the entity either has no applicationId yet (unsaved draft — previously slipped through the existing check), or belongs to the workspace custom application. <img width="926" height="370" alt="Screenshot 2026-04-17 at 18 50 31" src="https://github.com/user-attachments/assets/c7c163f4-17a6-4b69-a66d-90f9085d27a2" /> |
||
|
|
0cb50f8a9d | Fix indexFieldMetadata select missing workspaceId (#19806) | ||
|
|
3268a86f4b | Skip backfill record page layouts for missing standard objects (#19799) | ||
|
|
68746e22a0 |
Send Email Tool: Don't persist message on SMTP only connections (#19756)
Previously this blocked users who only had SMTP configured to send outbound emails, this fixes it by making messageChannel and persist layer conditional |
||
|
|
4ed6fcd19e |
chore: move TABLE_WIDGET view type migration to 1.23 fast instance command (#19797)
## Summary - Relocates `AddTableWidgetViewTypeFastInstanceCommand` from `1-22/` to `1-23/` and bumps its `@RegisteredInstanceCommand` version from `1.22.0` to `1.23.0`. The original timestamp `1775752190522` is preserved so the command slots chronologically into the existing 1.23 sequence; auto-discovered via `@RegisteredInstanceCommand`, no module wiring change needed. - Same pattern as #19792 (move `pageLayoutWidget.conditionalAvailabilityExpression` to 1.23). |
||
|
|
f94ee2d495 |
i18n - translations (#19795)
Created by Github action --------- Co-authored-by: github-actions <github-actions@twenty.com> |
||
|
|
bb464b2ffb |
Forbid other app role extension (#19783)
# Introduction Even though this would not possible through API at the moment, from neither API metadata or manifest ( as manifest `permissionsFlag` declarations etc are done from within a declared role ) Prevent any app to create permissions entities over another app role from the validation engine itself ## `isEditable` We might wanna deprecate this column at some point from the entity it self as now the grain would rather be `what app owns that role ?` --------- Co-authored-by: Charles Bochet <charles@twenty.com> |
||
|
|
beeb8b7406 |
chore: move pageLayoutWidget.conditionalAvailabilityExpression migration to 1.23 fast instance command (#19792)
## Summary - Replaces the standalone TypeORM migration `1775654781000-addConditionalAvailabilityExpressionToPageLayoutWidget.ts` with a registered fast instance command under `packages/twenty-server/src/database/commands/upgrade-version-command/1-23/`, so the `pageLayoutWidget.conditionalAvailabilityExpression` column is created through the unified upgrade pipeline. - Uses `ADD COLUMN IF NOT EXISTS` / `DROP COLUMN IF EXISTS` so the new instance command is a safe no-op for environments that already applied the previous TypeORM migration. - Keeps the original timestamp `1775654781000` so the command slots chronologically into the existing 1.23 sequence; auto-discovered via `@RegisteredInstanceCommand`, no module wiring needed. ## Context Reported error when creating a new workspace on `main`: > column PageLayoutWidgetEntity.conditionalAvailabilityExpression does not exist Aligns this column addition with the rest of the 1.23 schema changes that already use the instance-command pattern. |
||
|
|
5cd8b7899d |
shouldIncludeRecordPageLayouts deprecation (#19774)
## Context Deprecating shouldIncludeRecordPageLayouts in preparation for page layout release. See new workspace with standard page layout from standard app <img width="570" height="682" alt="Screenshot 2026-04-16 at 18 35 23" src="https://github.com/user-attachments/assets/bf7fa621-d40d-4c29-8d96-537c58b3eb40" /> |
||
|
|
76ea0f37ed |
Surface structured validation errors during application install (#19787)
## Summary - Add `WorkspaceMigrationGraphqlApiExceptionInterceptor` to `MarketplaceResolver` and `ApplicationInstallResolver` so validation failures during app install return `METADATA_VALIDATION_FAILED` with structured `extensions.errors` instead of generic `INTERNAL_SERVER_ERROR` - Update SDK `installTarballApp()` to pass the full GraphQL error object (including extensions) through the install flow - Add `formatInstallValidationErrors` utility to format structured validation errors for CLI output - Add integration test verifying structured error responses for invalid navigation menu items and view fields --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
b31f84fbb8 |
fix(server): workspace member permissions and profile onboarding (#19786)
## Summary Aligns **workspace member** editing and **onboarding** with how the product is actually used: profile and other “settings” fields go through **`updateWorkspaceMemberSettings`**, while **`/graphql`** record APIs follow **object-level** permissions for the `workspaceMember` object. ## Product behaviour ### Completing “Create profile” onboarding Users who must create a profile (empty name at sign-up) get `ONBOARDING_CREATE_PROFILE_PENDING` set. The onboarding UI saves the name with **`updateWorkspaceMemberSettings`**, not with a workspace record **`updateOne`**. **Before:** The server only cleared the pending flag on **`workspaceMember.updateOne`**, so the flag could stay set and onboarding appeared stuck. **After:** Clearing the profile step runs when **`updateWorkspaceMemberSettings`** persists an update that includes a **name** (same rules as before: non-empty name parts). Onboarding can advance normally after **Continue** on Create profile. ### Two ways to change workspace member data | Path | Typical use | Who can change what | |------|----------------|---------------------| | **`updateWorkspaceMemberSettings`** (metadata API) | Standard member fields the app treats as “my profile / preferences” (name, avatar-related settings, locale, time zone, etc.) | **Always** your **own** workspace member. Changing **another** member still requires **Workspace members** in role settings (`WORKSPACE_MEMBERS`). Custom fields are **not** allowed on this endpoint (unchanged). | | **`/graphql`** record mutations on **`workspaceMember`** | Custom fields, integrations, anything that goes through the generic record API | **`WorkspaceMember`** is special-cased in permissions: **read** stays **on** for everyone, but **update / create / delete** require **`WORKSPACE_MEMBERS`**, including updating **your own** row via `/graphql`. So a **Member** without that permission cannot fix their name through **`updateWorkspaceMember`**; they use **Settings** / **`updateWorkspaceMemberSettings`** instead. | This matches **`WorkspaceRolesPermissionsCacheService`**: for the workspace member object, `canReadObjectRecords` is always true; `canUpdateObjectRecords` (and delete-related flags) follow **`WORKSPACE_MEMBERS`**. ### Hooks and delete side-effects - Removed **`workspaceMember.updateOne`** pre-query hook and **`WorkspaceMemberPreQueryHookService`**: they duplicated the same rules the permission cache already enforces for `/graphql`. - **`WorkspaceMember.deleteOne`** pre-hook still tells users to remove members via the dedicated flow; the post-hook only runs the **`deleteUserWorkspace`** side-effect when a member row is actually removed—**no** extra settings-permission check there, since only callers that already passed **object** delete permission can remove the row. ## Tests - **`workspace-members.integration-spec.ts`**: clarifies and extends coverage so **`/graphql`** **`updateOne`** is denied for **own** record on a **standard** name field and on a **custom** field when the role lacks **`WORKSPACE_MEMBERS`**. ## Implementation notes - **`OnboardingService.completeOnboardingProfileStepIfNameProvided`** centralises the “clear profile pending if name present” logic; **`UserResolver.updateWorkspaceMemberSettings`** calls it after save, using the typed update payload’s **`name`** (no cast). - **`UserWorkspaceService.updateUserWorkspaceLocaleForUserWorkspace`**: drops a redundant **`coreEntityCacheService.invalidate`**; **`updateWorkspaceMemberSettings`** still invalidates the user-workspace cache after the mutation. |