ca1571676c0c67f026cd541fecbdc3dc09cc1c59
12097
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
ca1571676c |
fix(server): treat plaintext-under-isSecret rows as plaintext in app variable encryption migration (#20590)
## Summary Prod 2.5 upgrade failed on the slow instance command `EncryptApplicationVariableSlowInstanceCommand`: ``` [Nest] LOG [InstanceCommandRunnerService] 2.5.0_EncryptApplicationVariableSlowInstanceCommand_1798000005000 starting data migration... [Nest] WARN [SecretEncryptionService] Decrypted a legacy unprefixed AES-CTR ciphertext... [Nest] ERROR [InstanceCommandRunnerService] data migration failed TypeError: Invalid initialization vector ``` ### Root cause The migration assumes every row matching `isSecret = true AND value <> '' AND value NOT LIKE 'enc:v2:%'` is legacy AES-CTR ciphertext. In prod we found multiple `isSecret = true` rows whose `value` is plaintext (e.g. `SLACK_HOOK_URL = 'https://hooks.slack.com/services/...'`) — most likely the result of `isSecret` being flipped to true on a row that already held a plaintext value, or a write path that bypassed `ApplicationVariableEntityService.update`. Those values can't decode into the 16-byte IV that AES-CTR needs, so `Buffer.from(value, 'base64')` truncates at the first non-base64 char (`:`), the buffer is < 16 bytes, and `createDecipheriv` throws. ### Fix Follow the same policy as `EncryptConnectedAccountTokensSlowInstanceCommand`: anything that isn't already in the `enc:v2:` envelope is plaintext. Concretely: 1. Try `decryptVersioned` — legacy CTR rows decrypt fine. 2. If it throws (mis-classified plaintext), log a warning naming the row id and fall back to treating `row.value` as plaintext. 3. Encrypt the resulting plaintext into the `enc:v2:` envelope and update the row. In-loop `isSecret` guard is kept (alongside the SQL filter) so non-secret rows are never touched even if the SQL filter is ever loosened. ### Integration test coverage Added one new case alongside the existing ones in `…encrypt-application-variable.integration-spec.ts`: - `treats plaintext-under-isSecret=true as plaintext and re-encrypts as v2` — seeds a row with `isSecret = true` and a URL value (`:` and `/` are not base64, so this is the exact failure shape from prod), runs the migration, and asserts the value is now `enc:v2:...` and decrypts back to the original URL. Existing cases unchanged: legacy CTR happy path, non-secret rows untouched, idempotent across re-runs, `up()` adds the CHECK constraint, `down()` removes it. ### Why this is a 2-5 edit `TWENTY_CURRENT_VERSION` is now 2.6.0, so editing a 2-5 file trips the `server-previous-version-upgrade-mutation-guard` — `ci:allow-previous-version-upgrade-mutation` label is on the PR. `up()` and `down()` are unchanged; only `runDataMigration` is modified. ## Test plan - [ ] Re-deploy 2.5 to prod and confirm `EncryptApplicationVariableSlowInstanceCommand` completes - [ ] Inspect warning log to count rows that went through the plaintext fallback - [ ] Verify resulting secret rows all satisfy `value = '' OR value LIKE 'enc:v2:%'` and the CHECK constraint is in placev2.5.1 |
||
|
|
a5880bd8d0 |
fix(server): drop correlated subquery in getWorkspaceLastAttemptedCommandName (#20591)
## Summary
- The upgrade runner calls `getWorkspaceLastAttemptedCommandName` twice
per workspace step. Grafana showed it averaging ~4.4s and trending
upward as the `core.upgradeMigration` table grows during an in-flight
upgrade.
- The old query joined every outer row against a correlated subquery
(`attempt = (SELECT MAX(sub.attempt) ... WHERE sub.name = m.name AND
sub."workspaceId" = m."workspaceId")`). Even with the `(workspaceId,
name, attempt)` index added in 2.3, each outer row triggers an index
lookup — fine for a few rows, painful at production scale.
- Replaced with a two-level `DISTINCT ON`:
- Inner `DISTINCT ON ("workspaceId", name) ORDER BY "workspaceId", name,
attempt DESC` walks `IDX_UPGRADE_MIGRATION_WORKSPACE_ID_NAME_ATTEMPT`
directly and yields one row per `(workspaceId, name)` at max attempt.
- Outer `DISTINCT ON ("workspaceId") ORDER BY "workspaceId", "createdAt"
DESC` picks the most recent row per workspace.
- Semantically identical; planner now does a single index walk + one
sort instead of N correlated lookups.
The same correlated-subquery shape exists in
`getLastAttemptedCommandNameOrThrow`, `areAllWorkspacesAtCommand`, and
`getLastAttemptedInstanceCommand`. They run far less often during an
upgrade (per instance step, not per workspace step), so they're out of
scope for this hotfix — happy to follow up if we want them too.
## Benchmark (prod)
Run over all distinct workspaceIds in `core."upgradeMigration"`:
| Variant | Execution Time |
| --- | --- |
| Before (correlated subquery) | **2979.659 ms** |
| After (two-level DISTINCT ON) | **1225.690 ms** |
~2.4× faster, and the gap widens as the table grows over the course of
an upgrade.
Equivalence confirmed: the diff query below returned `0` divergent
workspaces on prod.
### Variant A — original (correlated subquery)
```sql
SELECT DISTINCT ON (m."workspaceId")
m."workspaceId", m.name, m.status, m."executedByVersion",
m."errorMessage", m."createdAt", m."isInitial"
FROM core."upgradeMigration" m
WHERE m."workspaceId" IN ($1, $2, ...)
AND m.attempt = (
SELECT MAX(sub.attempt)
FROM core."upgradeMigration" sub
WHERE sub.name = m.name
AND sub."workspaceId" = m."workspaceId"
)
ORDER BY m."workspaceId", m."createdAt" DESC;
```
### Variant B — new (two-level DISTINCT ON)
```sql
SELECT DISTINCT ON (latest_per_name."workspaceId")
latest_per_name."workspaceId",
latest_per_name.name,
latest_per_name.status,
latest_per_name."executedByVersion",
latest_per_name."errorMessage",
latest_per_name."createdAt",
latest_per_name."isInitial"
FROM (
SELECT DISTINCT ON ("workspaceId", name)
"workspaceId", name, status, "executedByVersion",
"errorMessage", "createdAt", "isInitial"
FROM core."upgradeMigration"
WHERE "workspaceId" = ANY($1)
ORDER BY "workspaceId", name, attempt DESC
) latest_per_name
ORDER BY latest_per_name."workspaceId", latest_per_name."createdAt" DESC;
```
### Equivalence check (returned 0 on prod)
```sql
WITH target_ids AS (
SELECT DISTINCT "workspaceId"
FROM core."upgradeMigration"
WHERE "workspaceId" IS NOT NULL
),
old_result AS (
SELECT DISTINCT ON (m."workspaceId")
m."workspaceId", m.name, m.status, m."executedByVersion",
m."errorMessage", m."createdAt", m."isInitial"
FROM core."upgradeMigration" m
WHERE m."workspaceId" IN (SELECT "workspaceId" FROM target_ids)
AND m.attempt = (
SELECT MAX(sub.attempt)
FROM core."upgradeMigration" sub
WHERE sub.name = m.name
AND sub."workspaceId" = m."workspaceId"
)
ORDER BY m."workspaceId", m."createdAt" DESC
),
new_result AS (
SELECT DISTINCT ON (latest_per_name."workspaceId")
latest_per_name."workspaceId", latest_per_name.name, latest_per_name.status,
latest_per_name."executedByVersion", latest_per_name."errorMessage",
latest_per_name."createdAt", latest_per_name."isInitial"
FROM (
SELECT DISTINCT ON ("workspaceId", name)
"workspaceId", name, status, "executedByVersion",
"errorMessage", "createdAt", "isInitial"
FROM core."upgradeMigration"
WHERE "workspaceId" IN (SELECT "workspaceId" FROM target_ids)
ORDER BY "workspaceId", name, attempt DESC
) latest_per_name
ORDER BY latest_per_name."workspaceId", latest_per_name."createdAt" DESC
),
diffs AS (
SELECT 'only_in_old' AS bucket, o."workspaceId", o.name, o.status, o."createdAt"
FROM old_result o
LEFT JOIN new_result n ON n."workspaceId" = o."workspaceId"
WHERE n."workspaceId" IS NULL OR n.name <> o.name OR n.status <> o.status
UNION ALL
SELECT 'only_in_new', n."workspaceId", n.name, n.status, n."createdAt"
FROM new_result n
LEFT JOIN old_result o ON o."workspaceId" = n."workspaceId"
WHERE o."workspaceId" IS NULL OR o.name <> n.name OR o.status <> n.status
)
SELECT COUNT(*) AS divergent_workspaces FROM diffs;
```
## Test plan
- [ ] `npx nx test twenty-server --testPathPattern upgrade-migration`
- [ ] Integration tests: `npx nx run
twenty-server:test:integration:with-db-reset --testPathPattern
sequence-runner`
- [ ] Verify on staging that the slow query disappears from the
PostgreSQL Grafana board during the next upgrade run
|
||
|
|
78b3092886 |
fix(server): batch upgrade migration inserts to stay under PG param limit (#20588)
## Summary
Prod deploy of v2.5.0 fails with a query failure inserting into
`core.upgradeMigration`:
```
query failed: INSERT INTO "core"."upgradeMigration" ("id", "name", "status", "attempt", "executedByVersion", "errorMessage", "isInitial", "workspaceId", "createdAt")
VALUES (DEFAULT, $1, $2, $3, $4, $5, DEFAULT, $6, DEFAULT),
(DEFAULT, $7, $8, $9, $10, $11, DEFAULT, $12, DEFAULT),
... (continues past $2515) ...
```
### Root cause
`UpgradeMigrationService.recordUpgradeMigration` writes one row per
workspace via a single `repository.save([...rows])` call.
`UpgradeMigrationEntity` has **6 user-provided columns** per row
(`name`, `status`, `attempt`, `executedByVersion`, `errorMessage`,
`workspaceId`), so the multi-row INSERT binds `6 * (1 + N_workspaces)`
parameters.
Postgres' wire protocol caps a single statement at **65,535 bind
parameters** (16-bit count). That gives a hard ceiling of ~10,920 rows
per call. Production has enough workspaces to overflow.
|
||
|
|
5a1d3841f4 |
Bump twenty-sdk, twenty-client-sdk, create-twenty-app to 2.5.0 (#20587)
## Summary - Bumps `twenty-sdk` from `2.4.2` to `2.5.0`. - Bumps `twenty-client-sdk` from `2.4.2` to `2.5.0`. - Bumps `create-twenty-app` from `2.4.2` to `2.5.0`. |
||
|
|
94748b7042 |
chore: bump version to 2.6.0 (#20585)
## 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 ## Checklist - [ ] Verify version constants are correct Co-authored-by: Github Action Deploy <github-action-deploy@twenty.com> |
||
|
|
663ef332ad |
feat(auth): resume workspace selection on /welcome with valid tokenPair cookie (#20575)
## Summary After a user completes a multi-workspace social-SSO sign-in, [auth.service.ts:988-1011](https://github.com/twentyhq/twenty/blob/main/packages/twenty-server/src/engine/core-modules/auth/services/auth.service.ts#L988-L1011) issues a **workspace-agnostic** access + refresh token pair and lands them on `app.twenty.com/welcome?tokenPair=…`. [SignInUpGlobalScopeFormEffect.tsx](packages/twenty-front/src/modules/auth/sign-in-up/components/internal/SignInUpGlobalScopeFormEffect.tsx) reads the URL param, writes the cookie, pushes them to `SignInUpStep.WorkspaceSelection`. The problem: if the user revisits `app.twenty.com/welcome` later (e.g. ChatGPT pings `/authorize` and the global page-change effect redirects them to `/welcome` with `returnToPath=/authorize?…`), the existing branch is a no-op — the URL param is gone. The user sees the regular email/SSO form and has to re-authenticate, even though the workspace-agnostic cookie is still valid. This PR adds a second branch in the same `useEffect` that handles the "valid cookie, no URL param" case: ```ts if (signInUpStep !== SignInUpStep.Init) return; if (!hasAccessTokenPair) return; loadCurrentUser(); setSignInUpStep(SignInUpStep.WorkspaceSelection); ``` Single `useEffect`, no `useRef`, no async then/catch. The synchronous `setSignInUpStep(WorkspaceSelection)` is the gate — once the step transitions, subsequent effect runs early-return. Mirrors the existing URL-param branch's pattern exactly. If the cookie is stale, `loadCurrentUser` triggers Apollo's renewal middleware. Renewal of a workspace-agnostic refresh token is supported end-to-end (verified in audit, see below) — if it succeeds the user sees their workspaces; if both tokens are expired, `onUnauthenticatedError` clears the cookie and the next render lands them on the regular sign-in form. Same fallback as if the cookie had never been there. ## Behavior matrix | State on /welcome mount | Before | After | |---|---|---| | No tokenPair anywhere | Show sign-in form | Show sign-in form | | tokenPair in URL (just bounced from SSO) | Set tokens → WorkspaceSelection | (unchanged) Set tokens → WorkspaceSelection | | tokenPair in cookie, access valid | Show sign-in form ❌ | **→ WorkspaceSelection ✓** | | tokenPair in cookie, access expired, refresh valid | Show sign-in form (Apollo eventually 401s on a query) | Renewal succeeds silently → WorkspaceSelection ✓ | | tokenPair in cookie, both expired | Show sign-in form | `onUnauthenticatedError` clears cookie → fall back to sign-in form | ## Workspace-agnostic renewal: confirmed working end-to-end Audit summary: - **Refresh token carries the type**: [refresh-token.service.ts:104](packages/twenty-server/src/engine/core-modules/auth/token/services/refresh-token.service.ts) preserves `targetedTokenType` in the JWT payload and returns it from `verifyRefreshToken`. - **Renewal branches on type** ([renew-token.service.ts:70-87](packages/twenty-server/src/engine/core-modules/auth/token/services/renew-token.service.ts)): ```ts const accessToken = isDefined(authProvider) && targetedTokenType === JwtTokenTypeEnum.WORKSPACE_AGNOSTIC && !isDefined(workspaceId) ? await this.workspaceAgnosticTokenService.generateWorkspaceAgnosticToken({...}) : await this.accessTokenService.generateAccessToken({...}); ``` Renewed refresh token preserves `targetedTokenType` (line 93). - **Resolver is workspace-agnostic**: `@UseGuards(PublicEndpointGuard, NoPermissionGuard)` on `renewToken` ([auth.resolver.ts:796-804](packages/twenty-server/src/engine/core-modules/auth/auth.resolver.ts)) — no `@AuthWorkspace()` requirement, callable from `app.twenty.com`. - **Frontend middleware is type-agnostic**: [apollo.factory.ts:180-209](packages/twenty-front/src/modules/apollo/services/apollo.factory.ts) just passes the refresh token blob. Net: no backend change needed. The full workspace-agnostic lifecycle (issue → cookie → renew → re-issue) already works. ## Test plan - [x] `npx oxlint` + `prettier --check` — clean. - [x] `npx nx typecheck twenty-front` — clean. - [ ] Manual: complete one full SSO flow ending on a workspace subdomain. Visit `https://app.twenty.com/welcome` directly — expect the workspace picker, not the sign-in form. - [ ] Manual: same but with tokenPair cookie cleared — expect the regular sign-in form (no regression). - [ ] Manual: sign-out from a workspace, then visit `app.twenty.com/welcome` — expect the regular form (sign-out clears the cookie via full page reload). - [ ] Manual: stale/expired tokenPair cookie — Apollo renewal kicks in transparently; if renewal fails, regular form (no infinite loop, no crash). - [ ] Manual: pair with #20572 — visit `app.twenty.com/authorize?…` with a stale workspace-agnostic cookie. Expected chain: `/authorize` renders → `PageChangeEffect` redirects to `/welcome?returnToPath=/authorize?…` → this effect lands the user on WorkspaceSelection → picking a workspace bounces to `<workspace>/authorize?…` where consent renders. ## Out of scope - Fixing `lastAuthenticatedWorkspaceDomain` for custom-domain users (separate cookie-scoping issue, tracked separately). |
||
|
|
09daccc3f9 |
fix(server): add subFieldName column early in upgrade sequence (#20584)
## Summary Cross-version upgrades from pre-2.3 still fail after #20581 / #20583 — different column, structurally similar problem: ``` column ViewSortEntity.subFieldName does not exist at WorkspaceFlatViewSortMapCacheService.computeForCache (...flat-view-sort/services/workspace-flat-view-sort-map-cache.service.js:40) ... triggered indirectly by DropMessageDirectionFieldCommand (2.3 workspace command) ``` (see https://github.com/twentyhq/twenty-infra/actions/runs/25862573418/job/75997337604) ### Why narrowing the `select` doesn't fit here In the previous two PRs the offender was a bare `findOne` on `WorkspaceEntity` — easy to narrow. Here the chain is: 1. The 2.3 `DropMessageDirectionFieldCommand` builds a workspace migration that deletes a `fieldMetadata` (the `direction` field). 2. `WorkspaceMigrationRunnerService.run` walks the metadata cascade graph (`getMetadataRelatedMetadataNames`) and pulls `viewSort` into the dependency set because `viewSort` is the inverse one-to-many of `fieldMetadata` (deleting a field cascades to view sorts that reference it). 3. That maps to cache keys → `flatViewSortMaps` gets requested → `WorkspaceFlatViewSortMapCacheService.computeForCache` runs. 4. `computeForCache` does `viewSortRepository.find({ where: { workspaceId }, withDeleted: true })` with no `select`, so TypeORM emits a SELECT that includes `subFieldName` — the column doesn't exist in DB yet (added by a 2.5 instance command much later in the sequence). 💥 Narrowing the cache provider's select would silently drop `subFieldName` from the cache for runtime use too, until something invalidates it. Brittle, and would re-break the next time anyone adds a `viewSort` column. ### Structural fix Ensure the column exists in DB before any 2.3 workspace command can trigger that cascade. Within a version, the upgrade runner sorts: fast instance → slow instance → workspace, so a new 2.3 fast instance command lands before `DropMessageDirectionFieldCommand`. - **Add** `2-3/2-3-instance-command-fast-1747234200000-add-sub-field-name-to-view-sort.ts` — `ALTER TABLE ... ADD COLUMN IF NOT EXISTS "subFieldName"`. Comment in the file explains the cascade and why this lives in 2.3 instead of 2.5. - **Make idempotent** the existing `2-5/...-add-sub-field-name-to-view-sort.ts` — switched to `ADD COLUMN IF NOT EXISTS` / `DROP COLUMN IF EXISTS` so it's a no-op on cross-upgrade paths while still creating the column on fresh-from-2.5 installs. - Register the new command in `instance-commands.constant.ts`. The 2.5 command body change is semantically preserving (idempotent), and v2.5.0 hasn't shipped to any production DB yet — so this doesn't violate the "never rewrite committed instance commands" rule in spirit. ### Note on the previous two PRs #20581 and #20583 narrowed `select` on `WorkspaceEntity` for `isInternalMessagesImportEnabled`. That's a band-aid that works because there's a small, enumerable set of bare `workspaceRepository.findOne` call sites. It could in principle be replaced with the same pattern as this PR (early 2.x instance command that adds the workspace column). Not doing that here to keep the diff tight, but happy to follow up if preferred. ## Test plan - [ ] Re-run twenty-infra cross-version-upgrade CI and confirm 2.3 workspace commands complete - [ ] Verify the new 2.3 instance command and the modified 2.5 instance command are both idempotent (running upgrade twice should not error) - [ ] Verify a fresh install path still ends with `subFieldName` present on `core.viewSort`v2.5.0 |
||
|
|
484037c179 |
fix(server): scope workspace findOne in ApplicationService (#20583)
## Summary Cross-version upgrade still fails after #20581: ``` column WorkspaceEntity.isInternalMessagesImportEnabled does not exist at ApplicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow (application.service.ts:84) at UpdateGlobalObjectContextCommandMenuItemsCommand.runOnWorkspace (1-23-…) at BackfillRecordPageLayoutsCommand.runOnWorkspace (1-23-…) ``` (see https://github.com/twentyhq/twenty-infra/actions/runs/25861366732/job/75993012161) ### Root cause Same class of bug as #20581, different location. `ApplicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow` does: ```ts await this.workspaceRepository.findOne({ where: { id: workspaceId }, withDeleted: true, }); ``` No `select`, so TypeORM emits a SELECT for every column declared on `WorkspaceEntity`. PR #20457 added `isInternalMessagesImportEnabled` to the entity; its DB column is only created by the 2-5 fast instance command `1778525104406-add-is-internal-messages-import-enabled`. Many workspace commands across versions 1-21 → 2-3 call this service (notably the 1-23 commands shown in the stack), and they all run before the 2-5 instance command — so the bare findOne hits a column that doesn't exist yet and the upgrade aborts. ### Fix The function only reads `workspace.id` (passed to cache) and `workspace.workspaceCustomApplicationId`. Narrow the select to just those. The `workspace: WorkspaceEntity` input variant of the function is unchanged — only the path where we fetch the workspace ourselves is narrowed. Callers don't see the workspace entity (the function only returns `{ twentyStandardFlatApplication, workspaceCustomFlatApplication }`). ### Why not edit the committed 1-23 workspace commands Same reasoning as #20581: the fix lives in the service that does the read, so future column additions to `WorkspaceEntity` don't risk re-breaking every caller. Per `CLAUDE.md`, instance command `up`/`down` is immutable; this isn't an instance command. ## Test plan - [ ] Re-run the failing cross-version-upgrade job and confirm it gets past 1-23 - [ ] Verify the function still resolves the standard + custom applications correctly for a workspace (no behavior change in returned shape) |
||
|
|
a5982b644c |
fix(server): scope workspace findOne in 1-21 backfill-datasource command (#20581)
## Summary Cross-version upgrades from pre-1-21 instances currently fail with: ``` error: column WorkspaceEntity.isInternalMessagesImportEnabled does not exist ``` (see https://github.com/twentyhq/twenty-infra/actions/runs/25857499266/job/75979993686) ### Root cause The 1-21 workspace command `backfill-datasource-to-workspace` does: ```ts const workspace = await this.workspaceRepository.findOne({ where: { id: workspaceId }, }); ``` No `select`, so TypeORM emits a SELECT for every column declared on `WorkspaceEntity`. PR #20457 added `isInternalMessagesImportEnabled` to the entity, but its DB column is only created by the 2-5 fast instance command `1778525104406-add-is-internal-messages-import-enabled`. On a fresh cross-version upgrade, the runner reaches the 1-21 workspace segment before that 2-5 instance command runs, the bare `findOne` issues SELECT on a column that doesn't exist yet, and the upgrade aborts. ### Fix Narrow the select to just the columns this command actually reads (`id`, `databaseSchema`). The query now ignores entity columns added later in the upgrade sequence. ### Why edit a committed workspace command Per `CLAUDE.md`, committed *instance* command `up`/`down` logic is immutable. Workspace commands are idempotent backfills — adding a `select` narrows the read but doesn't change behavior, so it's safe. ### Audit Verified this is the only unguarded `workspaceRepository.find*` across the entire upgrade subtree: - `WorkspaceIteratorService.iterate` uses `select: ['databaseSchema']` - `WorkspaceVersionService.getActiveOrSuspendedWorkspaceIds` uses `select: ['id']` - `UpgradeStatusService.loadActiveOrSuspendedWorkspaces` uses `select: ['id', 'displayName']` ## Test plan - [ ] Re-run the failing cross-version upgrade job and confirm it gets past 1-21 - [ ] Verify the 1-21 backfill still correctly skips workspaces with a non-empty `databaseSchema` and backfills those without |
||
|
|
4054ede5bb |
i18n - translations (#20582)
Created by Github action --------- Co-authored-by: github-actions <github-actions@twenty.com> |
||
|
|
af4765effe |
feat(twenty-server): one-hop relation filters in GraphQL API (#20527)
## Summary
Adds support for filtering records by fields on a related MANY_TO_ONE
object via the GraphQL API. Backend only — no frontend, no REST, no
view-filter persistence yet.
```graphql
{
people(filter: { company: { name: { like: "%Airbnb%" } } }) {
edges { node { id } }
}
}
```
### Where the work lands
- **Schema** — `relation-field-metadata-gql-type.generator.ts` now emits
`{relationName}: TargetFilterInput` alongside the existing
`{joinColumnName}: UUIDFilter` for MANY_TO_ONE relations. Mirrors the
order-by generator that already does this for sort. Lazy thunks in
`object-metadata-filter-gql-input-type.generator.ts` handle the cycle
between filter inputs.
- **Arg processor** — `FilterArgProcessorService` no longer hard-rejects
accessing a relation by its name. When the value is a nested object on a
MANY_TO_ONE field, it recurses into the target object's metadata so each
leaf still gets validated and coerced. Depth-capped at 1.
- **Query parser** — new `parseRelationSubFilter` branch in
`graphql-query-filter-field.parser.ts`. When triggered: looks up the
target object metadata, calls `ensureRelationJoin` against the outer
query builder, and recurses via a child
`GraphqlQueryFilterConditionParser` scoped to the target.
`and`/`or`/`not` inside the relation filter keep working because the
child dispatches through the same `parseKeyFilter`.
- **Shared join utility** — `ensureRelationJoin.util.ts` is a single
function that inspects `queryBuilder.expressionMap.joinAttributes` for
the alias before adding a `LEFT JOIN`. Rewired the existing inline
`qb.leftJoin` calls in the order parser and group-by service to use it,
so filter-driven joins no longer collide with sort-driven joins on the
same relation.
### Out of scope (explicit)
- ONE_TO_MANY reverse traversal (needs EXISTS subqueries)
- Aggregates (`company.people.count > 5` — needs HAVING)
- View-filter storage (no `relationPath` column on `ViewFilterEntity`)
- REST DSL changes
- Frontend filter-picker UX
- Nesting deeper than one hop (parser and arg-processor both reject)
### Open question for review
Permissions. The order-by-on-relation code path already lets users sort
People by Company.name without a Company read-permission check, and this
PR matches that behavior for filters — felt wrong to add a stricter gate
only on the filter side. If we want object-permission gating on the
relation target, it should be a follow-up that covers both paths
consistently. The only attack surface today is existence inference via
timing, identical to what sort already exposes.
## Test plan
- [x] `tsc --noEmit` — clean for changed files (5 unrelated pre-existing
errors on main untouched)
- [x] `oxlint --type-aware` + `prettier --check` — 0 errors on all 17
changed/new files
- [x] `jest filter-arg-processor.service.spec` — 229 tests pass (the new
optional `flatObjectMetadataMaps` arg is backwards-compatible)
- [x] Integration test (`filter-by-relation-field.integration-spec.ts`,
6 cases) — needs to be verified against a seeded test DB. Could not
exercise the happy path in my isolated worktree; depth-2 rejection
passed there.
- [ ] EXPLAIN ANALYZE on the integration test query to confirm the FK on
`person.companyId` is indexed for both standard and custom MANY_TO_ONE
relations.
### Integration test cases
1. Filter People by `company.name = "Airbnb"` (exact match)
2. Filter People by `company.name like "%irbnb%"`
3. Non-matching filter returns empty
4. Combined with a scalar filter at root via `and`
5. **Combined with `orderBy` on the same relation** — proves the
join-dedupe works (without `ensureRelationJoin`, TypeORM throws
"duplicate alias")
6. Depth-2 nesting (`company.accountOwner.name`) returns
`INVALID_ARGS_FILTER`
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
a941f6fe01 |
feat(server): migrate TOTP secret encryption to SecretEncryptionService (#20577)
## Summary
Removes the last `APP_SECRET`-derived at-rest encryption site by
migrating `core.twoFactorAuthenticationMethod.secret` from
`SimpleSecretEncryptionUtil` (AES-256-CBC with key derived from
`sha256(APP_SECRET + userId + workspaceId + 'otp-secret' +
'KEY_ENCRYPTION_KEY')`) to the versioned `enc:v2:` envelope
(ENCRYPTION_KEY → HKDF-SHA256 bound to `workspaceId` → AES-256-GCM).
- New `decrypt-legacy-aes-cbc.util.ts` faithfully reproduces the
pre-migration CBC derivation byte-for-byte;
`SecretEncryptionService.decryptVersioned` dispatches to it when callers
pass `legacyAesCbcPurpose`, with a dedicated one-shot WARN log family.
- `TwoFactorAuthenticationService` now uses `encryptVersioned` /
`decryptVersioned` (passing the legacy purpose so existing rows still
decrypt). `SimpleSecretEncryptionUtil` and its spec are deleted;
`TwoFactorAuthenticationModule` imports `SecretEncryptionModule` in
their place.
- `TwoFactorAuthenticationMethodEntity` gets a `@Check` decorator
(`CHK_twoFactorAuthenticationMethod_secret_encrypted`) restricting
`secret` to the `enc:v2:` envelope; the matching 2.5 slow instance
command (`1798000009000-encrypt-totp-secrets`) cursor-paginates `JOIN`ed
`userWorkspace` rows to recover the legacy `userId`, re-encrypts to
`enc:v2`, and applies the CHECK constraint in `up()`.
### Deviation note
The plan suggested wiring a workspace-only legacy derivation directly
into `decryptVersioned`. In practice the production rows are
user-and-workspace-scoped (the legacy purpose is
`\${userId}\${workspaceId}otp-secret`), so a workspace-only derivation
could not recover them. The PR keeps the public `decryptVersioned` API
intact and adds an optional `legacyAesCbcPurpose` so callers that can
reconstruct the legacy context (the 2FA service and the slow command)
opt in.
### Final state of remaining `APP_SECRET` usages
- HS256 JWT verify (read-only, self-retiring once asymmetric migration
completes).
- Express-session cookie signing.
- Approved-access-domain HMAC (signing root, not at-rest).
- Zero-friction fallback in `resolveEncryptionKeysOrThrow`
(intentional).
No production at-rest data is encrypted with `APP_SECRET`-derived keys
anymore.
## Test plan
- [x] `npx jest src/engine/core-modules/secret-encryption
src/engine/core-modules/two-factor-authentication` — 170 unit tests
pass, including new unit tests for the legacy CBC util and the new
`SecretEncryptionService` fallback branch.
- [x] `npx jest --config ./jest-integration.config.ts
test/integration/upgrade/suites/2-5-instance-command-slow-1798000009000-encrypt-totp-secrets.integration-spec.ts`
— 4 integration tests cover legacy-CBC seed → slow command → `enc:v2`
round-trip, idempotency, CHECK constraint enforcement on `up()`, and
rollback via `down()`.
- [x] `npx oxlint --type-aware` and `npx prettier --check` clean on all
touched files.
- [ ] CI on this PR (server validation, tests, lint, typecheck).
|
||
|
|
fc53f18a9f |
Twenty discord integration (#20530)
<img width="1290" height="777" alt="Screenshot 2026-05-14 at 8 55 02 AM" src="https://github.com/user-attachments/assets/37cde89b-b4c3-438a-8ccf-39621f9799b4" /> https://github.com/user-attachments/assets/cd8af6f9-f2e5-47cf-bf1f-57590bb358d1 https://github.com/user-attachments/assets/ff53c6b7-ee1a-42db-bbc5-d7df5b3fa6b0 https://github.com/user-attachments/assets/ddca2c16-774d-4672-aa61-05e878d8b2b7 |
||
|
|
ddaba26abe |
chore(.vscode): add remaining packages to VSCode workspace (#20570)
## Context This PR extends the multi-root VSCode workspace configuration introduced in #2937 by adding the remaining packages from the `packages/*` directories to the `twenty.code-workspace` folders array. ## Problem Previously, some packages were not added to the multi-root workspace configuration. As a result, when opening the repository as a vscode workspace, those packages were hidden from the VSCode Explorer because they were not part of the configured workspace folders. ## Benefits - Prevents packages from being hidden when the repository is opened as a vscode workspace. - Improves consistency and navigation across the monorepo workspace experience. ## Related PR - #2937 |
||
|
|
42975a4168 |
fix(server): decouple SDK client generation from workspace activation (#20514)
`activateWorkspace` enqueues SDK gen job inside `WorkspaceManagerService.init()` introduced by https://github.com/twentyhq/twenty/pull/19271 But if enqueue call fails it crashes cuz it doesn't have try catch so created workspace is in corrupted state <img width="636" height="812" alt="image" src="https://github.com/user-attachments/assets/09acd042-46d0-4225-adc0-c74ea770785d" /> FIx: Move SDK enqueue out of `init()` Call after `activateAndInitializeUpgradeState` succeeds, wrap in try catch. Mirror preInstalledAppsService.installOnWorkspace pattern. Assuming enqueue failure if Redis is unavailable we fallback to `SdkClientArchiveService.downloadArchiveBufferOrGenerate` which generates it on the fly Around 19 workspaces in prod affected with status `ONGOING_CREATION` |
||
|
|
dbc033b29b |
fix(auth): exclude /authorize from MinimalMetadataGater loading gate (#20572)
## Summary Fixes the blank `/authorize` page reported when a user reopens the OAuth consent screen on `app.twenty.com` after a prior multi-workspace SSO sign-in. ### Reproduction 1. ChatGPT (or any MCP client) opens `https://app.twenty.com/authorize?client_id=…` while signed out. 2. User picks "Continue with Google", lands in workspace selection, picks a workspace, authorizes the app. Works. 3. Some time later, ChatGPT re-opens `https://app.twenty.com/authorize?client_id=…`. 4. **Observed:** fully blank page, no console errors. Deleting the `tokenPair` cookie unblocks it. ### Root cause The multi-workspace social-SSO branch ([auth.service.ts:988-1011](https://github.com/twentyhq/twenty/blob/main/packages/twenty-server/src/engine/core-modules/auth/services/auth.service.ts#L988-L1011)) lands the user on `app.twenty.com/welcome?tokenPair=…` with a workspace-agnostic token. `SignInUpGlobalScopeFormEffect` writes that into the host-scoped `tokenPair` cookie on `app.twenty.com`, and nothing clears it after the user proceeds to a workspace subdomain. On the next visit to `app.twenty.com/authorize?…`, `MinimalMetadataGater` sees `hasAccessTokenPair === true` and renders `<UserOrMetadataLoader />` instead of `<Authorize />`. The loader never goes away because: - `IsMinimalMetadataReadyEffect` waits for `metadataStore.status === 'up-to-date'`. - `MinimalMetadataLoadEffect` only loads metadata when `hasAccessTokenPair && isActiveWorkspace`, and the default domain has no workspace context. Skeleton loader stays forever → user perceives "blank page". Some users get rescued by `WorkspaceProviderEffect` auto-redirecting them to their last-authenticated workspace subdomain, but that cookie is set with `domain: .twenty.com` and silently fails to persist for users on custom domains — so the bug is most visible there. ### Fix `/authorize` only issues `findApplicationRegistrationByClientId`, which is a `PublicEndpointGuard` query. It doesn't need workspace metadata. Add it to the gater's excluded-paths list alongside the existing pre-auth pages (`SignInUp`, `Verify`, `Invite`, …) so the page renders immediately regardless of token state. This is a one-line, minimum-blast-radius fix. Two related cleanups are separate concerns and not addressed here: - Clearing the workspace-agnostic `tokenPair` cookie on `app.twenty.com` after workspace selection. - Fixing `lastAuthenticatedWorkspaceDomain` propagation for custom-domain users. ## Test plan - [x] `npx oxlint` + `prettier --check` on the touched file — clean. - [x] `npx nx typecheck twenty-front` — clean. - [ ] Manual: with a stale `tokenPair` cookie set on `app.twenty.com`, open `https://app.twenty.com/authorize?client_id=<valid>&…` — consent screen renders, no blank. - [ ] Manual: signed-out → open the same URL — still redirects to `/welcome` and back through the flow. - [ ] Manual: signed-in on a workspace subdomain → open `https://app.twenty.com/authorize?…` — `WorkspaceProviderEffect` auto-redirect still kicks in (unchanged behavior). |
||
|
|
72f857fc10 |
[Website] Refine feature card scroll entrance to a subtle opacity fade (#20574)
Thomas mentioned the animations need to be subtle. Therefore, this PR removes transform-based slide/rotate animations in favor of a clean 0.6s opacity fade for a polished, professional appearance. Before: https://github.com/user-attachments/assets/bede627f-d4b3-4126-8eb0-c1d5a9b4d16a After: https://github.com/user-attachments/assets/85cb604b-a8c9-4730-85d3-8edb8ef0fc71 |
||
|
|
a47e1e0e5e |
Fix time consuming search ilike fallback (#20544)
## Context
When the tsvector full-text search returns 0 hits on the first page,
SearchService falls back to ILIKE '%word%' over searchVector::text. The
leading wildcard makes the GIN index unusable, so it seq-scans the
table.
On large searchable custom objects (e.g. a workspace with ~500k rows in
_logs) a single fallback can take 2–3s, multiplied across all searchable
objects in one request.
## Implementation
Wrap the fallback query in a tiny TypeORM transaction and apply a
Postgres per-statement timeout via set_config('statement_timeout', ms,
true) (= SET LOCAL). On timeout, Postgres throws 57014 (QUERY_CANCELED);
we catch it, warn-log with workspace/object context, and return [] for
that object
## Note
This PR bounds the slow fallback and doesn't make it fast. The right
structural fix is to let the fallback use an index. Since tsvector does
not work with certain language (which is the reason why the ILIKE
fallback was implemented in the first place), we should probably use the
pg_trgm extension instead (@FelixMalfait)
---------
Co-authored-by: Charles Bochet <charles@twenty.com>
|
||
|
|
0d5617d446 |
chore(server): drop unused postgresCredentials feature (#20573)
## Summary Drops the `postgresCredentials` legacy feature: a never-finished "postgres proxy" that would have let users query their workspace data over a standard Postgres connection. Nothing — frontend, e2e, Zapier, docs, other server code — calls these mutations/query. ## History - **Introduced** June 2024 (#5767, Thomas Trompette) as "first step for creating credentials for database proxy", alongside the Postgres FDW / remote-server work and the custom `twenty-postgres-spilo` image. Planned follow-ups (provisioning a DB on the proxy, mapping users, exposing it as a remote server) never landed. - **Abandoned** January 2026 (#17001, Weiko) when the sibling "remote integration" feature was removed as a BREAKING CHANGE — "not maintained for more than a year and never officially launched". The spilo image was then replaced with vanilla `postgres:16` (#19182, March 2026), retiring the FDW infrastructure entirely. - This PR finishes the cleanup: removes the orphaned module, the `allPostgresCredentials` relation, `JwtTokenTypeEnum.POSTGRES_PROXY` + payload, the reserved metadata keywords, and adds a 2.5.0 fast instance command that drops `core.postgresCredentials` (reversible `down`). Regenerated frontend GraphQL types + SDK metadata client. ## Test plan - [x] `tsgo --noEmit` clean on twenty-server + twenty-front; lint + prettier clean on touched files. - [x] `database:migrate:generate` reports no pending schema diff; server boots and serves the new schema. |
||
|
|
61683d8bda |
[Website] Replace product page hero visual with interactive CRM depicting AI chat in action. (#20566)
Before: <img width="1439" height="518" alt="image" src="https://github.com/user-attachments/assets/4d294a1b-c5a0-43b2-9895-61a8ee19da62" /> After: https://github.com/user-attachments/assets/c019586f-ef9f-4ae0-8afe-14f08e8cb057 |
||
|
|
ab705b14d7 |
i18n - translations (#20569)
Created by Github action --------- Co-authored-by: github-actions <github-actions@twenty.com> |
||
|
|
90537b3b88 |
i18n - translations (#20567)
Created by Github action --------- Co-authored-by: github-actions <github-actions@twenty.com> |
||
|
|
a754a95d38 | Regrant id token write to claude for oidc swap (#20564) | ||
|
|
8dd0aa46e3 |
Update OAuth consent modal design (#20540)
## Summary Updates the OAuth consent screen to match the provided modal treatment, including the header artwork, app-to-Twenty logo layout, scope icons, and a content-hugging title that stays on one line until its max width. Also preserves `returnToPath` through Google and Microsoft social SSO so users who sign in from `/authorize` return to the OAuth consent flow with the original OAuth query parameters intact. ## Reference  ## Validation - `yarn nx test twenty-front --testFile=src/modules/auth/hooks/__tests__/useAuth.test.tsx --coverage=false` - `yarn nx test twenty-server --testFile=src/engine/core-modules/auth/services/auth.service.spec.ts --coverage=false` - `yarn nx typecheck twenty-front` - `yarn nx typecheck twenty-server` - `git diff --check` ## Notes The reference screenshot is linked from a separate branch-hosted image commit so it renders in the PR body without adding that screenshot to the product diff. Co-authored-by: Charles Bochet <charles@twenty.com> |
||
|
|
91b2390ee8 |
chore(deps-dev): bump vite-plugin-svgr from 4.3.0 to 4.5.0 (#20561)
Bumps [vite-plugin-svgr](https://github.com/pd4d10/vite-plugin-svgr) from 4.3.0 to 4.5.0. <details> <summary>Release notes</summary> <p><em>Sourced from <a href="https://github.com/pd4d10/vite-plugin-svgr/releases">vite-plugin-svgr's releases</a>.</em></p> <blockquote> <h2>v4.5.0</h2> <p><em>No significant changes</em></p> <h5> <a href="https://github.com/pd4d10/vite-plugin-svgr/compare/v4.4.0...v4.5.0">View changes on GitHub</a></h5> </blockquote> </details> <details> <summary>Commits</summary> <ul> <li><a href="https://github.com/pd4d10/vite-plugin-svgr/commit/d1f8887202a6edd119053c665f0679419df70025"><code>d1f8887</code></a> 4.5.0</li> <li><a href="https://github.com/pd4d10/vite-plugin-svgr/commit/b326999d5a2546227bb4b2d2cdcf6092392f4d11"><code>b326999</code></a> ci: fix npm publish</li> <li><a href="https://github.com/pd4d10/vite-plugin-svgr/commit/79300b869297ab2063a730f33ec6f7984b8acbe8"><code>79300b8</code></a> 4.4.0</li> <li><a href="https://github.com/pd4d10/vite-plugin-svgr/commit/b7d3c36dd658a6b8344b82307f36dc3acb0d1479"><code>b7d3c36</code></a> chore: update deps</li> <li><a href="https://github.com/pd4d10/vite-plugin-svgr/commit/6f9d9af421ec891c1594b5511d4865cea6c8f79a"><code>6f9d9af</code></a> feat: support rolldown-vite via <code>transformWithOxc</code> (<a href="https://redirect.github.com/pd4d10/vite-plugin-svgr/issues/130">#130</a>)</li> <li><a href="https://github.com/pd4d10/vite-plugin-svgr/commit/cef5adead59da0abfc0b81edf4b2022b193e4607"><code>cef5ade</code></a> ci: fix release note</li> <li>See full diff in <a href="https://github.com/pd4d10/vite-plugin-svgr/compare/v4.3.0...v4.5.0">compare view</a></li> </ul> </details> <br /> [](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) --- <details> <summary>Dependabot commands and options</summary> <br /> You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) </details> Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|
|
b36345a699 |
chore(deps-dev): bump @mui/material from 7.3.8 to 7.3.11 (#20562)
Bumps [@mui/material](https://github.com/mui/material-ui/tree/HEAD/packages/mui-material) from 7.3.8 to 7.3.11. <details> <summary>Release notes</summary> <p><em>Sourced from <a href="https://github.com/mui/material-ui/releases">@mui/material's releases</a>.</em></p> <blockquote> <h2>v7.3.11</h2> <p>A big thanks to the 5 contributors who made this release possible.</p> <h3><code>@mui/material@7.3.11</code></h3> <ul> <li>[autocomplete] Fix highlight sync and scroll preservation (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48350">#48350</a>) <a href="https://github.com/mj12albert"><code>@mj12albert</code></a></li> <li>[autocomplete] Fix popper rendering issues (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48343">#48343</a>) <a href="https://github.com/mj12albert"><code>@mj12albert</code></a></li> <li>[autocomplete] Improve highlight tracking and selection state (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48318">#48318</a>) <a href="https://github.com/mj12albert"><code>@mj12albert</code></a></li> <li>[button] Fix <code>startIcon</code> alignment (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48339">#48339</a>) <a href="https://github.com/mj12albert"><code>@mj12albert</code></a></li> <li>[button] Remove duplicated className entries (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48284">#48284</a>) <a href="https://github.com/silviuaavram"><code>@silviuaavram</code></a></li> <li>[checkbox] Set <code>aria-checked=mixed</code> when indeterminate (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48286">#48286</a>) <a href="https://github.com/mj12albert"><code>@mj12albert</code></a></li> <li>[dialog][drawer][focus trap] Fix initial focus target (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48324">#48324</a>) <a href="https://github.com/mj12albert"><code>@mj12albert</code></a></li> <li>[drawer] Fix transition jump (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48340">#48340</a>) <a href="https://github.com/mj12albert"><code>@mj12albert</code></a></li> <li>[input] Fix layout shift with display: flex (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48359">#48359</a>) <a href="https://github.com/oliviertassinari"><code>@oliviertassinari</code></a></li> <li>[inputs] Fix autofocus in SSR environment (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48307">#48307</a>) <a href="https://github.com/mj12albert"><code>@mj12albert</code></a></li> <li>[popper] Persist positioning styles when popperOptions changes reference (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48302">#48302</a>) <a href="https://github.com/mj12albert"><code>@mj12albert</code></a></li> <li>[switch] Fix incorrect <code>role</code> with <code>slotProps.input</code> (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48472">#48472</a>) <a href="https://github.com/mj12albert"><code>@mj12albert</code></a></li> <li>[utils] Add shadow dom utils (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48309">#48309</a>) <a href="https://github.com/mj12albert"><code>@mj12albert</code></a></li> </ul> <h3>Docs</h3> <ul> <li>[docs] Update banner to announce v9 (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48299">#48299</a>) <a href="https://github.com/siriwatknp"><code>@siriwatknp</code></a></li> <li>[docs] Add v9 in the versions select in v7.mui.com (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48233">#48233</a>) <a href="https://github.com/alexfauquette"><code>@alexfauquette</code></a></li> </ul> <h3>Core</h3> <ul> <li>[internal] Update some host-reference entries (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48225">#48225</a>) <a href="https://github.com/silviuaavram"><code>@silviuaavram</code></a></li> </ul> <p>All contributors of this release in alphabetical order: <a href="https://github.com/alexfauquette"><code>@alexfauquette</code></a>, <a href="https://github.com/mj12albert"><code>@mj12albert</code></a>, <a href="https://github.com/oliviertassinari"><code>@oliviertassinari</code></a>, <a href="https://github.com/silviuaavram"><code>@silviuaavram</code></a>, <a href="https://github.com/siriwatknp"><code>@siriwatknp</code></a></p> <h2>v7.3.10</h2> <p>A big thanks to the 15 contributors who made this release possible. A few highlights ✨:</p> <ul> <li>📖 Added the Menubar component page to the docs.</li> </ul> <h3><code>@mui/material@7.3.10</code></h3> <ul> <li>[alert][dialog] Accessibility improvements (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48161">#48161</a>) <a href="https://github.com/silviuaavram"><code>@silviuaavram</code></a></li> <li>[autocomplete] Add <code>root</code> slot (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/47916">#47916</a>) <a href="https://github.com/GerardasB"><code>@GerardasB</code></a></li> <li>[autocomplete] Fix helper text focusing input when clicked (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48162">#48162</a>) <a href="https://github.com/mj12albert"><code>@mj12albert</code></a></li> <li>[autocomplete] Fix popup reopening on window focus regain with openOnFocus (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/47914">#47914</a>) <a href="https://github.com/aman44444"><code>@aman44444</code></a></li> <li>[autocomplete] Optimize selected option lookup (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48027">#48027</a>) <a href="https://github.com/anchmelev"><code>@anchmelev</code></a></li> <li>[autocomplete] Support full slots for clearIndicator and popupIndicator (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/47913">#47913</a>) <a href="https://github.com/silviuaavram"><code>@silviuaavram</code></a></li> <li>[button-base] Fix native button detection (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/47994">#47994</a>) <a href="https://github.com/mj12albert"><code>@mj12albert</code></a></li> <li>[input] Fix high contrast cutoff on first character (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48160">#48160</a>) <a href="https://github.com/silviuaavram"><code>@silviuaavram</code></a></li> <li>[list item text][card header] Revert cleanup of duplicated CSS rules (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/47938">#47938</a>) <a href="https://github.com/sai6855"><code>@sai6855</code></a></li> <li>[popper] Add missing classes export (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48033">#48033</a>) <a href="https://github.com/mj12albert"><code>@mj12albert</code></a></li> <li>[select] Fix focus visible always set on menu item (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48022">#48022</a>) <a href="https://github.com/silviuaavram"><code>@silviuaavram</code></a></li> <li>[slider] Accept readonly array for the value prop (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/47961">#47961</a>) <a href="https://github.com/pcorpet"><code>@pcorpet</code></a></li> <li>[switch] Add border to make it visible in high contrast mode (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48210">#48210</a>) <a href="https://github.com/silviuaavram"><code>@silviuaavram</code></a></li> </ul> <!-- raw HTML omitted --> </blockquote> <p>... (truncated)</p> </details> <details> <summary>Changelog</summary> <p><em>Sourced from <a href="https://github.com/mui/material-ui/blob/v7.3.11/CHANGELOG.md">@mui/material's changelog</a>.</em></p> <blockquote> <h2>7.3.11</h2> <!-- raw HTML omitted --> <p><em>May 6, 2026</em></p> <p>A big thanks to the 5 contributors who made this release possible.</p> <h3><code>@mui/material@7.3.11</code></h3> <ul> <li>[autocomplete] Fix highlight sync and scroll preservation (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48350">#48350</a>) <a href="https://github.com/mj12albert"><code>@mj12albert</code></a></li> <li>[autocomplete] Fix popper rendering issues (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48343">#48343</a>) <a href="https://github.com/mj12albert"><code>@mj12albert</code></a></li> <li>[autocomplete] Improve highlight tracking and selection state (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48318">#48318</a>) <a href="https://github.com/mj12albert"><code>@mj12albert</code></a></li> <li>[button] Fix <code>startIcon</code> alignment (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48339">#48339</a>) <a href="https://github.com/mj12albert"><code>@mj12albert</code></a></li> <li>[button] Remove duplicated className entries (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48284">#48284</a>) <a href="https://github.com/silviuaavram"><code>@silviuaavram</code></a></li> <li>[checkbox] Set <code>aria-checked=mixed</code> when indeterminate (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48286">#48286</a>) <a href="https://github.com/mj12albert"><code>@mj12albert</code></a></li> <li>[dialog][drawer][focus trap] Fix initial focus target (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48324">#48324</a>) <a href="https://github.com/mj12albert"><code>@mj12albert</code></a></li> <li>[drawer] Fix transition jump (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48340">#48340</a>) <a href="https://github.com/mj12albert"><code>@mj12albert</code></a></li> <li>[input] Fix layout shift with display: flex (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48359">#48359</a>) <a href="https://github.com/oliviertassinari"><code>@oliviertassinari</code></a></li> <li>[inputs] Fix autofocus in SSR environment (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48307">#48307</a>) <a href="https://github.com/mj12albert"><code>@mj12albert</code></a></li> <li>[popper] Persist positioning styles when popperOptions changes reference (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48302">#48302</a>) <a href="https://github.com/mj12albert"><code>@mj12albert</code></a></li> <li>[switch] Fix incorrect <code>role</code> with <code>slotProps.input</code> (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48472">#48472</a>) <a href="https://github.com/mj12albert"><code>@mj12albert</code></a></li> <li>[utils] Add shadow dom utils (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48309">#48309</a>) <a href="https://github.com/mj12albert"><code>@mj12albert</code></a></li> </ul> <h3>Docs</h3> <ul> <li>[docs] Update banner to announce v9 (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48299">#48299</a>) <a href="https://github.com/siriwatknp"><code>@siriwatknp</code></a></li> <li>[docs] Add v9 in the versions select in v7.mui.com (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48233">#48233</a>) <a href="https://github.com/alexfauquette"><code>@alexfauquette</code></a></li> </ul> <h3>Core</h3> <ul> <li>[internal] Update some host-reference entries (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48225">#48225</a>) <a href="https://github.com/silviuaavram"><code>@silviuaavram</code></a></li> </ul> <p>All contributors of this release in alphabetical order: <a href="https://github.com/alexfauquette"><code>@alexfauquette</code></a>, <a href="https://github.com/mj12albert"><code>@mj12albert</code></a>, <a href="https://github.com/oliviertassinari"><code>@oliviertassinari</code></a>, <a href="https://github.com/silviuaavram"><code>@silviuaavram</code></a>, <a href="https://github.com/siriwatknp"><code>@siriwatknp</code></a></p> <h2>7.3.10</h2> <!-- raw HTML omitted --> <p><em>Apr 8, 2026</em></p> <p>A big thanks to the 15 contributors who made this release possible. A few highlights ✨:</p> <ul> <li>📖 Added the Menubar component page to the docs.</li> </ul> <h3><code>@mui/material@7.3.10</code></h3> <ul> <li>[alert][dialog] Accessibility improvements (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48161">#48161</a>) <a href="https://github.com/silviuaavram"><code>@silviuaavram</code></a></li> <li>[autocomplete] Add <code>root</code> slot (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/47916">#47916</a>) <a href="https://github.com/GerardasB"><code>@GerardasB</code></a></li> <li>[autocomplete] Fix helper text focusing input when clicked (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48162">#48162</a>) <a href="https://github.com/mj12albert"><code>@mj12albert</code></a></li> </ul> <!-- raw HTML omitted --> </blockquote> <p>... (truncated)</p> </details> <details> <summary>Commits</summary> <ul> <li><a href="https://github.com/mui/material-ui/commit/6ddda377e979d1783b2cc00226098fde4509bb2e"><code>6ddda37</code></a> [release] v7.3.11 (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48478">#48478</a>)</li> <li><a href="https://github.com/mui/material-ui/commit/a2c7fa0eaeec48767ef7728a1d418609e0bf44ea"><code>a2c7fa0</code></a> [switch] Fix incorrect <code>role</code> with <code>slotProps.input</code> (<a href="https://github.com/mj12albert"><code>@mj12albert</code></a>) (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48472">#48472</a>)</li> <li><a href="https://github.com/mui/material-ui/commit/c3628bd4930e9089fdf41cba78fc2b5798a2bc76"><code>c3628bd</code></a> [input] Fix layout shift with display: flex (<a href="https://github.com/oliviertassinari"><code>@oliviertassinari</code></a>) (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48359">#48359</a>)</li> <li><a href="https://github.com/mui/material-ui/commit/290c89694eae625cf4ad20f874098f0b8889ceb4"><code>290c896</code></a> [autocomplete] Fix highlight sync and scroll preservation (<a href="https://github.com/mj12albert"><code>@mj12albert</code></a>) (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48350">#48350</a>)</li> <li><a href="https://github.com/mui/material-ui/commit/6e172b760815dc3dc8254f58e0dcd793fe690979"><code>6e172b7</code></a> [autocomplete] Fix popper rendering issues (<a href="https://github.com/mj12albert"><code>@mj12albert</code></a>) (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48343">#48343</a>)</li> <li><a href="https://github.com/mui/material-ui/commit/3f2323ce5e780134dcbe75802cf5ab5035822dd4"><code>3f2323c</code></a> [drawer] Fix transition jump (<a href="https://github.com/mj12albert"><code>@mj12albert</code></a>) (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48340">#48340</a>)</li> <li><a href="https://github.com/mui/material-ui/commit/1afbdfebd521eb96c2bd4d43ec1f9b2f48960a00"><code>1afbdfe</code></a> [button] Fix <code>startIcon</code> alignment (<a href="https://github.com/mj12albert"><code>@mj12albert</code></a>) (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48339">#48339</a>)</li> <li><a href="https://github.com/mui/material-ui/commit/dcef24179ffd7a08f47e4c6b656cd58181c34fdc"><code>dcef241</code></a> [dialog][drawer][focus trap] Fix initial focus target (<a href="https://github.com/mj12albert"><code>@mj12albert</code></a>) (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48324">#48324</a>)</li> <li><a href="https://github.com/mui/material-ui/commit/4b0275ea82d1807324eb04034db50501eac1f802"><code>4b0275e</code></a> [autocomplete] Improve highlight tracking and selection state (<a href="https://github.com/mj12albert"><code>@mj12albert</code></a>) (...</li> <li><a href="https://github.com/mui/material-ui/commit/47e41decac18a891f329f854b0d22e2585fa95be"><code>47e41de</code></a> [internal] Update some host-reference entries (<a href="https://github.com/mui/material-ui/tree/HEAD/packages/mui-material/issues/48225">#48225</a>)</li> <li>Additional commits viewable in <a href="https://github.com/mui/material-ui/commits/v7.3.11/packages/mui-material">compare view</a></li> </ul> </details> <br /> [](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) --- <details> <summary>Dependabot commands and options</summary> <br /> You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) </details> Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|
|
34d9fcaba1 |
chore(auth): drop unused workspacePersonalInviteToken from SSO state (#20557)
## Summary
Pure dead-code removal. The Google and Microsoft SSO strategies have
been packing `workspacePersonalInviteToken` into the OAuth `state` blob
and re-emitting it on `validate()`, but
[`signInUpWithSocialSSO`](packages/twenty-server/src/engine/core-modules/auth/services/auth.service.ts)
never destructures or reads it from the user object. The SSO flow
resolves invitations by the IdP-verified email instead:
```ts
const invitation =
currentWorkspace && email
? await this.findInvitationForSignInUp({
currentWorkspace,
email, // ← matched against appToken.context.email
})
: undefined;
```
So the strategy plumbing is write-only and confusing for readers.
Removed from:
-
[`SocialSSOState`](packages/twenty-server/src/engine/core-modules/auth/types/social-sso-state.type.ts)
- `GoogleRequest['user']` and `MicrosoftRequest['user']`
- The `state` JSON in both strategies' `authenticate()`
- The user object in both strategies' `validate()`
No frontend change needed — `useAuth.buildRedirectUrl` still sets the
`inviteToken` query param when a personal invite token is present (used
by other paths), and nothing on the SSO server side was reading it.
The token-based invitation lookup is preserved for the password signup
flow via `auth.resolver.signUp` → `findInvitationForSignInUp({
currentWorkspace, workspacePersonalInviteToken })`. Unrelated,
untouched.
## Test plan
- [x] `npx jest engine/core-modules/auth` (twenty-server) — 26 suites /
178 tests pass.
- [x] `tsgo -p tsconfig.json --noEmit` — no new errors on the touched
files (pre-existing `IS_REST_METADATA_API_NEW_FORMAT_DIRECT` errors on
main are unrelated).
- [x] `oxlint` + `prettier --check` on touched files — clean.
- [ ] Manual smoke: Google sign-in still works (workspace selection /
verify flow unaffected since `workspaceInviteHash`, `workspaceId`,
`action`, `locale`, `billingCheckoutSessionState`, `returnToPath` still
flow correctly).
|
||
|
|
8edc76d3d0 |
chore(deps): bump react-dropzone from 14.2.3 to 14.4.1 (#20560)
Bumps [react-dropzone](https://github.com/react-dropzone/react-dropzone) from 14.2.3 to 14.4.1. <details> <summary>Release notes</summary> <p><em>Sourced from <a href="https://github.com/react-dropzone/react-dropzone/releases">react-dropzone's releases</a>.</em></p> <blockquote> <h2>v14.4.1</h2> <h2><a href="https://github.com/react-dropzone/react-dropzone/compare/v14.4.0...v14.4.1">14.4.1</a> (2026-02-10)</h2> <h3>Bug Fixes</h3> <ul> <li>prevent tarball from being included in published package (<a href="https://github.com/react-dropzone/react-dropzone/commit/7919a235412ee5a224e76b9e9bef25fdb5e8ff0b">7919a23</a>)</li> </ul> <h2>v14.4.0</h2> <h1><a href="https://github.com/react-dropzone/react-dropzone/compare/v14.3.8...v14.4.0">14.4.0</a> (2026-01-29)</h1> <h3>Bug Fixes</h3> <ul> <li>accept files with empty type during drag events (<a href="https://github.com/react-dropzone/react-dropzone/commit/eaa8ba54963480afbba50415b1dd792514fefac1">eaa8ba5</a>)</li> <li>correct dragLeave filter logic and add dragend test (<a href="https://github.com/react-dropzone/react-dropzone/commit/273aff4a151aba05ddd473cbc49dc3db59132f2f">273aff4</a>)</li> </ul> <h3>Features</h3> <ul> <li>add isDragGlobal state for document-level drag detection (<a href="https://github.com/react-dropzone/react-dropzone/commit/f0874b0ad8e94dbf662b16bc82d0aa2b082ec8ee">f0874b0</a>)</li> </ul> <h2>v14.3.8</h2> <h2><a href="https://github.com/react-dropzone/react-dropzone/compare/v14.3.7...v14.3.8">14.3.8</a> (2025-02-24)</h2> <h3>Bug Fixes</h3> <ul> <li><strong>event_type:</strong> 🎨 Update drop event type to include FileSystemFileHandle (<a href="https://github.com/react-dropzone/react-dropzone/commit/d6911c991e077151e302b599b92269432ab0472b">d6911c9</a>)</li> </ul> <h2>v14.3.7</h2> <h2><a href="https://github.com/react-dropzone/react-dropzone/compare/v14.3.6...v14.3.7">14.3.7</a> (2025-02-24)</h2> <h3>Bug Fixes</h3> <ul> <li>make ESM build compatible with native Node.js (<a href="https://github.com/react-dropzone/react-dropzone/commit/201687900724b45ec98d26cde3626a1c6687c9e1">2016879</a>)</li> </ul> <h2>v14.3.6</h2> <h2><a href="https://github.com/react-dropzone/react-dropzone/compare/v14.3.5...v14.3.6">14.3.6</a> (2025-02-23)</h2> <h3>Bug Fixes</h3> <ul> <li><strong>types:</strong> fix React 19 incompatible JSX type import (<a href="https://github.com/react-dropzone/react-dropzone/commit/356d9d5cb604c47e393f332f3dfe0e8d12c58d95">356d9d5</a>)</li> </ul> <h2>v14.3.5</h2> <h2><a href="https://github.com/react-dropzone/react-dropzone/compare/v14.3.4...v14.3.5">14.3.5</a> (2024-11-04)</h2> <!-- raw HTML omitted --> </blockquote> <p>... (truncated)</p> </details> <details> <summary>Commits</summary> <ul> <li><a href="https://github.com/react-dropzone/react-dropzone/commit/9247d71a4dd6d9f36f177fee31c90a327a34d297"><code>9247d71</code></a> chore: add *.tgz to .gitignore</li> <li><a href="https://github.com/react-dropzone/react-dropzone/commit/7919a235412ee5a224e76b9e9bef25fdb5e8ff0b"><code>7919a23</code></a> fix: prevent tarball from being included in published package</li> <li><a href="https://github.com/react-dropzone/react-dropzone/commit/e2ae36d0c910e64ea7e269e741f26c909f758000"><code>e2ae36d</code></a> build: remove <code>@semantic-release/git</code></li> <li><a href="https://github.com/react-dropzone/react-dropzone/commit/84ff2d6671f95c925cb3e5c4b785fabd1a13c9fc"><code>84ff2d6</code></a> build: update deps for semantic-release and node version</li> <li><a href="https://github.com/react-dropzone/react-dropzone/commit/65095ea5c15b34724793ad38248bf10cd3fef147"><code>65095ea</code></a> build: add --ignore-engines flag to test workflow for Node 18 compatibility</li> <li><a href="https://github.com/react-dropzone/react-dropzone/commit/d70ea9632b70770b8d5b68d214f9532842120005"><code>d70ea96</code></a> build: enable passwordless publishing with NPM trusted publishers</li> <li><a href="https://github.com/react-dropzone/react-dropzone/commit/2c2c56ee46aefaaa3e81778d822dca4eb2e3c7f7"><code>2c2c56e</code></a> build: add full git history fetch and build step to release workflow</li> <li><a href="https://github.com/react-dropzone/react-dropzone/commit/a9e97413c8a7684d5495ba4c869a8c629e210c59"><code>a9e9741</code></a> build: add semantic-release configuration with trusted publishing support</li> <li><a href="https://github.com/react-dropzone/react-dropzone/commit/1455f4cc0a3c389a056532027017e92874d41fba"><code>1455f4c</code></a> build: Update checkout action to version 4 in release workflow</li> <li><a href="https://github.com/react-dropzone/react-dropzone/commit/f4e67064ebc9310ac0e6e04d6f04cc7c4b4da057"><code>f4e6706</code></a> build: Update Node.js setup action to version 4</li> <li>Additional commits viewable in <a href="https://github.com/react-dropzone/react-dropzone/compare/v14.2.3...v14.4.1">compare view</a></li> </ul> </details> <details> <summary>Maintainer changes</summary> <p>This version was pushed to npm by <a href="https://www.npmjs.com/~GitHub%20Actions">GitHub Actions</a>, a new releaser for react-dropzone since your current version.</p> </details> <br /> [](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) --- <details> <summary>Dependabot commands and options</summary> <br /> You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) </details> Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|
|
7fa136f305 |
feat(twenty-server): migrate remaining at-rest encryption sites to versioned envelope (#20550)
## Summary Second PR in the encryption key rotation series. The previous PR (#20528) introduced `ENCRYPTION_KEY` + the versioned `enc:v2:<keyId>:<base64>` envelope inside `SecretEncryptionService` and migrated `ConnectedAccountTokenEncryptionService` as the first consumer. This PR routes every remaining at-rest encryption site through the versioned envelope so that `ENCRYPTION_KEY` (and the future `FALLBACK_ENCRYPTION_KEY`) actually covers them. The legacy unprefixed CTR ciphertext remains readable as a fallback during the rollout window — every migrated read site uses `decryptVersioned`, which transparently delegates to the legacy CTR decrypt when it sees an unprefixed payload. ### Service migrations - **`ApplicationVariableEntityService` (#8)** — workspace-scoped. HKDF info is bound to each row's `workspaceId`. A new `decryptAndMaskVersioned` helper lands on `SecretEncryptionService` for the resolver display path. - **`ApplicationRegistrationVariableService` (#7)** + consumers — **instance-scoped**. Registration variables are server-level config readable by every workspace that installs the application, so HKDF info is `instance`. Updated consumers: - `LogicFunctionExecutorService.buildServerVariableEnvMap` - `ConnectionProviderService.getClientCredentials` - **`LogicFunctionExecutorService.buildEnvVar` (#9)** — workspace-scoped. Each variable's `workspaceId` is threaded into `decryptVersioned`, so per-workspace HKDF contexts are honoured at execution time. - **`UpdateApplicationVariableActionHandlerService`** (workspace-migration runner) — threads `workspaceId` through the secret/non-secret toggle. - **`JwtKeyManagerService` (#3)** — instance-scoped. Signing keys are shared across the JWKS. - **`ConfigStorageService` (#6)** — instance-scoped sensitive STRING config variables. ### Slow instance commands (2.5.0) Each migrated site has a paired backfill that re-encrypts existing rows into the v2 envelope before the column is constrained: | timestamp | command | scope | CHECK constraint | |---|---|---|---| | `1798000005000` | encrypt-application-variable | workspaceId | `"isSecret" = false OR value = '' OR value LIKE 'enc:v2:%'` | | `1798000006000` | encrypt-application-registration-variable | instance | `"encryptedValue" = '' OR value LIKE 'enc:v2:%'` | | `1798000007000` | encrypt-signing-key-private-keys | instance | `"privateKey" IS NULL OR value LIKE 'enc:v2:%'` | | `1798000008000` | encrypt-sensitive-config-storage | instance | _none_ — heterogeneous jsonb column | All backfills are idempotent (the SELECT filter skips rows already in v2 form) and run before their respective `up()` adds the CHECK constraint. Every `down()` deliberately stops at dropping the CHECK constraint — they intentionally do not re-introduce plaintext on rollback. ### Tests - Unit specs for each new slow command cover the v2 upgrade path, the idempotency invariant, and the instance vs workspace HKDF scope. - New `JwtKeyManagerService` spec asserts `decryptVersioned`/`encryptVersioned` are called without `workspaceId` (instance scope). - Updated existing specs for `ApplicationVariableEntityService`, `ConfigStorageService`, and `buildEnvVar` to assert the versioned API and the workspace HKDF context plumbing. - New `SecretEncryptionService.decryptAndMaskVersioned` cases in the service spec. - Updated the `applicationRegistrationVariable` integration spec to assert the column now stores `enc:v2:<keyId>:<base64>` instead of raw legacy CTR. ### Out of scope (future PRs) - `PostgresCredentialsService` — bespoke `jwtWrapperService.generateAppSecret`–derived key + `encryptText`/`decryptText` from `auth.util.ts`; deserves its own migration. - `SimpleSecretEncryptionUtil` (TOTP) — entirely different `aes-256-cbc` `iv:enc` format; deserves its own migration. ## Test plan - [x] `npx nx typecheck twenty-server` - [x] `npx nx lint:diff-with-main twenty-server` (oxlint + prettier) - [x] Local jest run for `secret-encryption | connected-account-token | application-variable | application-registration-variable | build-env-var | jwt-key-manager | config-storage | encrypt-application-variable | encrypt-application-registration-variable | encrypt-signing-key | encrypt-sensitive-config-storage` — 17 suites, 106 tests pass. - [x] Local jest run for `upgrade | instance-command` — 12 suites, 86 tests pass. - [ ] CI green - [ ] Manual review of CHECK constraint shapes by a server reviewer (each one matches `enc:v2:%` rather than `enc:v_:%` since none of the migrated columns can legitimately hold `enc:v1:` ciphertext). |
||
|
|
04eb913551 |
chore(page-layout): remove IS_RECORD_PAGE_LAYOUT_* feature flags (#20556)
## Summary
- Both \`IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED\` and
\`IS_RECORD_PAGE_LAYOUT_GLOBAL_EDITION_ENABLED\` are force-enabled on
every existing workspace by the 1.23.0 upgrade command
\`BackfillRecordPageLayoutsCommand\` and seeded enabled for new
workspaces via \`DEFAULT_FEATURE_FLAGS\` +
\`seed-feature-flags.util.ts\`. They are no longer load-bearing.
- Unwrap all \`if (flag) { … }\` conditionals to their enabled branch on
both server and front.
- Delete legacy fallback files that only the disabled branch reached:
\`PageLayoutRelationWidgetsSyncEffect\`,
\`usePageLayoutWithRelationWidgets\`,
\`reInjectDynamicRelationWidgetsFromDraft\`,
\`injectRelationWidgetsIntoLayout\`, \`isDynamicRelationWidget\` (and
their tests).
- Strip the two \`enableFeatureFlags\` calls from the 1.23 upgrade
command — the page-layout backfill data logic itself is kept intact
since old workspaces upgrading from < 1.23 still need it.
- No DB cleanup migration: stale \`featureFlag\` rows are left in place,
matching the precedent set by #20531 and #20460.
Net diff: 37 files, +106 / -1727.
## Test plan
- [x] \`npx nx typecheck twenty-shared twenty-server twenty-front\` —
all pass
- [x] \`npx nx lint:diff-with-main twenty-server twenty-front\` — all
pass
- [x] \`cd packages/twenty-front && npx jest page-layout\` — 1240 tests,
all pass
- [x] \`cd packages/twenty-server && npx jest
workspace-entity-manager.spec\` — pass
- [ ] Manual smoke: open a record page, verify tabs render and \"Edit
Layout\" command-menu action is available
- [ ] Manual smoke: Settings → Data model → object → Layout tab is
visible (and hidden for remote / Dashboard objects)
- [ ] Manual smoke: edit a tab title, save, reload — confirm persistence
|
||
|
|
6dd1e8a471 |
feat(upgrade): expose twenty_upgrade_workspaces_up_to_date_total (#20555)
## Summary Adds a fourth gauge alongside the existing `twenty_upgrade_workspaces_behind_total` / `twenty_upgrade_workspaces_failed_total` so dashboards can show how many workspaces are currently healthy, not just the ones that need attention. - New gauge: `twenty_upgrade_workspaces_up_to_date_total` - New count is computed during `UpgradeStatusService.refreshInstanceAndAllWorkspacesStatus` (cheap — we already iterate over every workspace), persisted in the existing `UpgradeStatusCacheService` so the cache-hit path stays a single round trip, and surfaced via `InstanceAndAllWorkspacesUpgradeStatusDTO` for the admin panel. ## Files - `packages/twenty-server/src/engine/core-modules/upgrade/upgrade-gauge.service.ts` — register the new ObservableGauge - `packages/twenty-server/src/engine/core-modules/upgrade/services/upgrade-status.service.ts` — count UP_TO_DATE workspaces during refresh, propagate through cached path - `packages/twenty-server/src/engine/core-modules/upgrade/services/upgrade-status-cache.service.ts` — persist `upToDateWorkspaceCount` next to behind/failed sets - `packages/twenty-server/src/engine/core-modules/upgrade/dtos/instance-and-all-workspaces-upgrade-status.dto.ts` — `Int` field on the admin DTO - Tests: extended `upgrade-status.service.spec.ts` (14/14 green) — cached and refresh paths both assert on `upToDateWorkspaceCount` ## Follow-up A companion `twenty-infra` PR adds the new tile + line on the upgrade-status Grafana dashboard. |
||
|
|
d81756f2e8 |
Add default value to apiKey for authentication method (#20552)
- fix authentication method default value when docker instance to apiKey |
||
|
|
49b9660420 |
fix(auth): preserve returnToPath across Google/Microsoft SSO redirects (#20537)
## Summary Fixes the consent-modal-not-reopening half of [#20535](https://github.com/twentyhq/twenty/issues/20535): when a signed-out user opens an OAuth `/authorize?...` URL (e.g. ChatGPT connecting to `api.twenty.com/mcp`) and signs in with **Google or Microsoft**, the original `/authorize` request was lost and the consent screen never reopened. ### Root cause `PageChangeEffect` already saves the deep link as `returnToPath` (Jotai atom) before navigating to `/welcome`. That atom is in-memory: it survives SPA navigation, and the cross-subdomain workspace hop is handled by `useBuildSearchParamsFromUrlSyncedStates` round-tripping the value through the URL. But the social-SSO path leaves `app.twenty.com` entirely — `app.twenty.com/welcome` → `api.twenty.com/auth/google` → Google → `api.twenty.com/auth/google/redirect` → frontend — so the atom is wiped. None of the existing code paths plumbed `returnToPath` through that hop: - `useAuth.buildRedirectUrl` packed `workspaceInviteHash`/`action`/etc. but not `returnToPath`. - `SocialSSOState` / the Google + Microsoft strategies didn't carry it through the OAuth `state` blob. - `signInUpWithSocialSSO` + `computeRedirectURI` didn't re-emit it on the redirect back to the frontend. The email path worked because all transitions stay on the default frontend domain, so the atom survives until `SignInUpGlobalScopeForm` bakes it into the workspace URL. ### What changed Plumb `returnToPath` through the SSO state the same way `workspaceInviteHash` and `action` already flow: - **Frontend** (`useAuth.buildRedirectUrl`): read `returnToPath` from the Jotai store and append it to `/auth/google` / `/auth/microsoft` when set and structurally valid. - **Server types** (`SocialSSOState`, `GoogleRequest['user']`, `MicrosoftRequest['user']`): add optional `returnToPath`. - **Strategies** (`google.auth.strategy.ts`, `microsoft.auth.strategy.ts`): include `returnToPath: req.query.returnToPath` in the JSON `state` and read it back in `validate`. - **auth.service.ts** (`signInUpWithSocialSSO`, `computeRedirectURI`): forward `returnToPath` on both branches — the multi-workspace redirect to `AppPath.SignInUp?tokenPair=...` and the single-workspace redirect to `<workspace>/verify?loginToken=...`. Validated via a new `isValidReturnToPath` helper so a tampered query value can't become an open-redirect vector. After the round-trip, `useInitializeQueryParamState` rehydrates the atom from the URL and `usePageChangeEffectNavigateLocation` resolves it as the post-auth destination — same mechanism the email path already relied on. Out of scope: the OAuth `resource` parameter handling tracked in [#20296](https://github.com/twentyhq/twenty/issues/20296) is independent and not addressed here. ## Test plan - [x] `npx jest src/engine/core-modules/auth` (twenty-server) — 27 suites / 183 tests pass, including new `is-valid-return-to-path.util.spec.ts`. - [x] `npx jest src/modules/auth` (twenty-front) — 13 suites / 52 tests pass, including two new cases in `useAuth.test.tsx` covering the happy path and the protocol-relative open-redirect guard. - [x] `npx nx typecheck twenty-server` / `twenty-front` — clean. - [x] `npx oxlint` + `prettier --check` on touched files — clean. - [ ] Manual: signed-out user opens `https://app.twenty.com/authorize?client_id=...` → Continue with Google → completes Google → selects workspace → consent screen renders. - [ ] Manual: same flow, single workspace — lands on consent screen directly after Verify. - [ ] Manual: email path still works (regression). - [ ] Manual: tamper `returnToPath=//evil.com` on the `/auth/google` URL → server validation rejects, user lands at default home, not at `evil.com`. E2E note: existing `return-to-path.spec.ts` already covers deep links with query params through the email path. A mock OAuth provider would be needed to cover the SSO path end-to-end; unit coverage stands in for now. |
||
|
|
2a9fef2341 |
feat(upgrade): emit structured logfmt logs for upgrade flow (#20539)
## Summary
Adds a small helper that lets every log line in the upgrade flow stay
human-readable while emitting a structured tail that Loki / the
upgrade-status Grafana dashboard can filter on.
Output shape per `logger.log()` call:
```
<humanMessage as-is, may span multiple lines>
[upgrade] event=<event> key=value … ← always single line
```
Same call produces **one** structured Loki event regardless of how
chatty the human-readable part gets — the dashboard's `|= "[upgrade]"`
filter only matches the trailing line.
## Helper API
```ts
formatUpgradeLog({
humanMessage: string, // free-form, multi-line OK, for engineers scrolling raw pod logs
event: string, // required anchor for Loki filtering / dashboards
logFields?: Record<string, // short structured key=value tail
string | number | boolean | null | undefined
>,
});
```
- `humanMessage` is preserved as-is. A thrown `new Error('line one\nline
two')` surfacing through `humanMessage` stays human-readable across
multiple lines.
- `logFields` values are logfmt-escaped: whitespace / `=` / `"` get
quoted, embedded `\` / `"` / `\n` / `\r` / `\t` are escaped, `null` /
`undefined` emit literally (`key=null`, `key=undefined`) instead of
being silently dropped — caught via `isDefined` from
`twenty-shared/utils`.
- `event` itself runs through the same escape so an event name with
whitespace or `=` can't break parsing.
## Example output
```
Initialized upgrade sequence: 8 step(s)
[upgrade] event=sequence.initialized stepCount=8 dryRun=false
Upgrading workspace abc-123 1/10
[upgrade] event=workspace.start workspaceId=abc-123 index=1 total=10 dryRun=false
Upgrade for workspace abc-123 completed.
[upgrade] event=workspace.success workspaceId=abc-123 executedByVersion=1.4.0 dryRun=false
Upgrade summary: 42 workspace(s) succeeded, 1 workspace(s) failed
[upgrade] event=summary totalSuccesses=42 totalFailures=1 dryRun=false
Upgrade failed: Workspace migration runner failed:
- Option id is required
- Option id is invalid
[upgrade] event=aborted totalSuccesses=41 totalFailures=2 dryRun=false
```
Loki query for the dashboard: `{namespace="twenty"} |= "[upgrade]" |
logfmt event, workspaceId, command, executedByVersion`
## Scope
Only the **upgrade-specific** call sites carry the tag:
- `upgrade.command.ts` — `sequence.initialized`, `sequence.step`
(verbose), `summary`, `aborted`
- `upgrade-sequence-runner.service.ts` — `sequence.stopped`,
`sequence.aborted`
- `workspace-command-runner.service.ts` — `workspace.start`,
`workspace.success`, `cache.invalidate.failed`
`instance-command-runner.service.ts` is intentionally **not** tagged —
`runFastInstanceCommand` / `runSlowInstanceCommand` are also invoked
from `RunInstanceCommandsCommand` (DB init / `run-instance-commands`),
so an `[upgrade]` tag there would mislead at init time. Those lines stay
plain-text; stacks still flow on their own via NestJS
`logger.error(message, error.stack)`.
`chalk` is dropped from `upgrade.command.ts` — ANSI escapes break log
parsers and chalk is a no-op without a TTY anyway.
## Tests
9 inline-snapshot tests in `format-upgrade-log.util.spec.ts` surface the
actual output of every interesting shape (summary call site, multi-line
humanMessage, quoted / escaped / control-character logField values,
null/undefined fields, event name escaping). Snapshots double as
documentation of what a real upgrade log line looks like.
## Test plan
- [x] Unit tests green (`jest format-upgrade-log`)
- [x] oxlint + prettier clean
- [x] tsgo typecheck clean on the upgrade module
- [x] CI green
- [ ] Smoke test on staging: run `upgrade` command, confirm `[upgrade]`
structured lines surface in Loki and `| logfmt` extracts fields
|
||
|
|
0dc8426727 |
i18n - translations (#20554)
Created by Github action --------- Co-authored-by: github-actions <github-actions@twenty.com> |
||
|
|
e16977f97b |
[breaking: deploy server before front] feat(view-sort): pick sort sub-field inline on the chip (#20445)
## Summary Lets users choose which sub-field of a composite column to sort by — directly from the sort chip — by clicking the sub-field label and picking from a dropdown. Persists per view via a new nullable \`subFieldName\` column on \`ViewSort\`. Replaces #20438, which proposed a field-settings (admin) configuration for the same problem. The chip-level approach is more discoverable (the option lives where the user is looking) and per-view, so different views on the same object can sort by different sub-fields. ### What changes for users - **FullName columns**: previously sorted by \`firstName\` and \`lastName\` together as a stable dual-key sort. Now the user can pick which sub-field is primary (the other is the tie-breaker). Default remains \`firstName\` primary, \`lastName\` tie-breaker. - **Address columns**: previously not sortable at all (not in \`SORTABLE_FIELD_METADATA_TYPES\`). Now sortable, with a chip dropdown listing each enabled sub-field. Default is \`addressCity\` if enabled, else the first enabled sub-field. Disabling a sub-field at the field-metadata level (existing setting) removes it from the dropdown. - **Other composite types** (Currency, Phones, Emails, Links, Actor) and scalar fields keep their existing single-key sort behavior. ### UX ``` ┌─────────────────────────┐ ┌─────────────────────────┐ │ ↑ Name · Last name ✕ │ │ ↑ Address · City ✕ │ └────────┬────────────────┘ └────────┬────────────────┘ ▼ (click sub-field) ▼ ┌────────────┐ ┌────────────┐ │ First name │ │ Address 1 │ │ Last name ✓│ │ Address 2 │ └────────────┘ │ City ✓│ │ State │ │ Postcode │ │ Country │ └────────────┘ ``` The chip body still toggles direction on click — the \`Dropdown\`'s internal wrapper calls \`stopPropagation\` so the sub-field click doesn't bubble to the chip's onClick. ## What changed **Backend:** - \`ViewSortEntity\` — new nullable \`subFieldName: varchar\` column - \`ViewSortDTO\`, \`CreateViewSortInput\`, \`UpdateViewSortInputUpdates\` — new \`@Field(() => String, { nullable: true })\` - \`FLAT_VIEW_SORT_EDITABLE_PROPERTIES\` — \`'subFieldName'\` added so the property flows through the update merge path - \`ALL_ENTITY_PROPERTIES_CONFIGURATION_BY_METADATA_NAME.viewSort\` — new \`subFieldName\` entry with \`toCompare: true\` so cache diffs notice it - \`fromCreateViewSortInputToFlatViewSortToCreate\` — threads \`subFieldName\` through - Instance command migration (\`add-sub-field-name-to-view-sort\`) — single \`ALTER TABLE core.viewSort ADD subFieldName varchar\` / \`DROP\` **Frontend:** - \`RecordSort\` and \`ViewSort\` types — \`subFieldName?: string | null\` - \`VIEW_SORT_FRAGMENT\` — adds \`subFieldName\` so the field round-trips - \`mapRecordSortToViewSort\` + \`areViewSortsEqual\` — carry the new field through, include it in the diff so the usual \`useSaveRecordSortsToViewSorts\` create/update flow fires when it changes - \`useSaveRecordSortsToViewSorts\` — passes \`subFieldName\` in both \`CreateViewSortInput\` and \`UpdateViewSortInputUpdates\` - \`getOrderByForFieldMetadataType(field, direction, subFieldName?)\` — new optional third arg. \`turnSortsIntoOrderBy\` threads \`sort.subFieldName\` into it. - \`Address\` added to \`SORTABLE_FIELD_METADATA_TYPES\` - New helpers: \`getEnabledAddressSubFields\` (filters by the field's \`subFields\` setting, falls back to the 6 default visible address sub-fields), \`getDefaultSortSubFieldForAddress\`, \`getDefaultSortSubFieldForFullName\` - New shared types/constants: \`AllowedFullNameSubField\`, \`ALLOWED_FULL_NAME_SUBFIELDS\`, \`DEFAULT_VISIBLE_ADDRESS_SUBFIELDS\` - \`SortOrFilterChip\` — new \`labelSubField?: ReactNode\` slot; renders as \` · {sub-field}\` with subdued weight after the main label - \`EditableSortChip\` — builds options from field metadata (\`ALLOWED_FULL_NAME_SUBFIELDS\` for FullName, \`getEnabledAddressSubFields\` for Address), uses i18n-wrapped labels, persists picks via \`upsertRecordSort\` ## Test plan - [x] \`npx nx typecheck\` passes for twenty-shared, twenty-front, twenty-server - [x] \`oxlint --type-aware\` on all 19 frontend + 9 server changed files: 0 errors - [x] \`prettier --check\`: clean - [x] 16 unit tests pass — \`getOrderByForFieldMetadataType\` covers the new \`subFieldName\` override branch for FULL_NAME and ADDRESS; \`getDefaultSortSubFieldForAddress\` covers the city/first-enabled fallback path; \`getDefaultSortSubFieldForFullName\` exercises its constant - [ ] Manual: sort a People view by Full Name → click the chip's sub-field label → switch between First name and Last name → reload page → choice is preserved - [ ] Manual: sort a Company view by Address → confirm dropdown lists only enabled sub-fields → disable Address \`addressCity\` in field settings → confirm dropdown options update and runtime falls back to the first enabled sub-field 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> |
||
|
|
8bc21ec434 |
i18n - docs translations (#20553)
Created by Github action Co-authored-by: github-actions <github-actions@twenty.com> |
||
|
|
915ce37d20 |
i18n - translations (#20551)
Created by Github action Co-authored-by: github-actions <github-actions@twenty.com> |
||
|
|
0cc2194399 |
Simplify create-twenty-app command (#20512)
## Simplify `create-twenty-app` for zero-interaction use Makes `npx create-twenty-app@latest my-app` a fully non-interactive, single-command experience suitable for automated environments (Codex, Claude plugins). ### Changes - **Remove all interactive prompts** — app name, display name, description, and scaffold confirmation are now derived from CLI args with sensible defaults. `inquirer` dependency removed entirely. - **Replace OAuth with API key auth** — use the seeded dev API key (`DEV_API_KEY`) to authenticate against the Docker instance as `tim@apple.dev`, eliminating the browser-based OAuth flow. - **Docker-first with early validation** — check Docker is installed before scaffolding; if missing, print the install URL and exit. Detect alternative runtimes (Podman, nerdctl). - **Parallel image pull** — `docker pull` runs in the background during scaffold + dependency install, saving 10-30s on typical runs. - **Always pull latest image** — ensures the dev server is up-to-date on every run. - **Stop detecting port 3000** — only check port 2020 (Docker instance). - **Update CLI flags** — remove `--skip-local-instance` and `--yes`; add `--skip-docker`. - **Update CI workflows and docs** — align e2e workflows, package README, and template README/cd.yml with the new flow. |
||
|
|
70a3b25680 |
i18n - docs translations (#20549)
Created by Github action Co-authored-by: github-actions <github-actions@twenty.com> |
||
|
|
6a9509ec01 |
i18n - translations (#20548)
Created by Github action --------- Co-authored-by: github-actions <github-actions@twenty.com> |
||
|
|
fcd2d586ee |
chore(billing) - remove feature flag (#20531)
- remove feature flag - remove old enforce cap usage logic |
||
|
|
695a374efd |
fix - nav drawer expansion (#20545)
[PR](https://github.com/twentyhq/twenty/pull/20505), I merge, does not fix the whole issue Closes https://github.com/twentyhq/twenty/issues/20502 |
||
|
|
dea1f89904 |
Inject none secret env variables into front components (#20511)
## Summary - Inject non-secret application variables (`isSecret: false`) into front component `process.env` via the existing Web Worker `setWorkerEnv` mechanism - Filter secret variables server-side in the resolver so they never reach the browser - Set application variables before system variables (`TWENTY_API_URL`, `TWENTY_APP_ACCESS_TOKEN`) to prevent override - Wire up environment variable keys in the logic function code editor for TypeScript autocomplete ## Test plan - [x] Unit tests for `buildNonSecretEnvVar` (6 passing) - [x] Typecheck passes for `twenty-front` and `twenty-server` - [x] Install an app with both `isSecret: false` and `isSecret: true` variables, open a front component, verify only non-secret vars appear in `process.env` - [x] Open a logic function editor, verify autocomplete suggests declared variable keys |
||
|
|
59b993bdb3 |
i18n - translations (#20547)
Created by Github action --------- Co-authored-by: github-actions <github-actions@twenty.com> |
||
|
|
e0b4c9918b |
feat(twenty-server): introduce ENCRYPTION_KEY env var with versioned envelope (#20528)
## Summary
- Adds `ENCRYPTION_KEY` (primary) and `FALLBACK_ENCRYPTION_KEY`
(decrypt-only fallback for rotation) env vars to twenty-server, with
backward-compatible fallback to `APP_SECRET` when `ENCRYPTION_KEY` is
unset.
- Introduces a versioned ciphertext envelope `enc:v2:<keyId>:<base64>`
using AES-256-GCM with HKDF-SHA256 derived per-context keys. The 8-hex
`keyId` fingerprint lets every row identify which physical key encrypted
it, so rotation routes directly to primary or fallback without trial
decryption; GCM's auth tag gives true integrity (legacy CTR has none).
- Migrates `ConnectedAccountTokenEncryptionService` to the new envelope
and plumbs `workspaceId` through every caller, so per-workspace HKDF
context binds each row to its tenant.
The remaining encryption sites (`jwt-key-manager`, `config-storage`,
`postgres-credentials`, `application-variable`, TOTP) stay on the legacy
unprefixed CTR path and will be migrated in follow-up PRs. The
operator-facing rotation runbook is out of scope here.
### Format details
`enc:v{N}:{keyId}:{base64}` — `N=2` is the only version produced by new
writes (`v1` exists for backward-compatible decryption of existing
connected-account rows). `keyId =
sha256(rawKey).slice(0,4).toString('hex')`. The CHECK constraint on
`core.connectedAccount.{accessToken,refreshToken}` is relaxed from `LIKE
'enc:v1:%'` to `LIKE 'enc:v_:%'` so both versions pass.
### Key resolution
| `ENCRYPTION_KEY` | `FALLBACK_ENCRYPTION_KEY` | `APP_SECRET` | Encrypt
with | Decrypt try order |
|---|---|---|---|---|
| set | set | (any) | `ENCRYPTION_KEY` | match `keyId` → primary →
fallback |
| set | unset | (any) | `ENCRYPTION_KEY` | match `keyId` → primary |
| unset | set | set | `APP_SECRET` | match `keyId` → `APP_SECRET` →
fallback |
| unset | unset | set | `APP_SECRET` | match `keyId` → `APP_SECRET` |
| unset | unset | unset | startup error | n/a |
## Test plan
- [x] `npx nx typecheck twenty-server` — clean
- [x] `npx jest
'secret-encryption|connected-account-token-encryption|connected-account-refresh-tokens|encrypt-connected-account-tokens|connection-provider-oauth-flow'`
— 87 tests pass
- [x] New `secret-encryption.service.versioned.spec.ts` covers: key
resolution table (no-key error, APP_SECRET fallback, ENCRYPTION_KEY
precedence), v2 round-trip with/without workspaceId, GCM tamper
rejection, workspaceId-mismatch rejection, keyId-based primary→fallback
routing, missing-key error names the fingerprint, v1 legacy decryption,
no-prefix legacy decryption, malformed envelope rejection.
- [x] Updated `connected-account-token-encryption.service.spec.ts`
covers workspaceId binding and HKDF context isolation.
- [x] Updated slow instance command spec verifies workspaceId is
threaded through encryption and the relaxed `enc:v_:%` LIKE pattern
matches both v1 and v2.
- [ ] Manual E2E: connect a Gmail account on a freshly deployed instance
with `APP_SECRET` only → confirm `core.connectedAccount.accessToken` is
`enc:v2:<keyId>:<base64>`.
- [ ] Manual E2E: rotate — set `ENCRYPTION_KEY=<new>` and
`FALLBACK_ENCRYPTION_KEY=<old APP_SECRET>`, restart, confirm
pre-rotation rows still decrypt and new rows carry the new `keyId`.
- [ ] Manual E2E: missing key — set `ENCRYPTION_KEY=<new>` without the
fallback, confirm decrypt error names the old `keyId` so the operator
can identify the missing key.
|
||
|
|
aec2e01662 |
fix(server): handle ImapFlow socket errors instead of crashing the process (#20510)
## Summary `ImapFlow` is an `EventEmitter`; per Node.js semantics, an emitted `'error'` event with no listener becomes an uncaught exception that exits the process. Both ImapFlow construction sites in `twenty-server` (`ImapClientProvider` used by all messaging flows, and `testImapConnection` in the connection-wizard validator) currently build the client without attaching a permanent `'error'` listener, so a transient socket condition (idle timeout, network blip, server-side disconnect) crashes `twenty-server` and triggers a container restart with a ~1 min HTTP 502 window for end users. This patch attaches an `'error'` listener at each call site that logs the error and lets `imapflow`'s internal reconnect handle recovery. Same shape / same precedent as #20143 (Redis session-store client) which fixed #20144. Closes #20509. ## What changed - `packages/twenty-server/src/modules/messaging/message-import-manager/drivers/imap/providers/imap-client.provider.ts`: `ImapClientProvider.createConnection` now attaches `client.on('error', ...)` between construction and `connect()`. - `packages/twenty-server/src/engine/core-modules/imap-smtp-caldav-connection/services/imap-smtp-caldav-connection.service.ts`: `testImapConnection` does the same on its short-lived test client. Both listeners log via the existing `Logger` instance (matching the resolver-level logging already in `ImapClientProvider.getClient`) and surface `error.stack` so transient socket conditions are observable but no longer fatal. ## Crash this fixes (real production stack) ``` node:events:487 throw er; // Unhandled 'error' event ^ Error: Socket timeout at TLSSocket.<anonymous> (/app/node_modules/imapflow/lib/imap-flow.js:795:29) at TLSSocket.emit (node:events:509:28) at Socket._onTimeout (node:net:610:8) ... Emitted 'error' event on ImapFlow instance at: at ImapFlow.emitError (/app/node_modules/imapflow/lib/imap-flow.js:397:14) code: 'ETIMEOUT', ``` End-user impact: server process exits cleanly (code 0), Docker / k8s restarts it; the DB, worker, redis, and caddy containers are unaffected — only the API server dies, taking the GraphQL/REST surface offline for a ~1 min health-check warmup. ## Test plan - [x] `npx nx typecheck twenty-server` (planned — relying on CI for verification) - [x] `npx nx lint:diff-with-main twenty-server` (planned — relying on CI for verification) - [x] Manually reproduced the crash on `v2.2` by hitting an IMAP/SMTP/CalDAV outbound flow with Gmail; with the patch applied locally to the running container (verified the listener fires and logs without process exit), the server stays up across the same trigger sequence. - [ ] Unit-level coverage: behavior is "listener exists, doesn't throw" — not easily covered without a contrived socket-mock test. Existing call sites have no unit tests today; happy to add one if a reviewer prefers, otherwise mirroring the convention from #20143 which merged without a new test. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: neo773 <neo773@protonmail.com> |
||
|
|
aecfe699f4 |
feat(ai-chat) - Stop ai thinking if credits exhausted (#20526)
Billing is now decremented per-step, not per-turn. The onStepFinish callback in chat-execution.service.ts calls a new decrementAndCheckAvailableCredits method on each model step, so Redis is debited incrementally as the agent runs rather than all at once at the end. Credit exhaustion stops the stream mid-run. When a step depletes the remaining credits, a hasNoMoreAvailableCredits flag is set and passed into the stopWhen predicate of streamText, causing the agent to halt before starting the next step. A new credits-exhausted event is introduced. After the stream drains and the response is persisted, if credits ran out the job publishes a dedicated credits-exhausted event to the frontend instead of the normal message-persisted event. The frontend handles this new event. useAgentChatSubscription has a new credits-exhausted case that sets a BILLING_CREDITS_EXHAUSTED-coded error on the atom, closes the writer, and stops the streaming state — triggering the existing AiChatCreditsExhaustedMessage UI. |
||
|
|
068fc9363d |
fix(navigation): settings drawer should never appear collapsed (#20505)
## Summary Fixes #20502 /claim #20502 The navigation drawer's collapsed state is persisted to \`localStorage\` via \`isNavigationDrawerExpandedState\`. When a user collapses the drawer in the main app and then opens settings via a **direct URL, refresh, or new tab**, the settings layout renders in collapsed mode — no \`useOpenSettingsMenu\` call is made in those paths to force expansion. \`StyledAnimatedContainer\` (which controls the outer drawer width) used raw \`isNavigationDrawerExpanded\` with no settings-route override. Inner components (\`NavigationDrawerItemsCollapsableContainer\`) already guard with \`isExpanded = isNavigationDrawerExpanded || isSettingsPage\` — the outer container simply needed the same treatment. **Fix:** derive \`isExpanded = isSettingsDrawer || isNavigationDrawerExpanded\` using the already-in-scope \`useIsSettingsDrawer()\` result and pass it to both \`StyledAnimatedContainer\` and \`StyledContainer\`. 1 derived variable, 2 prop changes, no new hooks or state. **Changed files:** - \`packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawer.tsx\` ## Test plan Verified via code review and CI. The fix is structurally identical to the existing \`isSettingsPage\` guard already used in \`NavigationDrawerItemsCollapsableContainer\` — the outer container simply lacked the same treatment. No new hooks, no side effects, no state mutations. Manual UI verification (collapse → navigate to settings → confirm expanded) was not performed against a running instance. If the maintainers want to verify, the logic path is the same as the inner component guard that already ships in production. > **Note (2026-05-12):** PR #20508 was submitted after this PR with a \`useEffect\`-based approach. That approach has already received a review comment from a team member noting that \`useEffect\` should be a last resort per the project's own React guidelines. This fix uses no effects or imperative state — only a derived variable. --- > [!NOTE] > **AI-Assisted Contribution** > This patch was generated by [Mesopredator](https://github.com/GusFromSpace), an autonomous code intelligence system. > Static analysis located the bug, the fix was written and verified with \`tsc --noEmit\` + \`node\` test suite, and reviewed by a human before submission. > Please treat this as a community contribution and request changes if needed. |
||
|
|
99a5a038bc |
fix(server): add Apple seed workspace as fallback for single-workspace mode (#20498)
## Issue
As per the developer docs, the local setup uses the `npx nx
database:reset twenty-server` command, which seeds 4 workspaces. This
works correctly for multi-workspace mode
(`IS_MULTIWORKSPACE_ENABLED=true`) and integration tests but causes
issues in single-workspace mode (`IS_MULTIWORKSPACE_ENABLED=false`) or
when switching from multi-workspace mode back to single-workspace mode.
Also, the default mode is single-workspace but 4 workspaces are already
seeded in the database. As a result,
`WorkspaceDomainsService.getDefaultWorkspace()` selects the newest
workspace (Empty4), which is intended only for integration testing and
contains no user data.
There is also an existing warning log mentioning fallback to the Apple
seed workspace when multiple workspaces are found in single-workspace
mode i.e `IS_MULTIWORKSPACE_ENABLED=true`, but it was never implemented.
```
if (workspaces.length > 1) {
Logger.warn(
` ${workspaces.length} workspaces found in database. In single-workspace mode, there should be only one workspace. Apple seed workspace will be used as fallback if it found.`,
);
}
```
Although we could replace with `"nx command-no-deps --
workspace:seed:dev --light"` in `project.json` for `database:reset`,
which will only seed one workspace but it wont resolve issue when
switching from multi-workspace mode back to single-workspace mode or for
integration test `with-db-reset`.
## Changes
- Implement fallback behavior already hinted by existing warning logs.
- Ensure the Apple seed workspace is used as the fallback in
single-workspace mode when multiple workspaces exist.
This improves:
- Local developer onboarding experience.
- Switching between multi-workspace and single-workspace development
modes.
- Consistency during local development and integration testing.
## Related PR
- #19822
- #20464
These PRs only resolves the issue for docker environments but not for
local development setup.
---------
Co-authored-by: Charles Bochet <charles@twenty.com>
|