28adcbffb9cacea4e4ab4674b87cff8bdcd945b7
211 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
71a1ff7ac8 |
Cache twenty-client-sdk modules host-side via content-addressed URLs (#22981)
## Context Front component sources are fetched host-side and integrity-verified by the SHA-256 checksum embedded in their URL, cached in Cache Storage — a layer that exists specifically because their download URLs are presigned and rotate. The `twenty-client-sdk` modules (`core` and `metadata`) were re-fetched on every render and could not be cached safely: their URLs carried no checksum and the server exposed no freshness signal. This PR makes the SDK module URLs **content-addressed** and relies on the **browser HTTP cache** for immutability, and it keys the checksums on their real owners: the **application** for `core`, the **instance** for `metadata`. The checksum does double duty: cache invalidation (regeneration changes the checksum → the URL changes → guaranteed cache miss) and a server-side cacheability guard (the server only grants `immutable` when the checksum in the URL matches the authoritative checksum it knows for that module — persisted at generation time for `core`, hashed once at bootstrap for `metadata` — so no per-request hashing of the served bytes). Note this is **not** an end-to-end integrity guarantee: there is no client-side hash verification, and on a fingerprint mismatch the server still serves the current bytes with `no-store` (self-healing for stale URLs) rather than failing. <img width="2412" height="926" alt="image" src="https://github.com/user-attachments/assets/d97935d2-0fdb-4c44-89ac-596b7ca8ca64" /> Closes twentyhq/core-team-issues#2688. ## Routes | Module | URL | Scope | | --- | --- | --- | | `core` | `/rest/sdk-client/{applicationId}/core[/{checksum}]` | Per application (generated bundle) | | `metadata` | `/rest/sdk-client/metadata[/{checksum}]` | **Instance-wide**: no application segment, so every application converges on one URL and the browser downloads the module once per release instead of once per application | The previous application-scoped metadata path (`/rest/sdk-client/{applicationId}/metadata[/{checksum}]`) is **kept for backward compatibility**, new clients just stop generating those URLs. The instance-wide route is declared before the parameterized route so `metadata/{checksum}` is not swallowed as `:applicationId/:moduleName`. ## Caching model | Request | `Cache-Control` | Effect | | --- | --- | --- | | Fingerprinted URL, checksum matches the known module checksum | `immutable` | Cached indefinitely by the browser HTTP cache; a new checksum is a new URL | | Fingerprinted URL, checksum does not match | `no-store` | Current bytes served uncached (self-healing for stale URLs) | | Bare URL (pre-generation fallback, `core` only in practice) | `no-store` | Never cached | - Both responses also set `X-Content-Type-Options: nosniff` and `Content-Type: application/javascript`. - SDK modules are intentionally **not** placed in Cache Storage. That layer stays reserved for the presigned/rotating component-source URLs; SDK modules are served directly and authenticated, so the browser HTTP cache (keyed by the content-addressed URL) is their single cache layer. ## Checksum provenance - **core** — per **application**, persisted on `application.sdkClientCoreChecksum` at generation time and read back from `flatApplicationMaps` (never re-hashed per request). - **metadata** — **instance-wide**, hashed once from the installed `twenty-client-sdk/dist/metadata.mjs` package (warmed at bootstrap, memoized per process) and served straight from that package, so it is fresh from the first request after a release with no archive dependency. ## Server (twenty-server) - Hash `dist/core.mjs` at SDK generation and persist `sdkClientCoreChecksum` via `applicationRepository.update`. Adds the nullable text column to `application.entity.ts` (mirroring `packageJsonChecksum`) plus a fast instance command with up/down; `FlatApplication` picks it up automatically. - New **application-scoped** query `applicationSdkClientChecksums(applicationId: UUID!): SdkClientChecksums` on `ApplicationResolver` (metadata schema, `WorkspaceAuthGuard` + `NoPermissionGuard`). `SdkClientChecksums.core` is **nullable** and stays `null` until the SDK has been generated at least once; `metadata` is **always present** (bootstrap-warmed), so the metadata module is cacheable from the very first render of any app. The query itself returns `null` only for unknown applications. - `SdkClientChecksumsDTO` now lives in the shared `core-modules/sdk-client/dtos/`. `FrontComponentDTO` and the `frontComponent` resolver no longer carry checksums (decoupled from the front-component row). - `sdk-client` controller: instance-wide `metadata[/:checksum]` route (no workspace-cache or application lookup, serves the memoized installed module) + application-scoped `:applicationId/:moduleName[/:checksum]` route (serves `core` from the per-application archive, `metadata` kept for back-compat). Cacheability compares the URL checksum against the **known** checksum — persisted `sdkClientCoreChecksum` for `core`, memoized package hash for `metadata` — instead of hashing the served bytes on every request: `immutable` on match, `no-store` otherwise (bare URL or stale fingerprint), plus `nosniff`. A persisted checksum out of sync with the archive only downgrades to `no-store` until the next regeneration. ## Front (twenty-front) - New metadata query `GetApplicationSdkClientChecksums`, keyed by `applicationId`; removed the `sdkClientChecksums` selection from `FindOneFrontComponent`. - `getSdkClientUrls` builds the two module URLs independently: `/sdk-client/{applicationId}/core/{checksum}` and the **instance-wide** `/sdk-client/metadata/{checksum}` (no application segment → one shared browser cache entry per release across all applications). Each falls back to its bare URL when its checksum is absent — since `core` is nullable, a never-generated app still gets a content-addressed metadata URL and only `core` falls back. The checksum type is sourced from the codegen `SdkClientChecksums` type rather than a hand-maintained duplicate. - `FrontComponentRenderer` is split into a gating outer component (runs `FindOneFrontComponent`, renders nothing while loading) and a content component that receives a guaranteed-non-null `frontComponent`. Following project conventions, the side effects live in dedicated effect components: `FrontComponentLoadErrorSnackBarEffect` (query error → snackbar) and `FrontComponentApplicationTokenPairEffect` (mirrors the query-derived token pair into component state unconditionally, `null` included, so revoked credentials can never be retained or refreshed). The content component fetches checksums via the application-keyed query and **gates the mount of SDK-using components on that query**, so the very first module fetch is always the content-addressed (`immutable`) URL instead of the bare `no-store` one. Non-SDK components skip the query and are never blocked. - **Live invalidation without reload:** SDK regeneration updates the application row, and the server broadcasts an `application` metadata event carrying the new core checksum. `useOnApplicationSdkClientChecksumsUpdated` / `useUpdateSdkClientChecksumsApolloCache` patch the application-keyed checksum query cache (core only; the instance-wide metadata is preserved), so every mounted component of that application picks up the new URL at once. This replaces the previous frontComponent-derived field and closes the earlier "known gap" (a mounted component staying on a session-old checksum until a full reload). The cache-patching callback is memoized (`useCallback`) so the window listener is registered once per application, and the listener is **skipped entirely** for non-SDK components (`useListenToMetadataOperationBrowserEvent` gained a `skip` option) — they register no listener and never refetch a query they don't consume. ## Renderer (twenty-front-component-renderer) - SDK sources are fetched through a dedicated plain authenticated fetch, `fetchJavaScriptModuleSourceText` (Bearer header, `credentials: 'omit'`), instead of the Cache Storage `fetchComponentSource` path; `fetchSdkClientSources` uses it. Execution stays exclusively in the opaque-origin worker via blob URLs; the host only fetches and forwards source strings (no hashing host-side). Staleness self-resolves through the checksum: new checksum → new URL → cache miss. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22981?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. --> |
||
|
|
3ad3e8bd1a |
feat: kanban, calendar and group-by table layouts for dashboard view widgets (#22963)
## Context Dashboard view widgets previously only rendered flat tables. This PR ships the full feature: **Table with group-by**, **Kanban**, and **Calendar** layouts for dashboard view widgets — server API + frontend, end-to-end. (Originally staged as a 4-PR stack — #22966, #22967, #22968 — consolidated here per review.) ## Server / API - **View typing.** Adds `KANBAN_WIDGET` and `CALENDAR_WIDGET` to `ViewType` (following the `TABLE_WIDGET` precedent) so widget-backing views keep their layout in `view.type` while staying excluded from record-index pickers. Shared `getViewLayoutFromViewType()` maps widget types to their base layout; `isWidgetViewType()` centralizes the exclusions that were previously hardcoded per-site. - **Migrations.** Two fast instance commands (**2.23**): `ALTER TYPE core.view_type_enum ADD VALUE` for both values, and a widened `CHK_VIEW_CALENDAR_INTEGRITY` constraint covering `CALENDAR_WIDGET` (entity `@Check` updated for fresh installs). - **Validation.** `FlatViewValidatorService` keys kanban/calendar validation on the mapped layout, so widget views get the same invariants as index views (kanban needs a groupable group-by field; calendar needs a date field + layout). Calendar widget views default to month; a non-month (DAY/WEEK) layout is rejected at the API level **unless** the `IS_CALENDAR_WEEK_VIEW_ENABLED` feature flag is enabled for the workspace — the same flag that gates day/week on index calendars. - **API.** `upsertViewWidget` (LAYOUTS permission) accepts a nested `view` settings input (`type`, `mainGroupByFieldMetadataId`, `shouldHideEmptyGroups`, kanban aggregate/column-width, calendar layout/fields). Routes through the standard update path, so `viewGroups` auto-generate from SELECT options exactly like index views. Only widget view types accepted; only `RECORD_TABLE` widgets can change view settings. - **AI tools.** `create-complete-dashboard` + `create_view` now use/allow the `*_WIDGET` types (previously they created plain `TABLE` views that leak into index pickers). ## Frontend **Settings panel.** The **Source** (object) row comes first, since which layouts are available depends on it. The **Layout** row below is a working dropdown (Table / Kanban / Calendar); layouts the source object can't support are **disabled with a hint** ("Needs a Select field" / "Needs a Date field") rather than hidden. Group-by row (select fields; searchable) with a **Hide empty groups** toggle while grouped; **Date field** row replaces Group by while Calendar is active, and — when the `IS_CALENDAR_WEEK_VIEW_ENABLED` flag is on — a **Calendar view** row (Day / Week / Month) appears beside it; **Limit** row hidden while grouped (only the flat virtualized loader enforces it). Kanban keeps its group-by locked (no `None` option). **Instant edit-mode preview.** Draft snapshots carry `viewGroups`; picking a group-by synthesizes them client-side (`buildDraftViewGroupsForFieldMetadataItem`, mirroring the server's generation), so grouped tables/boards preview immediately before dashboard save. On save, `upsertViewWidget` responses hand back the server-generated groups, which replace the client-generated ones in the persisted snapshot. **Renderers.** `RecordTableWidgetRendererContent` branches on the backing view's layout: `RecordBoardWidget` (wraps the standard `RecordBoardContainer`) and `RecordCalendarWidget` (mounts the existing `RecordCalendar`, which renders month / day / week) inside the same per-widget provider sandbox the table uses. **Read-only semantics.** Two flags with distinct scopes, each documented on its state: - `isRecordBoardViewSettingsReadOnlyComponentState` — locks the board chrome that edits view settings (add group, column reorder/resize/menu, aggregates); **card drag still updates records** under object permissions. - `isRecordCalendarReadOnlyComponentState` — widget calendars are read-only by default (no drag, no add-new, no in-calendar layout switch); cards open the side panel. The one exception, behind `IS_CALENDAR_WEEK_VIEW_ENABLED`: a **live (non edit-mode) day/week** widget calendar allows drag-to-reschedule and record creation under object permissions. Month calendars and edit-mode previews stay read-only. **Calendar state componentization.** The calendar module's three settings move from global atoms to component states keyed on `RecordCalendarComponentInstanceContext` (same pattern as record-board), so several calendar widgets and an index-page calendar can coexist without leaking state. All readers resolve the ambient instance; calendar unit tests updated. **Multi-instance fixes that also fix index pages:** record drag states were written against a different instance than every reader resolves (now use the ambient instance); the board sticky-header DOM id is namespaced per board; dragged board cards portal to `document.body` while dragging so react-grid-layout's transforms can't offset the clone from the pointer. ## Scope (v1) - Widget calendars are month-only and read-only by default. With `IS_CALENDAR_WEEK_VIEW_ENABLED` enabled, day/week layouts become selectable (UI + API) and live day/week widget calendars support drag-to-reschedule and record creation under object permissions. - Widget group-by offers SELECT fields only (server auto-generates groups from options; widgets have no per-record add-group flow). ## Tests - Integration: `upsert-view-widget-view-settings.integration-spec.ts` (9 tests — group auto-creation, invalid type/field rejections, non-month calendar widget rejected while the week/day flag is off and accepted once it's enabled, combined settings+fields call); pre-existing `upsert-view-widget` suite (20) green. - Front: new suites for draft view-group generation and snapshot clone/build utils; calendar suites componentized; full `twenty-front` jest, typecheck, oxlint green; `twenty-server` typecheck + lint green. - Browser-verified end-to-end (real dev server + seeded workspace): configure → live edit-mode preview → save → reload for all three layouts; measured drag with pointer inside the card; index-page calendar re-verified (with the week/day flag enabled). https://claude.ai/code/session_01E5N87kwwZWhDtEQaP72cMf |
||
|
|
bc3112a999 |
Fix: allow API key creation without Roles permission (#23102)
## Problem A user with the **API keys & webhooks** permission but **without** the **Roles** setting permission cannot create an API key through the UI. The role selector relies on the `getRoles` query, which is guarded by the `ROLES` permission, so the roles list comes back empty, `SettingsDevelopersRoleSelector` early-returns, and no role can be selected — leaving the form unsavable. <img width="1058" height="408" alt="Screenshot 2026-07-21 at 13 38 34" src="https://github.com/user-attachments/assets/fe97ba78-e116-458d-af10-11c5969c4636" /> ## Fix Expose the assignable roles through the API-key permission scope so users can **pick** a role to assign to an API key without being able to **edit** roles. - **Backend**: add `getApiKeyRoles` query on `ApiKeyResolver` (already guarded by `API_KEYS_AND_WEBHOOKS`), backed by `ApiKeyRoleService.getApiKeyAssignableRoles` which returns roles where `canBeAssignedToApiKeys = true`. - **Frontend**: add a `GetApiKeyRoles` query and use it in the API key create and detail pages instead of `getRoles`. The role selector prop type is narrowed to the fields it actually uses. <img width="1025" height="455" alt="Screenshot 2026-07-21 at 13 45 01" src="https://github.com/user-attachments/assets/f1be8f97-5a30-4afc-9eee-c928f4607471" /> |
||
|
|
1be5a0e54a |
System side effect relations (#22882)
Closes twentyhq/core-team-issues#2667 ## What Default relations to the standard relation objects (`timelineActivities`, `attachments`, `noteTargets`, `taskTargets`) are now fully owned by the **metadata side-effect engine**. Neither the API transpilers nor the SDK manifest builder provision them anymore: any object creation, rename or deletion — regardless of the caller — goes through the same engine handlers. ## Why - Provisioning was duplicated across the API path and the SDK manifest builder, with diverging behavior. - Universal identifiers of relation fields were derived from object **names**, so renaming an object mutated them and forced lossy delete+create cycles on manifest sync. ## How ### Engine-owned lifecycle (side-effect handlers) - `objectSystemRelationsOnCreate`: provisions the 8 forward/reverse relation fields (+ join column indexes) when an object is created. - `objectSystemRelationsOnUpdate`: renames the reverse morph fields (`target<ObjectName>`) when their host object is renamed — a lossless `fieldMetadata.update`. - `objectSystemSideEffectsOnDelete`: cascades deletion of engine-owned fields/indexes when the object is deleted. - The API transpilers and the SDK `buildManifest` no longer inject these fields; `isSystemSideEffect: true` marks engine-owned entities, guarded by a granular property allowlist (only `isActive` is user-editable) and excluded from manifest deletion inference. ### Name-free deterministic universal identifiers New `getSystemRelationFieldUniversalIdentifier({ applicationUniversalIdentifier, objectUniversalIdentifier, relationTargetObjectUniversalIdentifier })` in `twenty-shared`, exported from `twenty-sdk/define`. The identifier is keyed on the two **object** identifiers instead of field names (direction encoded by argument order), so object renames never mutate relation field identifiers. It cannot collide with the name-based `getFieldUniversalIdentifier` derivation (field names cannot contain `:`). ### twenty-standard re-owned All 48 forward/reverse system relation field declarations in `STANDARD_OBJECTS` now pin the derived name-free identifiers (computed inline via the shared util) and carry `isSystemSideEffect: true`, with labels/icons declared explicitly (translated via `msg`). `twenty-standard` is projected as if the engine had generated these fields itself. ### 2.23 upgrade commands - `reconcile-system-relation-field-universal-identifier`: structurally matches existing default relation fields per workspace and backfills the derived universal identifiers, `isSystemSideEffect` flags, and standard labels/icons. - `upgrade-people-data-labs-application`: upgrades installed PDL apps to `1.0.7` right after the backfill to close the desync window (its views reference the re-derived identifiers). ### Misc - `people-data-labs` `1.0.7`: views temporarily pin the new derived identifiers (TODO: import from the next released `twenty-sdk`). - `UpgradeStatusModule` split out of `UpgradeModule` so the application module cluster can consume upgrade status/migration services without importing the versioned command bundles (fixes a require cycle that crashed boot). - Docs: `system-fields.mdx` documents the system relation fields and their resolver; `sync-and-recovery.mdx` plan example no longer shows auto-injected relations. ## Known red CI `people-data-labs (dockerhub-latest)` fails by design until the 2.23 server image is published: the app pins the new identifiers which only exist on a 2.23 server. The `local` leg (server built from this branch) is green. ## System fields are no longer manifest-authorable (accepted regression) The manifest converter no longer derives `isSystem` / `isSystemSideEffect` from field names. Reserved-system-named manifest fields (`id`, `createdAt`, `updatedAt`, `deletedAt`, `createdBy`, `updatedBy`, `position`, `searchVector`) are now skipped at conversion time when they carry the exact derived universal identifier (keeps manifests built with older SDKs installable), and rejected with `INVALID_INPUT` when they pin any other identifier. System fields are therefore fully engine-canonical: nothing a manifest carries can produce a system-flagged entity anymore. **Accepted regression**: a manifest can no longer influence system field properties at all. Previously a (legacy) re-declaration could shape them at creation — which actually produced broken system fields, e.g. a nullable, non-unique `id` — and could still toggle the allowlisted `isActive` / `universalSettings` afterwards. We consider this acceptable for now: per-app granularity over system fields will be reintroduced later through the **override framework**, which will also settle update semantics by forbidding direct updates over `isSystemSideEffect: true` entities and expressing divergence as overrides. `isSystemSideEffect`-only entities (the default relation fields provisioned by this PR) still have no engine-level update guard (see Follow-up below); that part is unchanged and also lands with the overrides refactor. ## Follow-up `isSystemSideEffect` field update/delete guards intentionally live at the API layer (`sanitize-raw-update-field-input.ts`, `from-delete-field-input-...util.ts`) rather than in the engine-level `FlatFieldMetadataValidatorService`. Moving them into the validator requires threading operation-origin (direct field mutation vs engine cascade) through the migration matrix, otherwise legitimate object rename/delete cascades (which carry `isSystemBuild=false`) would be rejected. Tracked in twentyhq/core-team-issues#2671. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22882?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. --> |
||
|
|
8d84a0b9f3 |
feat(app): allow non-admin developers to claim and list marketplace apps (#22621)
## Context Follow-up to #22609. Lets a non-admin developer claim ownership of a public Twenty app they published to npm, then request a marketplace listing that a server admin reviews. Marketplace state is per-instance for now. ## Claiming - Developer tab gets a **Claim an application** section: look up an unclaimed npm app by package name or universal identifier. - Ownership is proven with GitHub OAuth against the package's npm provenance (trusted publishing): the connected account must own the GitHub account or organization the package was published from. - Errors from the GitHub callback come back as a code and are shown inline with a link to the relevant documentation. - The old one-click claim stays admin-only. - A **Sync catalog** button triggers a catalog refresh instead of waiting for the hourly cron. - Gated behind the `IS_APP_CLAIMING_ENABLED` feature flag. ## Listing requests - Catalog-synced apps are created **unlisted**; a data migration unlists previously auto-listed unclaimed npm apps (owned or vetted rows are left untouched). - Owners request a listing from the Distribution tab (logo + description required); a server admin approves or rejects it from a **Listing requests** section in the Admin Panel. ## Screenshots <img width="1512" height="829" alt="image" src="https://github.com/user-attachments/assets/788d4362-97c4-4e42-810c-ef1f11517bec"/> <img width="1512" height="829" alt="image" src="https://github.com/user-attachments/assets/d6246190-c82a-4f64-87be-3bb668527645"/> <img width="1512" height="828" alt="image" src="https://github.com/user-attachments/assets/21a8dad4-610b-4d1f-8948-b9acab40d373"/> <img width="1512" height="829" alt="image" src="https://github.com/user-attachments/assets/58246130-41f7-451e-ae7f-57bd21d04bb6"/> --------- Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com> |
||
|
|
baa84bb2e0 |
Add auto-upgrade-in-apps (#23001)
We need to auto upgrade application, lets add this column in application entity, and add an admin button to autoupgrade all applications to latest app registrration version manually <img width="1131" height="372" alt="image" src="https://github.com/user-attachments/assets/4e755abc-38ad-4895-a2e8-d55ee1948ac2" /> <img width="906" height="533" alt="image" src="https://github.com/user-attachments/assets/ce002057-0341-4581-bf9a-66ac2bd84a9b" /> |
||
|
|
5bedd5b8cc |
feat(email-group): communications UX, per-record DNS status (#23002)
- Rename Communications label to singular, remove docs-home banner - Provision unsubscribe Cloudflare records at domain creation and surface per-record status badges; skip Cloudflare when not configured - Move sending-domain status into the section header and only show the records table when a record is unverified - Reply to the original recipients when replying to your own message - Fix DNS records table column/badge alignment; emit synthetic records in the log driver for local testing <img width="1496" height="849" alt="Screenshot 2026-07-17 at 7 47 24 PM" src="https://github.com/user-attachments/assets/a5a59adb-2df4-4154-98b2-acf87a8008da" /> |
||
|
|
5e27e04c0a |
Add mostly-empty field hints to data model settings (#22962)
## What
Fields that are empty in almost all records now show a subtle `Mostly
empty` hint next to their name in the object's Fields settings table
(same visual treatment as `Deactivated`), with a tooltip explaining the
signal and a matching **Mostly empty** toggle in the search filter
dropdown. The goal is to nudge admins to clean up and deactivate fields
nobody uses, while keeping the page untouched when the data model is
healthy.
## How
**No table scans.** Emptiness is read from Postgres planner statistics,
so the cost is a catalog lookup regardless of table size:
- `pg_class.reltuples` gates the feature on an approximate row count (≥
100 records; never-analyzed tables mean no hints). Reuses the shared
helper extracted from `ObjectRecordCountService`.
- `pg_stats.null_frac` plus the sampled frequency of the column type's
empty sentinel (`''` for text columns, `'{}'` for arrays, `'{}'`/`'[]'`
for json — matched per physical column type) gives a per-column empty
fraction. A value dominating ≥ 95% of a column is guaranteed to appear
in the most-common-values list, so the approximation is reliable exactly
at the threshold we care about.
**Decision rules** (pure util, unit-tested):
- Flag when every relevant column is ≥ 95% empty and the object has ≥
100 records.
- Skip system fields, the label identifier, relations, booleans, and
actor fields (exhaustive switch — a new `FieldMetadataType` fails to
compile until classified).
- Composite fields must have all their columns empty, with column sets
derived from `compositeTypeDefinitions`; only default-bearing code
columns (`currencyCode`, phone country/calling codes) are excluded so
stamped defaults don't mask emptiness.
- Anything unknown (missing stats, new column since last ANALYZE)
degrades to silence — no hint is ever shown on missing data.
**API:** one `mostlyEmptyFieldMetadataIds(objectMetadataId)` query on
the metadata schema, guarded by the `DATA_MODEL` settings permission,
fetched lazily when the fields page opens.
**UI:** exception-based — no new columns, no persistent controls. The
badge and the filter toggle only materialize when at least one field
qualifies, and disappear once things are cleaned up.
## Test
- Unit tests for the decision util (threshold,
system/label-identifier/inactive exclusion, missing statistics,
composite all-columns rule, links label/secondary data, currency
narrowing, excluded types).
- Catalog SQL validated against Postgres 16 with a table mimicking
Twenty's column shapes (text `''` defaults, enums, arrays, jsonb,
currency pairs), including the type-aware sentinel matching (a text
column full of literal `"{}"` strings does not count as empty).
- End-to-end on a seeded dev instance: 899 companies with a mix of
filled/empty fields — the API returned exactly the five fields predicted
by the raw statistics (`annualRevenue`, `employees`, `introVideo`,
`tagline`, `workPolicy`) and correctly excluded `address` (city 33%
filled), actor/system fields, and the label identifier.
- UI driven with Playwright: badge, tooltip copy, filter toggle, and
filtered table all verified visually.
- `lint:diff-with-main`, `typecheck` (server + front), and all three
`graphql:generate` configurations + SDK metadata client regenerated and
committed.
|
||
|
|
79f3a5243a |
Add callAppRoute to RestApiClient (#22863)
Adds a `callAppRoute` method to `RestApiClient` in `twenty-client-sdk/rest`. It calls one of the app's own HTTP routes using the injected `TWENTY_FUNCTIONS_URL`, resolved internally the same way the client already resolves `TWENTY_API_URL`, so app code no longer reads env vars or knows how function routes are hosted. Both app runtimes already go through `RestApiClient` for route calls (logic functions and front components), so both get this in one place; front components keep the existing 401 token-refresh flow. Pairs with #22825, which makes the injected `TWENTY_FUNCTIONS_URL` callable in every topology (app custom domain -> workspace isolated functions domain -> `SERVER_URL/s`). Once this ships in an SDK release, Call Recorder's own-route plumbing (logic-function and front-component utils) drops its URL resolution and calls `client.callAppRoute(path, body)`. --------- Co-authored-by: martmull <martmull@hotmail.fr> Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com> |
||
|
|
8e03921372 |
Add CREATED workspace activation status (read path + enum migration) (#22904)
## Context Since v2 onboarding (#22303), workspaces are activated **before** the billing plan step (now the last onboarding step). Users abandoning at the plan step leave ACTIVE workspaces with a Stripe customer but no subscription (~60–110/day on cloud, 935+ so far), and no cleanup mechanism ever touches them: billing webhooks never fire (no subscription), the suspended-workspaces cron only handles SUSPENDED, the onboarding cron only handles PENDING_CREATION/ONGOING_CREATION. Target lifecycle (across two PRs): `PENDING_CREATION → ONGOING_CREATION → CREATED → ACTIVE → SUSPENDED → deleted`. **`CREATED`** = the workspace schema is provisioned but onboarding is not complete — no billing subscription yet. It is **not** considered active: | Concern | CREATED behavior | |---|---| | Sign-in / invited teammates joining | allowed (invite-team step precedes the plan step) | | Member + metadata loading (app shell) | allowed (user must finish onboarding) | | Permissions | real permission checks (no PENDING-style bypass) | | Version upgrades / workspace migrations | **included** (schema must not drift) | | Messaging/calendar/workflow/etc. crons | **excluded** — no background processing until a plan is chosen | | PLAN_REQUIRED onboarding lock | unchanged (still derived from subscription existence) | ## What this PR does (read path only) The enum addition ships as a **slow** instance command, which can run after deploy — so nothing in this PR ever **writes** `CREATED`. The write path (setting it at activation, the cleanup sweep, the backfill of the existing zombie cohort) is a follow-up PR that ships once this migration has run everywhere. - **twenty-shared**: `CREATED` enum value; `PROVISIONED_WORKSPACE_ACTIVATION_STATUSES` + `isWorkspaceProvisioned` ("schema exists": CREATED | ACTIVE | SUSPENDED), replacing `isWorkspaceActiveOrSuspended` — all call sites (server member loading, access-token workspace-member lookup, front metadata-store gates) meant "has schema/members". - **Slow instance command** (2.22.0): swaps `core.workspace_activationStatus_enum` using the rename→recreate→alter-column idiom. The CHECK constraints on `core.workspace` embed casts to the enum type and would break the swap — the command captures them from `pg_constraint`, drops them, swaps the type, and restores them. - **Pre-migration-safe queries**: Postgres rejects `IN ('CREATED', ...)` when the enum value does not exist yet — even for reads, and the instance-command runner itself queries provisioned workspaces before migrating (a fresh database could never initialize). All provisioned-status filters go through a new `activationStatusIn` util comparing on `"activationStatus"::text`, valid before and after the migration. - **Upgrade path**: workspace iterator, command runner, upgrade-status and workspace-version services iterate CREATED workspaces. Since they now cover more than ACTIVE/SUSPENDED, the stale names were renamed to `ProvisionedWorkspaceCommandRunner`, `hasProvisionedWorkspaces`, `getProvisionedWorkspaceIds`, `loadProvisionedWorkspaces` (the mechanical import rename in old version-command dirs is why this PR carries the `ci:allow-previous-version-upgrade-mutation` label). - **Sign-in**: `throwIfWorkspaceIsNotReadyForSignInUp` accepts CREATED so invited members can join during onboarding (join authorization itself is unchanged — enforced upstream in `checkAccessForSignIn`); `activateWorkspace` idempotent-retry accepts CREATED as a terminal state. - **Transitions out of CREATED** (only write ACTIVE — safe to ship now, dead until the write path lands): the Stripe webhook reactivation branch also promotes CREATED, and `syncSubscriptionToDatabase` promotes synchronously; both gated on `WORKSPACE_ACTIVATING_SUBSCRIPTION_STATUSES` (Active/Trialing — extracted from `shouldReactivateWorkspace`, behavior-preserving) so an `incomplete` subscription created by the payment-intent flow before payment never promotes the workspace. - Deliberately untouched: all background crons, permission guards, JWT strategy, PLAN_REQUIRED logic, admin panel (renders the raw status string). ## Follow-up PR (after this migration has run) 1. `activateWorkspace` sets `hasWorkspaceAnySubscription ? ACTIVE : CREATED` (billing disabled → always ACTIVE, self-hosted unchanged). 2. Cleanup: suspend CREATED workspaces older than N days (config var), handing them to the existing suspended pipeline (warn → soft-delete → destroy). 3. Backfill: cloud-only slow command moving ACTIVE workspaces with no billingSubscription row (created since Jul 1) to CREATED. ## Verification - Migration exercised against a real database via the command class: up → down → up; `enum_range` and `pg_get_constraintdef` checked after each step (constraints restored against the new type, `DEFAULT 'INACTIVE'` preserved). - Pre-migration safety exercised for real: with the migration rolled back (enum without CREATED), `run-instance-commands` — the exact fresh-database CI path that failed before the `::text` fix — completes cleanly. - End-to-end with a workspace manually set to CREATED and the branch server+front running: sign-in issues tokens, `currentUser` loads workspaceMember(s), the full app loads with no console errors; GraphQL returns `activationStatus: CREATED`. - Workspace creation ran end-to-end locally in **both billing modes** on this branch: - billing disabled: signup → workspace creation → ACTIVE immediately → onboarding completes with no plan step → app loads (unchanged behavior); - billing enabled (Stripe test mode): signup creates the Stripe customer eagerly → activation ends ACTIVE → subscription-less workspace is pinned to the plan-required page → no-card trial checkout creates a `trialing` subscription via `createDirectSubscription`/`syncSubscriptionToDatabase` → app loads. - `twenty-shared` unit tests, server specs on touched services, `lint:diff-with-main` and `typecheck` for shared/server/front all green; full CI green. |
||
|
|
25bd2897a3 |
Add weekly layout to record calendar (#22819)
## Summary - Add a week layout to record calendar views and persist the selected layout. - Render `DATE` calendars as an all-day week and `DATE_TIME` calendars as an hourly week. - Add an optional end date field across calendar configuration, metadata, persistence, and complete-view upserts. - Use configured end values for ranged and multi-day events, with a one-hour fallback when a `DATE_TIME` end is absent or invalid. - Keep calendar cards consistent with the existing compact view, including checkbox selection and whole-card record opening. - Gate the weekly layout and end-date behavior behind the public Labs `IS_CALENDAR_WEEK_VIEW_ENABLED` workspace feature flag. ## Week interactions - Show overlapping timed events side by side and cap the visible records at two per day. - Display start and end times on timed cards, enforce a readable 30-minute minimum height, and keep today’s text contrast stronger. - Drag timed events between days and times with 30-minute snapping while preserving their duration, including zero-duration events. - Show a create button when hovering a 30-minute slot; keyboard users can focus a day, move the slot with the arrow keys, and reach the same contextual action. - Initialize new records with the selected slot time and a compatible writable end value one hour later. - Show the workspace time zone and current-time indicator in timed weeks; date-only weeks keep the all-day section without an hourly grid. ## Configuration and data loading - Only allow end fields that match the start field type, and prevent selecting the same field for both boundaries. - Load records whose ranges overlap the visible period so month and week layouts display the same relevant records. - Resolve and persist calendar end fields when updating existing views through `upsert_complete_view`. - Fall back to Month and ignore the configured end field while the flag is disabled, without overwriting either persisted setting, so re-enabling restores the previous configuration. - Expose the flag in Labs and keep it default-off for workspaces without a stored value; enable it in the development seeder. <img width="1285" height="808" alt="Screenshot 2026-07-15 at 15 50 17" src="https://github.com/user-attachments/assets/b7e3f7f1-ca77-492f-8cce-cca186ebca0b" /> |
||
|
|
0dbae2eda3 |
Address #22827 review comments and converge application file endpoints (#22868)
Follow-up to #22827, addressing the review comments left around merge time and applying the endpoint convergence discussed afterwards. ## Review comments from #22827 - **Swallowed error in dev sync asset read** ([comment](https://github.com/twentyhq/twenty/pull/22827#discussion_r3571513265)): the swallow is intentional (a missing public asset must not fail the whole dev sync) but it now logs a warning with the asset path and error, and the registration keeps its previously stored file for that path instead of losing it. - **`isAbsoluteUrl` location** ([comment](https://github.com/twentyhq/twenty/pull/22827#discussion_r3571524234)): moved to `twenty-shared/utils/url`. The server, and now also `twenty-sdk`'s `normalize-application-assets`, use the shared util. - **Soft delete vs file cleanup** ([comment](https://github.com/twentyhq/twenty/pull/22827#discussion_r3571589558)): per review, deleting a registration is now a hard delete. Stored assets (bytes + rows) are deleted with it, dependent rows are removed by their existing FK cascades, and installed applications keep working with their registration link nulled. No soft-delete/cron mechanism. - **Asset cap too generous** ([comment](https://github.com/twentyhq/twenty/pull/22827#discussion_r3571595745)): lowered to 10MB per review and documented in the publishing and public-assets docs pages. - **One missing image retriggers a full asset sync** ([comment](https://github.com/twentyhq/twenty/pull/22827#discussion_r3571646243)): `storeRegistrationAssets` now takes `skipAlreadyStoredPaths`; the catalog sync passes it when the package version is unchanged, so only assets missing a stored file are fetched instead of re-downloading everything. - **`existing.logo` already contains the new logo** ([comment](https://github.com/twentyhq/twenty/pull/22827#discussion_r3571667847)): correct, `updateFromManifest` runs first, so the previous "keep fileId when the path did not change" guard compared the new logo against itself. The fileId preservation is now keyed on the stored server file for the exact path (files are unique per `(applicationRegistrationId, path)`): a changed logo path no longer inherits the old file's id, and a transient download failure on an unchanged path still keeps the working file. This also removed the fileId-preservation bookkeeping from `storeRegistrationAssets`. ## Endpoint convergence - **Path-addressed public route for registration assets**: `GET /file/server/application-registration/:fileId` is replaced by `GET /files/application-registrations/:registrationId/*path`, mirroring the manifest's public-folder paths and leaving room for a future `:version` segment. Assets stay addressable by stable ids server-side; the fileId now only marks a path as stored. No URL is ever persisted (all are built at query time), and the old route never shipped in a release, so there is nothing to migrate. - **`Application.logoUrl` resolved server-side**: new `ResolveField` on the `Application` type builds the `/public-assets/...` display URL (or passes absolute URLs through). `useApplicationChipData` now reads it from `currentWorkspace.installedApplications`, and the frontend `buildApplicationLogoUrl` util is deleted, so clients no longer construct file URLs themselves. ## Validation - Unit: `file.controller.spec` (route renamed, traversal case added), `server-file-storage.service.spec` (`findServerFile`, `deleteByApplicationRegistrationId`), `application-registration-asset-url.service.spec` (new URL shape, url-encoding), new `isAbsoluteUrl` test; all application/file suites pass. - Live against a local server: new route serves tarball and rehosted npm assets with `public, max-age=3600` (nested paths included), 404s on missing files, unknown registrations, traversal attempts, and the removed old route; `findManyApplicationRegistrations` returns path-addressed URLs for stored assets, CDN fallback for npm, absolute passthrough; `installedApplications.logoUrl` resolves the public-assets URL and stays null for logo-less apps. Registration hard delete verified against the DB: file rows cascade, application rows keep a nulled registration link. - Typecheck + lint on twenty-server, twenty-front, twenty-shared, twenty-sdk; metadata codegen and client-sdk regenerated. |
||
|
|
94192a2164 |
Resolve application registration logo and gallery image urls at query time (#22827)
## Context
Application registration logo and gallery image urls were baked into the
stored manifest and display columns at write time, with each source flow
doing it differently: npm catalog sync baked CDN urls, local dev sync
baked `public-assets` urls, and tarball uploads left raw manifest paths
that never displayed in the UI. The entity also carried a `logoUrl`
getter computed field.
This moves url generation to query time, the same way the workspace logo
works.
## What changed
**Read side**
- `ApplicationRegistrationAssetUrlService` builds display urls when
queried: stored files are served by fileId, absolute urls pass through
untouched, and not-yet-rehosted npm assets fall back to the registry CDN
from `sourcePackage@latestAvailableVersion`.
- The `logoUrl` getter on `ApplicationRegistrationEntity` is replaced by
`logoUrl` and `galleryImages` `@ResolveField`s on the metadata resolver,
the admin panel resolver, and a new resolver for
`ApplicationRegistrationSummary` (used by
`Application.applicationRegistration`).
- The marketplace detail/card DTOs and the public OAuth authorize DTO
(`findApplicationRegistrationByClientId`) go through the same url
builder.
- New public route `GET /file/application-registration/:id` streams
registration server files (these are instance-global marketplace assets,
also shown on the public OAuth authorize page).
`ServerFileStorageService.readServerFileById` now returns the mime type
alongside the stream.
**Write side**
- New `logoFileId` column on `applicationRegistration` (2.21 fast
instance command, constraint names match TypeORM naming), complementing
the fileIds already stored in the `galleryImages` jsonb.
- `ApplicationRegistrationAssetService` copies the manifest logo and
gallery images into instance-global server file storage, so all three
sources behave the same:
- **TARBALL**: from the uploaded package (previously only gallery images
were stored, never the logo).
- **LOCAL**: dev sync reads the already-uploaded public assets from
workspace storage (the CLI uploads files before syncing).
- **NPM**: catalog sync downloads the assets from the registry CDN.
Downloads are skipped when the package version is unchanged and the
files are already stored; failed or pending downloads fall back to CDN
urls at query time.
- Write-time url rewriting is removed
(`ManifestAssetUrlResolverService`, `resolveManifestAssetUrls`);
manifests now keep raw asset paths. Existing rows with baked absolute
urls keep working through the absolute-url passthrough, so no backfill
is needed.
- `updateFromManifest` and `upsertFromCatalog` preserve stored gallery
fileIds for unchanged paths, so installs and the hourly catalog sync no
longer clobber them.
## How it was verified
Against a local Postgres/Redis with the server running:
- Fresh database init runs the new instance command; column and FK/UQ
constraint names match TypeORM's generated names, and the CI
pending-migration check produces no diff.
- `findManyApplicationRegistrations { logoUrl galleryImages }` returns
fileId-served urls for a TARBALL registration (absolute urls passed
through), and null/[] for a LOCAL registration without assets.
- Ran `marketplace:catalog-sync` against the real npm registry: 14
packages synced, logos and gallery images rehosted from unpkg with
fileIds set; a second run re-downloaded nothing (version-unchanged
skip); `findMarketplaceAppDetail` for `twenty-linear` returns
fileId-served urls for the logo and all four gallery images.
- `GET /file/application-registration/:id` serves stored files with the
right content type (png and svg verified), 404s on unknown ids, and the
token-guarded generic `/file/:folder/:id` route still returns 403
without a token.
- Unit tests for the url builder and the assets-stored check; server
unit test suites for the application module pass; typecheck and lint
clean.
|
||
|
|
ffdda50afc |
remove nestjs-query auto-resolver from index-metadata (#22775)
## Summary
Removes the `NestjsQueryGraphQLModule.forFeature` block from
`IndexMetadataModule`, continuing the incremental migration off
`@ptc-org/nestjs-query`.
- Drops the dead auto-generated read surface: `index` and
`indexMetadatas` queries, the `IndexConnection` /
`IndexObjectMetadataConnection` types, and the `Index.objectMetadata`
field. No client consumes these — the frontend reads indexes via
`ObjectMetadata.indexMetadatas`.
- Keeps `IndexMetadataDTO` nestjs-query-compatible (`@Authorize`,
`@FilterableField`, `@QueryOptions`, `@IDField`) because
`ObjectMetadataDTO` still references it via
`@CursorConnection('indexMetadatas')` until object-metadata is migrated.
- Hand-written `createOneIndex` / `deleteOneIndex` mutations and the
`indexFieldMetadataList` resolve-field are unchanged.
- Deletes the now-obsolete `index-metadatas` integration test and
regenerates the GraphQL schema artifacts (frontend + client-sdk).
## Breaking change
This is an intentional GraphQL schema breaking change
(`api-breaking-changes` CI will flag it)
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22775?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. -->
|
||
|
|
4ab36a630b |
remove nestjs-query from app-token and drop unused createOneAppToken mutation (#22765)
## What Migrates `app-token` off `@ptc-org/nestjs-query` and removes the auto-generated `createOneAppToken` mutation, which was unused dead API surface. ## Why The `createOneAppToken` mutation was reachable only from the schema — no frontend query, SDK caller, or test used it. It was also non-functional (its input couldn't set the token `value`), a leftover from nestjs-query's default `create.one` being left enabled. Real app tokens (refresh, password-reset, email-verification, invitation, OAuth, enterprise) are all created directly via the repository in ~14 services, none of which touched this mutation. ## Notes - ⚠️ This removes `AppToken`, `createOneAppToken`, `CreateAppTokenInput`, and `CreateOneAppTokenInput` from the `/metadata` schema, so the **api-breaking-changes check will flag it** <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22765?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: Weiko <corentin@twenty.com> |
||
|
|
23cae2040a |
Improve application asset management (#22564)
App manifests could point the logo and screenshots at either external
URLs or public folder paths, and that was handled inconsistently across
install, sync and the marketplace.
This makes assets always bundled files:
- Manifests now use `logo` and `galleryImages` (a `string[]` of public
folder paths) instead of `logoUrl` and `screenshots`. The old fields
still work but are deprecated. Gallery order comes from the array index.
Normalization (deprecated-field migration, and warning about + ignoring
external URLs) happens in `defineApplication`, so the warnings surface
at define time.
- Logo is stored as a File record (`logoFileId`).
- The registration gallery is configured via a `settings` jsonb column
on `applicationRegistration` (`{ galleryImages: string[] }`) — populated
from the manifest, read by the marketplace detail (falling back to the
legacy `screenshots` column, then the manifest). No dedicated gallery
table.
- The marketplace detail DTO and front now use `galleryImages`.
Verified against a local Postgres: the fast instance commands run with
no pending-migration diff, the schema is correct, and the server boots.
Typecheck, lint, codegen and the application unit tests pass.
Not included yet: rehosting assets into storage for npm catalog and
tarball registrations, versioned cache busting on the serving route, and
a backfill for existing installs.
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22564?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>
|
||
|
|
9c405384f3 |
Only propose configured apps during onboarding install step (#22712)
## What - Onboarding "Install your first apps" now proposes only apps that are actually installable: it intersects the onboarding list with `findManyMarketplaceApps`, which the backend already filters to listed + configured apps (all required server variables set). - If none are available, the step auto-skips. If the marketplace query fails, it shows an intentional fallback (heading + Skip) instead of silently skipping or rendering an empty install card. - `findManyMarketplaceApps` now accepts `universalIdentifiers`, so onboarding fetches and configuration-checks only its own apps instead of the entire catalog. ## Why Previously the step rendered all hardcoded apps regardless of configuration, only borrowing logos from the marketplace, so a user could be offered an app the admin never configured. This centralizes onboarding availability on the marketplace's existing logic and keeps the query bounded as the marketplace grows. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22712?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. --> |
||
|
|
3a5545c753 |
chore: remove IS_MESSAGING_CALENDAR_WEBHOOK_ENABLED flag (#22680)
Messaging/calendar webhook subscriptions are now always on; drop the feature flag gate and its enum/public-flag registration. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22680?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. --> |
||
|
|
0ea0c23556 |
refactor(app-marketplace): rename featured to vetted (#22674)
## What Renames the application-registration "featured" flag to "vetted" across the backend, frontend, GraphQL schema/DTOs, and the marketplace UI. "Vetted" better describes what the flag actually does today: it marks an app as reviewed and approved by the Twenty team (a trust signal), rather than "featured" which reads as spotlighting/promotion. The admin toggle description was already "Mark this app as reviewed and approved". ## How - Renamed `isFeatured` -> `isVetted` on the `ApplicationRegistration` entity, DTOs (`MarketplaceApp`, `MarketplaceAppDetail`, `UpdateApplicationRegistrationPayload`), services, GraphQL fragments, and the settings/admin UI (labels: "Featured" -> "Vetted", "Featured only" -> "Vetted only", etc.). - Renamed the `MARKETPLACE_FEATURED_APPLICATIONS` constant/file to `MARKETPLACE_VETTED_APPLICATIONS`. - Regenerated GraphQL client artifacts (`generated-metadata`, `generated-admin`, `twenty-client-sdk`). ### Database The `isFeatured` column is renamed in place to `isVetted` via a single 2.20 fast instance command (`ALTER TABLE ... RENAME COLUMN`). No new column, no data-copy backfill. - Since all 2.19 commands (including the existing `isFeatured` backfill) complete before any 2.20 command runs, the rename carries over the values that backfill set. - The entity uses `@WasRenamedInUpgrade` so the upgrade-aware layer queries the old column name until the rename step runs during an upgrade. ## Testing - `nx typecheck` and `nx lint:diff-with-main` pass for twenty-server and twenty-front. - Ran `database:reset` on a fresh dev DB: the 2.19 `isFeatured` backfill runs first, then the 2.20 rename; the column ends up as `isVetted` (and `isFeatured` no longer exists), values preserved. - Booted the server: the `@WasRenamedInUpgrade` decorator validates against the upgrade sequence, and GraphQL introspection confirms all four types expose `isVetted` and none expose `isFeatured`. - Ran the three `graphql:generate` configs and the SDK metadata client generator so the committed generated files match the generator output (field ordering included). ## Notes - The `api-breaking-changes` check flags the removal of the `isFeatured` GraphQL field — that is expected and inherent to this rename. - Translation catalogs (`locales/`) are intentionally not touched here since they are managed via Crowdin; new English strings render via Lingui's default-message fallback until translated. |
||
|
|
9423af7f67 |
feat(server): add public marketplace resolver for vetted app catalog (#22647)
## What Adds a public GraphQL resolver so unauthenticated clients (the public website) can read the listed/vetted marketplace catalog without a workspace token. - `MarketplacePublicResolver` (metadata schema) exposes two public queries guarded by `PublicEndpointGuard` + `NoPermissionGuard`: - `publicMarketplaceApps` - `publicMarketplaceAppDetail(universalIdentifier)` Both delegate to the existing `MarketplaceQueryService` (no new logic, no new REST routing). The existing workspace-guarded `findManyMarketplaceApps` / `findMarketplaceAppDetail` queries are untouched. - Adds a shared `ApplicationCategory` type in `twenty-shared` (known values plus `string` for backward compatibility) used to type `ApplicationManifest.category`. A warning is logged server-side when an app declares a category outside the known set. ## Why This is the backend half of the public apps marketplace on the website. Splitting it out so the server-side catalog exposure can be reviewed independently from the website UI. ## Follow-up The website PR (the `/apps` marketplace UI) consumes `publicMarketplaceApps` and should merge after this one. --- _Generated by [Claude Code](https://claude.ai/code/session_01GBfegArtJcoiTLSsnWPH8R)_ <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22647?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: martmull <martin@twenty.com> |
||
|
|
72d728ea8e |
fix(messaging): add sender name to IMAP/SMTP From headers (#22603)
Manual IMAP/SMTP outbound emails were being composed with a bare email address in the From header, so recipients did not see the sender's display name. Gmail already built a proper sender header from Google profile data, but manually configured IMAP/SMTP accounts had no equivalent path and fell back to the raw address only. Fix this by storing the optional sender display name in the IMAP/SMTP/CALDAV connection parameters, exposing it through the settings flow and metadata API, and reusing a shared From-header formatter when composing outbound messages. The formatter now builds a properly encoded sender header when a name is available and falls back to the bare email address when it is not, keeping the behavior safe for blank or missing names. Gmail keeps using its existing Google-derived display name source; this change only brings manual accounts up to the same header formatting standard and removes duplicated formatting logic between outbound drivers. After this change, manual SMTP sends and IMAP draft creation include the configured sender name in the From header, while blank names are normalized away instead of producing malformed headers. Existing manual account names are preserved when updates omit the field, and edited accounts can still explicitly clear it through the settings flow. Add focused utility coverage for the shared From-header formatter. Fixes: #22608 <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22603?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: neo773 <neo773@protonmail.com> |
||
|
|
48730df0d2 |
feat(workflow): scaffold core workflowVersion entity + trigger cache (phase 0) (#21674)
## What **Phase 0 (scaffold)** of migrating `workflowVersion` data to **core**. Gated by `IS_WORKFLOW_VERSION_IN_CORE_ENABLED` with **no behavior change** — nothing reads or writes the new core entity yet. ## Plan `workflowVersion` becomes a thin **workspace shell** over a core entity (the `dashboard`/`pageLayout` pattern), so navigation, the metadata relations, and the record UI keep working while the heavy data (`triggers`, `steps`) lives in core. Trigger dispatch will derive from active core versions via a per-workspace cache, letting us **eliminate** the denormalized `workflowAutomatedTrigger` object. `workflow` and `workflowRun` stay as workspace objects. Phases: **0 — scaffold (this PR)** → A — backfill + dual-write → B — switch reads to core → C — drop the workspace `trigger`/`steps` columns + the `workflowAutomatedTrigger` object. ## Included - **Core `WorkflowVersionEntity`** (`extends WorkspaceRelatedEntity`) — stores version data, with triggers as an **array** (`triggers: WorkflowTrigger[]`), a long-due shape change. Storage only: dispatch reads the primary trigger, so behavior stays single-trigger for now. - **Fast create-table instance command** for `core."workflowVersion"` (v2.19.0). - **`IS_WORKFLOW_VERSION_IN_CORE_ENABLED`** feature flag. - **Per-workspace automated-trigger cache provider** deriving CRON/DATABASE_EVENT dispatch from the active version's trigger — groundwork for removing `workflowAutomatedTrigger`. ## Notes - `WorkspaceRelatedEntity`, **not** `SyncableEntity`: this is user runtime data (like `connectedAccount`/`apiKey`/`file`), not application-manifest metadata. - No frontend behavior; the generated `FeatureFlagKey` enums are updated to include the new flag. |
||
|
|
435073e9c5 |
Display featured applications in marketplace (#22635)
## After <img width="1060" height="589" alt="image" src="https://github.com/user-attachments/assets/74dfadcf-8698-4404-81c6-b309cc4cbf79" /> <img width="732" alt="image" src="https://github.com/user-attachments/assets/0e1a3644-04bc-4208-aa77-3842d9db9cc8" /> <img width="797" alt="image" src="https://github.com/user-attachments/assets/0456ecce-607a-4705-8a89-c77029bfb6ac" /> - Remove IS_MARKETPLACE_SETTING_TAB_VISIBLE feature flag - add vetted toggle in admin app tab - added people data labs, last contact and call recorder to default vetted applications <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22635?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> --------- Co-authored-by: prastoin <paul@twenty.com> |
||
|
|
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 |
||
|
|
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. --> |
||
|
|
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. |
||
|
|
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> |
||
|
|
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> |
||
|
|
1a85b88d38 |
feat(files): direct-to-storage upload endpoints with pending file lifecycle (#22449)
<img width="1484" height="404" alt="image" src="https://github.com/user-attachments/assets/b2d363bf-d9e1-49fb-9811-8cc98041aa79" /> ## Context Uploading large files currently OOMs the server: every upload resolver buffers the whole file in memory (`streamToBuffer`) before writing it to storage. This PR is the first of a series introducing direct client-to-storage uploads. It adds the server-side endpoints and driver support only — it is non-breaking and nothing consumes the new flow yet. Follow-up PRs will migrate the frontend upload paths, add a stale-pending-file cleanup cron, and cap the legacy buffered resolvers. ## What it does **New upload flow (initiate → PUT → confirm):** - `createFileUpload(filename, size, fileFolder, fieldMetadataId?)` validates the request (folder allowlist: `FilesField`/`Workflow`, max size, extension-derived mime type), creates the file record in a new `PENDING` status, and returns an upload target: - **S3 with presign enabled** → a presigned PUT URL with `Content-Type`/`Content-Length` pinned in the signature, so the client uploads straight to the bucket; - **local storage, or S3 without presign** → a token-authenticated streaming endpoint on the server (`PUT /file-upload/:id?token=…`, new `FILE_UPLOAD` JWT type) that pipes the request body to the storage driver with constant memory usage and a declared-size cap. - `completeFileUpload(fileId)` verifies the bytes actually landed in storage (HEAD + size match against the declared size) and flips the record to `UPLOADED`. Idempotent. **Pending lifecycle safety:** - New `status` column on `core.file` (`PENDING`/`UPLOADED`, default `UPLOADED` so all existing rows and the legacy upload path are unaffected) + fast instance command. - Files are refused by the serving endpoints and by FILES-field sync while `PENDING`. **Driver support (both drivers):** - `getPresignedUploadUrl` (S3: presigned PUT; local: `null` → server-endpoint fallback) - `writeFileStream` (local: `fs` pipeline with the existing symlink/containment hardening, partial-file cleanup on error; S3: `@aws-sdk/lib-storage` `Upload` for bounded-memory streaming) - `getFileMetadata` (HEAD/stat for confirm-time verification) ## Tests - `file-upload.service.spec.ts`: initiate validation (folder allowlist, size), presigned vs fallback target, confirm verification (missing object, size mismatch, happy path, idempotency) - `local.driver.spec.ts`: `writeFileStream` (content, symlink rejection, partial-file cleanup on stream error), `getFileMetadata` - `s3.driver.spec.ts`: `getPresignedUploadUrl` (disabled → null, PUT command with signed content-type/content-length) - `direct-file-upload.integration-spec.ts`: full end-to-end flow against the local driver (initiate → PUT → complete → download), plus error paths (complete without upload, oversized PUT → 413, invalid token → 403, unsupported folder, size above max) ## Notes for reviewers - The upload-size ceiling for direct uploads is `settings.storage.maxDirectUploadFileSize` (1GB), separate from the 10MB `maxFileSize` used for pictures. - Since content can't be sniffed before it reaches storage, the mime type is derived from the file extension (with the existing `TWENTY_MIME_POLICY` override) and unknown extensions fall back to `application/octet-stream`; the serving path already forces `Content-Disposition: attachment` for anything not on the inline-safe allowlist. - Self-hosters using S3 presign will need a bucket CORS policy allowing `PUT` from the frontend origin (config variable description updated). 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/22449?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. --> |
||
|
|
8b191d6fcc |
chore(server): remove the five dead FileFolder values and their legacy serving pipeline (#22516)
Follow-up cleanup after #22510: shrink `FileFolder` and `fileFolderConfigs` to only folders that actually exist, so per-folder policy entries are real decisions. ## What **Remove the five dead enum values** — `ProfilePicture`, `WorkspaceLogo`, `Attachment`, `PersonPicture`, `File`. They were already marked replaced/removed in the enum, have no production write path, and `FileByIdGuard`'s `SUPPORTED_FILE_FOLDERS` allowlist already rejects them at the serving endpoint. **Delete the legacy path-based serving pipeline that existed only for them** — verified wired to no route: - `FilePathGuard` — registered as a provider in `FileModule` but applied to no controller - `extractFileInfoFromRequest` (parsed the old `/files/profile-picture/original/TOKEN/file.jpg` format) — only consumer was `FilePathGuard` - `checkFileFolder` — only consumer was `extractFileInfoFromRequest` - `settings.storage.imageCropSizes` — keyed exclusively by the three dead picture folders, zero consumers - the crop-size helpers in `utils/image.ts` (`getCropSize`, `ShortCropSize`, `CropSize`) — zero consumers outside the file; `getImageBufferFromUrl` is kept - `AllowedFolders` type — last consumer was `checkFileFolder` **Test fixtures** referencing dead folders were moved to living ones; the specs of deleted utils are deleted with them. **Generated files** (`twenty-front/src/generated-metadata/graphql.ts`, `twenty-client-sdk` schema) hand-updated to match the shrunk GraphQL enum. ## Legacy data safety Workspaces may still hold `File` rows whose `path` starts with a dead prefix (e.g. `attachment/…`). These stay inert, exactly as today: - Serving: `FileByIdGuard` rejects non-supported folders before any config lookup, and file lookups filter by `path LIKE '<current-folder>/%'`, so dead-prefix rows are unreachable. - Every consumer that feeds stored paths into `removeFileFolderFromFileEntityPath` (which throws on unknown prefixes) is upstream-guarded by a current-folder filter or allowlist — audited all seven call sites. - Stored legacy member `avatarUrl` strings are parsed with `extractFileIdFromUrl(url, FileFolder.CorePicture)` and already fall back to `''` for old formats; unchanged. ## GraphQL note `FileFolder` is exposed as a GraphQL enum (input of the dev-only `uploadApplicationFile` mutation, which only accepts application-code folders). Clients sending a removed value were already rejected at the resolver allowlist; they now fail GraphQL enum validation instead. No supported client sends them — the frontend only uses `CorePicture`. Net: **+10 / −301** across 17 files. https://claude.ai/code/session_01AKwhTxYFDhWhCZ4b7sf35W --- _Generated by [Claude Code](https://claude.ai/code/session_01AKwhTxYFDhWhCZ4b7sf35W)_ <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22516?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. --> |
||
|
|
af1b89c788 |
feat(server): import application logo into file storage at install (#22437)
Installed apps stored the logo as the manifest's relative path but never imported the file, so the public-assets URL 404'd and logos went missing in the UI for npm/tarball sources. Import the logo (best-effort — a declared but unshipped logo is skipped, not fatal) and record it as a first-class logoFileId on the application so it can be served reliably. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22437?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. --> |
||
|
|
25fe66565c |
feat(applications): add type and options to application variables (#22157)
## Before <img width="1452" height="709" alt="image" src="https://github.com/user-attachments/assets/cd384ffa-cbe6-49d5-a807-ca8d580f55a9" /> <img width="1074" height="452" alt="image" src="https://github.com/user-attachments/assets/720d38db-3495-4032-8831-17d24ec6a7e7" /> ## After <img width="1421" height="865" alt="image" src="https://github.com/user-attachments/assets/2275c996-c895-4800-8324-2aa2ddfddd43" /> <img width="1348" height="870" alt="image" src="https://github.com/user-attachments/assets/3e1a891d-6db0-4cbd-870a-2a5bbde4929d" /> ## Summary Adds typed application variables with optional select **options**. This is the other half of #22059, split out from the custom-settings-tab removal. ## Changes - **Shared types**: `ApplicationVariable` / `ServerVariables` gain an optional `type` (a `FieldMetadataType` subset — `TEXT`, `BOOLEAN`, `NUMBER`, `DATE`, `SELECT`, `MULTI_SELECT`, `RAW_JSON`, `RICH_TEXT`, `ARRAY`, …) and select `options`. New `serializeApplicationVariableValue` / `deserializeApplicationVariableValue` helpers convert typed values to/from the encrypted string storage. - **Server**: `type`/`options` columns on `applicationVariable` and `applicationRegistrationVariable` (entities + DTOs), a fast `2-17` instance command, manifest processing via the serialization helpers, and a `QueryDeepPartialEntity` cast where the manifest JSON column is persisted. - **Frontend**: a polymorphic `SettingsApplicationVariableInput` that renders the native `Form*` field component for each type (boolean, number, date/date-time, select, multi-select, array, raw JSON, rich text, text); fragment/query updates to fetch `type`/`options`. - **SDK**: `defineApplication` validates that `SELECT`/`MULTI_SELECT` variables declare non-empty `options` at build time (since `options` is kept structurally optional for TypeORM/SDK compatibility). Variables default to `TEXT` when no type is given, so existing manifests are unaffected. ## Notes The generated GraphQL artifacts (`type`/`options` on the variable types) are regenerated by codegen; that change accompanies this PR. https://claude.ai/code/session_013Z7UB35V2mvUozh55QHG23 --- _Generated by [Claude Code](https://claude.ai/code/session_013Z7UB35V2mvUozh55QHG23)_ <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22157?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. --> |
||
|
|
6f64be5751 |
Gate and meter email group: enterprise license (self-host) + credits (cloud) (#22390)
Email group (marketing email) was gated only by the `IS_EMAIL_GROUP_ENABLED` feature flag with no server-side enforcement. This adds real gating, split by deployment: - **Self-hosted** (`IS_BILLING_ENABLED=false`): requires a valid Enterprise plan. - **Cloud** (billing enabled): metered by credits, mirroring the existing AI credit system. Priced on AWS SES cost ($0.10/1,000 outbound) × 3 margin = $0.30/1,000 (300 micro-credits/email). Pre-flight blocks sends when out of credits; each email is charged after SES accepts it, in the async send job — matching how AWS bills us (no refund on bounce). Enforcement is applied at every email group resolver, and denials surface as proper client errors through a dedicated GraphQL exception filter. |
||
|
|
a28887bba6 |
feat(server): workspace opt-out of root-domain directory listing (#22423)
## What Lets a workspace opt out of being surfaced in the multi-workspace root-domain (app.twenty.com) picker via **email-domain discovery**. Adds `isDirectoryListingEnabled` (default `true`) on the workspace. When `false`, the workspace is filtered out of the approved-access-domain branch of `findAvailableWorkspacesByEmail`, so a user whose email domain matches an approved access domain no longer sees the workspace in the sign-up picker. ## Scope of the opt-out (deliberately narrow) The filter is applied **only** to the approved-access-domain discovery source: - **Members** (`availableWorkspacesForSignIn`) — never filtered; they keep access. - **Explicit invitations** — never filtered; the intent is one-to-one. - **Approved-access-domain discovery** — the only "listing" source, gated by the flag. A hidden workspace stays fully reachable by members and invited users via the direct workspace subdomain; it just isn't advertised in the global picker. > Open question for review: do we also want a stronger mode that hides the workspace from the root-domain picker even for existing members (forcing them to use the subdomain directly)? That would additionally filter the member/invitation sources and is a larger behavior change — not included here. ## Changes **Backend** - `workspace.entity.ts` — new `isDirectoryListingEnabled` column (`@Field`, default `true`). - `user-workspace.service.ts` — filter the approved-access-domain branch on the flag. - `update-workspace-input.ts` — expose the field on `updateWorkspace`. - `workspace.service.ts` — `PermissionFlagType.SECURITY` (same as the other discovery/security toggles). - Fast instance command adding the column (default `true`, so no existing workspace is hidden). **Frontend** - Settings > Security: a **"List in workspace directory"** toggle (shown only in multi-workspace mode) that flips the flag via `updateWorkspace`, mirroring the existing `isInternalMessagesImportEnabled` toggle. - Threaded the field through the current-user fragment, `CurrentWorkspace` type, and mock data. - Regenerated the metadata + client-sdk GraphQL types (`generated-metadata`, `twenty-client-sdk/.../generated`) — generated against a server booted from this branch. ## Verification - `tsgo` typecheck: 0 errors. `oxlint`: 0/0. `oxfmt`: clean. - Codegen diff verified to contain **only** the new field (no unrelated drift). - Tests not run locally; CI covers unit/integration + the codegen/migration freshness checks. --------- Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> |
||
|
|
4aaf171d63 |
feat(ai): add ask_questions interactive clarifying-question tool (#22346)
## What & why Adds an `ask_questions` tool that lets the in-app **Ask AI** assistant **pause a turn to ask the user one or more multiple-choice questions** (per the [Figma design](https://www.figma.com/design/xt8O9mFeLl46C5InWwoMrN/Twenty?node-id=105959-117153)) and resume once answered — instead of guessing on ambiguous/consequential decisions. The tool is **harness-only**: an interactive question UI is meaningless without a user to answer it, so it must be absent from MCP and from head-less workflow agents. ## Design — true tool-result resume (not a synthetic user message) The user's answer is a **structured tool result bound to the `toolCallId`**, and the **same agent turn resumes** — exactly how Anthropic (`tool_result` by `tool_use_id`) and OpenAI (`function_call_output`) model human-in-the-loop. The naive form of this (leave the tool call in `input-available` to mean "pending") is **impossible** here: `finalizeDanglingToolParts` rewrites `input-available` → `output-error` ("Tool execution was interrupted") on both the persist path (`addMessage`) and the model-reload path (`chat-execution.service.ts`). That util is a load-bearing safety net, so weakening it is the wrong move. Instead: - `ask_questions` is an **inline, chat-only tool with an `execute` that returns a `status: 'pending'` result immediately**, so the tool part is always `output-available` and **immune to `finalizeDanglingToolParts`**. `stopWhen(hasToolCall('ask_questions'))` halts the turn right after the call (the model never sees the placeholder). - A nullable **`thread.pendingQuestionMessageId`** marker records that a turn is awaiting an answer. - The new **`answerAgentChatQuestion`** mutation atomically *claims* the question (clears the marker, marks the thread streaming), **writes the answer onto the same tool part** (`status: 'answered'`), and **re-enqueues the turn via the existing `existingTurnId` plumbing** (`isResume` bypasses the per-turn dedup guard). On resume `finalizeDanglingToolParts` leaves the `output-available` part untouched and `convertToModelMessages` emits `assistant(tool_use)` + `tool_result(answers)`, so the model continues. This achieves the platform-aligned semantics **without** weakening the finalize safety net or inventing a fragile new part state. ### Meets the two requirements - **Survives refresh, scoped per-thread** — the pending state is a normal persisted `output-available` part + the thread marker; the frontend card is derived per-thread from the loaded messages, so it re-appears on reload and only on its own thread. - **Takes priority over the queue** — a unified `isBlocked = activeStreamId || pendingQuestionMessageId` gate is applied in both `sendChatMessage` (new messages queue) and `flushNextQueuedMessage` (the drain). The queue cannot unpile until the question is answered and the resumed turn completes. ### Harness-only by construction `ask_questions` is added **only** to the chat's inline `activeTools` (like `learn_tools`/`execute_tool`/`load_skills`). It never enters the tool registry/catalog, so it is invisible to MCP and to workflow agents — no `MCP_EXCLUDED_TOOL_NAMES` entry needed. ## UX While a question is pending, the **composer is replaced by the question card** (matching the Figma): question title + pager (`1/2`), numbered option rows (`IconSquareNumber*`) with per-option info-icon descriptions and a "Recommended" badge, and the normal composer as the free-text fallback ("Type anything to do differently."). The transcript shows a compact "Asking questions…" status line that becomes an answered summary. ## Changes **twenty-shared** - `ai/types/AskQuestionsToolTypes.ts` — `AskQuestionItem/Option/Answer/Result`, `ASK_QUESTIONS_TOOL_NAME`. **twenty-server** - `ai-chat/tools/ask-questions.tool.ts` — inline tool factory (pending-result `execute`, zod schema, 1–4 questions × 2–4 options). - `chat-execution.service.ts` — add to `activeTools` + `preloadedToolNames`; `hasToolCall` in `stopWhen`. - `chat-system-prompts.const.ts` — when-to-use guidance. - `entities/agent-chat-thread.entity.ts` — `pendingQuestionMessageId` column. - `stream-agent-chat.job.ts` — set the marker on a question pause; bypass the dedup guard on resume; suppress the no-text warning for question pauses. - `agent-chat-streaming.service.ts` — gate `flushNextQueuedMessage`; `enqueueResumeStream`. - `agent-chat.resolver.ts` — gate `sendChatMessage`; `answerAgentChatQuestion` mutation. - `agent-chat.service.ts` — `resolvePendingQuestion` (atomic claim + write answer). - `dtos/agent-chat-question-answer.input.ts`, `ai.exception.ts` (`QUESTION_NOT_PENDING`), `utils/find-pending-question-part.util.ts`. **twenty-front** - `components/AiChatQuestionCard.tsx` — the interactive card (matches Figma tokens) + `__stories__/AiChatQuestionCard.stories.tsx`. - `components/AiChatEditorSection.tsx` — swap the composer for the card while pending. - `components/AiChatQuestionStatusRenderer.tsx` + branch in `AiChatAssistantMessageRenderer.tsx`. - `states/selectors/agentChatPendingQuestionComponentSelector.ts`, `types/AgentChatPendingQuestion.ts`. - `hooks/useSubmitQuestionAnswer.ts` + `utils/markQuestionAnswered.ts` (optimistic) + `graphql/mutations/answerAgentChatQuestion.ts`. A design doc lives at `packages/twenty-server/docs/ASK_USER_QUESTION_TOOL_PLAN.md`. ## Migration Adds a nullable `pendingQuestionMessageId` (uuid) column to `core.agentChatThread`. Needs a generated **fast instance command** (`database:migrate:generate --name addThreadPendingQuestion --type fast`) — see "Verification status". ## Tests - Server: `ask-questions.tool.spec.ts` (pending echo + schema bounds), `find-pending-question-part.util.spec.ts`. - Front: `markQuestionAnswered.test.ts`, plus the Storybook story. ## Verification status (please read) This branch was authored in an environment where the monorepo `yarn install` repeatedly failed on transient TLS resets from the package registry, so I could **not** locally run the mechanical gates. The logic was reviewed by hand and the `ai@6.0.97` exports used (`hasToolCall`, `stepCountIs`, `generateId`) were confirmed against the package's type defs. Still **TODO** (will rely on CI / a follow-up once deps install): - [ ] `nx run twenty-shared:generateBarrels` (the `ai/index.ts` export was added by hand; regen to reconcile) - [ ] `nx run twenty-front:graphql:generate` (new mutation + input type) - [ ] generate the fast instance command (migration) for the new column - [ ] `typecheck` + `lint:diff-with-main` (front + server) — expect minor import-ordering autofixes - [ ] run the unit tests **Screenshots:** reproducing the live flow needs an AI provider API key (to get the model to actually call `ask_questions`), which isn't available here. The card can be screenshotted from its **Storybook story** (`AiChatQuestionCard.stories.tsx`) with no API key — I'll add that image once deps install, or a reviewer can run `nx storybook twenty-front`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01AArS8H3y3Z1Qwm763xhPLB --- _Generated by [Claude Code](https://claude.ai/code/session_01AArS8H3y3Z1Qwm763xhPLB)_ <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22346?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. --> |
||
|
|
d709467902 |
feat(ai): surface AI chat stream failures through one typed error channel (#22434)
## Context Investigating a report where the AI chat showed only a `...` spinner while the network response clearly contained `No AI models are available`. Root cause: terminal stream failures reach the client on **two mismatched channels**. | Representation | Persisted (survives reload) | Rendered by client | |---|---|---| | AI-SDK `error` chunk (inside `stream-chunk`) | ✅ RPUSH'd to Redis | ❌ dropped by `readUIMessageStream` (no message part, no error state) | | typed `stream-error` event | ❌ never persisted | ✅ sets the error atom | Live, the `stream-error` event renders. But on reload, `chatStreamCatchupChunks` replays only the persisted **error chunk** — which the reducer discards — and the streaming indicator never clears. ## Change Collapse to a single typed error contract: - **Suppress the opaque `error` chunk** in the stream job; every failure is surfaced through the typed `stream-error` event. Errors are mapped via `mapErrorToStreamError` so an `AiException` keeps its `AiExceptionCode` (e.g. `API_KEY_NOT_CONFIGURED` → the existing "AI not configured" banner) instead of leaking a raw string. - **Persist the terminal error** next to the accumulated chunks and expose it as an explicit `error { code message }` field on `ChatStreamCatchupChunks`, so a client catching up after a reload recovers it — no dependency on the AI SDK's internal chunk shape. - **Reset per-thread stream state at job start**, so a failed turn's leftover chunks/error never replay on the next stream. - **Client replays the catchup error** as a terminal `stream-error` event, which clears the streaming indicator and renders the error (fixes the infinite spinner on a stream that ended in error). ## Notes - `ChatStreamError` is a new metadata GraphQL type; generated types (twenty-front metadata + client-sdk) were hand-updated to keep the tree consistent and will be reconciled by CI's `graphql:generate` check if anything differs. - Server unit test added for the error mapping. No schema/DB migration. ## Test plan - [ ] With no AI provider configured, send a chat message → error renders immediately (not a spinner). - [ ] Reload the thread → the error still renders (recovered from catchup), indicator not spinning. - [ ] Configure a provider and send again → normal streaming; no stale error from the previous failed turn. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22434?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. --> |
||
|
|
5a4ebca226 |
refactor(server): unify the two metadata override mechanisms into one (#22417)
## Unify the two metadata override mechanisms into one Twenty had **two** override mechanisms: - **`standardOverrides`** — a bespoke JSONB column on `objectMetadata`/`fieldMetadata` with typed DTOs and a per-locale `translations` map, resolved by two i18n-aware resolvers. - **`OverridableEntity.overrides`** — a flat, registry-driven JSONB blob on view / view-field / view-field-group / command-menu-item / page-layout-tab / page-layout-widget, resolved by a plain spread. This PR collapses them into **one** concept: a single `overrides` blob, one registry-driven overridable set, one i18n-aware read path, and one write path (`computeMetadataOverridesBlob`, extracted in #22404). Object/field **stay on `SyncableEntity`** (not reparented to `OverridableEntity`) so their `isActive` default stays **FALSE** — this sidesteps the `isActive` default conflict entirely. ### GraphQL breaking change (accepted) The `standardOverrides` field is **removed** with no deprecation alias — `overrides` (a `JSON` scalar) is exposed instead on `Object` and `Field`. Product confirmed negligible external usage; the front-end has no hand-written consumer (only generated types), which are regenerated here. ### Commit structure (reviewable commit-by-commit) 1. **Unified resolver + parity harness** — `resolveEffectiveEntityProperty` is a strict superset of the three legacy resolvers; a corpus parity spec compares it against a *frozen reference* of the old logic across every locale, `isStandardApp` branch and override shape. 2. **Registry-driven** — object/field presentation props tagged `isOverridable` + `translatable`; the overridable/translatable sets are derived from the registry (a test asserts they equal the legacy hardcoded lists). 3. **Rename + swap + delete** — `standardOverrides` → `overrides` across entities, DTOs, flat/universal types, producers, the ~12 resolve/write/create/sync call sites, mocks and specs; the reconciler's two compare entries collapse to one; the three legacy resolvers, both DTOs and the hardcoded constants/types are deleted. 4. **Migration (zero-downtime, two-phase)** — split across two releases so a rolling deploy never drops a column a previous-release pod still `SELECT`s: - **2.19 fast** — add the `overrides` column (schema only). - **2.19 slow** — backfill `overrides` from `standardOverrides` in `runDataMigration` (kept out of the schema transaction so the bulk write doesn't hold the ACCESS EXCLUSIVE lock; skipped on fresh installs, which have no data to copy). - **2.20 fast** — drop the legacy `standardOverrides` column (gated by `TWENTY_NEXT_VERSIONS`, so it stays dormant until the instance reaches 2.20). 5. **Front/client-SDK regen** — regenerated metadata GraphQL types. 6. **Integration specs + i18n** — updated the standard object/field update integration specs + snapshots, and the reworded validator message catalog entry. ### Rolling-deploy safety `standardOverrides` is retained through 2.19 and only dropped in 2.20, mirroring the codebase's deferred-drop convention (`isUIReadOnly`/`isCustom`). During the 2.19 rollout both columns exist, so old and new pods coexist without "column does not exist" errors. The backfill lives in a slow `runDataMigration` (per the `no-data-mutation-in-fast-instance-command` rule) so it doesn't stall reads. ### `isActive` guard The migration never reads or writes `isActive`; the backfill asserts the active-row count is unchanged and aborts otherwise. Verified on a real DB: apply + revert preserves the blob **and** the nested `translations` map, with `isActive` counts identical before/after. ### Verification (local) - `nx typecheck twenty-server` + `nx typecheck twenty-front` — green - `nx lint:diff-with-main twenty-server` (oxlint `--type-aware` + oxfmt) — green - `nx test twenty-server` — green (unit + parity + registry + migration tests) - `nx run twenty-server:test:integration:with-db-reset` — green - `database:reset` applies the 2.19 phases and leaves **both** columns present (2.20 drop stays dormant); backfill + revert round-trip verified on a real DB - Metadata integration suites (standard object/field update, application sync) pass end-to-end against the two-column schema - Metadata GraphQL types regenerated against a booted server; zero `standardOverrides` references remain in application code (only the migration commands + the legacy schema baseline) --------- Co-authored-by: prastoin <paul@twenty.com> |
||
|
|
3bbc08d41f |
refactor(schema): reorganize IndexField and related types (#22439)
## Summary
Querying `indexMetadatas { indexFieldMetadatas { ... } }` on the
`/metadata` GraphQL endpoint fails with a 500:
> Nest could not find IndexFieldMetadataDTOAuthorizer element (this
provider does not
> exist in the current context)
The `@CursorConnection('indexFieldMetadatas', ...)` decorator on
`IndexMetadataDTO` makes nestjs-query auto-generate a relation resolver
that injects an authorizer for `IndexFieldMetadataDTO`. That authorizer
is never provided, because the DTO was never registered as a resolver in
`IndexMetadataModule` — so the field has been broken since it was
introduced in #7162.
Since the working, DataLoader-backed `indexFieldMetadataList` field
already exposes the same data (and is what the frontend uses), this PR
removes the dead connection instead of wiring up the authorizer.
## Changes
- Remove `@CursorConnection('indexFieldMetadatas', ...)` from
`IndexMetadataDTO`
- Regenerate frontend metadata GraphQL types
(`twenty-front/src/generated-metadata`)
- Regenerate client SDK metadata schema/types
(`twenty-client-sdk/src/metadata/generated`)
## Notes
- Not a breaking change in practice: the removed field always threw, so
no consumer can have been relying on it. Callers now get a standard
GraphQL validation error suggesting `indexFieldMetadataList` instead of
an internal server error.
- Verified locally: the failing query now returns `Cannot query field
"indexFieldMetadatas" on type "Index". Did you mean
"indexFieldMetadataList"?` and `indexFieldMetadataList` continues to
work.
Fixes [sonarly issue #54098](https://sonarly.com/issue/54098?type=bug)
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22439?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. -->
|
||
|
|
d8cc81cb91 |
Improve billing settings UI (#22377)
## Summary - Refresh the settings billing subscription and credits cards with clearer status, usage, and action states. - Add the credit-package picker flow and route past-due/cancellation actions to billing management instead of credit modals. - Clean up related billing UI helpers and formatting. ## Screens ### regular <img width="1007" height="781" alt="image" src="https://github.com/user-attachments/assets/dc9c0c59-8cda-422a-a0d3-56292421a744" /> ### downgrading <img width="1049" height="818" alt="image" src="https://github.com/user-attachments/assets/0dc3dea8-5b55-4cf2-bc92-997cf6e9bebc" /> <img width="1048" height="901" alt="image" src="https://github.com/user-attachments/assets/fba805ad-f523-444b-93a0-2011b6b43443" /> ### Trialing without card <img width="1008" height="903" alt="image" src="https://github.com/user-attachments/assets/d5dd11b6-4c92-4102-ac22-ad1ad4e9fbfb" /> with card <img width="1008" height="806" alt="image" src="https://github.com/user-attachments/assets/0dbeb00e-890e-4448-94fe-0cc0ef411e8d" /> ### Past due & Unpaid <img width="1052" height="860" alt="Past due" src="https://github.com/user-attachments/assets/25ba53ef-6e74-4b4c-bfe1-d6c74e165917" /> <img width="1138" height="860" alt="Unpaid" src="https://github.com/user-attachments/assets/bb16fe65-84e4-40a7-8951-90b01030aded" /> --------- Co-authored-by: Etienne <45695613+etiennejouan@users.noreply.github.com> |
||
|
|
27dea0ed0b |
Add installed workspaces view to application registration (#22359)
## After <img width="895" height="344" alt="image" src="https://github.com/user-attachments/assets/33591753-f248-45ce-b32d-cc1112f50579" /> <img width="889" height="425" alt="image" src="https://github.com/user-attachments/assets/469ee228-9abb-486f-b2ec-9efb490bb2c8" /> <img width="766" height="343" alt="image" src="https://github.com/user-attachments/assets/2d88444a-6d98-4f97-8e5d-109197cfad27" /> ## Summary Add a new "Installed workspaces" section to the application registration settings page that displays all workspaces that have installed a given application, with pagination support. ## Key Changes - **Backend Service**: Added `getInstalledWorkspaces()` method to `ApplicationRegistrationService` that queries installed applications across workspaces with pagination support - **Backend DTO**: Created `ApplicationRegistrationInstalledWorkspacesDTO` and `InstalledWorkspaceDTO` to structure the response with workspace details (id, displayName, logo, version), total count, and hasMore flag - **GraphQL Resolver**: Added `findApplicationRegistrationInstalledWorkspaces` query resolver with pagination (page parameter, default page size of 10) and proper authorization guards - **Frontend Component**: Created `SettingsApplicationRegistrationInstalledWorkspaces` component that: - Displays installed workspaces in a table with workspace logo, name, and version - Shows initial 3 workspaces with "Show all" button to expand - Implements pagination with "Show more" button to load additional pages - Handles empty state (returns null if no workspaces installed) - **GraphQL Query**: Added `FindApplicationRegistrationInstalledWorkspaces` query document for frontend data fetching - **Integration**: Integrated the new component into `SettingsApplicationRegistrationGeneralTab` ## Implementation Details - Pagination uses offset-based approach with configurable page size (10 workspaces per page) - Query results are ordered by workspace displayName and id for consistent ordering - Soft-deleted applications and workspaces are excluded from the list and counts - Apollo Client's `fetchMore` with `updateQuery` merges paginated results into the cache - Component respects existing authorization (API_KEYS_AND_WEBHOOKS permission required) - Uses existing UI components (Table, Card, Avatar, Button) from twenty-ui library - Supports internationalization with Lingui ## Screenshots The new "Installed workspaces" section on the app registration General tab (admin app detail page), captured against a local instance with a demo app installed in 14 workspaces. The three PNGs are committed under `.github/assets/screenshots/installed-workspaces/` and render inline in the **Files changed** tab of this PR: - `1-first-3-show-all.png` — Collapsed: the first 3 installed workspaces (avatar + name + installed version) with a "Show all" button. - `2-expanded-show-more.png` — "Show all": the first page of 10 workspaces, with a "Show more" button (more remain). - `3-all-paginated.png` — "Show more": all 14 workspaces loaded, button gone. Review in cubic: https://cubic.dev/pr/twentyhq/twenty/pull/22359?utm_source=github https://claude.ai/code/session_012nWtviSBdfFeHEASTtwvJ7 |
||
|
|
2e6077383b |
Add install your first apps onboarding V2 step (#22347)
https://github.com/user-attachments/assets/5326d48f-1842-4db1-bc7c-94852145c035 <img width="838" height="754" alt="CleanShot 2026-06-30 at 16 25 05@2x" src="https://github.com/user-attachments/assets/5c7d53d7-4d65-4e35-aed1-edf0c104e140" /> Adds an "Install your first apps" step to the V2 onboarding, shown right after import-contacts. It lets users opt into installing marketplace apps (Call recorder and People Data Labs for now) during onboarding. - New backend `OnboardingStatus.APPS_INSTALLATION` (between SYNC_EMAIL and PROFILE_CREATION); V1 auto-skips it. - The primary button sends the selected app ids to the server via `triggerInstallAppsOnboardingStep`, which enqueues a dedicated job that installs them asynchronously so onboarding isn't blocked. Skip continues without installing. - The workspace is credited per app on successful installation. Credits are env-driven via `ONBOARDING_INSTALL_APPS_CREDITS_REWARD_PER_APP`, shown as "Earn +N free credits (1 per tool)". <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22347?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. --> |
||
|
|
9f3ebaaf22 |
feat(messaging): sync draft emails and edit them in the thread composer (#22178)
Stop excluding drafts from sync across all three providers (Gmail DRAFT label, Microsoft/IMAP Drafts folder) and add an isDraft boolean field on Message so drafts are queryable by the API and AI agents. Drafts render in the thread with a Draft tag; clicking one opens the existing reply composer pre-filled with the draft's recipients, subject and body, and Send reuses the existing send-email flow. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22178?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> --------- Co-authored-by: Félix Malfait <felix.malfait@gmail.com> |
||
|
|
b0d7516951 |
Deprecate asExpression from field metadata search_vector (#22287)
## Summary Fully deprecates the cached `asExpression` / `generatedType` settings on `TS_VECTOR` (searchVector) fields. Previously the generated-column expression was stored in `FieldMetadataSettings` and kept in sync via imperative recompute side-effects. It is now **derived at DDL time** from the `searchFieldMetadata` rows that describe which fields feed the search vector, making `searchFieldMetadata` the single source of truth and removing a whole class of cache-drift bugs. This is delivered across the milestones tracked in #2587 and coordinates with the frontend migration (#1428). ## Why - The searchVector expression lived in two places (stored `settings.asExpression` + the actual generated column), kept consistent by bespoke side-effects (`recompute-search-vector-on-field-rename`, label-identifier recompute, etc.). - The frontend reconstructed the searchable-fields list by **regex-parsing** the stored `asExpression`. - Both are brittle. Deriving the expression from `searchFieldMetadata` rows at build/run time removes the cache and the parsing. ## What changed ### Server - data model & derivation - Introduce the `tsVectorFieldMetadata` relation on `searchFieldMetadata` (`tsVectorFieldMetadataId` / universal identifier) linking each searchable-field row to its target `TS_VECTOR` field. - New runtime derivation `deriveSearchVectorAsExpressionForTsVectorField` (`flat-search-field-metadata/utils/...`) used by the create-object and update-field handlers to generate the column expression from `searchFieldMetadata` rows. - Remove `asExpression` / `generatedType` from stored settings: `FieldMetadataSettings.TS_VECTOR` is now `null`; the column builder (`generate-column-definitions.util.ts`) hardcodes `generatedType: 'STORED'` and requires the derived expression. - Delete the imperative recompute side-effects and the `compute-search-vector-universal-settings-from-object-manifest` path; drop the `settings` block from all 28 standard `compute-*-standard-flat-field-metadata` utils. ### Server - migration runner - New `rebuildSearchVector` marker on `update-field` actions: the orchestrator synthesizes targeted column rebuilds (`compute-search-vector-rebuild-target-universal-identifiers.util.ts` + the deprioritize aggregator) only when a searchFieldMetadata change or indexed-field rename actually requires it - instead of rebuilding on every settings touch. - Deferrable FKs + in-flight ID resolution so a `searchFieldMetadata` row and its `TS_VECTOR` field can be created in the same transaction (deterministic UUIDs). ### Frontend (contract change, #1428) - New `SearchFieldMetadataDTO` + dataloader exposing `searchFieldMetadataList` on object metadata. - `SettingsObjectSearchSection` now reads `objectMetadataItem.searchFieldMetadatas` instead of parsing `asExpression`; new `SearchFieldMetadataItem` type, fragment, and mapping updates. ### Upgrade commands (2.18) - `2-18-instance-command-fast-...-add-ts-vector-field-metadata-id-to-search-field-metadata` - `2-18-instance-command-fast-...-make-search-field-metadata-fks-deferrable` - `2-18-instance-command-slow-...-backfill-ts-vector-field-metadata-id-on-search-field-metadata` (These were relocated from 2.16 to 2.18 and re-timestamped into an ordered block - add column -> make FK deferrable -> backfill data - since 2.16/2.17 are released.) ### Tests - Updated search-vector side-effect integration specs to assert behavior (search works) rather than the now-removed `asExpression`; removed the obsolete expression-validation specs; refreshed the application-sync snapshot (`universalSettings: null`). ## Upgrade / compatibility notes - Existing workspaces keep their stored `settings` until a later cleanup; nothing reads it anymore. The new derivation drives all DDL going forward. - Schema changes are gated behind the 2.18 instance commands above. ## Known follow-up (separate PR) https://github.com/twentyhq/core-team-issues/issues/2620 - The column rebuild (`DROP`/`ADD` of the `searchVector` STORED column) cascade-drops its GIN index and does not recreate it - a pre-existing regression on `main` inherited here. A follow-up PR will fix the rebuild handler to recreate the GIN index and add a 2.18 workspace command to recompute every search vector + strip the deprecated settings. (Planned.) ## Test plan - [ ] `npx nx typecheck twenty-server` / `twenty-front` - [ ] `npx nx lint:diff-with-main twenty-server` / `twenty-front` - [ ] Server integration: create/update/delete field, rename indexed field, update object - search returns expected records - [ ] Run the 2.18 instance commands on a seeded DB; verify `tsVectorFieldMetadataId` backfilled and FKs deferrable - [ ] Frontend: object Search settings tab lists the correct searchable fields (no `asExpression` parsing) close https://github.com/twentyhq/core-team-issues/issues/2587 <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22287?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. --> |
||
|
|
6e319283c4 |
fix: Vite 8/Rolldown build warnings in library packages (#22205)
Clean up Vite 8/Rolldown build warnings that showed up during yarn start: - `twenty-client-sdk`: `relativeImportPath.ts` now imports `node:path`, so the generate bundle treats it as a Node external instead of stubbing it for the browser. - Remove rollup’s `interop: 'auto'` from CJS output options - Rolldown don’t support it and was showing `Invalid key: Expected never but received "interop"`. - Replaced deprecated `inlineDynamicImports: true` with `codeSplitting: false` in the worker config. References: - https://v7.vite.dev/guide/rolldown#option-validation-warnings - https://vite.dev/guide/troubleshooting.html#module-externalized-for-browser-compatibility <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22205?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. --> --------- Signed-off-by: Parship Chowdhury <parshipchowdhury@gmail.com> Co-authored-by: Charles Bochet <charles@twenty.com> |
||
|
|
538b180824 |
feat(dpa): self-serve Data Processing Agreement generator (#22243)
## What
A single, region-aware DPA that serves all customers, generated
automatically from the customer's deployment. Two layers:
1. **Click-through DPA** — recorded at signup (acceptance = execution),
resolving merge fields from the deployment region. Cloud only.
2. **In-app signed-PDF generator** — Settings → Legal → Generate DPA:
preview the agreement, enter legal entity + authorized signatory,
download a PDF pre-signed by Twenty, and store the executed copy against
the workspace with its template version + timestamp. Deep-linkable at
`/dpa` (login-gated) for `twenty.com/dpa`.
## How it resolves
A typed variable matrix (`dpa-region-config.constant.ts`) maps the
deployment region to the contracting Processor entity and terms:
- **EU (default)** → Twenty.com SAS, hosting EU/Frankfurt, governing law
France, SCC section dormant.
- **US (custom)** → Twenty, Inc., hosting US, SCC section active.
Region is a deployment-wide setting (`DPA_DEPLOYMENT_REGION`, default
EU) behind a `DpaRegionService` seam so it can later become
per-workspace without touching callers. The legal text is verbatim from
the template (generated into `dpa-template.constant.ts` directly from
the source `.docx`); only the 6 merge fields are filled and the SCC
sections (7.2–7.5) stay in the document for every region per the spec —
only field values branch. Sub-processors are deferred to
trust.twenty.com (not enumerated). Billing stays decoupled (Twenty, Inc.
remains merchant of record regardless of Processor).
## UI
Standard list + create-page pattern (mirrors API keys / webhooks): a
list of executed copies (with re-download) — or the agreement preview
when none exists — and a top-right blue **Generate DPA** CTA opening a
standard create page. The "Legal" item is intentionally **not** in the
settings menu; the page is reached via the `/dpa` deep link.
## Notable implementation details
- **PDF** is rendered server-side with `@react-pdf/renderer`. The
built-in standard-14 fonts only encode ASCII and crash on the template's
curly quotes / em–en dashes / accented Latin, so Liberation Sans (OFL)
is **subset to a Latin glyph set and embedded as base64 data: URLs** —
no font files to ship or resolve at runtime (works in dev, prod-Docker
and CI).
- New `core.dpaAgreement` table via a fast instance command (FK hash
reproduced to match TypeORM).
- Self-hosted deployments (billing disabled) skip click-through
recording and stamp a prominent "not a valid agreement" banner on the
preview and PDF.
## Tests
- Unit: resolver (per-region entity/law/SCC state, EU default, no
unresolved `{{ }}`, SCC sections present in both regions, self-hosted
notice) and HTML renderer.
- Integration (`test/integration/graphql/suites/dpa`): preview has no
unresolved fields; `generateSignedDpa` renders + persists + returns a
downloadable PDF (asserted with accented input to guard the font
regression); list re-download.
## ⚠ Needs legal input before go-live (marked `TODO_CONFIRM` in
`dpa-region-config.constant.ts`)
- Registered-office addresses for Twenty.com SAS and Twenty, Inc.
- US deployment governing law (the template only specifies France).
- DPO name and the Twenty pre-signed authorized signatory name/title.
## Out of scope (flagged per spec)
Intra-group legal agreement and any Stripe/billing-entity changes. A
future e-sign provider would plug in at `DpaService.generateSignedDpa` +
the signatory input.
> Draft until the integration test passes in CI and the legal
`TODO_CONFIRM` values are supplied.
https://claude.ai/code/session_01Ahjydxx6J1souz1s1NeA9a
---
_Generated by [Claude
Code](https://claude.ai/code/session_01Ahjydxx6J1souz1s1NeA9a)_
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22243?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. -->
|
||
|
|
0e22ae0521 |
feat: create calendar events on Google and Microsoft accounts (#22231)
## Context Twenty can import calendar events and send emails, but cannot create calendar events. This adds calendar event creation on connected **Google** and **Microsoft** accounts, mirroring the existing email-send architecture (`message-outbound-manager`). ## What it adds The capability is exposed three ways, all backed by the same composer → driver → persist pipeline: - **GraphQL mutation** `createCalendarEvent` (metadata API) - **AI agent tool** `create_calendar_event` (flows to MCP automatically), gated by a new `CREATE_CALENDAR_EVENT_TOOL` permission flag - **Workflow builder node** "Create Calendar Event" in the **Core** section, with a full settings form (variable interpolation supported) CalDAV/IMAP is intentionally out of scope for now (different long pole). ## Design notes - **Reuse over reinvention** — the created event is run through the existing inbound formatters (`formatGoogleCalendarEvents` / `formatMicrosoftCalendarEvents`) and persisted immediately via the existing `CalendarSaveEventsService`, so it appears in Twenty right away and is reconciled by the next provider sync (dedup on external id). Persistence is best-effort. - **OAuth scopes** — Google already requests `calendar.events` (read+write), so no change there. Microsoft moves `Calendars.Read` → `Calendars.ReadWrite`; existing Microsoft accounts must re-consent (surfaced as a clear "reconnect" error via a missing-scope check). - **Deliberate invitation semantics** — `sendInvitations` is off by default. When off, the event is created with **no attendees** on either provider, so creating an event never silently emails external people. When on, attendees are attached and notified (Google `sendUpdates: all`, Microsoft's default). This sidesteps Microsoft Graph having no per-request suppression. - **Timezone correctness** — Microsoft Graph interprets `dateTime` as wall-clock in the supplied `timeZone` and ignores the offset, so the absolute instant is converted to its wall-clock form before sending (Google honors the offset directly). Both providers end up scheduling the same instant. - **Conferencing** — optional Google Meet (`conferenceData.createRequest`, with a follow-up `events.get` to resolve the async link) / Microsoft Teams (`isOnlineMeeting`). - Attendees are a comma-separated string everywhere (tool input, GraphQL DTO, workflow input), consistent with `send_email` recipients; the composer parses to its internal list. ## Test plan - **Unit**: 45 tests covering the composer (validation, all-day boundaries, offset enforcement, timezone, scope checks, default-account resolution), both provider drivers, the dispatcher, and the workflow step-log builder. - **Integration**: `createCalendarEvent` on the `/metadata` API fails closed with a structured error for a non-existent account (the auth/ownership/validation path that doesn't require provider mocking). - **Manual**: verified the workflow node appears in the Core section, the settings form renders and round-trips (edit → autosave → reload), and the live mutation returns a structured failure for a bogus account. ## Open question for reviewers The metadata mutation `createCalendarEvent` shares a name with the core schema's auto-generated `createCalendarEvent(data:)` CRUD mutation for the CalendarEvent object — they live on different endpoints (`/metadata` vs `/graphql`) so there's no runtime conflict, but it's a potential point of confusion for API consumers. Happy to rename (e.g. `createCalendarEventOnConnectedAccount`) if preferred. ## Out of scope / follow-ups - CalDAV/IMAP support - Event update/delete and recurrence - Existing Microsoft accounts need re-consent for the widened scope <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22231?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: neo773 <neo773@protonmail.com> |
||
|
|
e747bc3e42 |
Add backfill installation feature for pre-installed apps (#22199)
## After <img width="1070" height="514" alt="image" src="https://github.com/user-attachments/assets/9cbd2ff5-1678-4c2f-84da-074568f37c51" /> ## Summary Adds the ability to backfill application installations across all existing workspaces. This allows admins to retroactively install a pre-registered application on every active and suspended workspace through a background job, making the feature idempotent and non-blocking. ## Key Changes - **Backend Service**: Added `backfillApplicationOnAllWorkspaces()` method to `PreInstalledAppsService` that: - Validates the application registration exists - Iterates through all workspaces using `WorkspaceIteratorService` - Installs the app on each workspace - Swallows `APP_ALREADY_INSTALLED` errors for idempotency - Logs success/failure counts - **Background Job**: Created `BackfillApplicationInstallationJob` to process backfill requests asynchronously via the message queue - **GraphQL Mutation**: Added `backfillApplicationInstallation` mutation to `AdminPanelResolver` that: - Validates the application registration exists - Enqueues the background job - Returns immediately without blocking the request - **UI Components**: Enhanced `SettingsAdminApplicationRegistrationGeneralToggles` with: - New "Pre-install on new workspaces" toggle for the `isPreInstalled` flag - "Backfill on all workspaces" button with confirmation modal - Loading state and success/error snack bar feedback - **Data Model**: Added `isPreInstalled` field to `UpdateApplicationRegistrationPayload` input type - **Tests**: Added comprehensive unit tests for `PreInstalledAppsService.backfillApplicationOnAllWorkspaces()` covering: - Missing registration validation - Successful multi-workspace installation - Idempotent handling of already-installed errors - Proper error propagation for unexpected failures ## Implementation Details The backfill operation is designed to be: - **Idempotent**: Already-installed apps are skipped without error - **Non-blocking**: Runs as a background job via message queue - **Resilient**: Per-workspace failures don't block other installations - **Observable**: Logs aggregated success/failure counts for monitoring <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22199?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: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com> |
||
|
|
f86bf637d1 |
[BREAKING CHANGE] remove call recording feature flag and backfill upgrade command for existing command menu items navigation command (#22176)
<!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22176?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. --> |
||
|
|
a20ebfa880 |
feat(applications): remove the application custom settings tab (#22156)
## Summary Removes the application **custom settings tab** feature. This is one half of #22059, split out so it can be reviewed/merged independently from the variable-types enrichment. ## Changes - Remove the `SettingsApplicationCustomTab` component and its tab entry/rendering in `SettingsApplicationDetails`. - Stop syncing `settingsCustomTabFrontComponent` from application manifests — `ApplicationManifestMigrationService` now only syncs the default role. - Deprecate the now-unused fields (kept for backward compatibility, no longer read or synced): - `ApplicationDTO.settingsCustomTabFrontComponentId` (GraphQL `@deprecated`) - `ApplicationManifest.settingsCustomTabFrontComponentUniversalIdentifier` - the `settingsCustomTabFrontComponentId` column comment on `ApplicationEntity` The DB column is intentionally **not dropped**, so existing installations upgrade cleanly. |