mirror of
https://github.com/twentyhq/twenty.git
synced 2026-06-13 02:19:02 -04:00
e293c3331162cd4f60e8bb55f2bfa8a27e9fdb8a
12686 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
e293c33311 |
Normalize defaultValue properly (#21511)
<img width="1506" height="338" alt="image" src="https://github.com/user-attachments/assets/3ee0d5c9-af32-4ef3-83c9-2f4671219126" /> |
||
|
|
926cf2d0cf |
fix(zapier): support Select and Multi Select fields (#21509)
## Context Closes #15970 Select and Multi Select fields did not show up in the Zapier integration. ## Root cause `computeInputFields` only emitted input fields for an explicit allow-list of field types and silently dropped everything else, so `SELECT` and `MULTI_SELECT` were never surfaced. On top of that, `SELECT`, `MULTI_SELECT` and `RATING` are **GraphQL enums** in Twenty's API. Their values must be sent as unquoted enum literals in a mutation (`stage: SCREENING`), but `handleQueryParams` quoted every string value (`stage: "SCREENING"`), which produces an invalid mutation. So even surfacing the fields would not have been enough to create/update records. ## Changes - Surface `SELECT` (string) and `MULTI_SELECT` (string list) as input fields in `computeInputFields`. - Fetch field `options` in the metadata query and expose them as Zapier `choices` (`value -> label`) so users pick from the real options instead of typing raw enum values. - Emit enum field values **unquoted** in create/update mutations. This is derived from the object schema in `crud_record` and threaded into `handleQueryParams`. This also fixes `RATING`, which was silently broken for the same reason. ## Tests - `computeInputFields`: new test asserting `SELECT`/`MULTI_SELECT` produce the right fields with `choices` and `list`. - `handleQueryParams`: new test asserting enum values are emitted unquoted (scalar and array). ``` Test Suites: 2 passed Tests: 5 passed ``` <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21509?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. --> |
||
|
|
9bb98fa5b5 |
fix(billing): don't crash when workspace has no active subscription (#21510)
## Problem
Sentry (high severity, SLA-breaching): `Billing Subscription Not Found:
No active subscription found for workspace …`
The `billingSubscription` workspace-cache provider
(`WorkspaceBillingSubscriptionCacheService.computeForCache`) called
`getCurrentBillingSubscriptionOrThrow`. For a workspace whose
subscription is fully canceled, `getCurrentBillingSubscription` filters
out `Canceled` and returns `undefined`, so the provider **threw**
`BILLING_SUBSCRIPTION_NOT_FOUND`.
That cache key is read on every usage-recording path:
- workflow execution
(`WorkflowExecutorWorkspaceService.sendWorkflowNodeRunEvent`)
- AI usage (`AiBillingService`)
- logic-function execution (`LogicFunctionExecutorService`)
- app charges (`AppBillingService`)
- the gate `BillingUsageService.canFeatureBeUsed` /
`hasAvailableCredits` / `decrementAvailableCreditsInCache`
- the cancellation webhook
(`invalidateAndRecompute('billingSubscription')`)
So any of these throws an unhandled exception for a
no-active-subscription workspace. The intent was clearly to tolerate
this state — `canFeatureBeUsed` already guards with
`isDefined(billingSubscription)` and the workflow runner logs *"there is
no subscription for this workspace"* — but the throwing provider made
those guards unreachable.
## Fix
- `computeForCache` now returns `FlatBillingSubscription | null` via the
non-throwing `getCurrentBillingSubscription`, and the cache type allows
`null`.
- Every consumer guards the absent case (`isDefined` / optional
chaining) and no-ops: usage events still emit with an undefined
`periodStart`, credits aren't decremented, `hasAvailableCredits` returns
`false`.
- `getCurrentBillingSubscriptionOrThrow` is **left untouched** for the
many callers (resolver, subscription-update, etc.) that genuinely
require a subscription.
## Test
Adds `workspace-billing-subscription-cache.service.spec.ts`: the
provider returns `null` when there's no active subscription (regression)
and the flattened subscription when one exists.
All 142 tests across the billing / ai-billing / workflow-executor suites
pass; `oxlint --type-aware` and `oxfmt` are clean.
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21510?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. -->
|
||
|
|
adb60a3867 |
fix(sdk): avoid shell command injection in CLI exec calls (#21508)
## What Fixes the 5 open [CodeQL code-scanning alerts](https://github.com/twentyhq/twenty/security/code-scanning) of type `js/shell-command-constructed-from-input` (medium severity) in the `twenty-sdk` CLI. All flagged call sites built a shell command string by interpolating library inputs (`containerName`, `image`, `npmTag`) and ran it via `execSync`, which executes through a shell. A value containing shell metacharacters could break out of the intended command. ## Changes - `packages/twenty-sdk/src/cli/utilities/server/docker-container.ts` — every `docker` call switched from `execSync` with a template string to `execFileSync('docker', [...args])`. Arguments are passed as an array, so the binary runs directly without a shell and inputs are never reparsed. The single quotes that were shell-quoting the `-f` format strings are removed since there is no shell. - `packages/twenty-sdk/src/cli/operations/publish.ts` — `npm publish` switched to `execFileSync` with `--tag`/value as separate array elements. On Windows the binary resolves to `npm.cmd` (no shell to do the lookup). ## Verification - `npx nx typecheck twenty-sdk` passed - `npx nx lint twenty-sdk` passed <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21508?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. --> |
||
|
|
d22fa377e7 |
fix(front): store auth tokenPair in localStorage instead of a cookie (#21507)
## Problem A client hit an AWS S3 `RequestHeaderSectionTooLarge` error (`MaxSizeAllowed 8192`) when opening a `https://<workspace>.twenty.com/verify?loginToken=<JWT>` link — the request to load the `/verify` SPA page is served from S3, which rejects it before the app loads. The dominant cause is the **`tokenPair` cookie**. The auth tokenPair (access + refresh JWTs, ~2–5KB) was persisted in a host-scoped, JS-readable cookie. Nothing server-side ever reads it — the access token is sent to the API via an `Authorization: Bearer` header set in the Apollo auth link (`ExtractJwt.fromAuthHeaderAsBearerToken()` on the backend; no `cookie-parser`). Yet the browser attached that cookie to **every** request to the origin, including static assets and the `/verify` page. Combined with the `loginToken` in the URL, the request header section exceeds S3's 8192-byte limit. ## Fix Move `tokenPair` from cookie storage to **localStorage**, which is never transmitted in request headers. - `tokenPairState` now uses `useLocalStorage` (with `getOnInit: true`). - `getTokenPair` (the synchronous read used by the Apollo auth link) reads from localStorage under the same key. - A one-time migration (`migrateTokenPairCookieToLocalStorage`) runs before React renders: it ports any existing `tokenPair` cookie into localStorage and **deletes the cookie**, so already-authenticated users aren't logged out and the oversized cookie stops being sent. ## Why this is safe **Behavior:** equivalent. The cookie was host-scoped (no `domain` attribute), so it never provided cross-subdomain sharing — cross-workspace auth already re-establishes the token per-origin via the `loginToken`-in-URL → `/verify` handoff. localStorage has identical origin scoping. **Security:** neutral-to-positive. - No XSS protection lost — the cookie was **not** `httpOnly` (it can't be; JS reads it to build the Bearer header), so it was already XSS-exposed exactly like localStorage. - No CSRF surface change — the token was never sent as a cookie credential (no `credentials: 'include'`). - **Reduced exposure** — the token no longer leaks into CDN/proxy/server access logs or request headers, which is the actual bug. - Server-side revocation (`revokedAt`) and the 60-day refresh-token JWT expiry govern validity, so localStorage's lack of auto-expiry is moot. ## Testing - `getTokenPair` unit tests updated to localStorage. - New unit tests for the migration util (port, no-op, no-clobber, error-safety). - `nx test twenty-front` auth + apollo suites: 125 passing. - `lint:diff-with-main` clean; changed files typecheck clean. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21507?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. --> |
||
|
|
84a8504473 |
Add Enter HotKey to aAuth authroize screen (#21512)
as title <img width="1263" height="834" alt="image" src="https://github.com/user-attachments/assets/73dbfca2-8c7f-469d-8273-6adec5472abd" /> <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21512?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. --> |
||
|
|
e269b51f40 |
Fix: Improve UX by explaining email edit restriction for multi-workspace users (#18750)
### Fix: inform users why email edit is disabled for multi-workspace accounts (#18733) #### Issue Users were unable to change their email even when: - Email field is enabled in Security settings - User has permission to edit profile details This occurs when the user belongs to **multiple workspaces**, but there was no feedback explaining why the edit action was disabled, causing confusion. #### Solution - Added a tooltip/popup on hover over the email edit (pen) icon - Tooltip informs users that email cannot be changed if they belong to **2 or more workspaces** #### Result - Provides clear feedback to users - Reduces confusion around email edit restrictions - Improves overall user experience #### Notes - No changes to permission or backend logic - Purely a UI/UX improvement Fixes twentyhq/core-team-issues#2335 --------- Co-authored-by: Charles Bochet <charles@twenty.com> |
||
|
|
247e422eac |
fix(front): prevent timeline "Invalid configuration" on update events without a diff (#21460)
## Fixes #20597 ### Problem A person's (or any record's) timeline renders the whole widget as **"Invalid configuration"** when it contains an `*.updated` event without a usable `properties.diff`. The error-boundary fallback (`PageLayoutWidgetInvalidConfigDisplay`) is triggered because `EventRowMainObjectUpdated` **throws** during render: ```ts const diff = event.properties?.diff; // can be undefined const diffEntries = Object.entries(diff); // throws TypeError when undefined if (diffEntries.length === 0) { throw new Error('Cannot render update description without changes'); } ``` `filterOutInvalidTimelineActivities` only validates activities that **already carry** a diff (`canSkipValidation = !diff`), so a main-object `*.updated` event with a missing diff passes straight through to this renderer and crashes it. A single malformed row takes down the entire timeline. ### Fix Render nothing instead of throwing when an update event has no changes to show. This mirrors the sibling `EventRowMainObject` default branch (which returns `null`) and the filter's own behaviour of dropping empty diffs, and keeps one bad row from crashing the whole widget. The fix is intentionally kept in the renderer rather than the filter: the filter cannot distinguish a diff-less main-object update (must be dropped) from a diff-less `linked-task`/`linked-note` update (legitimately has `properties: {}` and renders fine via `EventRowActivity`) without duplicating routing logic. ### Test Added `EventRowMainObjectUpdated.test.tsx` — a regression test asserting the component renders nothing (no throw) for both a missing-diff and an empty-diff update event. |
||
|
|
577b22df46 |
fix(upgrade): invalidate upgrade-status cache on command end (#21497)
## Problem The "Twenty / Upgrade Status" Grafana dashboard shows stale workspace counts (e.g. `N behind / 0 up-to-date` while the instance reads `UP_TO_DATE`) that disagree with `command:prod upgrade:status`. The CLI is correct; the dashboard lags, sometimes for the full hour. ## Root cause The dashboard is fed by the `twenty_upgrade_workspaces_*` gauges, which read their workspace counts from a Redis snapshot (`UpgradeStatusCacheService`). That snapshot is only invalidated **per-command, inside the runners' `finally` blocks**. Two gaps: 1. An instance command that is already applied returns **before** its invalidation runs (`isAlreadyCompleted` early-return in `InstanceCommandRunnerService`). So a plain **redeploy** — which changes the deployed upgrade sequence, and thus the "behind" answer, without executing any command — never refreshes the snapshot. This is most visible on an instance-only release. 2. The snapshot then stays frozen until its 60-minute TTL, while the CLI reads live and disagrees. "Behind" is derived from the deployed sequence, not just the ledger, so the correct answer changes on events (deploys) that run no command — which is exactly why per-command invalidation isn't enough on its own. ## Fix Invalidate the upgrade-status cache **once, unconditionally, at the end of both upgrade entrypoints** — `run-instance-commands` (the deploy/migrate step) and `upgrade` — in a `finally`. Every run, including a no-op redeploy where all commands are already applied, now clears the snapshot, so the next gauge scrape recomputes against the current sequence. Best-effort (failures are logged, never block the command). The existing per-command invalidation is kept for mid-run progress. This keeps the read path untouched. ## Reproduction + verification (live, local) Served twenty-server (`NODE_PORT=4000`, `METER_DRIVER=prometheus`) against the seeded DB, whose latest version `2.12.0` is instance-only. 1. Froze the gauge at `behind 4 / up_to_date 0` while the DB was brought up-to-date (snapshot not invalidated) — reproduced the dashboard/CLI divergence. 2. Ran the **patched** `run-instance-commands --force`. Every step logged `already executed, skipping` — and the `finally` still deleted the Redis snapshot. 3. On the next recompute the gauge self-healed to `instance_health 1, behind 0, up_to_date 4`, matching the live CLI. With the old code the snapshot stayed frozen at `behind 4` until the TTL. |
||
|
|
9884769ce4 |
fix(ci): bump claude-code-action to v1.0.146 (CVE-2026-47751) (#21499)
## What Bumps both \`anthropics/claude-code-action\` pins in \`.github/workflows/claude.yml\` from the floating \`v1\` SHA (\`dde2242\`) to \`v1.0.146\` (\`ac7e24b\`). ## Why The old pin predates the fix for **CVE-2026-47751** (Tenable TRA-2026-27, CVSSv4 5.3): claude-code-action checks out the PR head branch and unconditionally sets \`enableAllProjectMcpServers: true\`, so an attacker could ship a malicious \`.mcp.json\` in a PR branch and get arbitrary command execution in the runner — with access to workflow secrets — once a privileged user triggers the action. Fixed upstream in **claude-code-action 1.0.78** (released 2026-03-24). This pins to the current release, v1.0.146. ## Notes - \`claude.yml\` is the only workflow in the repo referencing the action; both job invocations (\`claude\` and \`claude-cross-repo\`) were updated. - All existing \`with:\` inputs remain valid in v1.0.146 — no deprecated args. Ref: https://www.tenable.com/security/research/tra-2026-27 <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21499?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. --> |
||
|
|
ba94c3b857 |
feat(workflow): idempotent stop + retry failed runs from failing step (#21458)
https://github.com/user-attachments/assets/5a25396f-8959-4bd8-93cb-1187559ffe5f ## Summary Two workflow-run improvements, with all non-trivial logic isolated in pure, unit-tested utils. ### 1. Idempotent stop `stopWorkflowRun` no longer throws when a run is already in a terminal status (`COMPLETED` / `FAILED` / `STOPPED`) or already `STOPPING`; it returns the run unchanged. This fixes: - bulk stop aborting on the first non-stoppable run in a mixed/select-all selection, - the click-vs-processing race on a single run (run finishes between click and mutation). It also releases the cached not-started throttle slot when stopping a `NOT_STARTED` run (prevents counter drift), and ends runs with no `state` directly. ### 2. Retry a failed run from the failing step New `retryWorkflowRun` mutation (same guards/passthrough as `stopWorkflowRun`). It resets the failed step(s) to `NOT_STARTED`, flips the run to `RUNNING`, and enqueues a `RunWorkflowJob` with the steps to re-execute; downstream execution and status computation are unchanged. Logic lives in pure utils: - `build-retry-step-infos.util.ts` - decides per failed step what to reset; delegates iterator-specific logic to `build-retry-iterator-step-infos.util.ts` (an iterator that failed mid-loop is restored to `RUNNING` with cursor preserved, an iterator that failed itself restarts its whole loop). - `get-runnable-step-ids.util.ts` - reuses the executor's `shouldExecuteStep` to also resume branches that never started (avoids hangs), excluding loop-interior steps. The service method only orchestrates; the job's status check is a race guard (retriability is enforced in the service before enqueue). A "Retry" command menu item surfaces only for `FAILED` runs (`someEquals(selectedRecords, "status", "FAILED")`). ### 3. Keep the run diagram visible across regenerations The run diagram is regenerated on every run state change, producing fresh nodes without the dimensions Reactflow had measured. Reactflow hides unmeasured nodes until it re-measures them, so the diagram could flicker and disappear when the last regeneration before going idle left nodes unmeasured (reproducible after retrying a failed run). The regenerated nodes now carry over the previously measured dimensions (by id) so they stay rendered. ## Test plan - [x] Unit tests for both retry utils (9 cases: plain failed step, non-failed untouched, iterator mid-loop restore, iterator self-failure, frontier parent gating, entry steps, loop-interior exclusion, parallel branches) - [x] `twenty-server` + `twenty-front` typecheck - [x] `lint:diff-with-main` clean for both packages - [x] Manual: retry a failed run repeatedly and confirm the diagram stays visible - [ ] Manual: stop a COMPLETED/mixed selection (no error), retry a failed run and confirm it resumes from the failing step |
||
|
|
79f7c96939 |
Move Claude workflow concurrency to job level so PR chatter can't cancel queued runs (#21498)
## Problem A second `@claude` request on a PR while a Claude run is already in progress gets silently cancelled. Concrete case on #21443: [this comment](https://github.com/twentyhq/twenty/pull/21443#discussion_r3404037004) ("@claude please investigate and report", 14:24:28) produced [a run](https://github.com/twentyhq/twenty/actions/runs/27421867558) that was cancelled 3 seconds after creation, while [the run for an earlier comment](https://github.com/twentyhq/twenty/actions/runs/27421856742) was still in progress. Claude never responded. ## Root cause The `concurrency` block is declared at the **workflow** level, keyed on the PR number. Two GitHub Actions behaviors combine badly here: 1. A concurrency group holds at most one running + **one pending** run; every new run entering the group cancels the previously pending one (`cancel-in-progress: false` only protects the *running* run). 2. Workflow-level concurrency is acquired **before** job-level `if` conditions are evaluated — so every `issue_comment` / `pull_request_review_comment` / `pull_request_review` event on the PR enters the group, even ones with no `@claude` that end up skipped. So while a Claude run is in progress, any PR activity evicts the queued `@claude` run. It's even self-defeating: a review-thread reply fires *two* webhook events (`pull_request_review_comment` + a companion `pull_request_review` with an empty body), so the companion event cancels the queued comment run ~2s later. Claude's own "finished" reply also fires events that kill whatever is queued. ## Fix Move concurrency to the **job** level. A job whose `if` evaluates false is skipped before it ever requests the concurrency slot, so only genuine `@claude` jobs enter the queue. Real Claude runs on the same PR are still serialized (no parallel pushes to the same branch). Also gives `claude-cross-repo` its own group keyed on source repo + issue number — previously a cross-repo dispatch for issue N shared a group with PR N in this repo and they could needlessly queue behind each other. ## Remaining limitation GitHub keeps only one pending job per group: posting three `@claude` requests while the first is still running will still cancel the second when the third arrives. Zero-loss queueing would require a unique group per comment, which would allow concurrent runs pushing to the same PR branch — not worth it. 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21498?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. --> |
||
|
|
b36c0c51c3 |
fix(server): keep workflow command menu item label in sync with workflow name (#21490)
## Summary Fixes #20766 — manual-trigger workflows showed `Manual Trigger` in the command menu instead of the workflow's name. Root cause (confirmed against a live instance): the command menu item's `label` is written **only at activation** in `createOrUpdateCommandMenuItem`, from `workflow.name`, with a hardcoded `'Manual Trigger'` fallback. So: - a workflow activated while unnamed gets the misleading `Manual Trigger` label, and - renaming the workflow afterwards never updates the label (`workflow.updateOne` had no label-related hook). Changes: - Add `getWorkflowCommandMenuItemLabel` helper and use it in activation; the empty-name fallback is now `Untitled Workflow` (consistent with the rest of the UI) instead of `Manual Trigger`. - Add `WorkflowCommandMenuSyncWorkspaceService` that updates the active version's command menu item label/shortLabel from the workflow name (idempotent, no-op for non-manual / inactive workflows). - Add `workflow.updateOne` and `workflow.updateMany` post-query hooks that call the sync service, registered in `WorkflowQueryHookModule`. Out of scope (separate follow-up): the activation create path can produce duplicate command items for one `workflowVersionId`; recommend making it idempotent / adding a unique constraint. ## Test plan - [x] `oxlint --type-aware` + `oxfmt` clean on changed files - [x] Editor TS diagnostics clean (full `nx typecheck` was starved by local dev servers) - [ ] New integration test `workflow-command-menu-label.integration-spec.ts`: - labels the command menu item with the workflow name on activation - updates the label when the workflow is renamed - falls back to `Untitled Workflow` when the name is cleared <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21490?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. --> |
||
|
|
a8a8bbb2ed |
feat(workflow): add offset to Find Records node for pagination (#21484)
<img width="471" height="362" alt="Capture d’écran 2026-06-12 à 15 38 45" src="https://github.com/user-attachments/assets/9656d3a6-6f56-4587-add6-55c0a0a32482" /> ## Summary The workflow Find Records (search) node previously exposed only `objectName`, `filter`, `sort`, and `limit` (capped at `QUERY_MAX_RECORDS` = 200), with no way to page beyond the first page of results. This adds an optional **Offset** to the node so a workflow can fetch an arbitrary page (`offset = pageIndex * limit`) while keeping the same filter and sort. The underlying `FindRecordsService` already accepts `offset` (it forwards it to the query runner's `skip`, and stabilizes ordering with an `id` tiebreaker), so this change just threads `offset` through the remaining layers: - `workflowFindRecordsActionSettingsSchema` (shared zod schema) — new optional `offset` - `FindRecordsInput` type — new optional `offset?: number` - `find-records.workflow-action.ts` — forwards `offset` to `FindRecordsService.execute` - `WorkflowEditActionFindRecords.tsx` — new "Offset" number input (non-negative, defaults to 0) with form state + persistence - Default `FIND_RECORDS` step settings — `offset: 0` ### Notes / non-goals - Offset-only, single page: the node returns one page. Looping over all pages inside one run is not included (the Iterator action loops a static array and cannot re-query). The node output already returns `totalCount`, so a workflow can compute total pages as `ceil(totalCount / limit)`. - Offset on very large/changing datasets can be slow or skip/duplicate rows; cursor/keyset pagination would be a future follow-up. ## Test plan - [x] Create a Find Records node, set Limit=50, Offset=0 → returns first page - [x] Set Offset=50 with the same filter/sort → returns the second page (no overlap) - [x] Negative offset shows a validation error and is not saved - [x] Existing Find Records nodes (no offset stored) still run, defaulting to offset 0 - [x] Typecheck/lint pass in CI <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21484?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. --> |
||
|
|
49026a7368 |
chore: bump version to 2.13.0 (#21492)
## Summary - Moves current version to previous versions array - Sets TWENTY_CURRENT_VERSION to the new version - Updates TWENTY_NEXT_VERSIONS with the next minor version - Bumps twenty-client-sdk, twenty-sdk, and create-twenty-app to the same version ## Checklist - [ ] Verify version constants are correct - [ ] Verify npm package versions match <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21492?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> Co-authored-by: Github Action Deploy <github-action-deploy@twenty.com> |
||
|
|
1934fcc261 |
Add last contact twenty app (#21464)
Adds a last contact at column in people object - backfill at installation - update last contact when receiving an email or a calendar event - cron to update last contact with recently passed calendar event @Bonapara can you check the app logo? <img width="909" height="464" alt="image" src="https://github.com/user-attachments/assets/ca1c01a5-9838-4cf0-b0b8-d66a7f88b5fc" /> --------- Co-authored-by: Thomas des Francs <tdesfrancs@gmail.com> Co-authored-by: Raphaël Bosi <71827178+bosiraphael@users.noreply.github.com> |
||
|
|
bd4161a905 |
Show app logo on workflow logic function nodes (#21482)
Workflow `LOGIC_FUNCTION` action nodes (functions provided by an installed app) previously rendered a generic ƒ icon in the diagram. They now display the owning app's logo instead. The node's logic function id is resolved to its `applicationId` and rendered via the existing `AppChip`. When no app can be resolved (e.g. the logic function isn't loaded yet, or has no application), it falls back to the original ƒ icon, so nodes never look broken. Inline `CODE` actions are unchanged. ## Before <img width="692" height="670" alt="CleanShot 2026-06-12 at 15 27 22@2x" src="https://github.com/user-attachments/assets/4d7c17ce-dbe3-45e6-9d44-41e363b2e4a5" /> ## After <img width="592" height="628" alt="CleanShot 2026-06-12 at 15 26 22@2x" src="https://github.com/user-attachments/assets/d55c308f-3cde-4153-8d5d-ec948bebc823" /> <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21482?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. --> |
||
|
|
c4453923f0 |
Update CI: Argos visual regression for twenty-front storybook (#21454)
## What Adds Argos visual regression for `twenty-front`, reusing the storybook CI already builds and the existing sharded test matrix. Stories in the `modules` and `pages` scopes are captured as PNGs during `front-sb-test`, merged into one artifact, and pixel-diffed against `main` on the self-hosted Argos with results posted as a PR comment — same pipeline as `twenty-ui` (#21210 / #21262). ## How - **Capture**: `@argos-ci/storybook` vitest plugin, same setup as `twenty-ui`. Skipped for `performance` stories (nondeterministic profiling reports). Freezes framer-motion to avoid flaky diffs (#21412). - **Sharding**: each modules/pages shard uploads a partial artifact; a new `front-sb-screenshots` job merges them into `argos-screenshots-twenty-front` (`overwrite: true` so re-runs work). - **Baselines**: `CI Front` now runs on `push: main` — Argos resolves base builds by exact merge-base commit, so every main commit needs a build (#21217/#21222 pattern). Main pushes get a per-SHA concurrency group so back-to-back merges can't cancel queued runs and leave baseline gaps; the `performance` scope is dropped on push. - **Dispatch**: `visual-regression-dispatch.yaml` watches `CI Front` → `project=twenty-front`. ## Rollout - ✅ Prod Argos project `twenty-front` created (id 68) + `ARGOS_TOKEN_FRONT` secret set - ⬜ Merge the twentyhq/ci-privileged companion PR **before** this one - First PR builds show as *orphan* until the first main push creates a baseline (expected, same as the twenty-ui rollout) |
||
|
|
ab7c760872 |
i18n - docs translations (#21493)
Created by Github action <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21493?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> Co-authored-by: github-actions <github-actions@twenty.com> |
||
|
|
e3cfbbffb5 |
Bill People Data Labs enrichments in Twenty credits (#21481)
Adds per-enrichment billing to the People Data Labs app. Each **matched** record charges the workspace in Twenty credits via `chargeCredits` (`twenty-sdk/billing`), following the same pattern as the exa app. - Person match: **336,000 micro-credits** ($0.336 — PDL list price $0.28 + 20% margin) - Company match: **120,000 micro-credits** ($0.12 — PDL list price $0.10 + 20% margin) > **Note:** the 20% margin is a first draft, not final — it's a single constant (`src/constants/billing-margin-multiplier.ts`) and easy to adjust once we settle on pricing. PDL only consumes a credit on a successful match, so `not_found`, errors, and skipped records are free. The charge is emitted once per PDL batch call (≤100 records) with `operationType: CODE_EXECUTION`, `quantity` = match count, and `resourceContext` `pdl/person` / `pdl/company`, at the moment PDL responds — a match whose record write later fails is still billed since the PDL cost was already incurred. Billing failures are non-fatal and never break an enrichment. Prices and margin live as constants in `src/constants/` for easy retuning. No SDK bump needed (`twenty-sdk@2.10.1` already ships `./billing`). |
||
|
|
fa9aeea408 |
Add dev:generate-client command to sdk (#21489)
## after `yarn twenty dev:generate-client` <img width="1149" height="246" alt="image" src="https://github.com/user-attachments/assets/1edcba03-2647-4bc8-8188-7ad69362ac52" /> <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21489?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. -->twenty/v2.12.0 sdk/v2.12.0 |
||
|
|
56e1d886d8 |
Add CI workflow for people data labs app (#21487)
Adds a dedicated CI workflow for the People Data Labs app that runs lint, typecheck, and unit tests only when files under `packages/twenty-apps/internal/people-data-labs/` change. The app is a standalone yarn project (its own lockfile, pinned published SDKs), so it isn't covered by the existing package CI workflows. The workflow gates on the app folder via the reusable `changed-files.yaml`, installs dependencies inside the app directory, and runs `yarn lint`, `yarn typecheck`, and `yarn test`. A status-check job aggregates results and stays green on skips, so it's safe to mark as a required check. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21487?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. --> |
||
|
|
7b6b624041 |
fix(server): bypass stale workspace cache when resolving currentUser during onboarding (#21461)
## Context On twenty-main (multi-replica), signing up and naming a workspace lands on a black screen at `/create/profile`; a manual refresh fixes it. From the network trace: the post-activation `currentUser` response carries `onboardingStatus: PROFILE_CREATION` (fresh) together with a non-ACTIVE `currentWorkspace` and `workspaceMember: null` (stale). ## Root cause `activateWorkspace` invalidates the core entity cache only on the instance that served the mutation. When the follow-up `currentUser` query is routed to a sibling instance, the auth context carries a memoized pre-activation workspace snapshot. A stale transient workspace cascades: - `workspaceMember`/`workspaceMembers` resolve to null/empty (`loadWorkspaceMember` skips non-active workspaces), permissions fall back to defaults - the client's metadata store never loads (`MinimalMetadataLoadEffect` skips non-active workspaces), so `MinimalMetadataGater` shows the loading skeleton forever on `/create/profile` #20322 fixed the same staleness for `onboardingStatus` by reading the workspace fresh from the database in the resolver — which is why the status is fresh while the workspace object isn't, and why the client navigates to a page it can't render. #21480 bounds the staleness window to the designed 10s (absolute memoizer TTL), but signup lives entirely inside that window: the client reads `currentUser` ~1s after `activateWorkspace` and never refetches while stuck. ## Fix Apply the #20322 approach at the workspace status resolution layer: `UserService.refreshWorkspaceIfPendingOrOngoingCreation` re-reads the workspace from the database when the auth-context copy is in a transient activation status (`PENDING_CREATION`/`ONGOING_CREATION`). Used in: - `UserResolver.currentUser` — fresh `currentWorkspace` and permissions - `UserService.loadWorkspaceMember` / `loadWorkspaceMembers` — covers the `workspaceMember`/`workspaceMembers` resolve fields No-op for active workspaces; the extra database read only happens for workspaces mid-creation. ## Test plan - Full `user.service.spec.ts` suite passes; lint and format clean. - After deploy to twenty-main: sign up, name the workspace, verify `/create/profile` renders the profile form with a populated `workspaceMember` and ACTIVE `currentWorkspace` without refreshing. |
||
|
|
cb653e4ecc |
feat: inline image thumbnails and legacy-label fallback for FILES field chips (#21294)
## Summary Custom FILES field chips now show an inline image thumbnail for image attachments and fall back to the legacy label when an attachment predates filename storage. This covers two of the UX complaints n2ojim collected in #20942: image files were indistinguishable from other attachments, and older attachments rendered with an empty chip label. The 10-file cap from the same issue already shipped in #20950; the gallery/grid layout and hover-delete affordances are deliberately left for follow-ups per the maintainer's cost notes on the thread. ## Why this matters #20942 is founder-tagged UX feedback on the new custom FILES field: once a record carries more than a couple of attachments, users scan chips visually, and a thumbnail answers "which one is the screenshot" without opening anything. The fallback keeps old records readable instead of showing blank chips. Changes stay inside `FileChip.tsx` and follow the existing file-display patterns; Storybook stories cover both behaviors. ## Testing Added 9 Storybook stories: image attachment (thumbnail), non-image (icon unchanged), missing filename (legacy fallback label), long names, and combinations. Targeted typecheck of the changed files surfaced no errors; the monorepo's CI lint/build covers the rest. Refs #20942 --------- Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Co-authored-by: Etienne <45695613+etiennejouan@users.noreply.github.com> |
||
|
|
214dc70b67 |
Fix missing WasIntroducedInUpgrade for overridable view entity (#21483)
<!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21483?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. --> |
||
|
|
08a36aa68f |
fix(server): restore absolute TTL in PromiseMemoizer (#21480)
## Context `PromiseMemoizer` sits in front of the staged lookup (local cache → Redis hash validation → Redis data → DB recompute) of both `CoreEntityCacheService` and `WorkspaceCacheService` (10s TTL each). The Redis hash check is the **only** cross-instance invalidation mechanism — there is no pub/sub — and it runs only when the memo entry expires. The TTL is currently **sliding**: every read refreshes `lastUsed`, and eviction compares against time-since-last-read. So any entry read more often than every 10s on a given instance never revalidates, and that instance serves stale data for as long as traffic continues. Affected data: auth-context entities (workspace, user, userWorkspace), API key revocations, role/permission maps, RLS predicates, feature flags, and all metadata maps. Observed manifestation: after `activateWorkspace`, a sibling instance kept serving a `PENDING_CREATION` workspace snapshot (kept alive indefinitely by the client's own polling), stranding signup on a permanent loading skeleton at `/create/profile` (#21461). Same staleness class as #20322 and the CI flakes investigated in #21435. ## Why it was sliding #11444 (April 2025) deliberately changed the TTL from absolute to sliding because the memoizer's then-consumer was the TypeORM datasource storage: absolute expiry was destroying datasources that were actively in use (`onDelete` → `destroy()`), causing worker `Connection terminated` errors. That consumer no longer exists — datasources moved to `GlobalWorkspaceOrmManager`, and neither remaining consumer passes `onDelete` or holds resources needing keep-alive. ## Fix Restore absolute expiry: `expiresAt` is set at write time and never refreshed on read. Every instance now re-enters the staged lookup (and thus the Redis hash validation) at least once per TTL, restoring the designed ≤10s cross-instance staleness ceiling. Concurrent dedup (`pending` map) and `onDelete` plumbing are unchanged. ## Test plan - New regression test: reads at half-TTL intervals must not extend an entry's lifetime (fails on the sliding implementation, passes now). - Full `promise-memoizer.storage.spec.ts` and `workspace-cache.service.spec.ts` suites pass (25 tests); lint and format clean. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21480?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. --> |
||
|
|
e334551da9 |
(Fix) Upsert no longer rewrites position on existing records (#21375)
## Fix: upsert no longer rewrites `position` on existing records ### Problem `createX(..., upsert: true)` resets the `position` of records that resolve to an **update**, even when the payload doesn't include a `position`. The create-many/upsert runner backfills `position` (to `"first"`) in `computeArgs` over the **whole batch**, before records are split into insert vs update. So existing rows get a freshly recomputed `position` written on every upsert. For callers that re-upsert their full dataset on a schedule (e.g. a daily sync), this rewrites `position` for every record on each run and drifts the values steadily negative — and it floods audit/event logs with position churn. The dedicated `updateOne`/`updateMany` runners already pass `shouldBackfillPositionIfUndefined: false`; the upsert path did not. ### Fix Only backfill `position` for records that are actually inserted: - `computeArgs` now passes `shouldBackfillPositionIfUndefined: !args.upsert` in both the create-many and create-one runners, so undefined positions are left untouched on upsert. - `performUpsertOperation` backfills `"first"` positions for `recordsToInsert` only, **after** categorization, via `RecordPositionService`. Explicit `position` values (`"first"`, `"last"`, or a number) in the payload are still honored. Plain (non-upsert) create behavior is unchanged. ### Behavior | Scenario | Before | After | |---|---|---| | Upsert updates existing row, no `position` sent | `position` rewritten | `position` untouched | | Upsert inserts new row, no `position` sent | gets `"first"` | gets `"first"` (unchanged) | | Explicit `position` on upsert | applied | applied | | Plain create | unchanged | unchanged | |
||
|
|
a0e3c43234 |
People data labs app: remove navigation menu items (#21478)
Removes the People Data Labs app navigation menu — the "People Data Labs" folder and its two view entries (Enriched People, Enriched Companies) — from the sidebar, along with the now-unused navigation menu item identifiers. The "Enriched (PDL)" view definitions are kept and remain available on the Person and Company objects; they just no longer appear as a folder in the navigation menu. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21478?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. --> |
||
|
|
fefd9d7704 |
feat(workflow) - Add validation layer (#21422)
Add workflow validation framework and consolidate output schema types/search logic into twenty-shared This PR introduces a comprehensive workflow validation system that catches configuration errors at build-time, and consolidates the fragmented output-schema type definitions and variable-search logic from the front-end into twenty-shared **Workflow validation** — A new system that checks workflows for errors before activation: graph connectivity (unreachable steps, dangling references), step parameter schemas (via Zod), variable references (typos, wrong step order), and workspace metadata (non-existent objects). Returns structured errors/warnings with "did you mean?" suggestions. Runs automatically after create_complete_workflow and update_workflow_version_step, and is also available as a standalone validate_workflow tool. **Output schema consolidation** — Moves all output schema types and the variable-search logic from scattered front-end files into twenty-shared, replacing ~800 lines of duplicated per-schema-type code with a single unified searchVariableInOutputSchema dispatcher. To do : - validation on CODE and AGENT step --------- Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
2538239e05 |
People data labs: update app logo (#21479)
Replaces the People Data Labs internal app icon with the new logo. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21479?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. --> |
||
|
|
adbd78767e |
feat(partners): lock admin-managed + ownership fields on Partner role (#21471)
## What Tightens the **Partner** self-service role's field-level permissions so a partner can edit its own profile but not admin/ops-controlled or ownership fields. All locks are `canUpdateFieldValue: false` on the Partner object. **Admin-managed scalar fields (7):** `slug`, `validationStage`, `reviewed`, `ranking`, `partnerTier`, `applicationNotes`, `lastMatchAt` **Ownership relation FKs (2):** `partnerUser`, `company` ## Why - The 7 scalar fields are admin/ops-controlled (validation, ranking, tiering, internal notes) — a partner must not be able to self-promote or alter ops data. - `partnerUser` is the **RLS pivot**: the row-level predicate scopes a partner to records where `partnerUser IS <their workspace member>`. If a partner could clear or repoint it, they'd drop their own record out of scope (an orphan only admins can see). It is already locked on Opportunity; this brings Partner in line. - `company` is read-only at the object level for partners, so its FK link must not be repointable from the Partner side either. The remaining Partner relations (`opportunities`, `persons`, `partnerContents`) need no lock — they are already protected by inverse-side field locks or object-level read-only / no-access rules. ## Scope - One source file: `src/roles/partner.role.ts` (9 field-permission entries). - No schema changes — additive permission tightening; upgrades cleanly via `deploy` + `install`. - Version: patch bump `0.5.0 → 0.5.1`. |
||
|
|
e6d730cd75 |
chore: sync AI model catalog from models.dev (#21476)
Automated daily sync of `ai-providers.json` from [models.dev](https://models.dev). This PR updates pricing, context windows, and model availability based on the latest data. New models meeting inclusion criteria (tool calling, pricing data, context limits) are added automatically. Deprecated models are detected based on cost-efficiency within the same model family. **Please review before merging** — verify no critical models were incorrectly deprecated. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21476?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> Co-authored-by: FelixMalfait <6399865+FelixMalfait@users.noreply.github.com> |
||
|
|
69a7e614ff |
fix: restore isCustom gate in metadata label resolvers (#21432)
## Context #21228 removed the stored `isCustom` column and, with it, the `isCustom` early-return in `resolveObjectMetadataStandardOverride` / `resolveFieldMetadataStandardOverride`, on the assumption that falling through the `standardOverrides` checks was equivalent. It isn't: custom object/field labels now reach the Lingui lookup. A custom label that collides with a standard catalog string (e.g. a custom field labeled "Status") gets translated for non-English locales against the user's intent, and every other custom label pays a hash + catalog miss — and, in production, an "Uncompiled message detected" warning (#21415) — on each metadata resolution. ## Fix Restore the gate. `isCustom` is no longer stored, so call sites that build the resolver input from flat entities (dataloader, minimal-metadata, view controller, command-menu-item navigation context) derive it via `belongsToTwentyStandardApp`; GraphQL resolvers keep passing DTOs, which already carry the derived value. ## Testing - Unit tests for both resolvers, including a new regression test: a custom label matching a standard catalog entry is returned verbatim, Lingui never called. --------- Co-authored-by: Weiko <corentin@twenty.com> |
||
|
|
cfb9772179 |
feat(server): convert view to overridable entity (#21436)
## Context Every entity created as a side effect of object creation must support the overridable pattern (`isActive` + `overrides` + override routing) before we can re-own side effects to their true application. Starting with View. viewField, viewFieldGroup, pageLayoutTab and pageLayoutWidget already extend `OverridableEntity`. This PR brings `view` to the same pattern. ## What this does - `ViewEntity` now extends `OverridableEntity<ViewOverrides>` (adds `isActive` boolean + `overrides` jsonb). All editable view properties are overridable; the 3 fieldMetadata foreign keys are converted to/from universal identifiers like viewField's `viewFieldGroupId`. - **Update**: mutations on a view not owned by the caller (e.g. standard views like "All Companies") are written into `overrides` instead of mutating the row. Reads merge overrides in the DTO. - **Delete/destroy**: views not owned by the caller are deactivated (`isActive = false`) instead of deleted. ~~- **INDEX invariant**: `key = INDEX` views can only be created via object-creation side effect. The API now rejects creating, deleting or destroying INDEX views (object-deletion cascade is unaffected). This was not really needed for this migration but was flagged during implementation.~~ - **Front**: views with `isActive = false` are filtered out of the views selector. - Fast instance command adds the two columns (`2-12-instance-command-fast-...-view-overridable-entity.ts`). ## Notes - Custom (caller-owned) views behave exactly as before: direct updates, soft delete. - View-group side effects (kanban groups) are computed on the override-merged view so overridden `mainGroupByFieldMetadataId` works. |
||
|
|
947a4d5253 |
Fix: prevent unexpected navigation when destroying record from side panel (#21391)
Fixes: #21243 # Issue When a user is on a specific record's page (for example, looking at a Person) and opens a related record (like a Note) in the right-side panel, clicking "Permanently Delete" on that Note would abruptly redirect the user to the main "Notes" list. This breaks the user's workflow, as they typically want to remain on the parent Person page after deleting a sub-record. # Root Cause Inside the `DestroyRecordsCommand` and `DeleteRecordsCommand` components, the application was programmed to unconditionally trigger a `navigateApp` redirect to the deleted object's index page upon successful deletion. The code did not account for whether the deletion was triggered from the main index page or from inside a contextual side panel. # How we fixed it I introduced a new `isInSidePanel` flag into the `HeadlessCommandContextApi`. The command menu now detects if the delete action originated from inside a side panel and passes this flag down the execution chain. If `true`, the `DestroyRecordsCommand` simply closes the panel (`closeSidePanelMenu()`) and keeps the user exactly where they were. ## After that fix, I encountered another issue (UI not updating automatically) Because the app now correctly kept the user on the Person page, a new bug surfaced: the "Note" chip inside the relation table did not disappear immediately. The user had to manually refresh the page to see the deletion. This happened because: 1. The Apollo optimistic cache occasionally failed to trace deeply nested morph-relationships back to the parent. 2. A standard local React `useState` fallback was insufficient. The table component aggressively unmounts and remounts relation cells whenever a user hovers over them to display interactive controls, which would wipe the local React state clean and cause the "ghost chip" to reappear. ##How I fixed that issue (and optimized it) I built an event-driven fallback using global Jotai state to permanently hide the chips: 1. **Precision ID Broadcasting**: I updated the `useDestroyManyRecords` and `useIncrementalDestroyManyRecords` hooks to extract and broadcast only the *confirmed* destroyed IDs directly from the Apollo mutation response, preventing false-positive UI removals if the backend performed a partial delete. 2. **Batch Optimizations**: During bulk deletions, events are now broadcasted per-batch rather than accumulating thousands of IDs in memory until the end, keeping the UI instantly responsive and memory bounded. 3. **Per-Cell State Scoping (`atomFamily`)**: Instead of a leaky global array, we used Jotai's `atomFamily` to dynamically generate a unique Noticeboard for every specific table cell (`${recordId}-${fieldName}`). This ensures the hidden-state survives mouse-hover unmounts while remaining cleanly isolated. 4. **Defensive Filtering**: The `RelationFromManyFieldDisplay` component was updated to defensively guard against `undefined` array entries and strictly match deleted IDs only against valid foreign keys (ending in `'Id'`), preventing false-positive removals if a UUID happened to be pasted into a description field. 5. **SSE Resilience**: We hardened the Server-Sent Event (SSE) listeners with optional chaining and null-filtering to ensure malformed backend payloads do not crash the real-time event pipeline. # Screen Recording https://github.com/user-attachments/assets/19fc8a3f-ba28-43c2-b1f3-a91127cdad97 --------- Co-authored-by: bosiraphael <raphael.bosi@gmail.com> Co-authored-by: Raphaël Bosi <71827178+bosiraphael@users.noreply.github.com> |
||
|
|
deb956f4fe |
security: bump wait-on 7.2.0 -> 9.0.10 to drop vulnerable joi (Dependabot alert 1437) (#21457)
## Context Two open Dependabot alerts; this PR fixes one with a parent bump (no resolutions), the other is dismissed with analysis (see below). ## joi RangeError DoS (alert 1437, fixed in joi 18.2.1) `joi@17.13.3`'s only parent is `wait-on@7.2.0` (twenty-sdk devDependency, used purely as a CLI: `yarn start`'s `wait-on tcp:3000` and CI's `wait-on http://localhost:3000/healthz --timeout --interval`). Bumping **wait-on 7.2.0 → 9.0.10** (which depends on `joi ^18.2.1`) evicts joi 17 from the lockfile entirely — no forced ranges. Verified: twenty-sdk builds; wait-on 9 smoke-tested with both invocation shapes used in the repo (`tcp:PORT`, `http://… --timeout --interval`). ## @cyntler/react-doc-viewer TXTRenderer "XSS" (alert 1436) — dismissed as inaccurate CVE-2026-30691 claims arbitrary JS execution via a crafted .txt because TXTRenderer "casts raw data as a ReactNode". Verified against the installed 1.17.1 dist: the renderer is `children: currentDocument?.fileData` where the txt fileLoader produces `fileData` via `FileReader.readAsText` — i.e. **always a string rendered as a React child, which React HTML-escapes**. There is no `dangerouslySetInnerHTML`/eval in the path (the only `dangerouslySetInnerHTML` occurrence in the bundle is styled-components' prop whitelist regex). String children cannot execute script in React; the advisory's premise is wrong, and consistently upstream has published no fix. Alert dismissed as *inaccurate* with this analysis. Longer-term, `@cyntler/react-doc-viewer` remains a liability (stale since 2025-09, already needs an ajv resolution) — replacing it with first-party preview renderers is tracked separately. |
||
|
|
41cdd83367 |
fix(ai): correct RICH_TEXT and MORPH_RELATION record filter operators (#21106)
## Problem
The AI find-records tool generates filter schemas via
`generateFieldFilterZodSchema`. `RICH_TEXT` currently shares the `TEXT`
case, so the agent is told it can use scalar text operators
(`like`/`ilike`/`startsWith`/`endsWith`/`eq`/…) directly on a rich-text
field.
But `RICH_TEXT` is a **composite** type (`markdown` + `blocknote`
sub-fields, see `rich-text.composite-type.ts`). Applying a scalar
operator to the composite root throws at query time:
```
ERROR [FindRecordsService] Failed to find records: Object person doesn't have any "ilike" field.
ERROR [FindRecordsService] Failed to find records: Sub field "ilike" not found for composite type: RICH_TEXT
```
`FindRecordsService` catches and returns `success: false`, so the agent
retries mid-turn — burning latency/tokens — and can **never** search
rich-text body content (note bodies, `about`, etc.).
## Fix
Give `RICH_TEXT` its own case in the filter-schema generator that
exposes the `markdown` and `blocknote` sub-fields, each carrying the
text operators — mirroring the existing composite patterns for `EMAILS`
(`primaryEmail`), `PHONES` (`primaryPhoneNumber`), `LINKS`
(`primaryLinkUrl`), `FULL_NAME`, and `ADDRESS`.
So the agent now emits:
```jsonc
{ "noteBody": { "markdown": { "ilike": "%onboarding%" } } } // valid composite sub-field filter
```
instead of:
```jsonc
{ "noteBody": { "ilike": "%onboarding%" } } // throws on composite root
```
This both **stops the throw** and **makes rich-text content actually
searchable** (the original intent). `TEXT` keeps its existing root-level
scalar operators unchanged.
## Test
Added `__tests__/field-filters.zod-schema.spec.ts`:
- `RICH_TEXT` routes pattern operators onto `markdown` / `blocknote`
- root-level scalar operators on `RICH_TEXT` are no longer accepted
- `TEXT` root-level operators unchanged
## Notes
- No DB/schema migration; render/tool-schema layer only.
- Reproduced against `twentycrm/twenty:latest`; the faulting code is
unchanged on `main` as of this PR.
---------
Co-authored-by: Rich Roberts <rich.roberts@talentpipe.ai>
Co-authored-by: Etienne <45695613+etiennejouan@users.noreply.github.com>
|
||
|
|
503c689f37 |
security: upgrade typeorm to 0.3.26 (CVE-2025-60542) (#21456)
## Context Retry of the typeorm upgrade that was pulled out of #21448 after CI showed "intermittently lossy metadata sync". **The investigation exonerated typeorm**: the postcard/seed failures were a pre-existing bug in `@ptc-org/nestjs-query-typeorm`'s batched relation paging (global LIMIT across parents) that scan-order luck had been hiding — reproduced byte-for-byte on typeorm **0.3.20** against a frozen repro DB. That bug is fixed in #21455, which this PR is stacked on (base branch = `charles/fix-nestjs-query-batch-relation-paging`; will retarget to main when it merges). ## Changes - typeorm `0.3.20` → `0.3.26` ([CVE-2025-60542](https://github.com/advisories/GHSA-q2pj-6v73-8rgj), MEDIUM). The CVE lives in TypeORM's MySQL path (`sqlstring`/`stringifyObjects`); Postgres-only Twenty never exercises it — this is scanner hygiene + staying current. - The local yarn patch (`PickKeysByType` + `DeleteResult.generatedMaps`) applies **verbatim** to 0.3.26 (verified against the pristine tarball) — renamed to `typeorm+0.3.26.patch`. - `WorkspaceRepository.query` restricted override adapted to the generic `query<T = any>()` base signature introduced in 0.3.24 (one-line change, still throws `RAW_SQL_NOT_ALLOWED`). - 0.3.26 ships `uuid ^11` natively → the scoped `typeorm/uuid` resolution from #21441 and its `//resolutions` comment clause (including the now-disproven "lossy sync" warning) are removed. ## Why we're confident this time The original failure signature was fully understood, not just retried: - On a frozen failing DB, **all fieldMetadata rows + workspace columns were intact** — only the batched metadata API read was truncated (`LIMIT 501` over 558 rows, no ORDER BY). - Same DB, typeorm 0.3.20: identical truncation, identical SQL → not a typeorm regression. - With #21455 applied: postcard install/uninstall stress loop **12/12 green on typeorm 0.3.26** (previously failed within 1–2 iterations), API returns 558/558 fields. ## Verification - `npx nx typecheck twenty-server` — clean - Full `twenty-server` unit suite — green (5651 passed) - `group-by-resolver` integration suite — 19/19 on a fresh 0.3.26-seeded test DB - Postcard app-sync stress loop — 12/12 on this exact stack - Lockfile: typeorm 0.3.26 + new `sql-highlight` dep, `esbuild`/uuid entries untouched |
||
|
|
d75685b8dc |
fix(metadata): nestjs-query batched relation queries truncate results across parents (#21455)
## TL;DR
The metadata API silently drops relation rows whenever a batched
relation query exceeds the requested page size. A dev-seeded workspace
already has **558 fieldMetadata rows across 31 objects**, so
`objects(paging:{first:50}) { fields(paging:{first:500}) }` executes:
```sql
SELECT DISTINCT ... FROM core."fieldMetadata" fields
WHERE workspaceId = $1 AND objectMetadataId IN (...31 ids...)
LIMIT 501 OFFSET 0 -- no ORDER BY
```
…and returns exactly **501 of 558** fields — ~57 rows dropped, and
*which object loses which field is scan-order-dependent*. This is what
made `example-app-postcard` CI flap with "PostCard object missing field
X" (different X per run).
## Root cause
`@ptc-org/nestjs-query-typeorm`'s `batchQueryRelations` (the DataLoader
batch path behind every `@CursorConnection`) applies the **per-parent**
page size as a **single global LIMIT** on the batched query, then groups
rows per parent in memory. Any batch whose combined relation rows exceed
`first + 1` truncates arbitrary parents. This affects production
metadata reads, not just CI — any workspace with enough fields/objects
loses rows in `objects.fields`-style connections.
## Fix
Yarn patch on `@ptc-org/nestjs-query-typeorm@9.4.0` (same vehicle as the
existing `nestjs-query-graphql` patch):
- `RelationQueryBuilder.batchSelect`: only apply LIMIT/OFFSET when the
batch has a **single parent**; multi-parent batches stay **bounded**
with `parents × (offset + limit)` — the upper bound a correct per-parent
pager can ever need, so it cannot wrongly truncate while still guarding
against unbounded fetches on high-cardinality relations;
- `batchQueryRelations`: enforce paging **per parent** by slicing after
`mapRelations` (preserves the `first + 1` hasNextPage probe semantics).
## Verification
- On a frozen repro DB (postcard installed, 558 fields): unpatched
returns 501 fields with `postCard` missing `deliveredAt`; patched
returns **558/558** with the full `postCard` field set. Reproduced
identically on typeorm 0.3.20 and 0.3.26 — pre-existing bug, **not** a
typeorm regression (this unblocks the typeorm upgrade that was reverted
from #21448).
- Postcard install/uninstall stress loop: unpatched fails within 1–2
iterations; patched **12/12 green**.
- `npx nx typecheck twenty-server` clean, full `twenty-server` unit
suite green (5651 passed).
## Related
#21435 chases the **same CI symptom** (postcard randomly missing a
freshly synced field) at a different layer — a workspace-cache write
racing invalidation. The two are complementary: the repro behind this PR
survives a **cold server restart + `redis-cli FLUSHALL`** with all rows
intact in Postgres, which no cache race can explain — the truncation
happens on the DB read itself (`LIMIT 501` over 558 matching rows,
captured via `log_statement=all`). Both fixes are likely needed for the
postcard job to be fully reliable.
## Notes
Worth upstreaming to `@ptc-org/nestjs-query` eventually; the proper
upstream fix is per-parent windowed pagination (`ROW_NUMBER() OVER
(PARTITION BY parentId)`), but the in-memory per-parent slice is correct
and proportionate at metadata-API scale.
|
||
|
|
d0884bd708 |
Fix missing datetime filter type (#21451)
Currently datetime fields are only typed to be filtered by string Add a proper typing to match gql filters ## Before <img width="750" height="492" alt="image" src="https://github.com/user-attachments/assets/ff3a5423-3bb0-4295-84c9-e404489354f6" /> ## After <img width="537" height="511" alt="image" src="https://github.com/user-attachments/assets/d8c8219f-b7de-41b0-96cb-5adbfda7a91d" /> |
||
|
|
0ac4f237c0 |
fix(server): stop redundant lambda rebuilds causing build-lock acquisition failures (#21442)
## Context `Lambda invocation failed for function '<id>' during build: Failed to acquire lock for key: lambda-build:<id>` fires ~1000 times/day in production. ## Root cause `LambdaExecutorManagerService.buildExecutor` re-checks `canSkip` inside the `lambda-build:<functionId>` lock, but the re-check reuses the `flatApplication` snapshot captured when the request started. `canSkip` depends on `!flatApplication.isSdkLayerStale`, so: 1. An app sync/install regenerates the SDK client and sets `isSdkLayerStale = true` 2. All in-flight executions of the function fail `canSkip` and queue on the lock 3. The first holder rebuilds and `markSdkLayerFresh` clears the flag in DB + workspace cache 4. Queued waiters can't see that fix — their in-memory snapshot still says stale — so **each waiter redoes the full rebuild serially** (download SDK archive, delete + republish layer, update function config, wait for update) 5. The lock is held back-to-back for minutes; everyone deeper in the queue exhausts the 120s retry budget and throws The local driver already handles this correctly (`LocalLayerManagerService` refreshes the flat application from the workspace cache inside its lock); the lambda driver missed it. ## Fix - Refresh `flatApplication` from the workspace cache inside the lock before re-checking `canSkip`, so waiters skip in ~100ms once the first holder finishes - Degrade gracefully on lock-acquisition timeout: re-check build status with fresh data and proceed with the invocation if the executor is already usable, instead of failing the run (introduces a typed `CacheLockAcquisitionError` so only that case is caught) ## Test plan - [x] `cache-lock.service.spec.ts` passes - [x] `lint:diff-with-main` + typecheck pass - [ ] Monitor `Failed to acquire lock for key: lambda-build:*` error rate in production after deploy |
||
|
|
796f763bb7 |
i18n - website translations (#21453)
Created by Github action --------- Co-authored-by: github-actions <github-actions@twenty.com> |
||
|
|
184c4948d6 |
security: strip Node dev headers from images + lingui 5.9.5 (drops vulnerable esbuild) (#21448)
## Context
AWS Inspector flags the `prod-twenty` image (built from current main)
with 16 findings, and Dependabot alert 174 flags esbuild. This PR fixes
the OpenSSL scanner findings and the esbuild CVE. The typeorm bump
(CVE-2025-60542) was **pulled out of this PR** — see "typeorm status"
below.
## Changes
### Strip `/usr/local/include/node` from runtime stages
(`twenty-server`, `twenty-app-dev`)
15 OpenSSL CVEs (June 9 advisory, incl. CRITICAL CVE-2026-34182) are all
detected via **Node's bundled OpenSSL dev headers**: 3 GENERIC
`openssl/openssl` 3.5.6 detections per CVE at
`/usr/local/include/node/openssl/archs/linux-x86_64/{asm,asm_avx2,no-asm}/include/openssl/opensslv.h`.
The headers are only needed by node-gyp and native addons are compiled
in the build stages — nothing compiles at runtime. Dropping them clears
all 45 detection instances and permanently ends this class of finding
(third occurrence: 3.5.5 → 3.5.6 → 3.5.7). None of these CVEs are
reachable through Node (no CMS/PKCS#7 API, `pfx` is operator-supplied,
Node's QUIC uses ngtcp2, ASN.1 issues need ~2GB inputs).
**Follow-up (~June 17, 2026):** the `node` binary itself still
statically links OpenSSL 3.5.6 — invisible to the scanner after this PR
and unreachable in practice, but the real fix is bumping the pinned
`node:24-alpine` digest once the [announced June 17 Node.js security
releases](https://nodejs.org/en/blog/vulnerability/june-2026-security-releases)
ship a 24.x linking OpenSSL ≥ 3.5.7 (verify via
`deps/openssl/openssl/VERSION.dat` on the release tag — 24.16.0 is still
on 3.5.6). A dated TODO sits next to the cleanup in the Dockerfile.
### esbuild dev-server CORS CVE (Dependabot alert 174,
GHSA-67mh-4wv8-2f99)
`@lingui/cli@5.1.2` (pins `esbuild ^0.21.5`) was the last parent
resolving a vulnerable esbuild (≤ 0.24.2 lets any website send requests
to the dev server and read responses). Instead of a resolution override,
this bumps the lockstepped **lingui suite 5.1.2 → 5.9.5** (within-major;
lingui adopted `esbuild ^0.25.1` in 5.4.1), which:
- removes `esbuild@0.21.5` and all its platform packages from the
lockfile with no forced ranges;
- drops the `@lingui/core` lockstep resolution (its comment marked it
droppable on the next coordinated lingui bump — the tree now resolves a
single `@lingui/core@5.9.5`);
- `@lingui/swc-plugin` stays at `^5.11.0` (peers on `@lingui/core: 5`;
its 6.x line targets lingui 6).
**lingui 5.9.5 behavioral fallout handled here:**
- Translation functions now **throw without an active locale** (5.1.2
fell back silently). The global `i18n` singleton that backs server-side
`` t`…` `` calls only had a messages compiler set, never an activated
locale → activate the source locale in `I18nService.loadTranslations()`,
mirrored in the server jest setup (unit tests bypass Nest bootstrap).
- `msg`/`t` placeholders are now strictly typed (reject
`null`/`undefined`/`unknown`) → one server call site and 16 twenty-front
files adapted with minimal nullish-coalescing fixes that preserve
rendering.
- `.po`/compiled-catalog churn from the new extractor/compiler
(reference reordering, sorted keys — verified content-identical on
unchanged `.po` inputs) is intentionally not committed: the scheduled
i18n workflows regenerate those.
## typeorm status (pulled out)
typeorm 0.3.20 → 0.3.26 was originally in this PR but **made workspace
metadata sync intermittently lossy**: `example-app-postcard` failed
twice with a *different* field missing from the synced PostCard object
each run, and one integration shard's `DataSeedWorkspaceCommand` died
with "Could not find flat entity with universal identifier …" — versus
zero such failures on recent main. Local runs (db reset + seed, group-by
integration suite 19/19) pass, so it is a nondeterministic
CI-load-sensitive regression that needs dedicated debugging (typeorm
changed LIMIT/OFFSET 0 semantics, lazy count for `getManyAndCount`,
upsert WHERE construction, and topological-sort internals in that
range). The resolutions comment documents this as the blocker;
CVE-2025-60542 is MySQL-driver-only (`sqlstring`), so Postgres-only
Twenty is not exposed in the meantime.
## Verification
- `npx nx typecheck twenty-server` / `twenty-front` — clean (no cache)
- `npx nx test twenty-server` — full suite green
- `lingui:extract` + `lingui:compile` — clean for twenty-server /
twenty-emails / twenty-front
- `oxfmt --check` — clean for both packages
- Lockfile diff: lingui 5.9.5 entries, `esbuild@0.21.5` +
`@esbuild/*@0.21.5` platform packages removed, no typeorm changes
|
||
|
|
303c415dd1 |
fix(ai) - add logs + remove dashboard building (#21440)
- add logs for thread finishing without agent message - add logs to monitor toolCall token usage - remove dashboard building via AI (before fixing it) - fix Anthropic compute |
||
|
|
a6fcbf58e4 |
fix(billing) - enable upgrade if invoice already paid (#21450)
Had an issue concerning a user with credits, then invoice automatically paid. Upgrade failed |
||
|
|
7da6f25aaf |
Replace random remote images in stories to stop flaky Argos diffs (#21447)
### Problem `TabButton` and `AvatarOrIcon` stories (in both `twenty-ui` and `twenty-ui-deprecated`) used random remote image URLs — `picsum.photos/192/192` and `i.pravatar.cc/300`. Each Argos run fetched a different image, producing false-positive pixel diffs. This is the image counterpart to #21412, which froze framer-motion animations for the same reason. ### Fix Replace all 6 random URLs with `AVATAR_URL_MOCK` — a fixed base64 data URI already used across avatar stories. It's deterministic and network-free, so screenshots are now stable across runs. - `twenty-ui` / `twenty-ui-deprecated` → `TabButton.stories.tsx` (×2 each) - `twenty-ui` / `twenty-ui-deprecated` → `AvatarOrIcon.stories.tsx` (×1 each) Note: this changes the rendered image content, so it adds new baselines (one-time Argos approval, not flakiness). |
||
|
|
f7463886a6 |
feat(partners): partner role row-level security (RLS) with scoped edits (#21386)
## Summary
Adds an external **Partner** self-service role that sees and edits only
its own
records via row-level security (RLS), so a validated partner can sign in
and manage
just the deals they're matched on.
## What's included
- **`partnerUser` relation** on Partner, Person, Company, Opportunity (+
inverse
relations on Workspace Member) — the login member a record belongs to.
- **RLS predicates** scoping each of those objects to "partnerUser IS
the current
workspace member", plus a self-scope on Workspace Member so member-typed
relations
resolve without exposing the internal team roster. Applied out-of-band
via
`yarn rls:configure` (the app manifest cannot ship RLS predicates).
- **Assign / unassign cascade** (`on-opportunity-partner-assigned` logic
function):
assigning a Partner to an Opportunity stamps `partnerUser` onto the
Opportunity +
its Company + People; removing the Partner clears it (and cascades to
the
Company/People when no other deal of that member still uses them).
- **Partner role permissions**
- Partner profile: full read/update.
- Opportunity: read all; **update `stage` and `amount` only** (every
other
user-facing field locked).
- Company / Person: read-only.
- Workspace Member: read-only, RLS-scoped to self.
- **`partnerUser` column** added to the Validated Partners view so the
login member
can be assigned inline.
## Install / upgrade note
After install or reinstall, run `yarn rls:configure` (`:prod` variant
for prod) to
(re)apply the RLS predicates and verify the field-permission locks.
Manifest sync
handles object/field permissions; predicates are applied by this script.
## Platform gaps found (for the eng team)
1. **Manifest sync doesn't invalidate the roles-permissions Redis
cache.** Permission
changes deployed via `yarn twenty dev --once` persist to the DB but
aren't reflected
in the cached snapshot used for enforcement until
`engine:workspace:metadata:permissions:roles-permissions:<workspaceId>:{data,hash}`
is flushed. Relevant on any real workspace when permissions change.
2. **Locking a server-injected field silently breaks all updates.** The
`*.updateOne`
pre-query hook writes `updatedBy` into every update, so
`canUpdateFieldValue:false`
on `updatedBy` makes the permission check reject *every* record update
with
`PERMISSION_DENIED`. Field-permission lock lists must exclude
server-managed/injected
fields (`updatedBy`; and `position`, co-written with `stage` on kanban
drag).
## Version
Minor bump → `0.5.0` (new role, new fields, new behaviour;
backwards-compatible).
## Testing
- Verified locally as a partner user: edits own profile; edits
Opportunity stage +
amount; Company/Person read-only; sees only matched deals; unassigning a
partner
removes the deal (and its company/people) from the partner's view.
- `yarn rls:configure` passes (5 predicates upserted; 24 Opportunity
fields locked,
stage + amount editable).
- Lint clean.
|
||
|
|
166f7ee0d2 |
chore(deps): prune yarn resolutions down to load-bearing entries (#21446)
## Context Audit of all 28 `resolutions` entries in the root package.json against yarn.lock dependency graphs and the npm registry, to remove every entry that is no longer forcing anything a normal resolution wouldn't do — resolutions are hard to maintain and silently freeze versions. Net result: **28 → 22 entries**, two small dependency bumps replace pins, and every remaining entry now has its blocker + removal condition documented in `//resolutions`. ## Removed — dead weight (re-resolution lands on the same safe versions) | Entry | Why it was dead | |---|---| | `type-fest: 4.10.1` | Stale 2024 dedup pin that semver-overrode ~16 of 19 declared ranges (forced `^0.13`/`^0.20`/`^0.21` consumers up four majors, `^5.x` consumers down one). Types-only; each parent now resolves its own compatible copy. | | `typescript: 5.9.3` | No-op: every range (`^5.9.3`, `5.9.3`, `~5.9.2`) resolves to 5.9.3 naturally. Only the electron-forge scaffolding template regains its own nested `~5.4.5` (never builds this repo). | | `node-gyp: ^12.4.0` | All requesters are Yarn-injected `node-gyp: latest` = 12.4.0 today. The tar-6-era node-gyp versions it evicted have no requesting parent left. | | `cacache: ^20.0.0` | All four parents (arborist, metavuln-calculator, make-fetch-happen 15, pacote 21) already declare `^20`. Guarded by the kept `make-fetch-happen: ^15` resolution. | | `pacote/tar: ^7.5.16` | The original target (pacote 11/15 via zapier) is gone; the only pacote left is 21.5.0 which declares `tar ^7.4.3` natively. | ## Removed — replaced by a parent upgrade - **`nodemailer: 8.0.10`** → `imapflow` 1.2.1 → **1.3.6** (ships patched nodemailer 8.0.10 exact; 1.4.0 is blocked by the 3-day npm age gate). twenty-server's own `^8.0.5` range was already safe. - **`node-ical/uuid: 11.1.1`** → `node-ical` ^0.20.1 → **^0.21.0**, which drops uuid (and axios) entirely. The uuid removal happened at 0.21.0 — not in the 0.26 rrule-temporal type overhaul that #21441 flagged as the blocker. ## Narrowed — `qs: 6.15.2` global → two scoped entries Only three lockfile entries actually request vulnerable qs ranges: `express@4.22.0` (pinned by `@mintlify/previewing`), `express@4.22.1` + `@cypress/request@3.0.10` (pinned by verdaccio 6.7.2, latest). Replaced the global pin with `express/qs` + `@cypress/request/qs`, so the 12+ healthy parents (express 4.22.2/5.x, body-parser, stripe, …) are no longer frozen and will pick up future qs releases naturally. ## Re-pinned — `graphql-redis-subscriptions/ioredis` Changed `^5.6.0` → exact `5.10.1` and documented why: this must equal the exact ioredis version pinned by twenty-server and bullmq. Without it, graphql-redis-subscriptions' `^5.3.2` resolves to a second ioredis copy and `RedisPubSub`'s publisher/subscriber types reject the server's client (caught by twenty-server typecheck during this work — bump it in lockstep with the ioredis pin). ## Kept (all load-bearing, now documented inline) graphql (singleton below msw's `^16.12.0`), @lingui/core (suite lockstep), @types/qs (6.9.17 typing-break holdback), @opentelemetry/api (NoopMeterProvider singleton, #20231), chokidar v3 (NestJS CLI fsevents, #20316), tmp (zapier-platform-cli pins 0.2.5), make-fetch-happen + the two @electron tar entries (blocked on electron-forge adopting @electron/rebuild 4), @angular-devkit/core (blocked on a fixed @nestjs/cli > 11.0.23), yeoman-environment, webpack-dev-server, next/postcss (fix only in next 16.3.0 canaries), the remaining uuid pins, and react-doc-viewer/ajv. ## Follow-ups (separate PRs) - `typeorm` 0.3.20 → 0.3.30: re-roll the 46-line patch; clears the `typeorm/uuid` resolution **and** the open high-severity GHSA-q2pj-6v73-8rgj (SQL injection in `repository.save/update`, fixed in 0.3.26). - googleapis 105 → ≥152 migration clears `googleapis-common/uuid`. ## Verification - `yarn install` clean; lockfile contains **no** vulnerable qs (≤6.15.1)/tar 6/uuid <11/nodemailer <8.0.4/postcss 8.4.31/tmp <0.2.6 entries - `npx nx typecheck twenty-server` ✓ and `npx nx typecheck twenty-front` ✓ - CalDAV + IMAP unit tests (node-ical/imapflow consumers): 9 suites, 121 tests ✓ - `yarn npm audit --all`: only pre-existing typeorm finding remains (see follow-up) |
||
|
|
1ccede2309 |
security: scoped ajv 8.20.0 resolution for react-doc-viewer (Dependabot alert 481) (#21445)
Closes the **final** open Dependabot alert — ajv [481](https://github.com/twentyhq/twenty/security/dependabot/481) — with a scoped resolution. ### Why a resolution (no parent path) `ajv >= 7.0.0 < 8.18.0` is pulled **only** by `@cyntler/react-doc-viewer`, which pins `ajv ^7.2.4`. Its **latest (1.17.1) still pins `^7`** — there is no react-doc-viewer version on ajv 8 (no 1.18/2.0) — so it can't be closed by upgrading the parent. ### Why it's completely safe `@cyntler/react-doc-viewer` **never imports ajv** — zero references in its dist; ajv is a declared-but-unused dependency. So forcing it to ajv 8 has **no functional impact**, and the ajv 7→8 breaking-change concern is moot. Scoped to `@cyntler/react-doc-viewer/ajv` so it touches nothing else. ### Verification - `yarn install --immutable` ✓; every ajv now resolves to **8.18.0 / 8.20.0** (safe) or `6.12.x` (outside the advisory range) — no vulnerable ajv remains. Documented in the top-level `//resolutions` note with a removal trigger. **This was the last open alert.** |
||
|
|
462dd3b0e9 |
security: uuid CVE — bump bullmq/msal/blocknote + scoped resolutions for the rest (Dependabot alert 1289) (#21441)
Closes the uuid Dependabot alert — [1289](https://github.com/twentyhq/twenty/security/dependabot/1289) — by **upgrading the parents that bump cleanly** and **scope-resolving only the ones that genuinely can't**. `uuid < 11.1.1` (buffer-bounds check in v3/v5/v6) is pulled by ~9 transitives. ### Bumped (parent upgrade — drops uuid<11, no behavior change; typecheck verified) - **bullmq** 5.40.0 → 5.78.0 — also aligned **ioredis** 5.6.0 → 5.10.1 (bullmq pins it) and fixed the renamed `Job.returnValue→returnvalue` / `stackTrace→stacktrace` (now `string[]|null`) in `admin-panel-queue.service.ts`. - **@azure/msal-node** ^3.8.4 → ^5.2.3 (5.2.4 was age-gate-quarantined). - **@blocknote/** ×5 ^0.47.3 → ^0.51.4. ### Scope-resolved to uuid 11.1.1 (no clean bump exists) - **sockjs** (latest; pinned by webpack-dev-server) and **@ptc-org/nestjs-query-typeorm** (9.4.0 *is* latest, pins `^10`) — no version drops uuid. - **typeorm** — a `patch:` dep / ORM core, too risky to bump. - **node-ical** 0.26 (type-model overhaul → caldav-parser rewrite) and **googleapis** 173 (Gmail/OAuth, 105→173) — large breaking migrations; **deferred to dedicated PRs**. - **@cypress/request** — transitive (cypress isn't a direct dep). Resolutions are **per-package** and preserve the intentional **uuid 13.x** (twenty-sdk / create-twenty-app). ### Verification - `twenty-server` typecheck ✓ (0 errors), `twenty-front` typecheck ✓ (0 errors). - `yarn install --immutable` ✓; every uuid resolves to **11.1.1** or **13.0.2**. - bullmq/msal/typeorm runtime exercised by the **server integration tests**; @blocknote by the **storybook tests** in CI. |