[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
@@ -5,11 +5,13 @@ import { useEffect, useRef } from 'react';
import { buildHostFetchPolicyFromFrontComponentUrls } from '@/host/utils/buildHostFetchPolicyFromFrontComponentUrls';
import { createFrontComponentHostThread } from '@/host/utils/createFrontComponentHostThread';
import { createHostFetchEnforcingPolicy } from '@/host/utils/createHostFetchEnforcingPolicy';
import { fetchComponentSource } from '@/host/utils/fetchComponentSource';
import { FRONT_COMPONENT_SANDBOX_DOCUMENT } from '@/remote/sandbox/generated/frontComponentSandboxDocument';
import { createFrontComponentSandboxIframe } from '@/remote/sandbox/utils/createFrontComponentSandboxIframe';
import { createFrontComponentSandboxMessageHandler } from '@/remote/sandbox/utils/createFrontComponentSandboxMessageHandler';
import { type FrontComponentThread } from '@/types/FrontComponentThread';
import { type SdkClientUrls } from '@/types/SdkClientUrls';
import { buildAuthorizationHeadersFromAccessToken } from '@/utils/buildAuthorizationHeadersFromAccessToken';
type FrontComponentWorkerEffectProps = {
componentUrl: string;
@@ -71,24 +73,45 @@ export const FrontComponentWorkerEffect = ({
setThread(thread);
thread.imports
.render(newReceiver.connection, {
componentUrl,
applicationAccessToken,
apiUrl,
functionsBaseUrl,
sdkClientUrls,
hostFetchOrigins: hostFetchPolicy.allowedOrigins,
applicationVariables,
})
.catch((error: Error) => {
setError(error);
});
let isCancelled = false;
const resolveComponentSourceAndRender = async () => {
try {
const componentSource = await fetchComponentSource({
url: componentUrl,
headers: buildAuthorizationHeadersFromAccessToken(
applicationAccessToken,
),
});
if (isCancelled) {
return;
}
await thread.imports.render(newReceiver.connection, {
componentUrl,
componentSource,
applicationAccessToken,
apiUrl,
functionsBaseUrl,
sdkClientUrls,
hostFetchOrigins: hostFetchPolicy.allowedOrigins,
applicationVariables,
});
} catch (error) {
if (!isCancelled) {
setError(error instanceof Error ? error : new Error(String(error)));
}
}
};
resolveComponentSourceAndRender();
setReceiver(newReceiver);
isInitializedRef.current = true;
return () => {
isCancelled = true;
window.removeEventListener('message', handleSandboxMessage);
setThread(null);
channel.port1.close();
@@ -1,17 +0,0 @@
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();
});
});
@@ -1,286 +0,0 @@
import { createHash, webcrypto } from 'node:crypto';
import { TextEncoder as NodeTextEncoder } from 'node:util';
import { fetchComponentSource } from '@/remote/worker/utils/fetchComponentSource';
const COMPONENT_SOURCE = 'export default () => {};';
const computeSha256Hex = (content: string): string =>
createHash('sha256').update(content).digest('hex');
const buildFingerprintedUrl = (checksum: string): string =>
`https://api.twenty.com/rest/front-components/component-id/${checksum}.js`;
const FINGERPRINTED_URL = buildFingerprintedUrl(
computeSha256Hex(COMPONENT_SOURCE),
);
const BARE_URL = 'https://api.twenty.com/rest/front-components/component-id';
const LEGACY_MD5_URL = buildFingerprintedUrl(
createHash('md5').update(COMPONENT_SOURCE).digest('hex'),
);
const createFakeJsResponse = (body: string) => ({
ok: true,
status: 200,
statusText: 'OK',
headers: {
get: (name: string) =>
name === 'content-type' ? 'application/javascript' : null,
},
text: jest.fn(async () => body),
json: jest.fn(async () => undefined),
});
class FakeCache {
private readonly store = new Map<string, string>();
match = jest.fn(async (key: string) => {
const cached = this.store.get(key);
return isDefinedString(cached) ? { text: async () => cached } : undefined;
});
put = jest.fn(
async (key: string, response: { text: () => Promise<string> }) => {
this.store.set(key, await response.text());
},
);
delete = jest.fn(async (key: string) => this.store.delete(key));
}
const isDefinedString = (value: string | undefined): value is string =>
value !== undefined;
const setupCaches = (cache: FakeCache) => {
(globalThis as unknown as { caches: unknown }).caches = {
open: jest.fn(async () => cache),
};
};
describe('fetchComponentSource', () => {
const originalFetch = globalThis.fetch;
const originalCaches = (globalThis as unknown as { caches?: unknown }).caches;
const originalResponse = (globalThis as unknown as { Response?: unknown })
.Response;
const originalCrypto = Object.getOwnPropertyDescriptor(globalThis, 'crypto');
const originalTextEncoder = (
globalThis as unknown as { TextEncoder?: unknown }
).TextEncoder;
beforeEach(() => {
(globalThis as unknown as { Response: unknown }).Response = class {
body: string;
constructor(body: string) {
this.body = body;
}
async text() {
return this.body;
}
};
Object.defineProperty(globalThis, 'crypto', {
value: webcrypto,
configurable: true,
});
(globalThis as unknown as { TextEncoder: unknown }).TextEncoder =
NodeTextEncoder;
});
afterEach(() => {
globalThis.fetch = originalFetch;
(globalThis as unknown as { caches?: unknown }).caches = originalCaches;
(globalThis as unknown as { Response?: unknown }).Response =
originalResponse;
if (originalCrypto !== undefined) {
Object.defineProperty(globalThis, 'crypto', originalCrypto);
}
(globalThis as unknown as { TextEncoder?: unknown }).TextEncoder =
originalTextEncoder;
jest.clearAllMocks();
});
it('fetches from the network and caches the result on a miss', async () => {
const cache = new FakeCache();
setupCaches(cache);
const fetchMock = jest.fn(async () =>
createFakeJsResponse(COMPONENT_SOURCE),
);
globalThis.fetch = fetchMock as unknown as typeof fetch;
const source = await fetchComponentSource({ url: FINGERPRINTED_URL });
expect(source).toBe(COMPONENT_SOURCE);
expect(cache.put).toHaveBeenCalledTimes(1);
expect(cache.put.mock.calls[0][0]).toBe(FINGERPRINTED_URL);
});
it('serves a verified cache hit without hitting the network', async () => {
const cache = new FakeCache();
await cache.put(FINGERPRINTED_URL, {
text: async () => COMPONENT_SOURCE,
});
setupCaches(cache);
const fetchMock = jest.fn();
globalThis.fetch = fetchMock as unknown as typeof fetch;
const source = await fetchComponentSource({ url: FINGERPRINTED_URL });
expect(source).toBe(COMPONENT_SOURCE);
expect(fetchMock).not.toHaveBeenCalled();
});
it('evicts a poisoned cache entry and refetches from the network', async () => {
const cache = new FakeCache();
await cache.put(FINGERPRINTED_URL, {
text: async () => 'globalThis.injectedByAnotherComponent = true;',
});
setupCaches(cache);
const fetchMock = jest.fn(async () =>
createFakeJsResponse(COMPONENT_SOURCE),
);
globalThis.fetch = fetchMock as unknown as typeof fetch;
const source = await fetchComponentSource({ url: FINGERPRINTED_URL });
expect(source).toBe(COMPONENT_SOURCE);
expect(cache.delete).toHaveBeenCalledWith(FINGERPRINTED_URL);
expect(fetchMock).toHaveBeenCalledTimes(1);
});
it('falls back to the network when the cached response body is unreadable', async () => {
const cache = new FakeCache();
setupCaches(cache);
cache.match.mockResolvedValueOnce({
text: async () => {
throw new Error('body unreadable');
},
});
const fetchMock = jest.fn(async () =>
createFakeJsResponse(COMPONENT_SOURCE),
);
globalThis.fetch = fetchMock as unknown as typeof fetch;
const source = await fetchComponentSource({ url: FINGERPRINTED_URL });
expect(source).toBe(COMPONENT_SOURCE);
expect(fetchMock).toHaveBeenCalledTimes(1);
});
it('does not cache a network response whose checksum does not match the URL', async () => {
const cache = new FakeCache();
setupCaches(cache);
const staleUrl = buildFingerprintedUrl(
computeSha256Hex('some other build output'),
);
const fetchMock = jest.fn(async () =>
createFakeJsResponse(COMPONENT_SOURCE),
);
globalThis.fetch = fetchMock as unknown as typeof fetch;
const source = await fetchComponentSource({ url: staleUrl });
expect(source).toBe(COMPONENT_SOURCE);
expect(cache.put).not.toHaveBeenCalled();
});
it('never touches the cache for a non-fingerprinted URL', async () => {
const cache = new FakeCache();
setupCaches(cache);
const fetchMock = jest.fn(async () =>
createFakeJsResponse('bare url source'),
);
globalThis.fetch = fetchMock as unknown as typeof fetch;
const source = await fetchComponentSource({ url: BARE_URL });
expect(source).toBe('bare url source');
expect(cache.match).not.toHaveBeenCalled();
expect(cache.put).not.toHaveBeenCalled();
});
it('never touches the cache for a legacy md5-fingerprinted URL', async () => {
const cache = new FakeCache();
setupCaches(cache);
const fetchMock = jest.fn(async () =>
createFakeJsResponse(COMPONENT_SOURCE),
);
globalThis.fetch = fetchMock as unknown as typeof fetch;
const source = await fetchComponentSource({ url: LEGACY_MD5_URL });
expect(source).toBe(COMPONENT_SOURCE);
expect(cache.match).not.toHaveBeenCalled();
expect(cache.put).not.toHaveBeenCalled();
});
it('falls back to the network when CacheStorage is unavailable', async () => {
(globalThis as unknown as { caches?: unknown }).caches = undefined;
const fetchMock = jest.fn(async () =>
createFakeJsResponse(COMPONENT_SOURCE),
);
globalThis.fetch = fetchMock as unknown as typeof fetch;
const source = await fetchComponentSource({ url: FINGERPRINTED_URL });
expect(source).toBe(COMPONENT_SOURCE);
expect(fetchMock).toHaveBeenCalledTimes(1);
});
it('does not trust the cache when WebCrypto is unavailable', async () => {
const cache = new FakeCache();
await cache.put(FINGERPRINTED_URL, {
text: async () => COMPONENT_SOURCE,
});
cache.put.mockClear();
setupCaches(cache);
Object.defineProperty(globalThis, 'crypto', {
value: undefined,
configurable: true,
});
const fetchMock = jest.fn(async () =>
createFakeJsResponse(COMPONENT_SOURCE),
);
globalThis.fetch = fetchMock as unknown as typeof fetch;
const source = await fetchComponentSource({ url: FINGERPRINTED_URL });
expect(source).toBe(COMPONENT_SOURCE);
expect(fetchMock).toHaveBeenCalledTimes(1);
expect(cache.put).not.toHaveBeenCalled();
});
});
@@ -1,153 +0,0 @@
import { fetchComponentSourceFromNetwork } from '@/remote/worker/utils/fetchComponentSourceFromNetwork';
const COMPONENT_URL =
'https://api.twenty.com/rest/front-components/component-id/checksum-abc.js';
const CACHE_BUSTED_COMPONENT_URL = `${COMPONENT_URL}?cacheBust=v2`;
const PRESIGNED_URL = 'https://s3.example.com/component.js?signed=abc';
type FakeResponseInit = {
ok?: boolean;
status?: number;
statusText?: string;
contentType?: string;
body?: string;
json?: unknown;
};
const createFakeResponse = ({
ok = true,
status = 200,
statusText = 'OK',
contentType = 'application/javascript',
body = '',
json,
}: FakeResponseInit) => ({
ok,
status,
statusText,
headers: {
get: (name: string) => (name === 'content-type' ? contentType : null),
},
text: jest.fn(async () => body),
json: jest.fn(async () => json),
});
describe('fetchComponentSourceFromNetwork', () => {
const originalFetch = globalThis.fetch;
afterEach(() => {
globalThis.fetch = originalFetch;
jest.clearAllMocks();
});
it('returns the raw body and forwards headers for a direct JS response', async () => {
const fetchMock = jest.fn(async () =>
createFakeResponse({ body: 'export default () => {};' }),
);
globalThis.fetch = fetchMock as unknown as typeof fetch;
const source = await fetchComponentSourceFromNetwork({
url: COMPONENT_URL,
headers: { Authorization: 'Bearer token' },
});
expect(source).toBe('export default () => {};');
expect(fetchMock).toHaveBeenCalledWith(CACHE_BUSTED_COMPONENT_URL, {
headers: { Authorization: 'Bearer token' },
});
});
it('follows the JSON handoff and fetches the presigned URL header-less', async () => {
const fetchMock = jest
.fn()
.mockResolvedValueOnce(
createFakeResponse({
contentType: 'application/json',
json: { url: PRESIGNED_URL },
}),
)
.mockResolvedValueOnce(
createFakeResponse({ body: 'presigned bundle source' }),
);
globalThis.fetch = fetchMock as unknown as typeof fetch;
const source = await fetchComponentSourceFromNetwork({
url: COMPONENT_URL,
headers: { Authorization: 'Bearer token' },
});
expect(source).toBe('presigned bundle source');
expect(fetchMock).toHaveBeenNthCalledWith(1, CACHE_BUSTED_COMPONENT_URL, {
headers: { Authorization: 'Bearer token' },
});
expect(fetchMock).toHaveBeenNthCalledWith(2, PRESIGNED_URL);
});
it('preserves existing query parameters when appending the cache bust parameter', async () => {
const fetchMock = jest.fn(async () =>
createFakeResponse({ body: 'export default () => {};' }),
);
globalThis.fetch = fetchMock as unknown as typeof fetch;
await fetchComponentSourceFromNetwork({
url: `${COMPONENT_URL}?token=abc`,
});
expect(fetchMock).toHaveBeenCalledWith(
`${COMPONENT_URL}?token=abc&cacheBust=v2`,
{ headers: undefined },
);
});
it('throws when the initial response is not ok', async () => {
const fetchMock = jest.fn(async () =>
createFakeResponse({ ok: false, status: 404, statusText: 'Not Found' }),
);
globalThis.fetch = fetchMock as unknown as typeof fetch;
await expect(
fetchComponentSourceFromNetwork({ url: COMPONENT_URL }),
).rejects.toThrow(`Failed to fetch ${COMPONENT_URL}: 404 Not Found`);
});
it('throws when the JSON handoff payload is invalid', async () => {
const fetchMock = jest.fn(async () =>
createFakeResponse({
contentType: 'application/json',
json: { notUrl: 'nope' },
}),
);
globalThis.fetch = fetchMock as unknown as typeof fetch;
await expect(
fetchComponentSourceFromNetwork({ url: COMPONENT_URL }),
).rejects.toThrow(
`Invalid component source handoff response from ${COMPONENT_URL}`,
);
});
it('throws when the presigned response is not ok', async () => {
const fetchMock = jest
.fn()
.mockResolvedValueOnce(
createFakeResponse({
contentType: 'application/json',
json: { url: PRESIGNED_URL },
}),
)
.mockResolvedValueOnce(
createFakeResponse({ ok: false, status: 403, statusText: 'Forbidden' }),
);
globalThis.fetch = fetchMock as unknown as typeof fetch;
await expect(
fetchComponentSourceFromNetwork({ url: COMPONENT_URL }),
).rejects.toThrow('Failed to fetch presigned URL: 403 Forbidden');
});
});
@@ -8,6 +8,7 @@ describe('renderFrontComponent', () => {
connection: {} as RemoteConnection,
renderContext: {
componentUrl: 'https://api.twenty.test/rest/front-components/id',
componentSource: 'export default () => {};',
},
hostFetch: null,
}),
@@ -1,8 +0,0 @@
import { isNonEmptyString } from '@sniptt/guards';
export const buildAuthorizationHeadersFromAccessToken = (
applicationAccessToken?: string,
): Record<string, string> | undefined =>
isNonEmptyString(applicationAccessToken)
? { Authorization: `Bearer ${applicationAccessToken}` }
: undefined;
@@ -1,11 +0,0 @@
const SHA256_FINGERPRINTED_URL_PATTERN = /\/([0-9a-f]{64})\.js$/;
export const extractComponentChecksumFromUrl = ({
url,
}: {
url: string;
}): string | undefined => {
const match = url.match(SHA256_FINGERPRINTED_URL_PATTERN);
return match?.[1];
};
@@ -1,54 +0,0 @@
import { isDefined } from 'twenty-shared/utils';
import { extractComponentChecksumFromUrl } from '@/remote/worker/utils/extractComponentChecksumFromUrl';
import { fetchComponentSourceFromNetwork } from '@/remote/worker/utils/fetchComponentSourceFromNetwork';
import { frontComponentCacheStorageService } from '@/remote/worker/utils/frontComponentCacheStorageService';
export const fetchComponentSource = async ({
url,
headers,
}: {
url: string;
headers?: Record<string, string>;
}): Promise<string> => {
const expectedChecksum = extractComponentChecksumFromUrl({ url });
const cache = isDefined(expectedChecksum)
? await frontComponentCacheStorageService.open()
: undefined;
if (isDefined(cache) && isDefined(expectedChecksum)) {
const cachedSource = await frontComponentCacheStorageService.read({
cache,
url,
});
if (isDefined(cachedSource)) {
const cachedSourceChecksum =
await frontComponentCacheStorageService.computeChecksum({
source: cachedSource,
});
if (cachedSourceChecksum === expectedChecksum) {
return cachedSource;
}
// Poisoned or corrupt entry: any same-origin code can write to
// CacheStorage, so a mismatch means the content cannot be trusted.
frontComponentCacheStorageService.delete({ cache, url });
}
}
const source = await fetchComponentSourceFromNetwork({ url, headers });
if (isDefined(cache) && isDefined(expectedChecksum)) {
const sourceChecksum =
await frontComponentCacheStorageService.computeChecksum({ source });
if (sourceChecksum === expectedChecksum) {
frontComponentCacheStorageService.write({ cache, url, source });
}
}
return source;
};
@@ -1,63 +0,0 @@
import { z } from 'zod';
const componentSourceHandoffSchema = z.object({ url: z.url() });
// Changes the HTTP cache key so component responses cached before the
// front-component cache fix are bypassed instead of served corrupted.
const CACHE_BUST_QUERY_PARAMETER_NAME = 'cacheBust';
const CACHE_BUST_QUERY_PARAMETER_VALUE = 'v2';
const appendCacheBustQueryParameter = (url: string): string => {
try {
const parsedUrl = new URL(url);
parsedUrl.searchParams.set(
CACHE_BUST_QUERY_PARAMETER_NAME,
CACHE_BUST_QUERY_PARAMETER_VALUE,
);
return parsedUrl.toString();
} catch {
return url;
}
};
export const fetchComponentSourceFromNetwork = async ({
url,
headers,
}: {
url: string;
headers?: Record<string, string>;
}): Promise<string> => {
const response = await fetch(appendCacheBustQueryParameter(url), {
headers,
});
if (!response.ok) {
throw new Error(
`Failed to fetch ${url}: ${response.status} ${response.statusText}`,
);
}
const contentType = response.headers.get('content-type') ?? '';
if (!contentType.includes('application/json')) {
return response.text();
}
const handoff = componentSourceHandoffSchema.safeParse(await response.json());
if (!handoff.success) {
throw new Error(`Invalid component source handoff response from ${url}`);
}
const presignedResponse = await fetch(handoff.data.url);
if (!presignedResponse.ok) {
throw new Error(
`Failed to fetch presigned URL: ${presignedResponse.status} ${presignedResponse.statusText}`,
);
}
return presignedResponse.text();
};
@@ -1,81 +0,0 @@
import { isDefined } from 'twenty-shared/utils';
const FRONT_COMPONENT_SOURCE_CACHE_NAME = 'front-component-source-v1';
export const frontComponentCacheStorageService = {
computeChecksum: async ({
source,
}: {
source: string;
}): Promise<string | undefined> => {
if (typeof crypto === 'undefined' || !isDefined(crypto.subtle)) {
return undefined;
}
try {
const digest = await crypto.subtle.digest(
'SHA-256',
new TextEncoder().encode(source),
);
return Array.from(new Uint8Array(digest))
.map((byte) => byte.toString(16).padStart(2, '0'))
.join('');
} catch {
return undefined;
}
},
open: async (): Promise<Cache | undefined> => {
if (typeof caches === 'undefined') {
return undefined;
}
try {
return await caches.open(FRONT_COMPONENT_SOURCE_CACHE_NAME);
} catch {
return undefined;
}
},
read: async ({
cache,
url,
}: {
cache: Cache;
url: string;
}): Promise<string | undefined> => {
try {
const cachedResponse = await cache.match(url);
return isDefined(cachedResponse)
? await cachedResponse.text()
: undefined;
} catch {
return undefined;
}
},
write: ({
cache,
url,
source,
}: {
cache: Cache;
url: string;
source: string;
}): void => {
cache
.put(
url,
new Response(source, {
headers: { 'Content-Type': 'application/javascript' },
}),
)
.catch(() => undefined);
},
delete: ({ cache, url }: { cache: Cache; url: string }): void => {
cache.delete(url).catch(() => undefined);
},
};
@@ -1,16 +1,15 @@
import { isDefined } from 'twenty-shared/utils';
import { buildAuthorizationHeadersFromAccessToken } from '@/remote/worker/utils/buildAuthorizationHeadersFromAccessToken';
import { containsSdkClientImportSpecifier } from '@/remote/worker/utils/containsSdkClientImportSpecifier';
import { createJavaScriptModuleBlobUrl } from '@/remote/worker/utils/createJavaScriptModuleBlobUrl';
import { fetchComponentSource } from '@/remote/worker/utils/fetchComponentSource';
import { fetchSdkClientModulesAsBlobUrls } from '@/remote/worker/utils/fetchSdkClientModulesAsBlobUrls';
import { revokeSdkClientModuleBlobUrls } from '@/remote/worker/utils/revokeSdkClientModuleBlobUrls';
import { rewriteSdkClientImportsToBlobUrls } from '@/remote/worker/utils/rewriteSdkClientImportsToBlobUrls';
import { type SdkClientUrls } from '@/types/SdkClientUrls';
import { buildAuthorizationHeadersFromAccessToken } from '@/utils/buildAuthorizationHeadersFromAccessToken';
type LoadFrontComponentModuleInput = {
componentUrl: string;
componentSource: string;
sdkClientUrls?: SdkClientUrls;
applicationAccessToken?: string;
};
@@ -20,7 +19,7 @@ type FrontComponentModule = {
};
export const loadFrontComponentModule = async ({
componentUrl,
componentSource,
sdkClientUrls,
applicationAccessToken,
}: LoadFrontComponentModuleInput): Promise<FrontComponentModule> => {
@@ -28,11 +27,6 @@ export const loadFrontComponentModule = async ({
applicationAccessToken,
);
const componentSource = await fetchComponentSource({
url: componentUrl,
headers: authorizationHeaders,
});
const sdkModuleBlobUrls =
isDefined(sdkClientUrls) &&
containsSdkClientImportSpecifier(componentSource)
@@ -33,7 +33,7 @@ export const renderFrontComponent = async ({
setWorkerEnvironmentVariablesFromRenderContext(renderContext);
const componentModule = await loadFrontComponentModule({
componentUrl: renderContext.componentUrl,
componentSource: renderContext.componentSource,
sdkClientUrls: renderContext.sdkClientUrls,
applicationAccessToken: renderContext.applicationAccessToken,
});