3bacf7a24b
Only a closed allow-list of props crossed the front-component worker→host boundary (`id, className, style, title, tabIndex, role, aria-label, aria-hidden, data-testid`), so arbitrary `aria-*`/`data-*` attributes and `draggable` never reached the host DOM. That breaks headless UI libraries (Radix, cmdk, react-aria) that drive styling/state through those attributes. This widens the crossing set to all `aria-*`, all `data-*`, and `draggable`: - `draggable` becomes an enumerated remote property (it's a DOM IDL property React may set as a property, bypassing `setAttribute`, so it can't ride the prefix path). - Arbitrary `aria-*`/`data-*` are forwarded in the worker by patching `setAttribute`/`removeAttribute` through remote-dom's attribute channel, only for names not already synced as observed attributes. Security: only inert `aria-*`/`data-*`/`draggable` cross, and they still route through the host `filterProps` guards (non-function `on*` dropped, `javascript:` URLs denied) — nothing bypasses them. The enumerated `aria-label`/`aria-hidden`/`data-testid` keep their existing path. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22614?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. -->
195 lines
6.2 KiB
TypeScript
195 lines
6.2 KiB
TypeScript
import '@remote-dom/core/polyfill';
|
|
import '@remote-dom/react/polyfill';
|
|
|
|
import '../generated/remote-elements';
|
|
|
|
import { ThreadWebWorker } from '@quilted/threads';
|
|
import {
|
|
BatchingRemoteConnection,
|
|
type RemoteConnection,
|
|
type RemoteRootElement,
|
|
} from '@remote-dom/core/elements';
|
|
|
|
import { isDefined } from 'twenty-shared/utils';
|
|
|
|
import { installStyleBridge } from '@/polyfills/installStyleBridge';
|
|
import { installStylePropertyOnRemoteElements } from '@/remote/utils/installStylePropertyOnRemoteElements';
|
|
import { patchRemoteElementAttributes } from '@/remote/utils/patchRemoteElementAttributes';
|
|
import { installErrorEventBridge } from './utils/installErrorEventBridge';
|
|
import { type FrontComponentExecutionContext } from 'twenty-sdk/front-component';
|
|
import { frontComponentHostCommunicationApi } from '@/constants/frontComponentHostCommunicationApi';
|
|
import { HTML_TAG_TO_CUSTOM_ELEMENT_TAG } from '@/constants/HtmlTagToRemoteComponent';
|
|
import { setFrontComponentExecutionContext } from './utils/setFrontComponentExecutionContext';
|
|
import { type FrontComponentHostCommunicationApi } from '../../types/FrontComponentHostCommunicationApi';
|
|
import { type HostToWorkerRenderContext } from '../../types/HostToWorkerRenderContext';
|
|
import { type WorkerExports } from '../../types/WorkerExports';
|
|
import { exposeGlobals } from '../utils/exposeGlobals';
|
|
import {
|
|
createOpenCommandConfirmationModalAdapter,
|
|
handleCommandConfirmationModalResult,
|
|
} from './utils/createCommandConfirmationModalBridge';
|
|
import { setWorkerEnv } from './utils/setWorkerEnv';
|
|
|
|
installStylePropertyOnRemoteElements();
|
|
patchRemoteElementAttributes();
|
|
installErrorEventBridge();
|
|
|
|
exposeGlobals({
|
|
__HTML_TAG_TO_CUSTOM_ELEMENT_TAG__: HTML_TAG_TO_CUSTOM_ELEMENT_TAG,
|
|
});
|
|
|
|
const fetchComponentSource = async (
|
|
url: string,
|
|
headers?: Record<string, string>,
|
|
): Promise<string> => {
|
|
const response = await fetch(url, { headers });
|
|
|
|
if (!response.ok) {
|
|
throw new Error(
|
|
`Failed to fetch ${url}: ${response.status} ${response.statusText}`,
|
|
);
|
|
}
|
|
|
|
return response.text();
|
|
};
|
|
|
|
const SDK_IMPORT_SPECIFIERS = [
|
|
'twenty-client-sdk/core',
|
|
'twenty-client-sdk/metadata',
|
|
] as const;
|
|
|
|
// Rewrites bare SDK import specifiers to the blob URLs provided by the host.
|
|
const rewriteSdkImports = (
|
|
source: string,
|
|
sdkClientUrls: { core: string; metadata: string },
|
|
): string => {
|
|
const specifierToBlobUrl: Record<string, string> = {
|
|
'twenty-client-sdk/core': sdkClientUrls.core,
|
|
'twenty-client-sdk/metadata': sdkClientUrls.metadata,
|
|
};
|
|
|
|
let rewritten = source;
|
|
|
|
for (const [specifier, blobUrl] of Object.entries(specifierToBlobUrl)) {
|
|
rewritten = rewritten
|
|
.split(`"${specifier}"`)
|
|
.join(`"${blobUrl}"`)
|
|
.split(`'${specifier}'`)
|
|
.join(`'${blobUrl}'`);
|
|
}
|
|
|
|
return rewritten;
|
|
};
|
|
|
|
const render: WorkerExports['render'] = async (
|
|
connection: RemoteConnection,
|
|
renderContext: HostToWorkerRenderContext,
|
|
) => {
|
|
const batchedConnection = new BatchingRemoteConnection(connection);
|
|
const root = document.createElement('remote-root') as RemoteRootElement;
|
|
const renderContainer = document.createElement('remote-fragment');
|
|
root.connect(batchedConnection);
|
|
root.append(renderContainer);
|
|
document.body.append(root);
|
|
installStyleBridge(root);
|
|
|
|
if (isDefined(renderContext.applicationVariables)) {
|
|
setWorkerEnv({
|
|
applicationVariables: JSON.stringify(renderContext.applicationVariables),
|
|
});
|
|
}
|
|
|
|
// System variables are set after application variables so they cannot be overridden
|
|
if (isDefined(renderContext.apiUrl)) {
|
|
setWorkerEnv({
|
|
TWENTY_API_URL: renderContext.apiUrl,
|
|
});
|
|
}
|
|
|
|
if (isDefined(renderContext.functionsBaseUrl)) {
|
|
setWorkerEnv({
|
|
TWENTY_FUNCTIONS_URL: renderContext.functionsBaseUrl,
|
|
});
|
|
}
|
|
|
|
if (isDefined(renderContext.applicationAccessToken)) {
|
|
setWorkerEnv({
|
|
TWENTY_APP_ACCESS_TOKEN: renderContext.applicationAccessToken,
|
|
});
|
|
}
|
|
|
|
const authHeaders = isDefined(renderContext.applicationAccessToken)
|
|
? { Authorization: `Bearer ${renderContext.applicationAccessToken}` }
|
|
: undefined;
|
|
|
|
const componentSource = await fetchComponentSource(
|
|
renderContext.componentUrl,
|
|
authHeaders,
|
|
);
|
|
|
|
const hasSdkImports =
|
|
isDefined(renderContext.sdkClientUrls) &&
|
|
SDK_IMPORT_SPECIFIERS.some((specifier) =>
|
|
componentSource.includes(specifier),
|
|
);
|
|
|
|
const finalSource = hasSdkImports
|
|
? rewriteSdkImports(componentSource, renderContext.sdkClientUrls!)
|
|
: componentSource;
|
|
|
|
const componentBlob = new Blob([finalSource], {
|
|
type: 'application/javascript',
|
|
});
|
|
|
|
const importUrl = URL.createObjectURL(componentBlob);
|
|
|
|
try {
|
|
/* @vite-ignore */
|
|
const componentModule = await import(importUrl);
|
|
|
|
componentModule.default(renderContainer);
|
|
} finally {
|
|
URL.revokeObjectURL(importUrl);
|
|
}
|
|
};
|
|
|
|
const initializeHostCommunicationApi: WorkerExports['initializeHostCommunicationApi'] =
|
|
async () => {
|
|
const hostApi =
|
|
ThreadWebWorker.self.import<FrontComponentHostCommunicationApi>();
|
|
|
|
frontComponentHostCommunicationApi.navigate = hostApi.navigate;
|
|
frontComponentHostCommunicationApi.requestAccessTokenRefresh =
|
|
hostApi.requestAccessTokenRefresh;
|
|
frontComponentHostCommunicationApi.openSidePanelPage =
|
|
hostApi.openSidePanelPage;
|
|
frontComponentHostCommunicationApi.openCommandConfirmationModal =
|
|
createOpenCommandConfirmationModalAdapter(hostApi);
|
|
frontComponentHostCommunicationApi.unmountFrontComponent =
|
|
hostApi.unmountFrontComponent;
|
|
frontComponentHostCommunicationApi.enqueueSnackbar =
|
|
hostApi.enqueueSnackbar;
|
|
frontComponentHostCommunicationApi.closeSidePanel = hostApi.closeSidePanel;
|
|
frontComponentHostCommunicationApi.updateProgress = hostApi.updateProgress;
|
|
frontComponentHostCommunicationApi.copyToClipboard =
|
|
hostApi.copyToClipboard;
|
|
};
|
|
|
|
const onConfirmationModalResult: WorkerExports['onConfirmationModalResult'] =
|
|
async (result) => {
|
|
await handleCommandConfirmationModalResult(result);
|
|
};
|
|
|
|
const updateContext: WorkerExports['updateContext'] = async (
|
|
context: FrontComponentExecutionContext,
|
|
) => {
|
|
setFrontComponentExecutionContext(context);
|
|
};
|
|
|
|
ThreadWebWorker.self.export({
|
|
render,
|
|
initializeHostCommunicationApi,
|
|
onConfirmationModalResult,
|
|
updateContext,
|
|
});
|