f90137b536ad5165b78facd953ca024ea8858ffe
13441 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
f90137b536 |
chore(server): remove nestjs-query from user-workspace module (#22591)
## Summary Continues the incremental removal of `@ptc-org/nestjs-query`. Migrates the `user-workspace` module to plain NestJS/TypeORM. This module registered no resolvers, so `nestjs-query` was only acting as module wiring and providing the service's inherited query methods — no public API behavior depended on it. ## Changes - `user-workspace.module.ts`: replaced `NestjsQueryGraphQLModule.forFeature` with plain `TypeOrmModule.forFeature`; kept all module imports and the service provider unchanged. - `user-workspace.service.ts`: dropped `extends TypeOrmQueryService` and the `super()` call; added an explicit `findById` (the only inherited method used externally, by `agent-actor-context.service.ts`). - `user-workspace.entity.ts`: swapped `@IDField` for the standard `@Field` on `id` (renders identically as `UUID!`). <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22591?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
628ab153a8 |
App installation workspace version check engines constraint (#22613)
## What
Makes the app-installation version gate **workspace-scoped**. App
installation now validates a manifest's `engines.twenty` requirement
against the version the **target workspace has actually finished
upgrading to**, instead of the instance/server's inferred version.
## Why
The server binary and a given workspace's migration state can diverge.
In a multi-workspace deployment the instance can already report version
`X` while an individual workspace still hasn't completed its
workspace-scoped upgrade commands for `X` (it's mid-upgrade or a
migration failed). Gating on the instance version let an app that
requires `X` install into a workspace whose schema/metadata is
effectively still at `X-1`, which can break the app. The requirement
should be checked against what the *workspace* has completed, not what
the server reports.
## How
- **`UpgradeStatusService.getWorkspaceCompletedVersion(workspaceId)`**
(new): resolves the last fully-completed upgrade version for a workspace
by reading its upgrade cursor and walking the upgrade sequence:
- Returns the cursor's version when the cursor sits on the **last step
of its version segment** and its status is `completed`.
- Otherwise walks backwards to the previous fully-completed version
segment.
- Returns `null` when the cursor is missing, not found in the sequence,
or otherwise uninterpretable.
- **`ApplicationVersionValidationService`**:
- Adds `validateWorkspaceCompatibility({ requiredServerVersion,
workspaceId })`.
- Extracts the shared semver logic into a private
`validateVersionAgainstRange({ version, requiredVersionRange, scope })`
and makes error messages scope-aware (workspace vs. instance).
`validateServerCompatibility` is preserved and now delegates to it.
- New failure reason `INVALID_WORKSPACE_VERSION`.
- **`ApplicationInstallService`** now calls
`validateWorkspaceCompatibility` with the `workspaceId` instead of
`validateServerCompatibility`.
- **Exception plumbing**: new
`ApplicationExceptionCode.INVALID_WORKSPACE_VERSION`, surfaced as a
`UserInputError` (`BAD_USER_INPUT`) with a user-friendly message ("This
workspace's upgrade state could not be determined…"). The
tarball/registration path maps it onto the existing
`INVALID_SERVER_VERSION` registration code.
## Notes
- **Publishing (app registration) is intentionally not
workspace-gated.** The tarball/registration path
(`ApplicationTarballService`) still uses the instance-level
`validateServerCompatibility` check, not the new workspace-scoped one.
Publishing an app is not tied to any particular workspace's upgrade
state, so there is no workspace version to check at that point — the
workspace-completed-version gate only applies when installing an app
into a specific workspace.
## Testing
- Unit tests for `ApplicationVersionValidationService`
(`validateServerCompatibility` + new `validateWorkspaceCompatibility`)
covering: no requirement, invalid semver range, satisfied/unsatisfied
ranges, and the uninterpretable-cursor case.
- Unit tests for `UpgradeStatusService.getWorkspaceCompletedVersion`
against a three-segment mock upgrade sequence (multi-command version,
instance-only version, workspace-terminated version).
- New integration suite
`failing-app-installation-workspace-version.integration-spec.ts` (+
snapshots) exercising the real install flow: rejects installation when
the workspace hasn't completed the required version, and when the
workspace's upgrade cursor can't be interpreted. Adds a
`create-app-tarball.util.ts` test helper.
|
||
|
|
cabe5545ae |
Use SDK calendarEventRecordPageFields identifiers in call-recorder (#22618)
Replaces the hardcoded calendarEventRecordPageFields view/group identifiers in the call-recorder preference view-field with `STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.calendarEvent.views.calendarEventRecordPageFields`, resolving the TODO. The published `twenty-sdk@2.18.0` (already pinned by the app) ships these identifiers with values matching the previously hardcoded ones. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22618?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
18ca89bcdd |
i18n - docs translations (#22617)
Created by Github action <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22617?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> Co-authored-by: github-actions <github-actions@twenty.com> |
||
|
|
07a921f8ca |
Add Document Generator SDK app + step-by-step tutorial (#22522)
## What & why
This adds a **guided tutorial** that teaches the Twenty SDK by building
one real, useful app end to end — plus the finished app itself, ready
for the marketplace.
The app, **Document Generator**, turns reusable templates into
personalized documents using CRM data: write a template once with
`{{placeholders}}`, then generate a filled-in document for any Person or
Company from the command menu, an AI agent, or a workflow.
## Two parts
**1. The app — `packages/twenty-apps/public/document-generator`**
Each capability maps to one tutorial chapter:
- **Data:** `documentTemplate` + `document` objects, fields, and a
bidirectional relation
- **Logic:** a single `generate-document` handler exposed as an **AI
tool**, a **workflow action**, and an **HTTP POST route**; plus a public
**HTML view route**
- **UI:** two views + sidebar navigation, a **command-menu item** (on
Person selection) that opens a **React front component**
- **AI:** an agent + skill; a default application role; marketplace
metadata + logo
- **Tests:** unit tests for the template renderer + an install
integration test
**2. The tutorial —
`packages/twenty-docs/.../apps/tutorials/document-generator/`**
A six-chapter series under **Developers › Apps › Tutorial** (Overview →
Data model → Generating documents → HTTP routes → Building the UI → AI
agent → Publishing). Minimal prose, paste-ready code, inline links to
the matching reference pages, and real screenshots. Registers a new
"Tutorial" nav group and regenerates `docs.json` + the navigation
template.
## Verification
Validated against a running Twenty instance (`twenty-app-dev` on
`:2020`):
- `twenty dev --once` installs cleanly (28 metadata objects created)
- Generated a real document from a Person — placeholders resolved (name,
job title, `company.name`, email), zero missing tokens
- Command menu → front component → generate flow works in the UI
- Public HTML view route renders the document
- App gates green: `yarn lint` (0/0), `yarn typecheck`, `yarn test:unit`
(7/7)
All screenshots in the tutorial are captured from this run.
## Notes
- Left out per-app CI workflows (`.github/workflows`) to keep scope
tight — happy to add them if wanted.
https://claude.ai/code/session_012FoC76zPbPmpgtN7MXMPwy
---
_Generated by [Claude
Code](https://claude.ai/code/session_012FoC76zPbPmpgtN7MXMPwy)_
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22522?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
---------
Co-authored-by: github-actions <github-actions@twenty.com>
|
||
|
|
2a495c3477 |
feat(app): allow claiming ownership of unclaimed app registrations (#22609)
## After <img width="653" height="703" alt="image" src="https://github.com/user-attachments/assets/ebe800da-b00b-4239-99a9-e157f7bfd7a0" /> <img width="634" height="711" alt="image" src="https://github.com/user-attachments/assets/00a048bf-36b1-489f-a080-1ed2d069e625" /> ## Context App registrations track their owner via `ownerWorkspaceId`. Curated / catalog / CLI apps are seeded **unclaimed** (`ownerWorkspaceId: null`). Until now there was no way to take ownership of an unclaimed app from the UI — the only ownership action was **Transfer ownership**, which requires the caller to already be the owner, so it can't act on a null-owner app. This PR adds a way to **claim** an unclaimed app registration, and makes the owner always visible on the detail page. ## Behaviour Admin panel → app registration detail → General tab: - The **Owner** row is now always shown — an **Unclaimed** tag when there's no owner workspace (previously the row was hidden). - Danger zone buttons are ownership-aware: - **Unclaimed** app → **Delete app** + **Claim ownership** (claims it for the current workspace). - **Owned** app → **Delete app** + **Transfer ownership** (unchanged). Transfer is hidden for unclaimed apps because transferring requires the caller to already own the registration. ## Changes **Backend** - New `claimOwnership` service method: looks the registration up globally, rejects it if it already has an owner, otherwise assigns `ownerWorkspaceId` to the caller's workspace. - New `claimApplicationRegistrationOwnership` mutation, guarded by `WorkspaceAuthGuard` + `SettingsPermissionGuard(APPLICATIONS)` (same guards as transfer). - New `ClaimApplicationRegistrationOwnershipInput` DTO (`applicationRegistrationId`). **Frontend** - **Claim ownership** button (shown only when the registration has no owner workspace); opens a confirmation modal and calls the new mutation. - **Transfer ownership** button now renders only for owned registrations. - The **Owner** row in the general info card is always displayed, with an `Unclaimed` tag when there is no owner. **Generated** - Regenerated the checked-in GraphQL artifacts (`twenty-front` metadata, `twenty-client-sdk` schema/types) against the live server so codegen output matches. ## Verification - `nx typecheck twenty-front` and `nx typecheck twenty-server` pass. - `oxlint` + `oxfmt` pass on all changed source files. - Codegen is idempotent — re-running the three `graphql:generate` configs + `generate-metadata-client` produces no diff. - Verified end-to-end on the running app against the seeded unclaimed `Twenty CLI` registration (Owner shows `Unclaimed`; Danger zone shows Delete + Claim ownership). https://claude.ai/code/session_01U7rbxhBSUQRWBbdP5TmAgZ |
||
|
|
5dc9d7ab36 |
fix(server): make all view children reparentable across a workspace migration sync (#22600)
## Summary Uniformizes the workspace migration engine so **every** view child entity — `viewField`, `viewFieldGroup`, `viewGroup`, `viewFilter`, `viewSort`, `viewFilterGroup` — can be reparented from one view to another within a single manifest sync, including when the previous parent view is deleted in the same sync. ### Context When an app manifest deletes a view and reparents its children onto another view in the same sync (e.g. replacing a custom `FIELDS_WIDGET` view with a standard one), the sync failed with a builder validation error `View field to update parent view not found`. Root causes: 1. `viewField`, `viewFieldGroup` and `viewGroup` had `viewId.toCompare: false`, so the diff never detected the parent-view change and never emitted a reparent update (the already-reparentable siblings `viewFilter`/`viewSort`/`viewFilterGroup` had `toCompare: true`). 2. `validateFlatViewFieldGroupUpdate` resolved the *old* parent view (it ignored the update patch), inconsistent with the other view-child validators. 3. Once the builder no longer errors, the runner would fail silently: `view.delete` ran **before** the child reparent updates, and `viewId` is `onDelete: CASCADE`, so the old view's deletion cascade-deleted the children before they could be reparented (silent data loss, since `repository.update` on a missing row is a no-op). ### Changes - **`all-entity-properties-configuration-by-metadata-name.constant.ts`**: set `viewId.toCompare: true` for `viewField`, `viewFieldGroup`, `viewGroup`. Because `viewId` maps to `universalProperty: 'viewUniversalIdentifier'`, the diff compares **only** `viewUniversalIdentifier` (never the raw FK). Snapshot updated accordingly. - **`flat-view-field-group-validator.service.ts`**: merge `flatEntityUpdate` and resolve the **new** parent view, matching the `viewField`/`viewGroup`/`viewSort` validators. - **`compute-ordered-migration-actions.util.ts`**: move `view.delete` to run **after** all view-child create/update actions so a child can be reparented off a view that is being deleted in the same sync. Child `delete → create → update` order is preserved (needed for `viewField`'s partial-unique `(fieldMetadataId, viewId)`). - **New integration test** `successful-manifest-reparent-view-children.integration-spec.ts` covering reparenting of every view child (a) between two persisting views and (b) when the source view is deleted in the same sync. ## Test plan - [x] `nx typecheck twenty-server` - [x] oxlint + oxfmt on changed files - [x] Unit snapshot regenerated: `all-universal-flat-entity-properties-to-compare-and-stringify.constant.spec` - [x] New integration test passes (both scenarios) - [x] Verified the delete-source scenario **fails** on the old action ordering (children cascade-deleted, `Received length: 0`) and **passes** after the reorder — confirming it's a genuine regression guard Made with [Cursor](https://cursor.com) <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22600?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
1d3f6176b2 |
Use record pickers for People Data Labs enrichment workflow inputs (#22596)
Follow-up to #21494, which added record-typed logic function workflow inputs but deferred the People Data Labs migration until the SDK release. twenty-sdk 2.16.0 (published) now includes the `record`/`records` input schema support, so this types the enrichment inputs accordingly: `records` on enrich-people/enrich-companies and `recordId` on enrich-person/enrich-company render as record pickers bound to Person/Company. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22596?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
c55bfab11e |
Fix side panel open/close animation glitches in the page header and panel content (#22598)
When the side panel opens or closes, the pinned header button (e.g. New Company) was flat-clipped for the whole 300ms animation: the ⋮ toggle mounts instantly and shifts the button's slot, framer's `layout` prop compensates with a transform that the surrounding `overflow: hidden` containers don't follow, and the ResizeObserver-driven rerenders re-seed that transform every frame. Removing `layout` lets the button follow plain reflow, which cannot clip. The panel content also squeezed during the animation (labels re-truncating at every intermediate width) because the inner panel was `width: 100%` of the width-animating wrapper. Pinning it to `var(--side-panel-width)` turns the animation into a rigid drawer slide; drag-resize is unaffected since it writes the same CSS variable. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22598?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
d3b79320b1 |
Remove book a call step from onboarding (#22597)
The book a call screen was shown as a dedicated onboarding step after sending team invites. It is no longer part of the flow: the `BOOK_ONBOARDING` status, its pending user var, the `skipBookOnboardingStep` mutation and the `BookCallDecision` screen are removed, and onboarding completes right after the plan step. The `/book-call` Cal.com page remains, reachable only from the "Book a Call" link on the upgrade screen, with a back link to `/plan-required`. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22597?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
ee28ae363f |
feat(files): use direct-to-storage upload for email and AI-chat attachments (#22610)
## Context Follow-up to the direct-to-storage upload work (#22449 / #22531 / #22533 / #22576). That migrated files-field, attachments and workflow uploads off the buffered path. This PR does the same for the **last two user-facing upload surfaces**: email attachments and AI-chat files. ## What this does - Adds `EmailAttachment` and `AgentChat` to the server's `DIRECT_UPLOAD_FILE_FOLDERS` allowlist. Both folders already resolve through the workspace-custom-application path in `resolveUploadLocation`, so no other server change is needed. - Routes the two frontend hooks through the existing `useDirectFileUpload` handshake (`createFileUpload` → `PUT` → `completeFileUpload`): - `useUploadEmailAttachment` → `FileFolder.EmailAttachment` (keeps its existing `MAX_ATTACHMENT_SIZE` client check — email has a real send-size limit). - `useAiChatFileUpload` → `FileFolder.AgentChat`. Each hook keeps its public signature and return shape, so call sites are unchanged. No schema change and no codegen needed — the `CreateFileUpload`/`CompleteFileUpload` documents and the `FileFolder` enum values already exist in `generated-metadata` from #22576. ## Why these are safe to migrate Both server services (`file-ai-chat`, `file-email-attachment`) just `writeFile` (store) and return a signed URL — no synchronous processing of the bytes at upload time — so the store-and-reference direct-upload flow fits exactly, same as files-field/workflow. ## Out of scope `CorePicture` (avatars, member/workspace pictures, logos) stays on the buffered path on purpose: small images that go through server-side image handling and are served inline, where the 10 MB body limit is already appropriate. ## Tests Extends the `FileUploadService` unit spec with an `it.each` asserting `createFileUpload` supports the `EmailAttachment` and `AgentChat` folders. ## Verification `typecheck` and `lint:diff-with-main` green on both `twenty-front` and `twenty-server`. (The server jest suite couldn't run in my local sandbox due to an unrelated config-import quirk present on a clean `main` checkout too — CI runs it normally.) https://claude.ai/code/session_015UH8KWmsB9zdYaog8MFG1d --- _Generated by [Claude Code](https://claude.ai/code/session_015UH8KWmsB9zdYaog8MFG1d)_ <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22610?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
9086b031e8 |
chore: bump sdk packages to 2.19.0-alpha.1 prerelease (#22599)
## Summary - Bumps `twenty-sdk`, `twenty-client-sdk` and `create-twenty-app` from `2.19.0` to `2.19.0-alpha.1` so the CD pipeline can publish a prerelease of the SDK. ## Context #22565 made system field universal identifiers deterministic and fail-closed: any app package built with SDK ≤ 2.18 carries legacy system field identifiers in its `manifest.json` and is now rejected at install/sync time on servers running `main`. Rebuilding the apps requires a published SDK carrying the new derivation. Publishing `2.19.0-alpha.1` lets us rebuild all apps in `packages/twenty-apps` (follow-up PR) against the new derivation while keeping `latest` on `2.18.0` for authors targeting prod, which has not run the 2.19 backfill yet. ⚠️ The publish job must tag this prerelease under a non-`latest` dist-tag (e.g. `next`): the server resolves app installs and the upgrade version check against the `latest` dist-tag. Server version constants (`TWENTY_CURRENT_VERSION`, etc.) are intentionally untouched. ## Test plan - [ ] Verify the three package versions are `2.19.0-alpha.1` - [ ] Verify the publish workflow in the CD repo tags the release as `next` (not `latest`) Made with [Cursor](https://cursor.com) <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22599?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
c8f7315c67 |
fix(server): converge app-sync isUnique diff for single-column unique constraints (#22592)
## Context Fixes #22550. On app sync (`twenty dev`), a field backed by a single-column unique constraint produces a **permanent, non-converging `[isUnique] changed` field-metadata diff**: the change is reported, the apply succeeds, and the identical change is reported again on the very next sync. ## Root cause Since the uniqueness-source-of-truth moved from a `FieldMetadata.isUnique` column to `IndexMetadata` (@FelixMalfait's #20846 / #20883), `field.isUnique` is a **derived** property. The two sides of the app-sync diff derive it differently: - **"from" side** (workspace cache, `WorkspaceFlatFieldMetadataMapCacheService`) derives `isUnique` from indexes via `computeUniqueFieldMetadataIdsFromIndexes`, which counted **any** single-column `UNIQUE` index. - **"to" side** (`from-field-manifest-to-universal-flat-field-metadata.util.ts`) sets `isUnique` from the field-level manifest flag (`fieldManifest.isUnique ?? false`). For a field whose uniqueness is declared with `defineIndex({ isUnique: true, fields: [oneField] })` (no field-level flag): - "from" derives `true` (the custom unique index exists), - "to" is `false` (no field-level flag), so the diff emits a `fieldMetadata … [isUnique] changed` update forever. The field-update runner drops `isUnique` before the SQL `UPDATE` (it has no column), and the custom index persists, so the derived value never changes — the loop cannot converge. ## Fix Restrict the derivation in `computeUniqueFieldMetadataIdsFromIndexes` to the field's **engine-owned backing constraint** — a `UNIQUE` index with `isSystemSideEffect: true` — rather than any single-column unique index. This makes `field.isUnique` mean the same thing on both sides: | declaration | backing index (`isSystemSideEffect`) | "from" derived | "to" flag | converges | |---|---|---|---|---| | field-level `isUnique: true` | side-effect handler generates it → `true` | `true` | `true` | ✅ | | `defineIndex({ isUnique: true, fields:[x] })` | custom index → `false` | `false` | `false` | ✅ (index converges on its own) | Standard objects and the create/update side-effect backing indexes are all `isSystemSideEffect: true` (`create-standard-index-flat-metadata.util.ts`, `generate-deterministic-index-for-flat-field-metadata-or-throw.util.ts`), so their fields keep `isUnique = true`. Only a user-declared custom `defineIndex` unique index (`isSystemSideEffect: false`) is now excluded — which is also what stops the create/update side-effect from generating a **second, duplicate** backing index for a field the custom index already covers (which would otherwise trip `DUPLICATE_UNIQUE_INDEX` on first apply). ## Why this location (and not the manifest "to" side) An earlier attempt derived `isUnique` on the manifest "to" side from the built indexes. That breaks after #22295: `field.isUnique === true` is the **trigger** for the `fieldUniqueBackingIndexOnCreate/Update` side-effect handlers, so forcing it to derive from the compute-service maps (which don't yet contain the not-yet-generated backing index) would suppress the backing index for field-level unique fields. Narrowing the shared "from" derivation keeps the side-effect trigger intact and makes both sides symmetric in one place. ## For review — @FelixMalfait This touches the uniqueness model you own in #20883 ("make IndexMetadata the source of truth for uniqueness"), and interacts with the side-effect engine from #22295. The semantic change is: **`field.isUnique` now reflects only the field's backing constraint, not an arbitrary user-declared single-column unique index.** A `defineIndex`-declared single-column unique field now surfaces `isUnique: false` on the field (the constraint is still enforced by the index). If instead you'd want `defineIndex` single-column uniqueness to surface as `field.isUnique: true`, the fix would need to live in the side-effect engine (dedupe the backing index against the declared one) rather than the derivation — happy to take it that direction. Flagging for your call before this leaves draft. ## Related - Issue #22550 - @FelixMalfait #20883, #20846 (IndexMetadata as source of truth for uniqueness) - #22295 (centralized side-effect engine — unique field backing index) - #21383 (adjacent field-`isUnique` handling) https://claude.ai/code/session_01T1Cqvt5tHS6tZ1FeQQWyRo |
||
|
|
2e1117d442 |
feat(messaging): message campaign delivery stats + views (#22452)
<!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22452?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
8a4bcd1445 |
(Billing for self hosts) Tie enterprise key to server (#22464)
# Enterprise key: bind to a server, free dev instances, self-serve transfer, shorter license ## Summary Enterprise keys were being reused across multiple instances (e.g. one prod + one dev, or several environments), which broke seat accounting and made licensing ambiguous. This PR ties each enterprise key to a **single server**, while giving customers a legitimate, self-serve way to run a **free development instance** and to **move their key** when they replace a server. ## Product behavior ### 1. Enterprise key is bound to one server - The first server to validate an enterprise key **claims** it (claim-on-first-use). From then on, that key is bound to that one server (until unbound - see 3.). - Any other instance that presents the **same key from a different server is hard-rejected**: it does not receive a license, so enterprise features stay off there. - Each instance has a stable server identifier. If one isn't set, the instance generates and persists one automatically on first validation (in keyValuePair table), so existing customers generally don't need to do anything (unless they have disabled config variables in db then they should add it to .env). ### 2. Free development instance - Every enterprise subscription gets **one free, non-billable development instance** in addition to its production instance. - An instance registers as development by declaring its instance type as `development` (done by default when validating the enterprise key, then can be toggled from UI or by updating value in keyValuePair table). - The free dev slot is only granted while there is an **active production instance** on the same subscription (so it's a perk for paying customers, not a way to run for free). - Only **one** dev instance can be active at a time per subscription, and it is **not counted as a billable seat**. ### 3. Self-serve unbind / rebind (transfer) - Admins can **release** the binding from the enterprise settings, which frees the key so it can be **claimed by a new server**. - This is the intended path when **sunsetting an instance and standing up a new one** (migration, re-hosting, disaster recovery): release on the old/dead box, then the new box claims it on its next validation. - To prevent abuse, releases are **rate-limited (10 per rolling 30 days)**; hitting the limit shows a clear message. ### 4. Automatic release of dead servers - If a bound server stops checking in for **14 days**, its binding is considered stale and is **auto-released**, so a replacement can claim the key without any manual step. This covers the case where the old server is already gone and can't release itself. ### 5. Shorter license validity (30 → 7 days) - The license (validity token) now expires after **7 days** instead of 30. The daily background refresh keeps healthy instances licensed transparently. - This limits the value of copying a license from one instance to another, since a copied license now stops working within a week. ### 6. License issuance is rate-limited - Issuing a new license is capped at **twice per 24h, independently for production and for development**. This tolerates the normal daily refresh (including small drift between runs) while blocking bursts of license minting for cloned instances. - Hitting this limit never revokes an existing, still-valid license — the current one keeps working until it expires; the manual "refresh" button just reports that the daily limit was reached. ## What changes for existing self-hosted customers **If you run a single production instance with one enterprise key:** nothing to do. On the next validation your instance reports its server identifier, claims the binding, and keeps working. **If you reuse one key across several instances (e.g. prod + dev, or multiple environments):** only the **first** instance to validate keeps its license. The others will **lose enterprise features**. To migrate: - Keep your production instance as-is (it claims the binding). - For a secondary/testing box, mark it as a **development instance** (set the instance type to `development`) to use the free dev slot — no extra cost. - If you genuinely need multiple production instances, you'll need **separate subscriptions/keys** for each. **If you're replacing a server (decommissioning + rebuilding):** - **Release** the binding from enterprise settings on the old instance, then start the new one — it will claim the key automatically. - If the old server is already gone, just wait for the **14-day auto-release**, or contact support. **Legacy instances that can't persist a server identifier automatically:** set the server identifier explicitly in your environment configuration (the instance logs a message telling you to do so). **Offline instances:** because licenses now last 7 days, an instance that can't reach our licensing endpoint for more than a week will lose enterprise features until it can check in again. > A migration email will be sent to affected customers separately. ## Technical implementation (brief) - Binding state lives in the **subscription's billing metadata** (bound server id + last-seen timestamps for prod and dev, release timestamps, and license-issuance timestamps). No new database is introduced on the licensing side; the billing provider's subscription metadata is the source of truth. <img width="976" height="413" alt="metadata_3" src="https://github.com/user-attachments/assets/ccc64822-e177-4223-a65a-4a4602aedf0e" /> - On each validation, a pure **binding resolver** takes the reported server id + instance type + current metadata and returns `allowed` (with the metadata to persist and whether the seat is billable) or `rejected`. It handles claim-on-first-use, staleness/auto-release, the dev-requires-active-prod rule, and the single-dev-slot rule. - **Rate limits** (release + license issuance) use a shared sliding-window helper stored as pruned timestamp lists in the same metadata, so the metadata self-cleans and never grows unbounded. License issuance uses **separate windows per instance type**. - The self-hosted instance **generates and persists a server identifier** if none is configured, and sends it (plus instance type) as instance metadata on validation. - A rejected binding returns a specific error code; the instance **revokes its stored license** on that code. A license-issuance rate-limit instead **throws a typed exception that surfaces to the manual refresh** while leaving the existing license untouched; the daily refresh job swallows it. - License lifetime is a configurable duration (defaulted from 30 to **7 days**), clamped to the subscription's cancellation date when sooner. |
||
|
|
ed2b2f8911 |
feat: publish MCP & API discovery documents (well-known standards) (#22589)
## What & why
Makes Twenty's **MCP server** and **REST/GraphQL APIs**
auto-discoverable by catalogs (e.g. integrations.sh) and AI agents,
using vendor-neutral open standards rather than a proprietary manifest.
The tricky part is that Twenty is **multi-tenant and the REST OpenAPI is
generated per workspace** (it reflects each workspace's custom objects,
and with no token even the base schema is empty). So there is no single
public URL that describes the full API contract. This PR solves that
with two complementary layers.
## 1. Static standards on `twenty.com` (`twenty-website`)
The brand-level catalog entry, using `{your-workspace-url}` placeholders
since `twenty.com` is not a workspace host:
- `public/.well-known/mcp/server-card.json` — MCP Server Card (SEP-2127)
- `src/app/.well-known/api-catalog/route.ts` — RFC 9727 linkset (route
handler so the `application/linkset+json` content type survives the
global `nosniff` header)
- `public/llms.txt` — LLM-readable overview
## 2. Dynamic per-host serving from `twenty-server`
A new `well-known` core module serves the same documents built from the
**request host**, so every workspace subdomain, custom domain, and
self-hosted instance advertises its own **real, connectable** endpoints
(`https://{that-host}/mcp`, its live `/rest/open-api/core`, etc.) — no
placeholder:
- `GET /.well-known/mcp/server-card.json`
- `GET /.well-known/api-catalog`
Both are public + CORS + cached. The api-catalog's `service-desc` points
at each host's **live** per-workspace OpenAPI — the honest answer to
"it's generated per workspace" (real endpoint, real custom objects,
still token-gated). The `version` comes from `APP_VERSION`.
The two layers are complementary: the static one serves
catalog/marketing discovery at the brand domain; the dynamic one serves
connecting clients the real endpoints — which is where the MCP spec
expects the server card to live (same origin as `/mcp`).
## Refactor
Extracted the request→base-URL logic that `OAuthDiscoveryController` had
as a private method into a shared
`src/utils/get-request-base-url.util.ts`, now used by both it and the
new controller.
## Notes
- Docs URLs are sourced from the shared `DOCUMENTATION_BASE_URL`
(server) and the `SITE_URLS` registry (website) rather than hardcoded.
- MCP endpoint, transport (`streamable-http`), and protocol version
(`2025-06-18`) are read from the existing MCP constants.
- OAuth resource metadata (`/.well-known/oauth-protected-resource`)
already existed and is unchanged.
## Testing
- `twenty-server` unit tests for the builders and controller (host
derivation, version fallback, linkset shape) — passing.
- `nx typecheck twenty-server` — passing.
- `oxlint` + `oxfmt` clean on both packages; website `check-conventions`
OK.
https://claude.ai/code/session_01F6g7kefcfpjXSZjH6cwqhi
---
_Generated by [Claude
Code](https://claude.ai/code/session_01F6g7kefcfpjXSZjH6cwqhi)_
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22589?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
|
||
|
|
8580cd6f27 |
feat(ai): open Ask AI side panel with a preprompt in two modes (#22582)
Add the ability to open the Ask AI side panel pre-filled with a prompt from any frontend component, with a mode to control whether the message is sent automatically or left for the user to review. - agentChatPrepromptState: holds the pending preprompt and its mode (PREFILL = fill only, SEND = fill and auto-submit) - useOpenAskAiPageWithPreprompt: seeds the new-thread draft, opens a fresh Ask AI thread and stores the preprompt intent - AgentChatPrepromptEffect: applies the intent once the chat editor and send listener are mounted, either restoring the editor content or dispatching the send event and clearing the editor <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22582?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
62c7e8f6b4 |
Polish onboarding v2 verify animation and step screens (#22585)
https://github.com/user-attachments/assets/1d9b8dc2-ef01-4202-a97e-b41cab048f87 A few polish tweaks to onboarding v2: - **Verify/workspace-creation animation:** emphasize the key phrase of each message in medium weight, the rest regular (e.g. "Creating your **workspace**…"). - **Wider content column:** 340px → 440px. Collapses to full width on mobile via the existing `max-width: 100%` on every consumer. - **Sticky disabled buttons:** step submit buttons now stay disabled from submit through navigation instead of briefly re-enabling once the mutation resolves. - **Fewer pulse loaders:** stop the pulsing logo from flashing when navigating between onboarding steps (removed the step-page Suspense fallback loader). The verify animation, cold-boot gates, and sign-in fallbacks are unchanged. Note: the reworded activation messages get new Lingui catalog IDs, so non-English locales fall back to English until catalogs are re-extracted. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22585?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
6c40c7b91a |
Deterministic system field universal identifier (#22565)
# Introduction Close twentyhq/core-team-issues#2641 Auto-provisioned field metadata used to get its `universalIdentifier` from three unrelated sources: random `v4()` on the server when creating custom objects, hardcoded values in `STANDARD_OBJECTS`, and an ad-hoc `v5` derivation in the SDK manifest build. This PR unifies all of them behind the shared `getFieldUniversalIdentifier` derivation: ``` universalIdentifier = f(applicationUniversalIdentifier, objectUniversalIdentifier, fieldName) ``` ## Ownership model The rollout is built on an explicit split of who owns a field's universal identifier: - **The 8 system fields** (`id`, `createdAt`, `updatedAt`, `deletedAt`, `createdBy`, `updatedBy`, `position`, `searchVector`) are **server-owned**. Their universal identifiers are always the deterministic derivation, on **every** application (standard, workspace-custom, installed). Clients cannot provide custom values: a temporary check in `validateObjectMetadataSystemFieldsIntegrity` rejects any non-derived system field identifier at migration build time. This check stands in until system fields are generated exclusively server side by the metadata side-effect engine and stripped from client inputs — at which point it becomes structurally impossible to send one. - **`name` is a default field, not a system field**: it is auto-provisioned when absent (server side for custom objects, SDK side for application objects) but authors can define their own. It is only derived where it is guaranteed to be auto-provisioned. In particular, standard objects keep their **historical hardcoded** `name` identifiers: the standard app authors its `name` fields like any installed app would, and moving those identifiers would break every installed application referencing them (e.g. views on `opportunity.name`). - **User-created and author-provided fields** keep random / explicit identifiers, untouched. ## Server - `validateObjectMetadataSystemFieldsIntegrity` now validates, on top of the existing type/`isSystem` checks, that each system field's `universalIdentifier` equals the deterministic derivation. Runs for every object creation going through the migration orchestrator: app sync, custom object creation, standard provisioning - `build-default-flat-field-metadatas-for-custom-object.util.ts` derives the system field identifiers (and the auto-provisioned `name`) with `getFieldUniversalIdentifier` instead of `v4()` - `build-default-relation-flat-field-metadatas-for-custom-object.util.ts` derives both the forward and the reverse default relation field identifiers deterministically - `generateMorphOrRelationFlatFieldMetadataPair` accepts optional `sourceFieldUniversalIdentifier` / `targetFieldUniversalIdentifier` so callers can inject deterministic values; user-created relations still default to `v4()` ## twenty-shared - `STANDARD_OBJECTS` system field identifiers (the 8) are now computed at module load via `buildStandardObjectSystemFields`; `name` and every other identifier keep their hardcoded values - New snapshot test pinning **every** universal identifier of `STANDARD_OBJECTS`: any identifier change now requires an explicit snapshot update and should ship with a coordinated backfill ## SDK (breaking, pre-GA) - `generateDefaultFieldUniversalIdentifier` delegates to `getFieldUniversalIdentifier` and now requires `applicationUniversalIdentifier` - Reverse default relation field identifiers are derived from the field's real coordinates (standard object UID + actual field name, e.g. `targetRocket` on `attachment`) instead of the legacy custom-object UID + synthetic `${fieldName}Inverse` hash input. Field *names* are unchanged - The manifest build threads the application universal identifier through default field injection (two-pass over object configs) - `twenty dev:add` now resolves the application universal identifier upfront and refuses to scaffold anything until `defineApplication` declares one — no more `fill-later` placeholder for the app UID in generated files ## Upgrade A 2.19 **workspace command** backfills existing `fieldMetadata.universalIdentifier` rows to the deterministic derivation. Coverage follows the ownership model: - **The 8 system fields**: taken over for **every application**, whatever value they currently hold. This is both safe and required now that sync rejects non-derived values — leaving a row unconverged would make its application unsyncable - **`name`**: workspace-custom app → always taken over (server-generated, no author to clobber); installed applications → only rows still carrying the legacy SDK derivation are recomputed, author-provided identifiers are never touched; standard app → never touched (hardcoded in `STANDARD_OBJECTS`) - **Default relation fields**: workspace-custom app → forward fields on custom objects and reverse fields on the standard relation objects; installed applications → legacy-derivation probe only All identifiers of a workspace are updated inside a single transaction, then the command flushes the field-metadata-related workspace caches and bumps the metadata version. Stored `applicationRegistration.manifest` snapshots are intentionally **not** rewritten: installs and upgrades always sync from the `manifest.json` inside the resolved package (npm/tarball), the stored column is only used for display/marketplace purposes. ## Breaking behavior for old packages (fail closed) Packages built with an older SDK carry legacy system field identifiers in their tarball `manifest.json`. Installing or upgrading such a package now fails with an explicit `INVALID_SYSTEM_FIELD` validation error ("universal identifier is not deterministic") instead of silently mismatching against the backfilled rows and triggering a destructive delete+create. The remediation is to rebuild the package with the new SDK; the backfill has already converged the installed rows, so the rebuilt manifest syncs cleanly. ## Test plan - [x] `twenty-sdk` unit tests (526 tests) and typecheck - [x] `twenty-shared` unit tests (1635 tests) including the `STANDARD_OBJECTS` snapshot; `name` identifiers verified byte-for-byte identical to `main` - [x] Lint and typecheck clean on all touched packages - [x] Integration: create a custom object and verify system + default relation field identifiers match the deterministic derivation (`create-one-object-metadata-deterministic-field-universal-identifiers`, 13 assertions passing) - [x] Integration: `failing-sync-application-object-system-fields` extended with a non-derived system field identifier case; all identifiers in the spec pinned deterministically so snapshots embedding expected/actual values are stable across runs (verified with a double run) - [x] Integration: all application sync suites pass with the derived system field identifiers now required by the `buildDefaultObjectManifest` test helper (9 suites, 20 tests) - [x] Full test-database reset: standard app provisioning and seeded workspaces pass the new validation - [x] SDK manifest build verified on the postcard example app: all auto-generated default field identifiers match the derivation - [ ] Run `upgrade:2-19:backfill-deterministic-field-universal-identifiers` (dry-run then real) on a seeded workspace and verify identifier convergence with a rebuilt app manifest |
||
|
|
0706c7c1bc |
feat(front): upload files directly to storage for files-field, attachments and workflow (#22576)
## Context Final step of the direct-to-storage upload work (follows #22449 endpoints, #22531 reaper, #22533 content-verify). The server can now hand the client an upload URL so bytes go straight to storage instead of being buffered through the Node process (the original OOM problem). This PR switches the frontend to that flow for the three in-scope surfaces. ## What this does Adds **`useDirectFileUpload`** — the shared hook that runs the handshake: 1. `createFileUpload({ filename, size, fileFolder, fieldMetadataId? })` → `{ fileId, uploadUrl, contentType, expiresAt }` 2. `PUT` the raw file to `uploadUrl` with `Content-Type: contentType` 3. `completeFileUpload({ fileId })` → `FileWithSignedUrl` (`{ id, path, size, createdAt, url }`) Routes the three existing upload hooks through it, **keeping each hook's public signature and return shape unchanged** so no call sites change: | Hook | Folder | |---|---| | `useUploadFilesFieldFile` (FILES fields) | `FilesField` | | `useUploadAttachmentFile` (attachments — the Attachment object's `file` FILES field) | `FilesField` | | `useUploadWorkflowFile` (workflow send-email attachments) | `Workflow` | Adds the `CreateFileUpload` / `CompleteFileUpload` gql documents and regenerates `generated-metadata` types (+19 lines, scoped to the two new operations). ## Out of scope - AI-chat (`AgentChat`) and email-attachment (`EmailAttachment`) uploads keep the legacy buffered mutations — those folders aren't in the server's direct-upload allowlist (`[FilesField, Workflow]`). - Workflow serverless-function code is saved via metadata mutations, not the file path. ## Notes - The legacy `uploadFilesFieldFile` / `uploadWorkflowFile` mutations still exist server-side and remain used by the out-of-scope surfaces, so this is non-breaking. - Local storage routes the `PUT` to the token-authenticated streaming endpoint (`SERVER_URL/file-upload/:id?token=…`); S3 uses a presigned `PUT`. CORS is already enabled globally on the server and the token rides in the query string (no cookies), so the browser upload works cross-origin. ## Verification `typecheck` and `lint:diff-with-main` green on `twenty-front`; codegen ran against a live metadata schema so the generated file matches the drift check. No existing tests/stories cover these hooks. https://claude.ai/code/session_015UH8KWmsB9zdYaog8MFG1d --- _Generated by [Claude Code](https://claude.ai/code/session_015UH8KWmsB9zdYaog8MFG1d)_ <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22576?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
9657c59272 |
Inject functions URL into logic function env (#22583)
<!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22583?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
29920738dc |
Fix stale token race forcing re-login on verify pages (#22573)
Landing on /verify often forced users to refresh and log in again. The culprit is a logout side effect triggered by a stale token: when a previous session's token pair is still in localStorage, boot queries use it, fail, and the failed token renewal reacts by logging the user out (onUnauthenticatedError clears the token pair). That logout fires while the loginToken exchange is running, so it can wipe the fresh session that was just stored. Fix: clear the stale token pair right before exchanging the loginToken (in useVerifyLogin, so both /verify and /verify-email are covered) — with no stale token to renew, the logout side effect never fires against the new session. Also removes the redundant clientConfig gate on the verify effect, stops that same logout side effect from redirecting users off /verify-email mid-verification, and always re-enables app redirects after loading the user. Note: opening a loginToken link now replaces an existing valid session instead of keeping it. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22573?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
2327ae7122 |
Revert "feat(server): add instance-level file storage layer" (#22579)
Reverts twentyhq/twenty#22560 <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22579?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
22b49a502c |
chore(server): remove unused flat-field-metadata per-object mocks (#22581)
## Context The `flat-field-metadata/__mocks__/` directory contained 11 large per-object `as const` mock catalogs (`OPPORTUNITY_FLAT_FIELDS_MOCK`, `PERSON_FLAT_FIELDS_MOCK`, `PET_FLAT_FIELDS_MOCK`, ...) plus a `getRelationTargetFlatFieldMetadataMock` helper. An audit of the whole server package (searching both the constant names and any import of the directory) showed almost none of them are consumed anymore — tests have moved to building exactly the fields they need with the `getFlatFieldMetadataMock` factory. Usage found: - `getFlatFieldMetadataMock` (factory): ~25 spec files + 2 core-modules mocks — **kept** - `COMPANY_FLAT_FIELDS_MOCK`: 1 spec (`object-record-event-publisher.spec.ts`), which only used the `name` field - The other 10 `*_FLAT_FIELDS_MOCK` catalogs and `getRelationTargetFlatFieldMetadataMock`: **zero consumers** ## Changes - Delete the 11 unused `*-flat-fields.mock.ts` catalogs and `get-morph-or-relation-target-flat-field-metadata-mock.ts` (~4,900 lines). Only `get-flat-field-metadata.mock.ts` remains. - In `object-record-event-publisher.spec.ts`, build the company `name` field inline with `getFlatFieldMetadataMock` (wired to `COMPANY_FLAT_OBJECT_MOCK.id`/`workspaceId`) and replace the three `COMPANY_FLAT_FIELDS_MOCK.name.type` references with `FieldMetadataType.TEXT`. The sibling `flat-object-metadata/__mocks__/` catalogs are untouched — several of those are still consumed by the morph/relation specs. ## Verification - `object-record-event-publisher.spec.ts`: 27/27 passing - `npx nx lint:diff-with-main twenty-server`: green - `npx nx typecheck twenty-server`: green https://claude.ai/code/session_01XcGEtwdXQo9uJibGRPexuG --- _Generated by [Claude Code](https://claude.ai/code/session_01XcGEtwdXQo9uJibGRPexuG)_ <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22581?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
fb0a54c73a |
fix(server): pace lambda control-plane calls to avoid 'Rate exceeded' on release (#22569)
## Problem Logic functions intermittently fail with: ``` Lambda invocation failed for function '<id>' during build: Rate exceeded ``` `Rate exceeded` is AWS Lambda's control-plane throttling (`TooManyRequestsException`), thrown during the **build** phase — before invoke — inside `buildExecutor`. ### Why it spikes on release A build is skipped (`canSkip = true`, zero control-plane calls) unless the executor is missing/inactive **or** `flatApplication.isSdkLayerStale` is true. `isSdkLayerStale` is flipped to `true` for the **whole application at once** whenever the SDK client regenerates (app install / development / schema change). So on release, every logic function in the app goes stale simultaneously → each enters `ensureExecutor` in its own per-function lock → a burst of `Create`/`Update`/`PublishLayer`/`GetFunction` calls across many functions at once → the low, account-region-wide control-plane quota is exceeded → `Rate exceeded`. Between releases everything is warm and no control-plane calls happen — hence "spikes on release, silent otherwise". The Lambda client was created with no retry override, so it used the SDK default (`standard` mode, `maxAttempts = 3`): a few retries with backoff, but no client-side pacing. ## Change Configure the shared Lambda client with: - `retryMode: 'adaptive'` — adds a client-side token-bucket rate limiter that slows outgoing requests when it sees throttling, instead of fire-then-backoff. - `maxAttempts: 8` — rides out the burst. Applied after the options spread so it always takes effect, and covers **every** control-plane call including the `waitUntilFunctionActive/UpdatedV2` pollers (same client). ## Scope / follow-up This is the cheap, high-leverage mitigation and dampens the burst per process. It does **not** add a cross-function/cross-pod concurrency cap, so a large enough release across multiple replicas could still exceed the account quota. A follow-up could add a limiter (in-process semaphore, or a distributed token bucket via the existing Redis cache-lock) around `ensureExecutor`. ## Testing - `tsc --noEmit` on twenty-server: clean. - Not runtime-tested — AWS control-plane throttling can't be reproduced locally. Worth confirming against a real release-time CloudWatch window after deploy. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22569?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
0baf213fa4 |
feat(server): add instance-level file storage layer (#22560)
Part of the app settings architecture cleanup
(twentyhq/core-team-issues#2456) — PR 1 of the instance-level documents
plan. Today all file storage is workspace-scoped
(`FileEntity.workspaceId NOT NULL`, `{workspaceId}/{app}/…` storage
keys, workspace-anchored tokens); instance-level data like
application-registration manifests and tarballs for ownerless catalog
registrations has no first-class home, forcing raw-driver bypasses
(`DefaultAiCatalogService`, prototype #22556).
## Changes (core storage layer only — no HTTP serving, no GraphQL
exposure)
**New `instanceFile` table** (`InstanceFileEntity`) — deliberately
separate from the workspace-scoped `file` table so nothing about the
existing system changes:
- `id`, `path` (unique, `{fileFolder}/{relativePath}` mirroring
FileEntity's convention), `size`, `mimeType`, timestamps
- nullable `applicationRegistrationId` FK (`onDelete: CASCADE`) —
registration-owned documents follow their registration
- no `workspaceId`, no `applicationId`; plain repository (added to the
`prefer-workspace-scoped-repository` lint rule's global-table
exemptions, as the rule's own message directs)
**New `InstanceFileStorageService`** (exported from the global
`FileStorageModule`):
- storage keys under a literal `instance/{fileFolder}/…` prefix —
collision-free with workspace prefixes (UUIDs); scope-validation util
mirroring `validateStoragePathIsWithinWorkspaceOrThrow`
- `writeInstanceFile` (upsert row on `path` conflict + driver write;
throws on failure — no swallowing),
`readInstanceFile`/`readInstanceFileById` (missing file surfaces
`FILE_NOT_FOUND` like `FileStorageService.readFile`),
`checkInstanceFileExists`, `deleteInstanceFile`/`deleteByInstanceFileId`
(bytes best-effort, row authoritative),
`deleteByApplicationRegistrationId` (lifecycle hook for
registration-owned files)
- same driver path as `FileStorageService` (`FileStorageDriverFactory` →
`ValidatedStorageDriver`)
**Migration**: fast instance command `add-instance-file-table` (2.19,
generator-produced; post-command `database:migrate:generate` reports no
pending changes).
## Next PRs in the plan
- PR 2: HTTP serving + token type for instance files (new route + guard;
workspace file endpoints untouched)
- PR 3: application-registration manifests stored as versioned instance
files (supersedes draft #22556)
- PR 4 (optional): registration tarballs migrate to instance scope,
removing the cross-workspace `FileEntity` read in
`application-package-fetcher` and the `ownerWorkspaceId` requirement on
`uploadTarball`
## Verification
- New specs: scope-validation util (traversal cases) + service (upsert
conflict, missing-file error, best-effort byte deletion, registration
cascade) — 16/16; `npx jest "application"` still 31 suites / 160 green
- Typecheck, `lint:diff-with-main`, full `oxfmt --check src/` (6421
files) and full type-aware oxlint clean
- Fast command executed against the local DB — table, unique index, and
CASCADE FK verified via psql; generator then reports no schema drift
- Server boots with the new provider; `generate-metadata-client
--skip-nx-cache` zero diff (no GraphQL change)
---
_Generated by [Claude
Code](https://claude.ai/code/session_011sST4rPLU1Koi2oVGi84ei)_
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22560?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
|
||
|
|
11edd56505 |
Don't list headless front components in the widget picker (#22578)
Front components can be marked headless (`isHeadless: true`), meaning they render no UI and only run logic. Both the record-page and dashboard widget pickers were listing every front component, including headless ones, which have nothing to render as a widget. This filters out headless front components where each picker reads them from `FIND_MANY_FRONT_COMPONENTS`. The downstream select-item mapping, keyboard-navigation list, and the "Front Components" group guard all derive from that array, so filtering once excludes them everywhere and hides the section when every front component is headless. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22578?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
faaeeee6f2 |
refactor(server): remove nestjs-query from key-value-pair module (#22575)
## Summary First step toward removing `@ptc-org/nestjs-query` from the codebase (follow-up to the `indexFieldMetadatas` DI bug [discussion](https://github.com/twentyhq/twenty/pull/22439#issuecomment-4864265452)). The `key-value-pair` module wrapped its entity in `NestjsQueryGraphQLModule.forFeature`, but registered **no resolvers** — the `KeyValuePair` type is exposed in no GraphQL schema, and `KeyValuePairService` only uses a plain TypeORM repository. The nestjs-query layer was doing nothing except registering that repository as a side effect. ## Changes - Replace the empty `NestjsQueryGraphQLModule.forFeature({...})` wrapper with a plain `TypeOrmModule.forFeature([KeyValuePairEntity])` - Swap the entity's `@IDField` (nestjs-query) for the standard `@Field` from `@nestjs/graphql` `nestjs-query` is no longer referenced anywhere under `key-value-pair/`. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22575?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
4cd05b2b3e |
Reverse pinned command menu item order on record header (#22577)
## Before <img width="502" height="102" alt="CleanShot 2026-07-06 at 12 10 42@2x" src="https://github.com/user-attachments/assets/da186911-5a1f-446f-a590-1af4a191554f" /> ## After <img width="500" height="102" alt="CleanShot 2026-07-06 at 12 10 17@2x" src="https://github.com/user-attachments/assets/6e7bb914-55af-4968-a15a-fb17378fffc7" /> Pinned command menu items in the record page header rendered left-to-right by position, putting the first item on the left. They should read the other way: first item on the right, last on the left. Fixed with `flex-direction: row-reverse` on the items container so the reversal is purely visual. The DOM/source order stays in position order, so the responsive overflow logic still keeps the highest-priority items visible and keyboard/screen-reader order is unaffected. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22577?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
e5e3fadbbb |
feat(files): reap stale pending direct-upload files via hourly cron (#22531)
## Context Follow-up to #22449 (direct-to-storage upload endpoints). That PR introduced the `PENDING` → `UPLOADED` file lifecycle: `createFileUpload` inserts a file record in `PENDING`, the client uploads the bytes directly to storage, then `completeFileUpload` flips it to `UPLOADED`. A client that initiates an upload but never confirms — a crash, a closed tab, an expired presigned URL — leaves a `PENDING` file record and a possibly-partial storage object behind forever. This PR reaps them. ## What this does Adds an hourly cron that hard-deletes `PENDING` files older than 24h together with their storage objects, in bounded batches. - **`PendingFileCleanupService`** — finds `PENDING` files with `createdAt` older than `PENDING_FILE_MAX_AGE_MS` (24h), capped at `PENDING_FILE_CLEANUP_BATCH_SIZE` (200) per run, and deletes each via `FileStorageService.deleteByFileId` (which tolerates a missing object). A failure on one file is logged and skipped so the rest of the batch still gets cleaned. - **`PendingFileCleanupCronJob`** — `@Processor(cronQueue)` job that runs the service and reports exceptions. - **`PendingFileCleanupCronCommand`** — registers the job on the hourly pattern (`0 * * * *`). - Wired into `FileUploadModule` (providers + export) and registered in `cron:register:all`. ### Why 24h The reaper threshold sits well past the presigned URL expiry, so a `PENDING` file only becomes reapable long after any legitimate in-flight upload could still complete — the cleanup can never race a real upload. A file that was never confirmed is referenced by nothing; the client recovery path is simply re-uploading under a fresh `fileId`, so we never promote to `UPLOADED`. ## Tests `pending-file-cleanup.service.spec.ts` covers: the query shape (status + age threshold + batch cap), deleting each stale file and returning the count, continuing past a per-file deletion failure, and the empty-batch no-op. ## Scope Server-only, non-breaking, no user-facing change. Part of the incremental direct-upload rollout being split into small PRs. https://claude.ai/code/session_015UH8KWmsB9zdYaog8MFG1d --- _Generated by [Claude Code](https://claude.ai/code/session_015UH8KWmsB9zdYaog8MFG1d)_ <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22531?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
d6b6962604 |
feat(files): content-verify direct uploads and pin pending files to octet-stream (#22533)
## Context Follow-up to #22449 (direct-to-storage upload endpoints). In that flow `createFileUpload` inserts a `PENDING` file record before any bytes exist, and until now it guessed the mime type from the **filename extension** — an untrusted, client-controlled value. This PR makes a pending file opaque and only trusts a mime type that was verified against the actual stored bytes. ## What this does **1. A pending file is always `application/octet-stream`.** `createFileUpload` records the pending file — and signs the presigned PUT — as `application/octet-stream`. The extension is still kept on the stored object name so the content can be checked against it later. **2. Content verification at completion.** `completeFileUpload`, after the existing size check, reads a **bounded prefix** of the stored object (`readReadablePrefix`, capped at 64 KiB — a large object is never buffered in full) and runs the existing `extractFileInfoOrThrow` util to detect the real mime type from the content. It: - writes the detected type alongside `status = UPLOADED`, and - rejects a file whose bytes don't match its declared extension (the record stays `PENDING`, so it can never be served or attached, and is reaped by the pending-file cleanup cron). Serving already overrides `Content-Type` from the DB record, so storing the object as octet-stream is fine. **3. A database constraint as backstop.** `CHK_FILE_PENDING_MIME_OCTET_STREAM` — `"status" != 'PENDING' OR "mimeType" = 'application/octet-stream'` — added to `FileEntity` and applied by a fast instance command (`2-19`). It is added `NOT VALID` on purpose: an instance freshly upgraded past #22449 may still hold `PENDING` rows whose mime came from the old extension-guess path, and `NOT VALID` enforces the invariant on every new/updated row without failing on that legacy backlog (those rows get overwritten to octet-stream when completed — `status` flips to `UPLOADED`, so the check passes — or are reaped while pending). ## Tests - `read-readable-prefix.spec.ts` — prefix reader: short source, early stop on a large source (asserts it tears the stream down without draining it), error propagation, empty stream. - `file-upload.service.spec.ts` — create records octet-stream; complete sniffs and sets the detected type, overrides a spoofed extension with the real content type, and rejects content that can't be matched to the declared extension. - `direct-file-upload.integration-spec.ts` — end-to-end case rejecting a `.png` upload whose bytes are plain text. ## Verification `typecheck` green, `lint:diff-with-main` clean, unit suites pass (17 tests). No GraphQL schema change, so no codegen drift. ## Scope Server-only, part of the incremental direct-upload rollout being split into small PRs. Independent of the reaper-cron PR (#22531). https://claude.ai/code/session_015UH8KWmsB9zdYaog8MFG1d --- _Generated by [Claude Code](https://claude.ai/code/session_015UH8KWmsB9zdYaog8MFG1d)_ <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22533?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
e23f82f700 |
chore: sync AI model catalog from models.dev (#22568)
Automated daily sync of `ai-providers.json` from [models.dev](https://models.dev). This PR updates pricing, context windows, and model availability based on the latest data. New models meeting inclusion criteria (tool calling, pricing data, context limits) are added automatically. Deprecated models are detected based on cost-efficiency within the same model family. **Please review before merging** — verify no critical models were incorrectly deprecated. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22568?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> Co-authored-by: FelixMalfait <6399865+FelixMalfait@users.noreply.github.com> |
||
|
|
fe8ec0463a | Call recorder: parse conference links from calendar event text (#22555) | ||
|
|
3cd4498bb2 |
fix: convert Microsoft calendar event HTML body to plain text description (#22540)
## Summary Calendar events synced from Microsoft accounts now show a readable plain-text description instead of raw HTML. Microsoft Graph returns event bodies as HTML by default; the importer stored `event.body.content` verbatim, so descriptions rendered as markup soup in the record page and the event drawer. ## Why this matters Issue #22537 reports Microsoft-synced calendar events displaying full `<html><body>...` content in the description field. Google Calendar events don't have this problem because Google returns plain text. The fix converts HTML bodies with the `html-to-text` package that's already a `twenty-server` dependency (used the same way in `packages/twenty-server/src/modules/messaging/message-import-manager/drivers/microsoft/utils/format-text-body.utils.ts` for email bodies), then normalizes line endings, non-breaking spaces, and blank-line runs. Non-HTML bodies pass through unchanged, and the existing `sanitizeCalendarEvent` step still runs afterwards. ## Testing Added specs to `format-microsoft-calendar-event.util.spec.ts` covering HTML-to-text conversion with `<br>` and block elements, HTML entity decoding and ` ` handling, empty/null bodies, and text-body passthrough. Fixes #22537 <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22540?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> --------- Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Co-authored-by: neo773 <neo773@protonmail.com> |
||
|
|
3a21089e0a |
feat(server): abort ai stream jobs that outlive the shutdown drain budget (#22517)
## Why #22514 makes SIGTERM drain workers, but `worker.close()` waits for active jobs with **no upper bound** (BullMQ semantics). An AI stream job can run for 10 minutes; a deploy would either hang the rollout or hit the pod's termination grace deadline and get SIGKILLed anyway — back to the frozen-stream + stalled-rerun failure this series eliminates. ## What On shutdown, `aiStreamQueue` gets a bounded drain: active stream jobs have `AI_STREAM_SHUTDOWN_DRAIN_MS` (60s) to finish naturally; stragglers are then aborted and terminate exactly like a stream failure — `lastStreamError` persisted with the new typed `AiExceptionCode.STREAM_INTERRUPTED`, the pinned `stream-error` → `queue-updated` terminal sequence published, claim released. The client shows the interrupted state with Retry (#22434) within seconds instead of a stream frozen mid-sentence. This is deliberately **not** the user-cancel path, which resolves cleanly and persists no error. Mechanism — evaluated BullMQ 5.78's native cancellation vs a parallel in-process registry, and picked native: - The driver's processor now declares the 3-arg signature, which makes BullMQ create a per-job `AbortController` (`processor.length >= 3` is the trigger), and the signal is handed to job handlers as an optional `MessageQueueJobContext`. - `worker.cancelAllJobs()` is purely cooperative: it aborts the signal and nothing else, so the job's own persist/publish/cleanup still runs to completion and `worker.close()` still waits for it — no force-fail race, no second signaling channel to maintain, and the timer lives inside the same `closeWorker()` call so there is no dependence on Nest module-destroy ordering. - The stream job maps the shutdown signal onto its **existing** AbortController (the one already wired through the AI SDK for user cancel), with an `AiException(STREAM_INTERRUPTED)` reason to tell the two apart. One abort path end to end, no new infrastructure. Error-type choice: the job throws a plain `AiException`, not BullMQ's `UnrecoverableError`. Stream jobs are enqueued with `attempts: 1` (no `retryLimit`), so there is no BullMQ retry to suppress — retryability for this queue lives at the app layer (`lastStreamError` + client Retry), and an `UnrecoverableError` would only obscure the typed exception. `STREAM_INTERRUPTED` also replaces the string constant introduced on the base branch (#22482's reap now uses the same enum member) — one code, two producers (reap for dead workers, abort for live shutdowns), identical client behavior. Notes: - 60s is a static constant mirroring `AI_STREAM_LOCK_DURATION_MS` rather than an env var — it has to move in lockstep with the worker's `terminationGracePeriodSeconds` (120s, twenty-infra PR) anyway, and we ship multiple releases a day. Happy to lift it into a config variable if you want runtime tunability. - The `onModuleDestroy` scaffolding (drain logs, workers-close-before-queues) deliberately matches #22514; whichever lands second rebases clean. Stacked on #22482 (needs the heartbeat/reap base). Merge order: #22482 → this. Depends on #22514 for SIGTERM to reach the driver at all. ## Validation - `stream-agent-chat.job.spec.ts`: shutdown-abort persists `STREAM_INTERRUPTED`, publishes `stream-error` before `queue-updated`, releases the claim, skips the queued-message flush; user-cancel semantics unchanged with a wired-but-idle shutdown signal (60/60 ai-chat tests green). - Local end-to-end: real AI stream mid-flight, SIGTERM the worker → drain window → abort → interrupted state persisted, process exits on its own. (Transcript in the PR conversation.) <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22517?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
904957ea1e |
message campaign redesign (#22508)
<!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22508?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
e90ab56c7a |
chore: sync AI model catalog from models.dev (#22558)
Automated daily sync of `ai-providers.json` from [models.dev](https://models.dev). This PR updates pricing, context windows, and model availability based on the latest data. New models meeting inclusion criteria (tool calling, pricing data, context limits) are added automatically. Deprecated models are detected based on cost-efficiency within the same model family. **Please review before merging** — verify no critical models were incorrectly deprecated. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22558?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> Co-authored-by: FelixMalfait <6399865+FelixMalfait@users.noreply.github.com> |
||
|
|
a4ed561e11 |
Add focus-safe side panel shortcuts (#22499)
## Summary - Add side-panel-owned Escape and Backspace behavior for the side-panel search input. - Keep side-panel Escape scoped to side-panel focus and avoid left-content fallback behavior. - Add a Side Panel group to the keyboard shortcut menu. - Reuse the side-panel focus id for AI chat thread-list shortcuts. ## Demo https://github.com/user-attachments/assets/80a632d6-7ff7-496b-905f-a3f95f9cfc14 --------- Co-authored-by: Charles Bochet <charles@twenty.com> |
||
|
|
f14e62ec0c |
Run integration tests against the real BullMQ driver (#22551)
Migrate whole suite to real BullMQ Shard times unchanged, still 5-6 min. |
||
|
|
4e43a0fb4e |
refactor(server): regroup application resolvers by resource and unify install permission flag (#22532)
Part of the app settings architecture cleanup (twentyhq/core-team-issues#2456) — implements the API-surface regroup Charles asked for in #20825 ("in application resolvers we have uninstall, upgrade, findMany, etc. and for some reason install is part of the marketplace, and they are not protected by same guards"). ## Changes **Resolver regroup by resource** (GraphQL operation names and signatures unchanged): - `installApplication` + `installMarketplaceApp` (deprecation preserved) move from the marketplace resolver into `application-install.resolver.ts`, next to `findManyApplications`/`findOneApplication`/`uninstallApplication`. - `uninstallApplication` moves from the manifest resolver into `application-install.resolver.ts`. - `runWorkspaceMigration` is deleted outright (unused — no consumer anywhere in the repo, front/SDK/e2e/docs); the now-empty manifest resolver is deleted. Its `AllMetadataName` GraphQL enum registration moves to `collection-hash.dto.ts` (its remaining consumer). - `generateApplicationToken` moves from the development resolver into `application-oauth.resolver.ts` next to `renewApplicationToken`, keeping its effective guards (`WorkspaceAuthGuard` + `SettingsPermissionGuard(APPLICATIONS)`) and its token-bucket throttle verbatim. - `upgradeApplication` stays in the upgrade resolver (moving it into the install resolver would create a module cycle — the upgrade module imports the install module). - Marketplace resolver now only holds catalog concerns: `findManyMarketplaceApps`, `findMarketplaceAppDetail`, `syncMarketplaceCatalog`. **Permission unification** (the only behavior change): `installApplication`, `installMarketplaceApp` and `upgradeApplication` move from `MARKETPLACE_APPS` to `APPLICATIONS`, matching uninstall and the find queries. Front-end install/upgrade button gating updated accordingly (`SettingsApplicationDetails` / `SettingsAvailableApplicationDetails`). **Module wiring**: `MarketplaceModule` no longer imports `ApplicationInstallModule` (only the moved resolver needed it); `ApplicationInstallModule` now imports `MarketplaceModule` — no cycle. Exception filters follow the moved operations (`ApplicationRegistrationExceptionFilter` on the install resolver; `ApplicationExceptionFilter` on the oauth resolver, which also fixes `renewApplicationToken`'s previously unmapped FORBIDDEN). **Codegen**: `twenty-client-sdk` metadata client regenerated for the new schema ordering (pure reordering — no field changes); all front `graphql:generate` configurations produced zero diffs. ## Explicitly kept (per review discussion) `installMarketplaceApp` (deprecated) and `generateApplicationToken` are kept for SDK back-compat despite having no current consumers. `runWorkspaceMigration` was also consumer-less but, unlike those two, had no back-compat rationale (not a deprecated alias, not a token primitive), so it is removed rather than relocated. ## Deferred follow-ups (guard inconsistencies found in the audit, intentionally NOT changed here) - `findApplicationRegistrationByUniversalIdentifier` uses `NoPermissionGuard` and returns the full registration entity, bypassing the `API_KEYS_AND_WEBHOOKS` gate that `findOneApplicationRegistration` enforces on the same data (SDK CLI `ensure-app-registration` depends on it today). - `upgradeApplication` alone requires `UserAuthGuard` — an API key can install but not upgrade. - `uploadAppTarball` (`MARKETPLACE_APPS`) and `transferApplicationRegistrationOwnership` (`APPLICATIONS`) are flag-inconsistent with the rest of registration CRUD (`API_KEYS_AND_WEBHOOKS`). - `syncMarketplaceCatalog` triggers an instance-wide job but is gated only by a per-workspace settings flag. ## Verification - `npx nx typecheck twenty-server` / `twenty-front` ✓; `lint:diff-with-main` clean for both - `npx jest "application"` in twenty-server: 30 suites / 154 tests passed - Server boots with the new module graph (DI verified at runtime); codegen run against the live server - Repo-wide grep: no remaining imports of the deleted manifest resolver <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22532?utm_source=github" rel="nofollow noreferrer noopener" target="_blank">``<img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg">``</a> --------- Co-authored-by: Charles Bochet <charles@twenty.com> |
||
|
|
59c16ef46f |
Polish settings billing and MCP UI (#22554)
## Summary - Polish Billing credits progress rounding and secondary action styling. - Update MCP setup logos, card spacing, grouping, and badge color. ## Before/After MCP & APIs <img width="2258" height="2010" alt="MCP & APIs settings visual" src="https://github.com/user-attachments/assets/915c8b50-5b98-4ba9-8e5c-a33f36440f6e" /> Billing <img width="2240" height="1644" alt="Billing settings visual" src="https://github.com/user-attachments/assets/9e3eb332-5792-4a4a-80b0-1b984e574008" /> |
||
|
|
ea851f7d9c |
feat: expose marketplace app detail fields explicitly and deprecate manifest blob (#22526)
Part of the application settings architecture work: https://github.com/twentyhq/core-team-issues/issues/2456 — follow-up to #22513. `MarketplaceAppDetail` returned the entire `manifest` jsonb (100KB+) over GraphQL and the front dug display fields and roles out of it. This PR: - Adds explicit fields to `MarketplaceAppDetail`: `description, author, category, logo, websiteUrl, aboutDescription, termsUrl, emailSupport, issueReportUrl, screenshots, defaultRoleUniversalIdentifier`, sourced from the registration columns introduced in #22513, and `roles: [MarketplaceAppRole!]` (full permission shape — the permissions tab and install modal render object/field permissions), sourced from the manifest at detail time. - Marks the `manifest` field `@deprecated` (kept functional — removal would be a breaking change). - Front: the shared `marketplaceAppDetailFragment` no longer selects `manifest`; display and role reads are flattened across `SettingsAvailableApplicationDetails`, `SettingsApplicationDetails`, and the share-link buttons. The three consumers that genuinely need deep manifest structure (content-tab counts/`manifestContent`, permissions objects, `useApplicationManifest` page-layout/view reads) use a scoped `FindMarketplaceAppManifest` query until the manifest demotion PR removes that need. - Codegen regenerated where the documents live: front metadata config + twenty-client-sdk metadata client (data/admin configs verified untouched). Verified: server+front typecheck, lint (0 warnings), server marketplace suite 10/10, front marketplace/applications suites 41/41, live schema introspection confirms the new fields and the deprecation. <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22526?utm_source=github" rel="nofollow noreferrer noopener" target="_blank">``<img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg">``</a> |
||
|
|
3db09e4423 |
feat(server): refresh application registration on install (#22527)
Part of the app settings architecture cleanup (twentyhq/core-team-issues#2456) — unifies registration ingestion across sources. ## Problem The dev sync, catalog sync and tarball upload flows all refresh the `applicationRegistration` row (manifest + display columns) at ingestion time, but the install/upgrade flow never did. Installing or upgrading an app relied on catalog sync having run beforehand, so a registration could serve stale display data (name, logo, description, screenshots…) after an install that shipped a newer manifest. ## Changes - `doInstallApplication` now refreshes the global registration from the resolved manifest after all install steps succeed (post-install hook included, so a hook failure that triggers uninstall can't leave the registration refreshed for a failed install). - Downgrade guard: the refresh is skipped when the installed version is provably older than `latestAvailableVersion` (per-workspace installs of an older version never downgrade the global registration). Extracted as a pure util `shouldRefreshApplicationRegistrationOnInstall` with unit tests: - `latestAvailableVersion` null or invalid semver → refresh - installed ≥ latest → refresh, and `latestAvailableVersion` is bumped to the installed version - installed < latest, or installed not valid semver while latest is → skip - Asset URLs mirror the existing per-source ingestion behavior: NPM registrations get manifest `logoUrl`/`screenshots` resolved to registry CDN URLs (same as catalog sync); tarball and other sources persist the manifest as-is (same as tarball upload). - `updateFromManifest` gains an optional `latestAvailableVersion` param (same conditional-spread style as `sourceType`). - `ApplicationRegistrationModule` added to `ApplicationInstallModule` imports (no cycle: nothing in the registration module's import graph imports the install module). The dev sync flow (`syncRegistrationMetadata`) already goes through `updateFromManifest` and writes the display columns — verified, no change needed. ## Verification - New unit spec: 6 cases on the guard util - `npx jest "application-registration|application-install|marketplace"` → 3 suites, 21 tests passed - `npx nx typecheck twenty-server` → success - `npx nx lint:diff-with-main twenty-server` → clean <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22527?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> --------- Co-authored-by: Charles Bochet <charles@twenty.com> |
||
|
|
99f99adf8f |
fix(server): require confidential client auth in authorization_code grant (#22548)
## Summary
Closes a **confidential-client authentication bypass** in the OAuth
`authorization_code` grant.
`OAuthService.exchangeAuthorizationCode` only validated `client_secret`
**when one was supplied** (`if (clientSecret)`), and the fallback check
at the end (`if (!clientSecret && !storedCodeChallenge)`) treats a valid
PKCE `code_verifier` as sufficient to complete the exchange. As a
result, a **confidential client** — one registered with a
`client_secret` (`oAuthClientSecretHash` set) — could have its
authorization codes redeemed using PKCE alone, with **no client
authentication**.
PKCE is defense-in-depth for public clients; it is not a substitute for
authenticating a confidential client (RFC 6749 §4.1.3, OAuth 2.1
§4.1.3). The `refresh_token` grant already enforces this exact rule —
this PR mirrors that gate in the `authorization_code` grant so any
client issued a secret must always present it.
## The fix
```ts
// Confidential clients (those issued a secret) must always authenticate,
// even when PKCE is used.
if (applicationRegistration.oAuthClientSecretHash && !clientSecret) {
return this.errorResponse(
'invalid_client',
'Client authentication required for confidential clients',
);
}
```
The check runs immediately after client resolution and before the
authorization code is even looked up. Public (PKCE-only) clients — those
without a stored secret hash — are unaffected.
## Testing
Added `oauth.service.spec.ts` covering:
- **Regression:** a confidential client presenting only PKCE and no
`client_secret` is rejected with `invalid_client` before any code
lookup.
- A wrong `client_secret` for a confidential client is still rejected.
- A public (PKCE) client is **not** blocked by the new gate and proceeds
to the code lookup.
Verified the regression test fails without the fix and passes with it.
Existing `application-oauth` suites remain green (8/8). Lint (`oxlint
--type-aware`, `oxfmt`) clean; the touched files typecheck.
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22548?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
|
||
|
|
74b3a4216e |
chore(apps): remove the twenty-for-twenty app (consolidated into twenty-eng) (#22549)
Removes the internal `twenty-for-twenty` app from this public repo. It has been consolidated into the private `twenty-eng` monorepo, which now also owns the Resend module (moved there in a companion PR). - Removes `packages/twenty-apps/internal/twenty-for-twenty/**` (177 files). - No build wiring referenced it (no nx project, not in `nx.json`/root workspaces); the only mention elsewhere is a naming-convention comment in `twenty-linear`. ## ⚠️ Sequencing - This removes **source only** — it does **not** uninstall the app currently deployed on the workspace. Merge only **after** the twenty-eng app has taken over the Resend objects there, so the live integration isn't left orphaned. - Supersedes the migration-plan doc PR (#22546), which added a doc into this now-removed directory; that doc now lives in the twenty-eng app. #22546 can be closed. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22549?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
1a60d4eaa3 |
Add MCP setup screen (#22468)
## Summary - Add a first-tab MCP setup experience under MCP & APIs with quick install cards, manual configuration, client logos, and HTTPS gating for Claude install links. - Rename API/Webhooks settings surfaces to MCP & APIs and update related icons, permissions, breadcrumbs, and command menu entries. - Add the Tabler sparkle-2 icon wrapper and MCP setup visual assets. ## Screenshots | Before | After | | --- | --- | |  | <img alt="image" src="https://github.com/user-attachments/assets/a6ae2ae6-322b-4370-b9b6-0a3d73ff7fa7" /> | <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22468?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> --------- Co-authored-by: Charles Bochet <charles@twenty.com> |
||
|
|
cfe0fc7ce6 |
feat(ci): detect bot signatures in PR description, comments and reviews (#22547)
## What Extends the **Blocked Contributors Check** beyond commits so it also scans a PR's: - **Description** (PR body) - **Conversation comments** - **Inline review comments** - **Review summaries** ## Why The check already fails a PR when a commit is attributed to a known bot (via author/committer/email and `Co-Authored-By` trailers). But bot-generated content also leaks into PR prose — descriptions and comments carry attribution footers like `🤖 Generated with Claude Code` that the commit-only scan never saw. ## How - **Commits** keep matching on bot *identity* (`IDENTITY_PATTERNS`: `@anthropic.com`, `cursoragent@cursor.com`, `copilot-swe-agent[bot]`). - **Prose surfaces** are matched only on `SIGNATURE_PATTERNS` — the verbatim auto-generated attribution footers (`Generated with Claude Code`, `Co-Authored-By: Claude`, Cursor equivalents). This is deliberately tight: contributors legitimately discuss Claude/Cursor in comments, so a bare product-name mention must **not** trip the check. Verified that "I used Claude Code to draft this but rewrote it", "works great in Cursor", and human `Co-Authored-By` lines all stay clean while real footers flag. - The workflow now also triggers on `issue_comment`, `pull_request_review` and `pull_request_review_comment` (plus PR `edited`), so bot prose added *between* commit pushes is still caught. `issue_comment` is guarded to PRs only, and `PR_NUMBER` resolves from either event. - Each prose violation reports the surface kind and a clickable URL. ## Notes `SIGNATURE_PATTERNS` are conservative by design and won't catch a footer someone reworded by hand. Widening them is a follow-up if we decide to trade some false positives for broader coverage. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22547?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
26491ecdc6 |
fix(front): don't blank record title when the title cell is untouched (#22293)
## Problem
On a record show page, the record title (label identifier) can be
silently blanked. Repro:
1. Create a record, set its name, set a relation field (e.g. a
one-to-one/many relation).
2. Reload the page.
3. Click the **edit** affordance on the relation field.
→ The record's name clears to “Untitled”, and it persists (an
`updateOne` fires with `input: { name: "" }`).
## Root cause
`RecordTitleCellTextFieldInput` registers `onClickOutside` / `onEnter` /
`onTab` / `onShiftTab` and always forwards `draftValue ?? ''` to be
persisted. When the title cell is in edit mode but the user never typed
in it, `draftValue` is `undefined`, so the forwarded value is `""`.
Opening another field's input counts as a click-outside on the title
cell, which then persists `{ name: "" }` over the existing label
identifier.
## Fix
Skip persisting when the title draft is untouched (`draftValue ===
undefined`) by passing `skipPersist` on the blur-style events. An
unedited title can no longer overwrite the existing label identifier;
genuine edits set the draft and persist exactly as before.
## Test plan
- Repro above: name no longer clears when editing a relation after
reload.
- Creating a new record and naming it still works.
- Renaming an existing record via its title still works.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22293?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
---------
Co-authored-by: Charles Bochet <charles@twenty.com>
|
||
|
|
6b2405c2e9 |
feat(twenty-for-twenty): bump to SDK 2.18 with local UI components (#22542)
## What & why Bumps the **Twenty for Twenty** internal app to the current SDK line and serves it on the matching `twenty-app-dev` image. - `twenty-sdk` / `twenty-client-sdk`: `^2.14.0` → **`^2.18.0`** (npm latest) - Runs against `twentycrm/twenty-app-dev:v2.18.5` (same 2.18 line) - App version `0.1.0` → **`0.2.0`** ### The `twenty-sdk/ui` break twenty-sdk 2.18 **removed the `twenty-sdk/ui` subpath** (#22326, "Remove twenty-ui reexport from the SDK"). The intended replacement — `twenty-ui@1.0.0-alpha.1` subpaths — requires **React 19 + a monaco-editor peer** that this React-18 app can't adopt, so the app no longer builds against 2.18 as-is. Instead of migrating to twenty-ui, this replaces the four `twenty-sdk/ui` consumers with **self-contained local components** under `src/ui/`, imported via a new `@ui` alias: - `Callout`, `H2Title`, `Status` - Tabler-style inline-SVG icons (`IconAlertCircle`, `IconInfoCircle`, `IconMail`, `IconRefresh`, `IconHelp`) - a `ThemeColor` type They mirror the twenty-ui components 1:1 using the `--t-*` theme CSS variables the front-component host injects (the same inline-style pattern the app already used for theme tokens) — no new runtime deps, no React 19 requirement. ## Test plan - `twenty dev:build` ✓, `yarn typecheck` ✓, `yarn lint` ✓ - Synced into a local `twenty-app-dev:v2.18.5` container (`twenty dev --once`) — objects/fields/views/app install applied cleanly - Verified rendering live: Sync Status page (`H2Title` headings + `Status` "Not synced" pills) and all three `Callout` variants (error/info/neutral) with correct colors + icons <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22542?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> |
||
|
|
873f8ad24c |
feat(helm): parametrize liveness, readiness and startup probes for server (#22268)
Closes #22267 ## What Exposes `startupProbe`, `livenessProbe` and `readinessProbe` as configurable values under `server.*` in `values.yaml`, with sensible defaults that work out of the box on current Twenty releases. Also switches the probe path from `/` to `/healthz`. ## Why The probes are hardcoded in the chart template today, and the defaults are no longer realistic for the current product. On a clean install of `v2.16.1` the server takes about **111 seconds** to reach `Nest application successfully started`. The current hardcoded `livenessProbe` only gives the pod **110 seconds** before killing it (`initialDelaySeconds: 60` + `failureThreshold: 5` x `periodSeconds: 10`). The pod is killed roughly 1 second before it would have been healthy and the deployment enters `CrashLoopBackOff` indefinitely. Twenty's boot time grows release by release as new Nest modules are added (v2.16 already registers 16 minor versions worth of upgrade commands at startup), so the chart's hardcoded defaults will keep drifting away from a working configuration. The probe path `/` returns the SPA HTML (or a 404 depending on routing), not a health response. The correct endpoint is `/healthz`, which returns `{"status":"ok","info":{},"error":{},"details":{}}` from a dedicated Nest controller. ## How Uses the same `{{- with }}` pattern already present in the chart (`extraEnv`, `extraVolumeMounts`, and the four scheduling fields added in #22233): ```yaml {{- with .Values.server.startupProbe }} startupProbe: {{- toYaml . | nindent 12 }} {{- end }} {{- with .Values.server.livenessProbe }} livenessProbe: {{- toYaml . | nindent 12 }} {{- end }} {{- with .Values.server.readinessProbe }} readinessProbe: {{- toYaml . | nindent 12 }} {{- end }} ``` This pattern lets the user disable any probe by setting it to `null`, override individual fields by providing the full block, or fall back to the defaults shipped in `values.yaml`. ## Defaults ```yaml server: startupProbe: httpGet: path: /healthz port: http-tcp periodSeconds: 10 failureThreshold: 30 # 5 minutes total boot grace livenessProbe: httpGet: path: /healthz port: http-tcp periodSeconds: 30 failureThreshold: 3 readinessProbe: httpGet: path: /healthz port: http-tcp periodSeconds: 10 failureThreshold: 3 ``` ## Scope This PR only touches the server Deployment. The worker Deployment is intentionally out of scope: it does not expose HTTP, had no probes before this change, and any probe added there would need a different shape (`exec` or `tcpSocket`). It can be addressed in a follow-up if maintainers want it. ## Backward compatibility For any cluster that booted Twenty correctly with the previous defaults (boot time under 5 minutes), `helm template` output is functionally equivalent: the new `startupProbe` covers the boot window, then `livenessProbe` and `readinessProbe` take over with similar semantics. For clusters where the previous defaults were already failing (such as this one — see "Validation" below), the new defaults make the install work out of the box. Setting any probe value to `null` disables that probe entirely. ## Schema note `values.schema.json` updated with `startupProbe`, `livenessProbe` and `readinessProbe` under `server`, all typed as `["object", "null"]` to honour the disable-by-null contract. ## Validation - `helm lint` passes. - `helm template` with default values renders the three probe blocks on the server Deployment. - `helm template` with one probe set to `null` correctly omits that probe. - `helm template` with overridden values renders the user-supplied probe configuration. - Live install validated on a multi-node Kubernetes cluster running Twenty v2.16.1 on an Oracle Cloud ARM64 worker node. With the new `startupProbe` the server reaches `Ready 1/1` in around 2 minutes from fresh pod creation. With the previous hardcoded probes the same pod entered `CrashLoopBackOff` indefinitely (276 restarts in 21 hours observed before applying the fix). ```` ## Files touched - `packages/twenty-docker/helm/twenty/templates/deployment-server.yaml` - `packages/twenty-docker/helm/twenty/values.yaml` - `packages/twenty-docker/helm/twenty/values.schema.json` ## Related - Discovered while validating PR #22233 (nodeSelector / tolerations / DNS overrides). - Same chart, same pattern, same self-host audience. - Closes #22267. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22268?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> --------- Co-authored-by: Charles Bochet <charles@twenty.com> |