Skip to content

Commit 96bf6c4

Browse files
Add CSP nonce support
1 parent f65f177 commit 96bf6c4

File tree

5 files changed

+116
-20
lines changed

5 files changed

+116
-20
lines changed

src/components/echoes-provider/EchoesProvider.tsx

Lines changed: 61 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,29 @@
1818
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
1919
*/
2020

21+
import createCache from '@emotion/cache';
22+
import { CacheProvider } from '@emotion/react';
2123
import { HeadlessMantineProvider } from '@mantine/core';
22-
import { PropsWithChildren } from 'react';
24+
import { PropsWithChildren, useMemo } from 'react';
2325
import { useIntl } from 'react-intl';
2426
import { Toaster as ToastContainer } from 'sonner';
2527
import { ToastGlobalStyles } from '~common/components/Toast';
2628
import { TooltipProvider, TooltipProviderProps, TypographyGlobalStyles } from '..';
2729
import { SelectGlobalStyles } from '../select/SelectCommons';
30+
import { NonceContext } from './NonceContext';
2831

2932
export interface EchoesProviderProps {
33+
/**
34+
* A nonce value for inline styles (Content Security Policy - CSP) (optional).
35+
* When provided, this nonce will be:
36+
* - Applied to Emotion's CSS-in-JS style tags
37+
* - Made available to components like Spotlight that need it for inline styles
38+
* - Used to comply with strict Content Security Policy requirements
39+
*
40+
* This should be set once at the application root and will automatically
41+
* propagate to all Echoes components that require it.
42+
*/
43+
nonce?: string;
3044
/**
3145
* Custom class name for all the toasts (optional).
3246
*/
@@ -85,30 +99,57 @@ export interface EchoesProviderProps {
8599
* );
86100
* }
87101
* ```
102+
*
103+
* **Content Security Policy (CSP) Support**
104+
*
105+
* If your application uses a strict Content Security Policy, you can provide a nonce
106+
* to enable inline styles required by Echoes components:
107+
*
108+
* ```tsx
109+
* function App() {
110+
* // Get nonce from meta tag or server context
111+
* const nonce = document.querySelector('meta[name="csp-nonce"]')?.getAttribute('content');
112+
*
113+
* return (
114+
* <EchoesProvider nonce={nonce}>
115+
* {children}
116+
* </EchoesProvider>
117+
* );
118+
* }
119+
* ```
88120
*/
89121
export function EchoesProvider(props: PropsWithChildren<EchoesProviderProps>) {
90-
const { children, tooltipsDelayDuration, toastsClassName, toastsVisibleNb = 5 } = props;
122+
const { children, nonce, tooltipsDelayDuration, toastsClassName, toastsVisibleNb = 5 } = props;
91123
const intl = useIntl();
92124

125+
// Create Emotion cache with nonce support for CSP compliance
126+
const emotionCache = useMemo(() => {
127+
const cache = createCache({ key: 'echoes', nonce });
128+
cache.compat = true;
129+
return cache;
130+
}, [nonce]);
131+
93132
return (
94-
<>
95-
<TypographyGlobalStyles />
96-
<SelectGlobalStyles />
97-
<ToastGlobalStyles />
98-
<TooltipProvider delayDuration={tooltipsDelayDuration}>
99-
<HeadlessMantineProvider>{children}</HeadlessMantineProvider>
100-
<ToastContainer
101-
containerAriaLabel={intl.formatMessage({
102-
id: 'toasts.keyboard_shortcut_aria_label',
103-
defaultMessage: 'Focus toasts messages with',
104-
description: 'ARIA-label for the toasts container keyboard shortcut',
105-
})}
106-
position="bottom-right"
107-
toastOptions={{ className: toastsClassName }}
108-
visibleToasts={toastsVisibleNb}
109-
/>
110-
</TooltipProvider>
111-
</>
133+
<CacheProvider value={emotionCache}>
134+
<NonceContext.Provider value={nonce}>
135+
<TypographyGlobalStyles />
136+
<SelectGlobalStyles />
137+
<ToastGlobalStyles />
138+
<TooltipProvider delayDuration={tooltipsDelayDuration}>
139+
<HeadlessMantineProvider>{children}</HeadlessMantineProvider>
140+
<ToastContainer
141+
containerAriaLabel={intl.formatMessage({
142+
id: 'toasts.keyboard_shortcut_aria_label',
143+
defaultMessage: 'Focus toasts messages with',
144+
description: 'ARIA-label for the toasts container keyboard shortcut',
145+
})}
146+
position="bottom-right"
147+
toastOptions={{ className: toastsClassName }}
148+
visibleToasts={toastsVisibleNb}
149+
/>
150+
</TooltipProvider>
151+
</NonceContext.Provider>
152+
</CacheProvider>
112153
);
113154
}
114155

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/*
2+
* Echoes React
3+
* Copyright (C) 2023-2025 SonarSource Sàrl
4+
* mailto:info AT sonarsource DOT com
5+
*
6+
* This program is free software; you can redistribute it and/or
7+
* modify it under the terms of the GNU Lesser General Public
8+
* License as published by the Free Software Foundation; either
9+
* version 3 of the License, or (at your option) any later version.
10+
*
11+
* This program is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14+
* Lesser General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU Lesser General Public License
17+
* along with this program; if not, write to the Free Software Foundation,
18+
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19+
*/
20+
21+
import { createContext, useContext } from 'react';
22+
23+
/**
24+
* Context for providing CSP nonce value to components that need it.
25+
* This is used internally by Echoes components to access the nonce
26+
* configured in the EchoesProvider.
27+
*/
28+
export const NonceContext = createContext<string | undefined>(undefined);
29+
30+
/**
31+
* Hook to access the CSP nonce from the EchoesProvider.
32+
* Components that need to apply nonces to inline styles for CSP compliance
33+
* can use this hook to retrieve the globally configured nonce value.
34+
*
35+
* @returns The nonce string if configured, undefined otherwise
36+
*/
37+
export function useNonce(): string | undefined {
38+
return useContext(NonceContext);
39+
}

src/components/echoes-provider/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,4 @@
1919
*/
2020

2121
export { EchoesProvider, type EchoesProviderProps } from './EchoesProvider';
22+
export { useNonce } from './NonceContext';

src/components/spotlight/Spotlight.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import { PropsWithChildren } from 'react';
2222
import { useIntl } from 'react-intl';
2323
import ReactJoyride, { TooltipRenderProps } from 'react-joyride';
24+
import { useNonce } from '../echoes-provider/NonceContext';
2425
import { SpotlightModalForStep } from './SpotlightModalForStep';
2526
import { SpotlightModalPlacement, SpotlightProps, SpotlightStep } from './SpotlightTypes';
2627

@@ -97,6 +98,7 @@ export function Spotlight(props: Readonly<SpotlightProps>) {
9798
image,
9899
isRunning = true,
99100
nextLabel,
101+
nonce: nonceProp,
100102
shouldDisableOverlayClose,
101103
skipLabel,
102104
stepIndex,
@@ -105,6 +107,10 @@ export function Spotlight(props: Readonly<SpotlightProps>) {
105107
} = props;
106108

107109
const intl = useIntl();
110+
const nonceFromContext = useNonce();
111+
112+
// Use prop nonce if provided, otherwise fall back to context nonce
113+
const nonce = nonceProp ?? nonceFromContext;
108114

109115
const labels = {
110116
back: intl.formatMessage({
@@ -152,6 +158,7 @@ export function Spotlight(props: Readonly<SpotlightProps>) {
152158
nextLabelWithProgress: nextLabel ?? labels.next,
153159
skip: skipLabel ?? labels.skip,
154160
}}
161+
nonce={nonce}
155162
run={isRunning}
156163
stepIndex={stepIndex}
157164
/*--------------------------------------------------------------------------------------------

src/components/spotlight/SpotlightTypes.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,14 @@ export interface SpotlightProps {
122122
*/
123123
nextLabel?: string;
124124

125+
/**
126+
* A nonce value for inline styles (Content Security Policy - CSP) (optional).
127+
* In most cases, you should configure the nonce once in the EchoesProvider
128+
* instead of passing it to individual components. This prop is only needed
129+
* if you need to override the global nonce for this specific Spotlight instance.
130+
*/
131+
nonce?: string;
132+
125133
/**
126134
* Shoud we disable closing the spotlight when clicking the overlay?
127135
* Defaults to false when there's only one step, true otherwise (optional)

0 commit comments

Comments
 (0)