diff --git a/env.config.js b/env.config.js index f4585f66d..b8a8629e2 100644 --- a/env.config.js +++ b/env.config.js @@ -3,6 +3,15 @@ // Also note that in an actual application this file would be added to .gitignore. const config = { JS_FILE_VAR: 'JS_FILE_VAR_VALUE_FOR_EXAMPLE_APP', + piiOptions: { + selectors: { + username: [ + { attributeName: 'data-dd-privacy', attributeValue: 'mask' }, // Datadog example + { attributeName: 'data-hj-suppress', attributeValue: '' }, // Hotjar example + { className: 'fs-mask' }, // Fullstory example + ], + }, + }, }; export default config; diff --git a/example/ExamplePage.jsx b/example/ExamplePage.jsx index 1b513ded3..c1c7693fd 100644 --- a/example/ExamplePage.jsx +++ b/example/ExamplePage.jsx @@ -48,6 +48,7 @@ class ExamplePage extends Component {

EXAMPLE_VAR env var came through: {getConfig().EXAMPLE_VAR}

JS_FILE_VAR var came through: {getConfig().JS_FILE_VAR}

Visit authenticated page.

+

Visit PII page.

Visit error page.

); diff --git a/example/PiiPage.jsx b/example/PiiPage.jsx new file mode 100644 index 000000000..f4035174e --- /dev/null +++ b/example/PiiPage.jsx @@ -0,0 +1,95 @@ +import React, { + forwardRef, useContext, useEffect, useRef, useState, +} from 'react'; +import PropTypes from 'prop-types'; + +import { AppContext, usePii, withPii } from '@edx/frontend-platform/react'; + +// Example via `usePii` (hook) +const UsernameWithPii = forwardRef(({ children, ...rest }, ref) => { + const piiProps = usePii('username', rest); + return {children}; +}); +UsernameWithPii.displayName = 'UsernameWithPii'; +UsernameWithPii.propTypes = { + children: PropTypes.node.isRequired, +}; + +// Example via `withPii` (HOC) +const Username = forwardRef(({ children, ...rest }, ref) => ( + {children} +)); +Username.displayName = 'Username'; +Username.propTypes = { + children: PropTypes.node.isRequired, +}; +const UsernameWithPii2 = withPii('username')(Username); + +export default function PiiPage() { + const { authenticatedUser, config } = useContext(AppContext); + const firstUsernameRef = useRef(null); + const secondUsernameRef = useRef(null); + const [firstUsernameNode, setFirstUsernameNode] = useState(null); + const [secondUsernameNode, setSecondUsernameNode] = useState(null); + + useEffect(() => { + if (firstUsernameRef.current) { + setFirstUsernameNode(firstUsernameRef.current.outerHTML); + } + if (secondUsernameRef.current) { + setSecondUsernameNode(secondUsernameRef.current.outerHTML); + } + }, []); + + const { piiOptions } = config; + return ( +
+

Privacy options to obfuscate PII

+ +

Current PII configuration

+ {piiOptions ? ( +
+          {JSON.stringify(piiOptions, null, 2)}
+        
+ ) : ( +

+ No PII-related configuration found. Consider updating this application's env.config.js to + configure any vendor-specific PII-related attributes/values. +

+ )} + +

Examples

+

+ The following examples below demonstrate using usePii and withPii to + obfuscate PII (e.g., username) based on the application's configuration. Inspect the DOM + elements for the rendered usernames below to observe the configured PII attributes/values. +

+ + {/* Example 1 (usePii) */} +

usePii (hook)

+

+ Obfuscated username:{' '} + + {authenticatedUser.username} + +

+ Result:{' '} +
+        {firstUsernameNode}
+      
+ + {/* Example 2 (withPii) */} +

withPii (HOC)

+

+ Obfuscated username:{' '} + + {authenticatedUser.username} + +

+ Result:{' '} +
+        {secondUsernameNode}
+      
+
+ ); +} diff --git a/example/index.jsx b/example/index.jsx index 52b37aec5..bfe150662 100644 --- a/example/index.jsx +++ b/example/index.jsx @@ -16,6 +16,7 @@ import { Routes, Route } from 'react-router-dom'; import './index.scss'; import ExamplePage from './ExamplePage'; import AuthenticatedPage from './AuthenticatedPage'; +import PiiPage from './PiiPage'; subscribe(APP_READY, () => { ReactDOM.render( @@ -27,6 +28,7 @@ subscribe(APP_READY, () => { element={} /> } /> + } /> , document.getElementById('root'), diff --git a/package-lock.json b/package-lock.json index 0ea97a193..c0b0576dc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -36,7 +36,7 @@ "devDependencies": { "@edx/brand": "npm:@openedx/brand-openedx@^1.2.2", "@edx/browserslist-config": "1.2.0", - "@openedx/frontend-build": "14.0.10", + "@openedx/frontend-build": "14.0.15", "@openedx/paragon": "22.6.1", "@testing-library/jest-dom": "6.4.6", "@testing-library/react": "12.1.5", @@ -96,11 +96,10 @@ } }, "node_modules/@babel/cli": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/cli/-/cli-7.24.7.tgz", - "integrity": "sha512-8dfPprJgV4O14WTx+AQyEA+opgUKPrsIXX/MdL50J1n06EQJ6m1T+CdsJe0qEC0B/Xl85i+Un5KVAxd/PACX9A==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/cli/-/cli-7.24.8.tgz", + "integrity": "sha512-isdp+G6DpRyKc+3Gqxy2rjzgF7Zj9K0mzLNnxz+E/fgeag8qT3vVulX4gY9dGO1q0y+0lUv6V3a+uhUzMzrwXg==", "dev": true, - "license": "MIT", "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "commander": "^6.2.0", @@ -217,31 +216,30 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.7.tgz", - "integrity": "sha512-qJzAIcv03PyaWqxRgO4mSU3lihncDT296vnyuE2O8uA4w3UHWI4S3hgeZd1L8W1Bft40w9JxJ2b412iDUFFRhw==", + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.25.2.tgz", + "integrity": "sha512-bYcppcpKBvX4znYaPEeFau03bp89ShqNMLs+rmdptMw+heSZh9+z84d2YG+K7cYLbWwzdjtDoW/uqZmPjulClQ==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/core": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.7.tgz", - "integrity": "sha512-nykK+LEK86ahTkX/3TgauT0ikKoNCfKHEaZYTUVupJdTLzGNvrblu4u6fa7DhZONAltdf8e662t/abY8idrd/g==", + "version": "7.24.9", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.9.tgz", + "integrity": "sha512-5e3FI4Q3M3Pbr21+5xJwCv6ZT6KmGkI0vw3Tozy5ODAQFTIWe37iT8Cr7Ice2Ntb+M3iSKCEWMB1MBgKrW3whg==", "dev": true, - "license": "MIT", "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.24.7", - "@babel/generator": "^7.24.7", - "@babel/helper-compilation-targets": "^7.24.7", - "@babel/helper-module-transforms": "^7.24.7", - "@babel/helpers": "^7.24.7", - "@babel/parser": "^7.24.7", + "@babel/generator": "^7.24.9", + "@babel/helper-compilation-targets": "^7.24.8", + "@babel/helper-module-transforms": "^7.24.9", + "@babel/helpers": "^7.24.8", + "@babel/parser": "^7.24.8", "@babel/template": "^7.24.7", - "@babel/traverse": "^7.24.7", - "@babel/types": "^7.24.7", + "@babel/traverse": "^7.24.8", + "@babel/types": "^7.24.9", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -282,12 +280,12 @@ } }, "node_modules/@babel/generator": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.7.tgz", - "integrity": "sha512-oipXieGC3i45Y1A41t4tAqpnEZWgB/lC6Ehh6+rOviR5XWpTtMmLN+fGjz9vOiNRt0p6RtO6DtD0pdU3vpqdSA==", + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.0.tgz", + "integrity": "sha512-3LEEcj3PVW8pW2R1SR1M89g/qrYk/m/mB/tLqn7dn4sbBUQyTqnlod+II2U4dqiGtUmkcnAmkMDralTFZttRiw==", "dev": true, "dependencies": { - "@babel/types": "^7.24.7", + "@babel/types": "^7.25.0", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^2.5.1" @@ -324,14 +322,14 @@ } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.24.7.tgz", - "integrity": "sha512-ctSdRHBi20qWOfy27RUb4Fhp07KSJ3sXcuSvTrXrc4aG8NSYDo1ici3Vhg9bg69y5bj0Mr1lh0aeEgTvc12rMg==", + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.2.tgz", + "integrity": "sha512-U2U5LsSaZ7TAt3cfaymQ8WHh0pxvdHoEk6HVpaexxixjyEquMh0L0YNJNM6CTGKMXV1iksi0iZkGw4AcFkPaaw==", "dev": true, "dependencies": { - "@babel/compat-data": "^7.24.7", - "@babel/helper-validator-option": "^7.24.7", - "browserslist": "^4.22.2", + "@babel/compat-data": "^7.25.2", + "@babel/helper-validator-option": "^7.24.8", + "browserslist": "^4.23.1", "lru-cache": "^5.1.1", "semver": "^6.3.1" }, @@ -437,14 +435,13 @@ } }, "node_modules/@babel/helper-member-expression-to-functions": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.24.7.tgz", - "integrity": "sha512-LGeMaf5JN4hAT471eJdBs/GK1DoYIJ5GCtZN/EsL6KUiiDZOvO/eKE11AMZJa2zP4zk4qe9V2O/hxAmkRc8p6w==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.24.8.tgz", + "integrity": "sha512-LABppdt+Lp/RlBxqrh4qgf1oEH/WxdzQNDJIu5gC/W1GyvPVrOBiItmmM8wan2fm4oYqFuFfkXmlGpLQhPY8CA==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/traverse": "^7.24.7", - "@babel/types": "^7.24.7" + "@babel/traverse": "^7.24.8", + "@babel/types": "^7.24.8" }, "engines": { "node": ">=6.9.0" @@ -465,16 +462,15 @@ } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.24.7.tgz", - "integrity": "sha512-1fuJEwIrp+97rM4RWdO+qrRsZlAeL1lQJoPqtCYWv0NL115XM93hIH4CSRln2w52SqvmY5hqdtauB6QFCDiZNQ==", + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.25.2.tgz", + "integrity": "sha512-BjyRAbix6j/wv83ftcVJmBt72QtHI56C7JXZoG2xATiLpmoC7dpd8WnkikExHDVPpi/3qCmO6WY1EaXOluiecQ==", "dev": true, "dependencies": { - "@babel/helper-environment-visitor": "^7.24.7", "@babel/helper-module-imports": "^7.24.7", "@babel/helper-simple-access": "^7.24.7", - "@babel/helper-split-export-declaration": "^7.24.7", - "@babel/helper-validator-identifier": "^7.24.7" + "@babel/helper-validator-identifier": "^7.24.7", + "@babel/traverse": "^7.25.2" }, "engines": { "node": ">=6.9.0" @@ -497,11 +493,10 @@ } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.7.tgz", - "integrity": "sha512-Rq76wjt7yz9AAc1KnlRKNAi/dMSVWgDRx43FHoJEbcYU6xOWaE2dVPwcdTukJrjxS65GITyfbvEYHvkirZ6uEg==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.8.tgz", + "integrity": "sha512-FFWx5142D8h2Mgr/iPVGH5G7w6jDn4jUSpZTyDnQO0Yn7Ks2Kuz6Pci8H6MPCoUJegd/UZQ3tAvfLCxQSnWWwg==", "dev": true, - "license": "MIT", "engines": { "node": ">=6.9.0" } @@ -525,15 +520,14 @@ } }, "node_modules/@babel/helper-replace-supers": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.24.7.tgz", - "integrity": "sha512-qTAxxBM81VEyoAY0TtLrx1oAEJc09ZK67Q9ljQToqCnA+55eNwCORaxlKyu+rNfX86o8OXRUSNUnrtsAZXM9sg==", + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.25.0.tgz", + "integrity": "sha512-q688zIvQVYtZu+i2PsdIu/uWGRpfxzr5WESsfpShfZECkO+d2o+WROWezCi/Q6kJ0tfPa5+pUGUlfx2HhrA3Bg==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/helper-environment-visitor": "^7.24.7", - "@babel/helper-member-expression-to-functions": "^7.24.7", - "@babel/helper-optimise-call-expression": "^7.24.7" + "@babel/helper-member-expression-to-functions": "^7.24.8", + "@babel/helper-optimise-call-expression": "^7.24.7", + "@babel/traverse": "^7.25.0" }, "engines": { "node": ">=6.9.0" @@ -584,9 +578,9 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.7.tgz", - "integrity": "sha512-7MbVt6xrwFQbunH2DNQsAP5sTGxfqQtErvBIvIMi6EQnbgUOuVYanvREcmFrOPhoXBrTtjhhP+lW+o5UfK+tDg==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz", + "integrity": "sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==", "dev": true, "engines": { "node": ">=6.9.0" @@ -603,9 +597,9 @@ } }, "node_modules/@babel/helper-validator-option": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.7.tgz", - "integrity": "sha512-yy1/KvjhV/ZCL+SM7hBrvnZJ3ZuT9OuZgIJAGpPEToANvc3iM6iDvBnRjtElWibHU6n8/LPR/EjX9EtIEYO3pw==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.8.tgz", + "integrity": "sha512-xb8t9tD1MHLungh/AIoWYN+gVHaB9kwlu8gffXGSt3FFEIT7RjS+xWbc2vUD1UTZdIpKj/ab3rdqJ7ufngyi2Q==", "dev": true, "engines": { "node": ">=6.9.0" @@ -628,13 +622,13 @@ } }, "node_modules/@babel/helpers": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.7.tgz", - "integrity": "sha512-NlmJJtvcw72yRJRcnCmGvSi+3jDEg8qFu3z0AFoymmzLx5ERVWyzd9kVXr7Th9/8yIJi2Zc6av4Tqz3wFs8QWg==", + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.25.0.tgz", + "integrity": "sha512-MjgLZ42aCm0oGjJj8CtSM3DB8NOOf8h2l7DCTePJs29u+v7yO/RBX9nShlKMgFnRks/Q4tBAe7Hxnov9VkGwLw==", "dev": true, "dependencies": { - "@babel/template": "^7.24.7", - "@babel/types": "^7.24.7" + "@babel/template": "^7.25.0", + "@babel/types": "^7.25.0" }, "engines": { "node": ">=6.9.0" @@ -735,10 +729,13 @@ } }, "node_modules/@babel/parser": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.7.tgz", - "integrity": "sha512-9uUYRm6OqQrCqQdG1iCBwBPZgN8ciDBro2nIOFaiRz1/BCxaI7CNvQbDHvsArAC7Tw9Hda/B3U+6ui9u4HWXPw==", + "version": "7.25.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.3.tgz", + "integrity": "sha512-iLTJKDbJ4hMvFPgQwwsVoxtHyWpKKPBrxkANrSYewDPaPpT5py5yeVkgPIJ7XYXhndxJpaA3PyALSXQ7u8e/Dw==", "dev": true, + "dependencies": { + "@babel/types": "^7.25.2" + }, "bin": { "parser": "bin/babel-parser.js" }, @@ -1255,18 +1252,16 @@ } }, "node_modules/@babel/plugin-transform-classes": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.24.7.tgz", - "integrity": "sha512-CFbbBigp8ln4FU6Bpy6g7sE8B/WmCmzvivzUC6xDAdWVsjYTXijpuuGJmYkAaoWAzcItGKT3IOAbxRItZ5HTjw==", + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.25.0.tgz", + "integrity": "sha512-xyi6qjr/fYU304fiRwFbekzkqVJZ6A7hOjWZd+89FVcBqPV3S9Wuozz82xdpLspckeaafntbzglaW4pqpzvtSw==", "dev": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.24.7", - "@babel/helper-compilation-targets": "^7.24.7", - "@babel/helper-environment-visitor": "^7.24.7", - "@babel/helper-function-name": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/helper-replace-supers": "^7.24.7", - "@babel/helper-split-export-declaration": "^7.24.7", + "@babel/helper-compilation-targets": "^7.24.8", + "@babel/helper-plugin-utils": "^7.24.8", + "@babel/helper-replace-supers": "^7.25.0", + "@babel/traverse": "^7.25.0", "globals": "^11.1.0" }, "engines": { @@ -1293,12 +1288,12 @@ } }, "node_modules/@babel/plugin-transform-destructuring": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.24.7.tgz", - "integrity": "sha512-19eJO/8kdCQ9zISOf+SEUJM/bAUIsvY3YDnXZTupUCQ8LgrWnsG/gFB9dvXqdXnRXMAM8fvt7b0CBKQHNGy1mw==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.24.8.tgz", + "integrity": "sha512-36e87mfY8TnRxc7yc6M9g9gOB7rKgSahqkIKwLpz4Ppk2+zC2Cy1is0uwtuSG6AE4zlTOUa+7JGz9jCJGLqQFQ==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.24.8" }, "engines": { "node": ">=6.9.0" @@ -1498,13 +1493,13 @@ } }, "node_modules/@babel/plugin-transform-modules-commonjs": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.24.7.tgz", - "integrity": "sha512-iFI8GDxtevHJ/Z22J5xQpVqFLlMNstcLXh994xifFwxxGslr2ZXXLWgtBeLctOD63UFDArdvN6Tg8RFw+aEmjQ==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.24.8.tgz", + "integrity": "sha512-WHsk9H8XxRs3JXKWFiqtQebdh9b/pTk4EgueygFzYlTKAg0Ud985mSevdNjdXdFBATSKVJGQXP1tv6aGbssLKA==", "dev": true, "dependencies": { - "@babel/helper-module-transforms": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-module-transforms": "^7.24.8", + "@babel/helper-plugin-utils": "^7.24.8", "@babel/helper-simple-access": "^7.24.7" }, "engines": { @@ -1663,12 +1658,12 @@ } }, "node_modules/@babel/plugin-transform-optional-chaining": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.24.7.tgz", - "integrity": "sha512-tK+0N9yd4j+x/4hxF3F0e0fu/VdcxU18y5SevtyM/PCFlQvXbR0Zmlo2eBrKtVipGNFzpq56o8WsIIKcJFUCRQ==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.24.8.tgz", + "integrity": "sha512-5cTOLSMs9eypEy8JUVvIKOu6NgvbJMnpG62VpIHrTmROdQ+L5mDAaI40g25k5vXti55JWNX5jCkq3HZxXBQANw==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.8", "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7", "@babel/plugin-syntax-optional-chaining": "^7.8.3" }, @@ -1917,12 +1912,12 @@ } }, "node_modules/@babel/plugin-transform-typeof-symbol": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.24.7.tgz", - "integrity": "sha512-VtR8hDy7YLB7+Pet9IarXjg/zgCMSF+1mNS/EQEiEaUPoFXCVsHG64SIxcaaI2zJgRiv+YmgaQESUfWAdbjzgg==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.24.8.tgz", + "integrity": "sha512-adNTUpDCVnmAE58VEqKlAA6ZBlNkMnWD0ZcW76lyNFN3MJniyGFZfNwERVk8Ap56MCnXztmDr19T4mPTztcuaw==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.24.8" }, "engines": { "node": ">=6.9.0" @@ -2013,16 +2008,15 @@ } }, "node_modules/@babel/preset-env": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.24.7.tgz", - "integrity": "sha512-1YZNsc+y6cTvWlDHidMBsQZrZfEFjRIo/BZCT906PMdzOyXtSLTgqGdrpcuTDCXyd11Am5uQULtDIcCfnTc8fQ==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.24.8.tgz", + "integrity": "sha512-vObvMZB6hNWuDxhSaEPTKCwcqkAIuDtE+bQGn4XMXne1DSLzFVY8Vmj1bm+mUQXYNN8NmaQEO+r8MMbzPr1jBQ==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.24.7", - "@babel/helper-compilation-targets": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/helper-validator-option": "^7.24.7", + "@babel/compat-data": "^7.24.8", + "@babel/helper-compilation-targets": "^7.24.8", + "@babel/helper-plugin-utils": "^7.24.8", + "@babel/helper-validator-option": "^7.24.8", "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.24.7", "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.24.7", "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.24.7", @@ -2053,9 +2047,9 @@ "@babel/plugin-transform-block-scoping": "^7.24.7", "@babel/plugin-transform-class-properties": "^7.24.7", "@babel/plugin-transform-class-static-block": "^7.24.7", - "@babel/plugin-transform-classes": "^7.24.7", + "@babel/plugin-transform-classes": "^7.24.8", "@babel/plugin-transform-computed-properties": "^7.24.7", - "@babel/plugin-transform-destructuring": "^7.24.7", + "@babel/plugin-transform-destructuring": "^7.24.8", "@babel/plugin-transform-dotall-regex": "^7.24.7", "@babel/plugin-transform-duplicate-keys": "^7.24.7", "@babel/plugin-transform-dynamic-import": "^7.24.7", @@ -2068,7 +2062,7 @@ "@babel/plugin-transform-logical-assignment-operators": "^7.24.7", "@babel/plugin-transform-member-expression-literals": "^7.24.7", "@babel/plugin-transform-modules-amd": "^7.24.7", - "@babel/plugin-transform-modules-commonjs": "^7.24.7", + "@babel/plugin-transform-modules-commonjs": "^7.24.8", "@babel/plugin-transform-modules-systemjs": "^7.24.7", "@babel/plugin-transform-modules-umd": "^7.24.7", "@babel/plugin-transform-named-capturing-groups-regex": "^7.24.7", @@ -2078,7 +2072,7 @@ "@babel/plugin-transform-object-rest-spread": "^7.24.7", "@babel/plugin-transform-object-super": "^7.24.7", "@babel/plugin-transform-optional-catch-binding": "^7.24.7", - "@babel/plugin-transform-optional-chaining": "^7.24.7", + "@babel/plugin-transform-optional-chaining": "^7.24.8", "@babel/plugin-transform-parameters": "^7.24.7", "@babel/plugin-transform-private-methods": "^7.24.7", "@babel/plugin-transform-private-property-in-object": "^7.24.7", @@ -2089,7 +2083,7 @@ "@babel/plugin-transform-spread": "^7.24.7", "@babel/plugin-transform-sticky-regex": "^7.24.7", "@babel/plugin-transform-template-literals": "^7.24.7", - "@babel/plugin-transform-typeof-symbol": "^7.24.7", + "@babel/plugin-transform-typeof-symbol": "^7.24.8", "@babel/plugin-transform-unicode-escapes": "^7.24.7", "@babel/plugin-transform-unicode-property-regex": "^7.24.7", "@babel/plugin-transform-unicode-regex": "^7.24.7", @@ -2098,7 +2092,7 @@ "babel-plugin-polyfill-corejs2": "^0.4.10", "babel-plugin-polyfill-corejs3": "^0.10.4", "babel-plugin-polyfill-regenerator": "^0.6.1", - "core-js-compat": "^3.31.0", + "core-js-compat": "^3.37.1", "semver": "^6.3.1" }, "engines": { @@ -2194,34 +2188,30 @@ } }, "node_modules/@babel/template": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.7.tgz", - "integrity": "sha512-jYqfPrU9JTF0PmPy1tLYHW4Mp4KlgxJD9l2nP9fD6yT/ICi554DmrWBAEYpIelzjHf1msDP3PxJIRt/nFNfBig==", + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.0.tgz", + "integrity": "sha512-aOOgh1/5XzKvg1jvVz7AVrx2piJ2XBi227DHmbY6y+bM9H2FlN+IfecYu4Xl0cNiiVejlsCri89LUsbj8vJD9Q==", "dev": true, - "license": "MIT", "dependencies": { "@babel/code-frame": "^7.24.7", - "@babel/parser": "^7.24.7", - "@babel/types": "^7.24.7" + "@babel/parser": "^7.25.0", + "@babel/types": "^7.25.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.7.tgz", - "integrity": "sha512-yb65Ed5S/QAcewNPh0nZczy9JdYXkkAbIsEo+P7BE7yO3txAY30Y/oPa3QkQ5It3xVG2kpKMg9MsdxZaO31uKA==", + "version": "7.25.3", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.3.tgz", + "integrity": "sha512-HefgyP1x754oGCsKmV5reSmtV7IXj/kpaE1XYY+D9G5PvKKoFfSbiS4M77MdjuwlZKDIKFCffq9rPU+H/s3ZdQ==", "dev": true, "dependencies": { "@babel/code-frame": "^7.24.7", - "@babel/generator": "^7.24.7", - "@babel/helper-environment-visitor": "^7.24.7", - "@babel/helper-function-name": "^7.24.7", - "@babel/helper-hoist-variables": "^7.24.7", - "@babel/helper-split-export-declaration": "^7.24.7", - "@babel/parser": "^7.24.7", - "@babel/types": "^7.24.7", + "@babel/generator": "^7.25.0", + "@babel/parser": "^7.25.3", + "@babel/template": "^7.25.0", + "@babel/types": "^7.25.2", "debug": "^4.3.1", "globals": "^11.1.0" }, @@ -2230,12 +2220,12 @@ } }, "node_modules/@babel/types": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.7.tgz", - "integrity": "sha512-XEFXSlxiG5td2EJRe8vOmRbaXVgfcBlszKujvVmWIK/UpywWljQCfzAv3RQCGujWQ1RD4YYWEAqDXfuJiy8f5Q==", + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.2.tgz", + "integrity": "sha512-YTnYtra7W9e6/oAZEHj0bJehPRUlLH9/fbpT5LfB0NhQXyALCRkRs3zH9v07IYhkgpqX6Z78FnuccZr/l4Fs4Q==", "dev": true, "dependencies": { - "@babel/helper-string-parser": "^7.24.7", + "@babel/helper-string-parser": "^7.24.8", "@babel/helper-validator-identifier": "^7.24.7", "to-fast-properties": "^2.0.0" }, @@ -2258,9 +2248,9 @@ } }, "node_modules/@csstools/cascade-layer-name-parser": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/@csstools/cascade-layer-name-parser/-/cascade-layer-name-parser-1.0.12.tgz", - "integrity": "sha512-iNCCOnaoycAfcIot3v/orjkTol+j8+Z5xgpqxUpZSdqeaxCADQZtldHhlvzDipmi7OoWdcJUO6DRZcnkMSBEIg==", + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/@csstools/cascade-layer-name-parser/-/cascade-layer-name-parser-1.0.13.tgz", + "integrity": "sha512-MX0yLTwtZzr82sQ0zOjqimpZbzjMaK/h2pmlrLK7DCzlmiZLYFpoO94WmN1akRVo6ll/TdpHb53vihHLUMyvng==", "dev": true, "funding": [ { @@ -2276,14 +2266,14 @@ "node": "^14 || ^16 || >=18" }, "peerDependencies": { - "@csstools/css-parser-algorithms": "^2.7.0", - "@csstools/css-tokenizer": "^2.3.2" + "@csstools/css-parser-algorithms": "^2.7.1", + "@csstools/css-tokenizer": "^2.4.1" } }, "node_modules/@csstools/css-parser-algorithms": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-2.7.0.tgz", - "integrity": "sha512-qvBMcOU/uWFCH/VO0MYe0AMs0BGMWAt6FTryMbFIKYtZtVnqTZtT8ktv5o718llkaGZWomJezJZjq3vJDHeJNQ==", + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-2.7.1.tgz", + "integrity": "sha512-2SJS42gxmACHgikc1WGesXLIT8d/q2l0UFM7TaEeIzdFCE/FPMtTiizcPGGJtlPo2xuQzY09OhrLTzRxqJqwGw==", "dev": true, "funding": [ { @@ -2299,13 +2289,13 @@ "node": "^14 || ^16 || >=18" }, "peerDependencies": { - "@csstools/css-tokenizer": "^2.3.2" + "@csstools/css-tokenizer": "^2.4.1" } }, "node_modules/@csstools/css-tokenizer": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-2.3.2.tgz", - "integrity": "sha512-0xYOf4pQpAaE6Sm2Q0x3p25oRukzWQ/O8hWVvhIt9Iv98/uu053u2CGm/g3kJ+P0vOYTAYzoU8Evq2pg9ZPXtw==", + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-2.4.1.tgz", + "integrity": "sha512-eQ9DIktFJBhGjioABJRtUucoWR2mwllurfnM8LuNGAqX3ViZXaUchqk+1s7jjtkFiT9ySdACsFEA3etErkALUg==", "dev": true, "funding": [ { @@ -2322,9 +2312,9 @@ } }, "node_modules/@csstools/media-query-list-parser": { - "version": "2.1.12", - "resolved": "https://registry.npmjs.org/@csstools/media-query-list-parser/-/media-query-list-parser-2.1.12.tgz", - "integrity": "sha512-t1/CdyVJzOQUiGUcIBXRzTAkWTFPxiPnoKwowKW2z9Uj78c2bBWI/X94BeVfUwVq1xtCjD7dnO8kS6WONgp8Jw==", + "version": "2.1.13", + "resolved": "https://registry.npmjs.org/@csstools/media-query-list-parser/-/media-query-list-parser-2.1.13.tgz", + "integrity": "sha512-XaHr+16KRU9Gf8XLi3q8kDlI18d5vzKSKCY510Vrtc9iNR0NJzbY9hhTmwhzYZj/ZwGL4VmB3TA9hJW0Um2qFA==", "dev": true, "funding": [ { @@ -2340,8 +2330,8 @@ "node": "^14 || ^16 || >=18" }, "peerDependencies": { - "@csstools/css-parser-algorithms": "^2.7.0", - "@csstools/css-tokenizer": "^2.3.2" + "@csstools/css-parser-algorithms": "^2.7.1", + "@csstools/css-tokenizer": "^2.4.1" } }, "node_modules/@discoveryjs/json-ext": { @@ -3772,22 +3762,22 @@ } }, "node_modules/@openedx/frontend-build": { - "version": "14.0.10", - "resolved": "https://registry.npmjs.org/@openedx/frontend-build/-/frontend-build-14.0.10.tgz", - "integrity": "sha512-yTn8C+WV7tsDBQWz4PYKHQPNLtSj3KqT4qjuo13CUFk5hqUMZRNBZqw2ihT2cI9jBMPufra34TqYRtdK/xNqlQ==", + "version": "14.0.15", + "resolved": "https://registry.npmjs.org/@openedx/frontend-build/-/frontend-build-14.0.15.tgz", + "integrity": "sha512-D4j2IGAKkwVUpJGGZ5k926A6vDVuLSnpWYW1KLTmLTeh4JtBtMJlJ0D1TGXbImkiNje+GPjIxXUE9sjCLQGJUg==", "dev": true, "dependencies": { - "@babel/cli": "7.24.7", - "@babel/core": "7.24.7", + "@babel/cli": "7.24.8", + "@babel/core": "7.24.9", "@babel/eslint-parser": "7.22.9", "@babel/plugin-proposal-class-properties": "7.18.6", "@babel/plugin-proposal-object-rest-spread": "7.20.7", "@babel/plugin-syntax-dynamic-import": "7.8.3", - "@babel/preset-env": "7.24.7", + "@babel/preset-env": "7.24.8", "@babel/preset-react": "7.24.7", "@edx/eslint-config": "4.1.0", "@edx/new-relic-source-map-webpack-plugin": "2.1.0", - "@edx/typescript-config": "1.0.1", + "@edx/typescript-config": "1.1.0", "@formatjs/cli": "^6.0.3", "@fullhuman/postcss-purgecss": "5.0.0", "@pmmmwh/react-refresh-webpack-plugin": "0.5.15", @@ -3823,8 +3813,8 @@ "jest": "29.6.1", "jest-environment-jsdom": "29.6.1", "mini-css-extract-plugin": "1.6.2", - "postcss": "8.4.38", - "postcss-custom-media": "10.0.6", + "postcss": "8.4.39", + "postcss-custom-media": "10.0.8", "postcss-loader": "7.3.4", "postcss-rtlcss": "5.1.2", "react-dev-utils": "12.0.1", @@ -3852,11 +3842,10 @@ } }, "node_modules/@openedx/frontend-build/node_modules/@edx/typescript-config": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@edx/typescript-config/-/typescript-config-1.0.1.tgz", - "integrity": "sha512-w0g3nIX9oEch8Rip8q8sb/nrurGEHA1BEjK/I1LAQwA44K4FPMWvyvabmZErrdTJ9sXcZL10aWD3bat1obV8Bg==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@edx/typescript-config/-/typescript-config-1.1.0.tgz", + "integrity": "sha512-HF+7dsSgA2YQ6f/qV4HnrEYBoIhIdxVQZgDyYk/YGvaVGqT6IFuaHnYUP7ImpCUMOUmx/Jl7EyuVeaMe2LrMcA==", "dev": true, - "license": "MIT", "peerDependencies": { "typescript": "^4.9.4" } @@ -3893,13 +3882,12 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", "dev": true, - "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" }, "engines": { - "node": ">= 10.14.2" + "node": ">=4.2.0" } }, "node_modules/@openedx/paragon": { @@ -7175,9 +7163,9 @@ } }, "node_modules/browserslist": { - "version": "4.23.0", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz", - "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==", + "version": "4.23.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.3.tgz", + "integrity": "sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA==", "dev": true, "funding": [ { @@ -7194,10 +7182,10 @@ } ], "dependencies": { - "caniuse-lite": "^1.0.30001587", - "electron-to-chromium": "^1.4.668", - "node-releases": "^2.0.14", - "update-browserslist-db": "^1.0.13" + "caniuse-lite": "^1.0.30001646", + "electron-to-chromium": "^1.5.4", + "node-releases": "^2.0.18", + "update-browserslist-db": "^1.1.0" }, "bin": { "browserslist": "cli.js" @@ -7334,9 +7322,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001612", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001612.tgz", - "integrity": "sha512-lFgnZ07UhaCcsSZgWW0K5j4e69dK1u/ltrL9lTUiFOwNHs12S3UMIEYgBV0Z6C6hRDev7iRnMzzYmKabYdXF9g==", + "version": "1.0.30001651", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001651.tgz", + "integrity": "sha512-9Cf+Xv1jJNe1xPZLGuUXLNkE1BoDkqRqYyFJ9TDYSqhduqA4hu4oR9HluGoWYQC/aj8WHjsGVV+bwkh0+tegRg==", "dev": true, "funding": [ { @@ -7700,7 +7688,6 @@ "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", "dev": true, - "license": "MIT", "engines": { "node": ">= 6" } @@ -8788,9 +8775,9 @@ "dev": true }, "node_modules/electron-to-chromium": { - "version": "1.4.745", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.745.tgz", - "integrity": "sha512-tRbzkaRI5gbUn5DEvF0dV4TQbMZ5CLkWeTAXmpC9IrYT+GE+x76i9p+o3RJ5l9XmdQlI1pPhVtE9uNcJJ0G0EA==", + "version": "1.5.6", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.6.tgz", + "integrity": "sha512-jwXWsM5RPf6j9dPYzaorcBSUg6AiqocPEyMpkchkvntaH9HGfOOMZwxMJjDY/XEs3T5dM7uyH1VhRMkqUU9qVw==", "dev": true }, "node_modules/email-prop-type": { @@ -14615,9 +14602,9 @@ "dev": true }, "node_modules/node-releases": { - "version": "2.0.14", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", - "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", + "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", "dev": true }, "node_modules/nodemon": { @@ -15196,9 +15183,9 @@ } }, "node_modules/picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", + "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", "dev": true }, "node_modules/picomatch": { @@ -15444,9 +15431,9 @@ } }, "node_modules/postcss": { - "version": "8.4.38", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", - "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", + "version": "8.4.39", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.39.tgz", + "integrity": "sha512-0vzE+lAiG7hZl1/9I8yzKLx3aR9Xbof3fBHKunvMfOCYAtMhrsnccJY2iTURb9EZd5+pLuiNV9/c/GZJOHsgIw==", "dev": true, "funding": [ { @@ -15464,7 +15451,7 @@ ], "dependencies": { "nanoid": "^3.3.7", - "picocolors": "^1.0.0", + "picocolors": "^1.0.1", "source-map-js": "^1.2.0" }, "engines": { @@ -15522,9 +15509,9 @@ } }, "node_modules/postcss-custom-media": { - "version": "10.0.6", - "resolved": "https://registry.npmjs.org/postcss-custom-media/-/postcss-custom-media-10.0.6.tgz", - "integrity": "sha512-BjihQoIO4Wjqv9fQNExSJIim8UAmkhLxuJnhJsLTRFSba1y1MhxkJK5awsM//6JJ+/Tu5QUxf624RQAvKHv6SA==", + "version": "10.0.8", + "resolved": "https://registry.npmjs.org/postcss-custom-media/-/postcss-custom-media-10.0.8.tgz", + "integrity": "sha512-V1KgPcmvlGdxTel4/CyQtBJEFhMVpEmRGFrnVtgfGIHj5PJX9vO36eFBxKBeJn+aCDTed70cc+98Mz3J/uVdGQ==", "dev": true, "funding": [ { @@ -15536,12 +15523,11 @@ "url": "https://opencollective.com/csstools" } ], - "license": "MIT", "dependencies": { - "@csstools/cascade-layer-name-parser": "^1.0.11", - "@csstools/css-parser-algorithms": "^2.6.3", - "@csstools/css-tokenizer": "^2.3.1", - "@csstools/media-query-list-parser": "^2.1.11" + "@csstools/cascade-layer-name-parser": "^1.0.13", + "@csstools/css-parser-algorithms": "^2.7.1", + "@csstools/css-tokenizer": "^2.4.1", + "@csstools/media-query-list-parser": "^2.1.13" }, "engines": { "node": "^14 || ^16 || >=18" @@ -19572,9 +19558,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.0.13", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", - "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz", + "integrity": "sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==", "dev": true, "funding": [ { @@ -19591,8 +19577,8 @@ } ], "dependencies": { - "escalade": "^3.1.1", - "picocolors": "^1.0.0" + "escalade": "^3.1.2", + "picocolors": "^1.0.1" }, "bin": { "update-browserslist-db": "cli.js" diff --git a/package.json b/package.json index 71cde363b..d1a9e17c4 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,7 @@ "devDependencies": { "@edx/brand": "npm:@openedx/brand-openedx@^1.2.2", "@edx/browserslist-config": "1.2.0", - "@openedx/frontend-build": "14.0.10", + "@openedx/frontend-build": "14.0.15", "@openedx/paragon": "22.6.1", "@testing-library/jest-dom": "6.4.6", "@testing-library/react": "12.1.5", diff --git a/src/react/hooks.js b/src/react/hooks.js index b1d4e219e..ffe2856a4 100644 --- a/src/react/hooks.js +++ b/src/react/hooks.js @@ -1,7 +1,7 @@ -/* eslint-disable import/prefer-default-export */ import { useEffect } from 'react'; import { subscribe, unsubscribe } from '../pubSub'; import { sendTrackEvent } from '../analytics'; +import { getConfig } from '../config'; /** * A React hook that allows functional components to subscribe to application events. This should @@ -9,9 +9,10 @@ import { sendTrackEvent } from '../analytics'; * provide necessary data to a given component, rather than utilizing a non-React-like Pub/Sub * mechanism. * - * @memberof module:React * @param {string} type * @param {function} callback + * + * @memberof module:React */ export const useAppEvent = (type, callback) => { useEffect(() => { @@ -48,3 +49,69 @@ export const useTrackColorSchemeChoice = () => { }; }, []); }; + +/** + * A React hook that processes the given `piiSelector` to replace props with + * corresponding PII attributes/values based on the application's configuration. + * + * This hook looks up the specified `piiSelector` in the PII configuration, + * and if a match is found, it replaces the relevant props with the mapped + * PII attributes/values/classes for the configured element selectors. + * + * @param {string} piiSelector - The element selector used to identify the PII-related configuration. + * @param {Object} props - The original props object passed to the component. + * @returns {Object} - An updated props object with PII attributes/values/classes added. + * + * @example + * // Given a configuration like: + * { + * piiOptions: { + * selectors: { + * username: [ + * { attributeName: 'data-dd-privacy', attributeValue: 'mask' }, + * { attributeName: 'data-hj-suppress', attributeValue: '' }, + * { className: 'fs-mask' }, + * ], + * }, + * }, + * } + * + * // and calling the hook as follows: + * const piiProps = usePii('username', { otherProp: 'value' }); + * + * // The resulting `piiProps` will be: + * { otherProp: 'value', 'data-dd-privacy': 'mask', data-hj-suppress: '', className: 'fs-mask' } + * + * @memberof module:React + * @see module:React.withPii + */ +export function usePii(piiSelector, props) { + const { piiOptions } = getConfig(); + if (!piiSelector || !piiOptions) { + return props; + } + const piiProps = { ...props }; + Object.entries(piiOptions.selectors).forEach(([selector, selectorConfig]) => { + if (piiSelector !== selector) { + // Skip if the specificed `piiSelector` does not match current selector + return; + } + // Apply the PII attributes/values/classes for the matched selector + selectorConfig.forEach((opts) => { + const { attributeName, attributeValue, className } = opts; + if (attributeName) { + // Parse attributeValue as empty string if not provided, or falsey value is given (e.g., `undefined`). + const transformedAttributeValue = !attributeValue ? '' : attributeValue; + piiProps[attributeName] = transformedAttributeValue; + } + if (className) { + // Append the `className` to the existing `className` prop (if any) + const transformedClassName = piiProps.className ? `${piiProps.className} ${className}` : className; + piiProps.className = transformedClassName; + } + }); + // Delete the specified `piiSelector` from the returned props (no longer needed) + delete piiProps.piiSelector; + }); + return piiProps; +} diff --git a/src/react/index.js b/src/react/index.js index 0985d9271..852082590 100644 --- a/src/react/index.js +++ b/src/react/index.js @@ -13,4 +13,5 @@ export { default as ErrorBoundary } from './ErrorBoundary'; export { default as ErrorPage } from './ErrorPage'; export { default as LoginRedirect } from './LoginRedirect'; export { default as PageWrap } from './PageWrap'; -export { useAppEvent } from './hooks'; +export { default as withPii } from './withPii'; +export { useAppEvent, usePii } from './hooks'; diff --git a/src/react/withPii.jsx b/src/react/withPii.jsx new file mode 100644 index 000000000..239191946 --- /dev/null +++ b/src/react/withPii.jsx @@ -0,0 +1,44 @@ +import React, { forwardRef } from 'react'; +import { usePii } from './hooks'; + +/** + * A higher-order component (HOC) that enhances the wrapped component with PII (Personally Identifiable Information) + * masking capabilities. It uses the `usePii` hook to process the props and replace PII-related attributes/values + * according to the specified configuration. + * + * @param {string} [piiSelector] - The selector used to identify the PII-related configuration for a given element. + * + * @example + * // Given a configuration like: + * { + * piiOptions: { + * selectors: { + * username: [ + * { attributeName: 'data-dd-privacy', attributeValue: 'mask' }, + * { attributeName: 'data-hj-suppress', attributeValue: '' }, + * { className: 'fs-mask' }, + * ], + * }, + * }, + * } + * + * // and calling the HOC as follows: + * const ComponentWithPii = withPii(MyComponent); + * + * + * // The resulting `ComponentWithPii` will render `MyComponent` with the following props: + * { otherProp: 'value', 'data-dd-privacy': 'mask', data-hj-suppress: '', className: 'fs-mask' } + * + * @see module:React.usePii + * @memberof module:React + */ +const withPii = (piiSelector) => (WrappedComponent) => { + const WithPii = forwardRef((props, ref) => { + const piiProps = usePii(piiSelector, props); + return ; + }); + WithPii.displayName = `withPii(${WrappedComponent.displayName || WrappedComponent.name || 'Component'})`; + return WithPii; +}; + +export default withPii; diff --git a/src/react/withPii.test.jsx b/src/react/withPii.test.jsx new file mode 100644 index 000000000..ac497967b --- /dev/null +++ b/src/react/withPii.test.jsx @@ -0,0 +1,95 @@ +/* eslint-disable react/prop-types */ +import React, { useContext } from 'react'; +import { Route, Routes, MemoryRouter } from 'react-router-dom'; +import { render, screen } from '@testing-library/react'; + +import withPii from './withPii'; +import AppContext from './AppContext'; +import { getAuthenticatedUser } from '../auth'; +import { getConfig } from '../config'; + +jest.mock('../auth'); +jest.mock('../config'); + +const piiOptions = { + selectors: { + username: [ + { attributeName: 'data-dd-privacy', attributeValue: 'mask' }, // Datadog example + { attributeName: 'data-dd-privacy', attributeValue: 'mask' }, // Hotjar example + { className: 'fs-mask' }, // Fullstory example + ], + }, +}; + +function Username(props) { + const { authenticatedUser } = useContext(AppContext); + return ( + {authenticatedUser.username} + ); +} + +describe('withPii', () => { + beforeEach(() => { + jest.clearAllMocks(); + getAuthenticatedUser.mockReturnValue({ username: 'edx' }); + getConfig.mockReturnValue({}); + }); + + it.each([ + { + MockPiiComponent: withPii('username')(Username), + hasPiiOptions: true, + isPiiMasked: true, + }, + { + MockPiiComponent: withPii('invalid')(Username), + hasPiiOptions: true, + isPiiMasked: false, + }, + { + MockPiiComponent: withPii(undefined)(Username), + hasPiiOptions: true, + isPiiMasked: false, + }, + { + MockPiiComponent: withPii('')(Username), + hasPiiOptions: true, + isPiiMasked: false, + }, + { + MockPiiComponent: withPii('username')(Username), + hasPiiOptions: false, + isPiiMasked: false, + }, + ])('should return a component with the expected configured PII attributes/values, if any (%s)', ({ + MockPiiComponent, + isPiiMasked, + hasPiiOptions, + }) => { + if (hasPiiOptions) { + getConfig.mockReturnValue({ piiOptions }); + } + const App = ( + + + + } /> + + + + ); + render(App); + const element = screen.getByTestId('pii-username'); + expect(element).toBeInTheDocument(); + if (isPiiMasked) { + expect(element).toHaveAttribute('data-dd-privacy', 'mask'); + } else { + expect(element).not.toHaveAttribute('data-dd-privacy'); + } + }); +});