Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 19 additions & 5 deletions src/main/utils/settingsTools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,19 +36,33 @@ export const deleteGlobalSettings = async () => {

export const getDocumentData = (): Partial<CurrentDocumentSettings> => {
const pluginData = figma.root.getPluginData(TOLGEE_PLUGIN_CONFIG_NAME);
const result = pluginData
? (JSON.parse(pluginData) as Partial<CurrentDocumentSettings>)
// 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<string, unknown>)
: {};

// 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<CurrentDocumentSettings>),
};
};

const setDocumentData = (data: Partial<CurrentDocumentSettings>) => {
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 = () => {
Expand Down Expand Up @@ -93,10 +107,10 @@ export const setPluginData = async (data: Partial<TolgeeConfig>) => {
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,
Expand Down
13 changes: 11 additions & 2 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<GlobalSettings, "apiKey"> & {
namespace: string;
branch?: string;
documentInfo: true;
Expand All @@ -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<GlobalSettings, "apiKey"> &
CurrentDocumentSettings &
CurrentPageSettings;

export type FormattedNode = {
characters: string;
Expand Down
7 changes: 6 additions & 1 deletion src/ui/views/Router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,14 +64,19 @@ 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);
const pageStringDetailsNodeInfo = useGlobalState((c) => c.config?.nodeInfo);
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 = () => {
Expand Down