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');
+ }
+ });
+});