Files
twenty/packages/twenty-front-component-renderer/src/host/components/FrontComponentRenderer.tsx
T
martmull 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
2026-05-13 16:27:56 +00:00

136 lines
4.5 KiB
TypeScript

import { FrontComponentErrorEffect } from '@/remote/components/FrontComponentErrorEffect';
import { FrontComponentInitializeHostCommunicationApiEffect } from '@/remote/components/FrontComponentInitializeHostCommunicationApiEffect';
import { FrontComponentUpdateContextEffect } from '@/remote/components/FrontComponentUpdateContextEffect';
import { FrontComponentUpdateHostCommunicationApiEffect } from '@/remote/components/FrontComponentUpdateHostCommunicationApiEffect';
import { type FrontComponentHostCommunicationApi } from '@/types/FrontComponentHostCommunicationApi';
import { type SdkClientUrls } from '@/types/HostToWorkerRenderContext';
import { type WorkerExports } from '@/types/WorkerExports';
import { type FrontComponentExecutionContext } from 'twenty-sdk/front-component';
import { type ThreadWebWorker } from '@quilted/threads';
import {
type RemoteReceiver,
RemoteRootRenderer,
} from '@remote-dom/react/host';
import { useMemo, useState } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { ThemeProvider } from 'twenty-ui/theme-constants';
import { FrontComponentWorkerEffect } from '../../remote/components/FrontComponentWorkerEffect';
import { componentRegistry } from '../generated/host-component-registry';
type FrontComponentContentProps = {
componentUrl: string;
applicationAccessToken?: string;
apiUrl?: string;
sdkClientUrls?: SdkClientUrls;
applicationVariables?: Record<string, string>;
executionContext: FrontComponentExecutionContext;
frontComponentHostCommunicationApi: FrontComponentHostCommunicationApi;
onError: (error?: Error) => void;
colorScheme: 'light' | 'dark';
};
export const FrontComponentRenderer = ({
componentUrl,
applicationAccessToken,
apiUrl,
sdkClientUrls,
applicationVariables,
executionContext,
frontComponentHostCommunicationApi,
onError,
colorScheme,
}: FrontComponentContentProps) => {
const [receiver, setReceiver] = useState<RemoteReceiver | null>(null);
const [thread, setThread] = useState<ThreadWebWorker<
WorkerExports,
FrontComponentHostCommunicationApi
> | null>(null);
const [error, setError] = useState<Error | null>(null);
const [isExecutionContextInitialized, setIsExecutionContextInitialized] =
useState(false);
const MemoizedFrontComponentWorkerEffect = useMemo(() => {
return (
<FrontComponentWorkerEffect
componentUrl={componentUrl}
applicationAccessToken={applicationAccessToken}
apiUrl={apiUrl}
sdkClientUrls={sdkClientUrls}
applicationVariables={applicationVariables}
frontComponentId={executionContext.frontComponentId}
setReceiver={setReceiver}
setThread={setThread}
setError={setError}
/>
);
}, [
componentUrl,
setError,
setReceiver,
setThread,
applicationAccessToken,
apiUrl,
sdkClientUrls,
applicationVariables,
executionContext.frontComponentId,
]);
return (
<>
{MemoizedFrontComponentWorkerEffect}
{isDefined(error) && (
<>
<FrontComponentErrorEffect error={error} onError={onError} />
<div
style={{
padding: '12px 16px',
backgroundColor: '#fef2f2',
border: '1px solid #fecaca',
borderRadius: '6px',
color: '#991b1b',
fontFamily: 'monospace',
fontSize: '13px',
whiteSpace: 'pre-wrap',
wordBreak: 'break-word',
maxHeight: '200px',
overflow: 'auto',
}}
>
<strong>FrontComponent error:</strong> {error.message}
</div>
</>
)}
{isDefined(thread) && (
<>
<FrontComponentUpdateHostCommunicationApiEffect
thread={thread}
frontComponentHostCommunicationApi={
frontComponentHostCommunicationApi
}
/>
<FrontComponentInitializeHostCommunicationApiEffect thread={thread} />
<FrontComponentUpdateContextEffect
thread={thread}
executionContext={executionContext}
onExecutionContextInitialized={() =>
setIsExecutionContextInitialized(true)
}
/>
</>
)}
{isDefined(receiver) && isExecutionContextInitialized && (
<ThemeProvider colorScheme={colorScheme}>
<RemoteRootRenderer
receiver={receiver}
components={componentRegistry}
/>
</ThemeProvider>
)}
</>
);
};