## Summary
Today the avatar/icon shown for a record is hardcoded per object —
Company pulls a favicon from its domain link, Person uses `avatarUrl`,
etc. This PR replaces that hardcoding with a generic, data-driven
abstraction based on a configurable **image identifier field** on each
object's metadata (mirroring the existing **label identifier** concept).
An object's image identifier can point to:
- a **`FILES`** field → the uploaded image is used directly (rounded
avatar), or
- a **`LINKS`** field → a favicon is derived from the primary URL via
the Twenty icons service (squared avatar), gated by
`ALLOW_REQUESTS_TO_TWENTY_ICONS`.
This lets any object type (Opportunity, a custom "Listing", etc.) define
its own avatar/icon without code changes, and makes the field
configurable/overridable for standard objects.
## ❓ Open question: also allow `TEXT` → direct image URL?
Right now the image identifier is restricted to `FILES` (uploaded file)
and `LINKS` (favicon). We deliberately left out `TEXT` → **direct image
URL** (e.g. an imported/synced photo URL stored in a text field).
There's precedent for it — Person's avatar was originally a `TEXT`
`avatarUrl`, and WorkspaceMember still is — and it's unambiguous (a
`TEXT` field has no favicon-vs-image ambiguity, and selecting it as the
image identifier is itself the declaration of intent). It's a small,
clean extension:
- add `TEXT` to the allowed image-identifier types,
- add an explicit `TEXT → raw URL` case
- `getAvatarType`: `TEXT → rounded`.
Caveats: it relies on admin assertion that the text values are image
URLs (no data-level guarantee), and external image URLs load third-party
content in the browser (IP-leak/hotlinking, same as favicons — a
proxy/cache would be the more robust long-term answer).
### ✅ Resolution
Decision: **we will not support `TEXT` as an image identifier.** Image
identifiers stay restricted to `FILES` and `LINKS`, and any other type
fails closed (returns no avatar) on both the frontend and backend.
Instead, the legacy items that still rely on a `TEXT` avatar — Person's
deprecated `avatarUrl` and WorkspaceMember's `avatarUrl` — will be
migrated to `FILE` fields in a follow-up PR. Until then, WorkspaceMember
remains an exception (its `avatarUrl` still resolves through the
existing CorePicture path), and legacy Person `avatarUrl` values that
haven't been migrated will show initials placeholders.
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22644?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
Follow-up cleanup after #22510: shrink `FileFolder` and
`fileFolderConfigs` to only folders that actually exist, so per-folder
policy entries are real decisions.
## What
**Remove the five dead enum values** — `ProfilePicture`,
`WorkspaceLogo`, `Attachment`, `PersonPicture`, `File`. They were
already marked replaced/removed in the enum, have no production write
path, and `FileByIdGuard`'s `SUPPORTED_FILE_FOLDERS` allowlist already
rejects them at the serving endpoint.
**Delete the legacy path-based serving pipeline that existed only for
them** — verified wired to no route:
- `FilePathGuard` — registered as a provider in `FileModule` but applied
to no controller
- `extractFileInfoFromRequest` (parsed the old
`/files/profile-picture/original/TOKEN/file.jpg` format) — only consumer
was `FilePathGuard`
- `checkFileFolder` — only consumer was `extractFileInfoFromRequest`
- `settings.storage.imageCropSizes` — keyed exclusively by the three
dead picture folders, zero consumers
- the crop-size helpers in `utils/image.ts` (`getCropSize`,
`ShortCropSize`, `CropSize`) — zero consumers outside the file;
`getImageBufferFromUrl` is kept
- `AllowedFolders` type — last consumer was `checkFileFolder`
**Test fixtures** referencing dead folders were moved to living ones;
the specs of deleted utils are deleted with them.
**Generated files** (`twenty-front/src/generated-metadata/graphql.ts`,
`twenty-client-sdk` schema) hand-updated to match the shrunk GraphQL
enum.
## Legacy data safety
Workspaces may still hold `File` rows whose `path` starts with a dead
prefix (e.g. `attachment/…`). These stay inert, exactly as today:
- Serving: `FileByIdGuard` rejects non-supported folders before any
config lookup, and file lookups filter by `path LIKE
'<current-folder>/%'`, so dead-prefix rows are unreachable.
- Every consumer that feeds stored paths into
`removeFileFolderFromFileEntityPath` (which throws on unknown prefixes)
is upstream-guarded by a current-folder filter or allowlist — audited
all seven call sites.
- Stored legacy member `avatarUrl` strings are parsed with
`extractFileIdFromUrl(url, FileFolder.CorePicture)` and already fall
back to `''` for old formats; unchanged.
## GraphQL note
`FileFolder` is exposed as a GraphQL enum (input of the dev-only
`uploadApplicationFile` mutation, which only accepts application-code
folders). Clients sending a removed value were already rejected at the
resolver allowlist; they now fail GraphQL enum validation instead. No
supported client sends them — the frontend only uses `CorePicture`.
Net: **+10 / −301** across 17 files.
https://claude.ai/code/session_01AKwhTxYFDhWhCZ4b7sf35W
---
_Generated by [Claude
Code](https://claude.ai/code/session_01AKwhTxYFDhWhCZ4b7sf35W)_
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22516?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
Fixes an edge case when a user signs up with Google and the profile
avatar network request times out, we crash instead of creating the user
without an avatar.
Added `axios-retry` to retry max 2 times and if it still fails we
gracefully skip avatar image instead of crashing
Fixes
Sentry TWENTY-SERVER-FDQ
Sonarly https://sonarly.com/issue/6564
---------
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Co-authored-by: Charles Bochet <charles@twenty.com>
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
- Add URL validation in getImageBufferFromUrl utility
- Add response status validation and content-type checking
- Add timeout and connection error handling with specific error messages
- Validate buffer is not empty before processing
- Validate file type detection results before proceeding
- Ensure detected file type is actually an image format
- Add proper type safety for Axios error handling
This improves robustness when uploading images from URLs by:
- Preventing invalid URLs from being processed
- Providing clear error messages for different failure scenarios
- Ensuring only valid image files are processed
- Handling network errors gracefully
---------
Co-authored-by: GitTensor Miner <miner@gittensor.io>
* Add Enrich to frontend
* Naive backend implementation
* Add work email check
* Rename Enrich to Quick Action
* Refactor logic to a separate service
* Refacto to separate IntelligenceService
* Small fixes
* Missing Break statement
* Address PR comments
* Create company interface
* Improve edge case handling
* Use httpService instead of Axios
* Fix server tests