diff --git a/.changeset/lucky-panthers-joke.md b/.changeset/lucky-panthers-joke.md new file mode 100644 index 0000000000..75b24ee0f3 --- /dev/null +++ b/.changeset/lucky-panthers-joke.md @@ -0,0 +1,7 @@ +--- +"@hashicorp/design-system-components": minor +--- + +`hds-code-editor` modifier - Add `cspNonce` argument and automatice nonce detection + +`CodeEditor` - Add `cspNonce` argument and automatice nonce detection diff --git a/packages/components/src/modifiers/hds-code-editor.ts b/packages/components/src/modifiers/hds-code-editor.ts index 272b65758f..45f6d8c923 100644 --- a/packages/components/src/modifiers/hds-code-editor.ts +++ b/packages/components/src/modifiers/hds-code-editor.ts @@ -39,6 +39,7 @@ export interface HdsCodeEditorSignature { ariaDescribedBy?: string; ariaLabel?: string; ariaLabelledBy?: string; + cspNonce?: string; hasLineWrapping?: boolean; language?: HdsCodeEditorLanguages; value?: string; @@ -55,6 +56,27 @@ async function defineStreamLanguage(streamParser: StreamParserType) { return StreamLanguage.define(streamParser); } +export function getCSPNonceFromMeta(): string | undefined { + const meta = document.querySelector( + 'meta[http-equiv="Content-Security-Policy"]' + ); + + if (meta === null) { + return undefined; + } + + const content = meta.getAttribute('content'); + + if (content === null) { + return undefined; + } + + // searches for either "style-src" or "script-src" followed by anything until a token like 'nonce-' + const match = content.match(/(?:style-src|script-src)[^;]*'nonce-([^']+)'/); + + return match ? match[1] : undefined; +} + const LOADER_HEIGHT = '164px'; const LANGUAGES: Record< @@ -276,7 +298,7 @@ export default class HdsCodeEditorModifier extends Modifier { + async ({ cspNonce, language, hasLineWrapping }) => { const [ { keymap, @@ -337,6 +359,13 @@ export default class HdsCodeEditorModifier extends Modifier ) => { try { const { EditorState } = await import('@codemirror/state'); const extensions = await this._buildExtensionsTask.perform({ + cspNonce, language, hasLineWrapping: hasLineWrapping ?? false, }); @@ -395,6 +426,7 @@ export default class HdsCodeEditorModifier extends Modifier { + const meta = document.querySelector( + 'meta[http-equiv="Content-Security-Policy"]' + ); + + if (meta) { + meta.parentNode.removeChild(meta); + } + }); + + test('returns undefined when no meta tag is present', function (assert) { + const existing = document.querySelector( + 'meta[http-equiv="Content-Security-Policy"]' + ); + + if (existing) { + existing.parentNode.removeChild(existing); + } + + assert.strictEqual( + getCSPNonceFromMeta(), + undefined, + 'Should return undefined if no meta tag is found' + ); + }); + + test('returns undefined when meta tag is present without a content attribute', function (assert) { + const meta = document.createElement('meta'); + + meta.setAttribute('http-equiv', 'Content-Security-Policy'); + + document.head.appendChild(meta); + + assert.strictEqual( + getCSPNonceFromMeta(), + undefined, + 'Should return undefined if content attribute is missing' + ); + }); + + test('extracts nonce from a meta tag with a style-src directive', function (assert) { + const meta = document.createElement('meta'); + + meta.setAttribute('http-equiv', 'Content-Security-Policy'); + meta.setAttribute( + 'content', + "default-src 'none'; style-src 'nonce-ABC123';" + ); + + document.head.appendChild(meta); + + assert.strictEqual( + getCSPNonceFromMeta(), + 'ABC123', + 'Should extract nonce "ABC123" from style-src directive' + ); + }); + + test('extracts nonce from a meta tag with a script-src directive', function (assert) { + const meta = document.createElement('meta'); + + meta.setAttribute('http-equiv', 'Content-Security-Policy'); + meta.setAttribute( + 'content', + "default-src 'none'; script-src 'nonce-XYZ789';" + ); + + document.head.appendChild(meta); + + assert.strictEqual( + getCSPNonceFromMeta(), + 'XYZ789', + 'Should extract nonce "XYZ789" from script-src directive' + ); + }); + + test('returns undefined if nonce is not present in the meta content', function (assert) { + const meta = document.createElement('meta'); + + meta.setAttribute('http-equiv', 'Content-Security-Policy'); + meta.setAttribute( + 'content', + "default-src 'none'; style-src 'unsafe-inline';" + ); + + document.head.appendChild(meta); + + assert.strictEqual( + getCSPNonceFromMeta(), + undefined, + 'Should return undefined if nonce is not present' + ); + }); +}); diff --git a/website/docs/components/code-editor/partials/code/component-api.md b/website/docs/components/code-editor/partials/code/component-api.md index 181ba8c27d..f2a52971f4 100644 --- a/website/docs/components/code-editor/partials/code/component-api.md +++ b/website/docs/components/code-editor/partials/code/component-api.md @@ -26,6 +26,9 @@ This component uses [CodeMirror 6](https://codemirror.net/) under the hood. Override this value to provide a meaningful `aria-label` for the [`Copy::Button`](/components/copy/button) component. + + Provides a Content Security Policy nonce to use when creating the style sheets for the editor. If none is provided, the editor will attempt to extract a nonce from the Content Security Policy. + Used to control whether a toggle button for toggling full-screen mode will be displayed.