[Front-comp-renderer] Host componentSource caching (#22958)

## Context

The front component source cache introduced in the sandbox refactor was
a silent no-op: it ran inside the sandboxed worker (opaque-origin
`allow-scripts` iframe), where the `caches` global does not exist. Every
render re-fetched the component JS from the network — nothing ever
appeared in Cache Storage.

## Change
<img width="1580" height="622" alt="image"
src="https://github.com/user-attachments/assets/06903abf-b313-4d15-8db4-80950d4bf5ba"
/>

Moves component source resolution and caching from the worker to the
host, where Cache Storage works:

- `fetchComponentSource`, `fetchComponentSourceFromNetwork`,
`frontComponentCacheStorageService` and
`extractComponentChecksumFromUrl` relocated from `remote/worker/utils/`
to `host/utils/` (`buildAuthorizationHeadersFromAccessToken` to shared
`utils/`, still used by the worker for SDK module fetches)
- `FrontComponentWorkerEffect` resolves the source before
`thread.imports.render(...)` (with a cancellation guard) and passes
`componentSource` in the render payload
- `loadFrontComponentModule` no longer fetches: it keeps only
sandbox-side work (SDK import rewrite, blob URL creation, `import()`)
- New: stale-entry eviction — writing a new checksummed entry deletes
older entries of the same `front-components/{id}/` prefix

## Security invariant

The host only fetches, hashes and caches the source string — it never
executes it. Execution stays exclusively in the opaque-origin worker via
blob URL import. SHA-256 checksum verification is kept on both cache
read (poisoned-entry guard: any same-origin code can write to Cache
Storage) and cache write.

## Out of scope

SDK client module caching — follow-up tracked in
[twentyhq/core-team-issues#2688](https://github.com/twentyhq/core-team-issues/issues/2688),
requires content-addressed URLs (server-side checksum at SDK generation
time, exposed via GraphQL and embedded in the `/rest/sdk-client/...`
URL), then reuses this same host-side cache path.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22958?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. -->
This commit is contained in:
Paul Rastoin
2026-07-16 17:49:44 +02:00
committed by GitHub
parent 2a94736ece
commit 17d34a6fe3
13 changed files with 129 additions and 32 deletions
@@ -0,0 +1,17 @@
import { buildAuthorizationHeadersFromAccessToken } from '../buildAuthorizationHeadersFromAccessToken';
describe('buildAuthorizationHeadersFromAccessToken', () => {
it('should build a bearer authorization header from the access token', () => {
expect(buildAuthorizationHeadersFromAccessToken('access-token')).toEqual({
Authorization: 'Bearer access-token',
});
});
it('should return undefined when no access token is provided', () => {
expect(buildAuthorizationHeadersFromAccessToken(undefined)).toBeUndefined();
});
it('should return undefined when the access token is empty', () => {
expect(buildAuthorizationHeadersFromAccessToken('')).toBeUndefined();
});
});
@@ -0,0 +1,8 @@
import { isNonEmptyString } from '@sniptt/guards';
export const buildAuthorizationHeadersFromAccessToken = (
applicationAccessToken?: string,
): Record<string, string> | undefined =>
isNonEmptyString(applicationAccessToken)
? { Authorization: `Bearer ${applicationAccessToken}` }
: undefined;