diff --git a/src/main/utils/settingsTools.ts b/src/main/utils/settingsTools.ts index d00be79..aa19b25 100644 --- a/src/main/utils/settingsTools.ts +++ b/src/main/utils/settingsTools.ts @@ -36,19 +36,33 @@ export const deleteGlobalSettings = async () => { export const getDocumentData = (): Partial => { const pluginData = figma.root.getPluginData(TOLGEE_PLUGIN_CONFIG_NAME); - const result = pluginData - ? (JSON.parse(pluginData) as Partial) + // Parse into a loose record first so the self-heal can strip a field the + // declared type no longer admits, then narrow on return. + const stored = pluginData + ? (JSON.parse(pluginData) as Record) : {}; + // Self-heal: older versions persisted the apiKey into the shared document. + // Drop it so a leaked key can never override the per-user key from + // clientStorage (and so it stops being surfaced to other collaborators). + delete stored.apiKey; + return { ignorePrefix: "_", ignoreNumbers: true, - ...result, + ...(stored as Partial), }; }; const setDocumentData = (data: Partial) => { - figma.root.setPluginData(TOLGEE_PLUGIN_CONFIG_NAME, JSON.stringify(data)); + // Defense in depth: never write the apiKey into the shared document, even if + // a caller bypasses the type. The secret belongs in clientStorage only. + const documentData = { ...data }; + delete (documentData as { apiKey?: string }).apiKey; + figma.root.setPluginData( + TOLGEE_PLUGIN_CONFIG_NAME, + JSON.stringify(documentData), + ); }; export const deleteDocumentData = () => { @@ -93,10 +107,10 @@ export const setPluginData = async (data: Partial) => { updateScreenshots, variableCasing, } = data; + // apiKey -> per-user clientStorage only (never the shared document). await setGlobalSettings({ apiKey, apiUrl, ignorePrefix, ignoreNumbers }); setDocumentData({ addTags, - apiKey, apiUrl, branch, documentInfo: true, diff --git a/src/types.ts b/src/types.ts index ce41383..804b60f 100644 --- a/src/types.ts +++ b/src/types.ts @@ -130,7 +130,12 @@ export type GlobalSettings = { | "noSpaces"; }; -export type CurrentDocumentSettings = GlobalSettings & { +// NOTE: `apiKey` is intentionally omitted. The API key is a secret and must +// never be persisted in the shared document (figma.root pluginData), which is +// stored in plaintext in the .fig file, syncs to every collaborator and travels +// with file copies/exports. The key lives in figma.clientStorage (per-user) +// only — see settingsTools.ts. +export type CurrentDocumentSettings = Omit & { namespace: string; branch?: string; documentInfo: true; @@ -144,7 +149,11 @@ export type CurrentPageSettings = { nodeInfo?: NodeInfo; }; -export type TolgeeConfig = CurrentDocumentSettings & CurrentPageSettings; +// The full in-memory config the UI works with. `apiKey` comes from the +// per-user clientStorage layer, the rest from the document/page layers. +export type TolgeeConfig = Pick & + CurrentDocumentSettings & + CurrentPageSettings; export type FormattedNode = { characters: string; diff --git a/src/ui/views/Router.tsx b/src/ui/views/Router.tsx index ecc83fe..59956d1 100644 --- a/src/ui/views/Router.tsx +++ b/src/ui/views/Router.tsx @@ -64,6 +64,7 @@ export const Router = () => { const routeKey = useGlobalState((c) => c.routeKey); const globalError = useGlobalState((c) => c.globalError); const documentInfo = useGlobalState((c) => c.config?.documentInfo); + const apiKey = useGlobalState((c) => c.config?.apiKey); const pageInfo = useGlobalState((c) => c.config?.pageInfo); const pageCopy = useGlobalState((c) => c.config?.pageCopy); const pageStringDetails = useGlobalState((c) => c.config?.pageStringDetails); @@ -71,7 +72,11 @@ export const Router = () => { const { setRoute } = useGlobalActions(); const editorMode = useEditorMode(); - const forceSettings = !pageCopy && !documentInfo; + // The apiKey now lives in per-user clientStorage, not the shared document, so + // a collaborator can open a configured file (documentInfo is set) yet have no + // key of their own. Force them into Settings to enter one instead of dropping + // them on a non-working Index. + const forceSettings = !pageCopy && (!documentInfo || !apiKey); const errorOnTop = !forceSettings && routeKey !== "settings"; const handleResolveError = () => {