@elliemae/pui-cli 9.0.0-next.52 → 9.0.0-next.61
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app.tsconfig.json +6 -3
- package/dist/cjs/babel.config.cjs +1 -1
- package/dist/cjs/build/vite.config.js +45 -0
- package/dist/cjs/cli.js +3 -1
- package/dist/cjs/commands/buildcdn.js +74 -0
- package/dist/cjs/commands/lint.js +1 -1
- package/dist/cjs/commands/test.js +0 -1
- package/dist/cjs/commands/tscheck.js +8 -1
- package/dist/cjs/commands/utils.js +8 -8
- package/dist/cjs/commands/vitest.js +0 -1
- package/dist/cjs/index.js +5 -7
- package/dist/cjs/lint-config/eslint.config.js +259 -0
- package/dist/cjs/lint-config/stylelint.config.js +1 -1
- package/dist/cjs/monorepo/utils.cjs +2 -2
- package/dist/cjs/monorepo/utils.js +1 -1
- package/dist/cjs/server/cert.js +45 -0
- package/dist/cjs/server/certs/cert.crt +25 -0
- package/dist/cjs/server/certs/cert.csr +18 -0
- package/dist/cjs/server/certs/cert.ext +7 -0
- package/dist/cjs/server/certs/cert.key +30 -0
- package/dist/cjs/server/certs/cert.pfx +0 -0
- package/dist/cjs/server/certs/rootCA.crt +24 -0
- package/dist/cjs/server/certs/rootCA.key +30 -0
- package/dist/cjs/server/certs/rootCA.srl +1 -0
- package/dist/cjs/server/csp.js +40 -29
- package/dist/cjs/server/index.js +8 -3
- package/dist/cjs/server/logger.js +5 -2
- package/dist/cjs/server/middlewares.js +2 -2
- package/dist/cjs/server/wsServer.js +7 -4
- package/dist/cjs/testing/jest.config.cjs +1 -2
- package/dist/cjs/testing/jest.polyfills.cjs +1 -1
- package/dist/cjs/testing/mocks/iframe.js +24 -0
- package/dist/cjs/testing/mocks/svg.js +0 -11
- package/dist/cjs/testing/resolver.cjs +0 -1
- package/dist/cjs/testing/vitest.config.js +1 -5
- package/dist/cjs/transpile/.swcrc +1 -0
- package/dist/cjs/utils.cjs +1 -1
- package/dist/cjs/utils.js +3 -1
- package/dist/cjs/webpack/csp-plugin.js +79 -0
- package/dist/cjs/webpack/csp.js +158 -0
- package/dist/cjs/webpack/helpers.js +14 -14
- package/dist/cjs/webpack/interceptor-middleware.js +125 -0
- package/dist/cjs/webpack/webpack.dev.babel.js +16 -4
- package/dist/cjs/webpack/webpack.lib.base.babel.js +0 -2
- package/dist/cjs/webpack/webpack.lib.prod.babel.js +3 -1
- package/dist/cjs/webpack/webpack.prod.babel.js +12 -5
- package/dist/cjs/webpack/webpack.storybook.js +1 -1
- package/dist/esm/babel.config.cjs +1 -1
- package/dist/esm/build/vite.config.js +25 -0
- package/dist/esm/cli.js +3 -1
- package/dist/esm/commands/buildcdn.js +43 -0
- package/dist/esm/commands/lint.js +1 -1
- package/dist/esm/commands/test.js +0 -1
- package/dist/esm/commands/tscheck.js +8 -1
- package/dist/esm/commands/utils.js +8 -8
- package/dist/esm/commands/vitest.js +0 -1
- package/dist/esm/index.js +2 -4
- package/dist/esm/lint-config/eslint.config.js +228 -0
- package/dist/esm/lint-config/stylelint.config.js +1 -1
- package/dist/esm/monorepo/utils.cjs +2 -2
- package/dist/esm/monorepo/utils.js +1 -1
- package/dist/esm/server/cert.js +14 -0
- package/dist/esm/server/certs/cert.crt +25 -0
- package/dist/esm/server/certs/cert.csr +18 -0
- package/dist/esm/server/certs/cert.ext +7 -0
- package/dist/esm/server/certs/cert.key +30 -0
- package/dist/esm/server/certs/cert.pfx +0 -0
- package/dist/esm/server/certs/rootCA.crt +24 -0
- package/dist/esm/server/certs/rootCA.key +30 -0
- package/dist/esm/server/certs/rootCA.srl +1 -0
- package/dist/esm/server/csp.js +40 -29
- package/dist/esm/server/index.js +8 -3
- package/dist/esm/server/logger.js +5 -2
- package/dist/esm/server/middlewares.js +2 -2
- package/dist/esm/server/wsServer.js +7 -4
- package/dist/esm/testing/jest.config.cjs +1 -2
- package/dist/esm/testing/jest.polyfills.cjs +1 -1
- package/dist/esm/testing/mocks/iframe.js +4 -0
- package/dist/esm/testing/mocks/svg.js +0 -1
- package/dist/esm/testing/resolver.cjs +0 -1
- package/dist/esm/testing/vitest.config.js +1 -4
- package/dist/esm/transpile/.swcrc +1 -0
- package/dist/esm/utils.cjs +1 -1
- package/dist/esm/utils.js +3 -1
- package/dist/esm/webpack/csp-plugin.js +49 -0
- package/dist/esm/webpack/csp.js +128 -0
- package/dist/esm/webpack/helpers.js +14 -14
- package/dist/esm/webpack/interceptor-middleware.js +105 -0
- package/dist/esm/webpack/webpack.dev.babel.js +16 -4
- package/dist/esm/webpack/webpack.lib.base.babel.js +0 -2
- package/dist/esm/webpack/webpack.lib.prod.babel.js +3 -1
- package/dist/esm/webpack/webpack.prod.babel.js +12 -5
- package/dist/esm/webpack/webpack.storybook.js +1 -1
- package/dist/types/eslint.config.d.ts +3 -0
- package/dist/types/eslint.config.d.ts.map +1 -0
- package/dist/types/lib/build/vite.config.d.ts +3 -0
- package/dist/types/lib/build/vite.config.d.ts.map +1 -0
- package/dist/types/lib/commands/build.d.ts.map +1 -1
- package/dist/types/lib/commands/buildcdn.d.ts +3 -0
- package/dist/types/lib/commands/buildcdn.d.ts.map +1 -0
- package/dist/types/lib/commands/gendoc.d.ts.map +1 -1
- package/dist/types/lib/commands/pack.d.ts.map +1 -1
- package/dist/types/lib/commands/start.d.ts.map +1 -1
- package/dist/types/lib/commands/test.d.ts.map +1 -1
- package/dist/types/lib/commands/tscheck.d.ts.map +1 -1
- package/dist/types/lib/commands/utils.d.ts +2 -1
- package/dist/types/lib/commands/utils.d.ts.map +1 -1
- package/dist/types/lib/commands/vitest.d.ts.map +1 -1
- package/dist/types/lib/index.d.ts +1 -2
- package/dist/types/lib/index.d.ts.map +1 -1
- package/dist/types/lib/lint-config/eslint.config.d.ts +3 -0
- package/dist/types/lib/lint-config/eslint.config.d.ts.map +1 -0
- package/dist/types/lib/server/cert.d.ts +5 -0
- package/dist/types/lib/server/cert.d.ts.map +1 -0
- package/dist/types/lib/server/csp.d.ts +0 -1
- package/dist/types/lib/server/csp.d.ts.map +1 -1
- package/dist/types/lib/server/logger.d.ts.map +1 -1
- package/dist/types/lib/server/wsServer.d.ts +2 -2
- package/dist/types/lib/server/wsServer.d.ts.map +1 -1
- package/dist/types/lib/testing/jest.config.d.cts +2 -1
- package/dist/types/lib/testing/jest.node.config.d.cts +2 -1
- package/dist/types/lib/testing/mocks/cssModule.d.ts.map +1 -1
- package/dist/types/lib/testing/mocks/html.d.ts.map +1 -1
- package/dist/types/lib/testing/mocks/iframe.d.ts +3 -0
- package/dist/types/lib/testing/mocks/iframe.d.ts.map +1 -0
- package/dist/types/lib/testing/mocks/image.d.ts.map +1 -1
- package/dist/types/lib/testing/mocks/svg.d.ts.map +1 -1
- package/dist/types/lib/testing/resolver.d.cts.map +1 -1
- package/dist/types/lib/testing/vitest.config.d.ts +1 -1
- package/dist/types/lib/testing/vitest.config.d.ts.map +1 -1
- package/dist/types/lib/transpile/esbuild.d.ts.map +1 -1
- package/dist/types/lib/utils.d.cts +1 -1
- package/dist/types/lib/utils.d.cts.map +1 -1
- package/dist/types/lib/utils.d.ts +2 -2
- package/dist/types/lib/utils.d.ts.map +1 -1
- package/dist/types/lib/webpack/csp-plugin.d.ts +33 -0
- package/dist/types/lib/webpack/csp-plugin.d.ts.map +1 -0
- package/dist/types/lib/webpack/csp.d.ts +66 -0
- package/dist/types/lib/webpack/csp.d.ts.map +1 -0
- package/dist/types/lib/webpack/helpers.d.ts +0 -1
- package/dist/types/lib/webpack/helpers.d.ts.map +1 -1
- package/dist/types/lib/webpack/interceptor-middleware.d.ts +11 -0
- package/dist/types/lib/webpack/interceptor-middleware.d.ts.map +1 -0
- package/dist/types/lib/webpack/webpack.base.babel.d.ts.map +1 -1
- package/dist/types/lib/webpack/webpack.dev.babel.d.ts.map +1 -1
- package/dist/types/lib/webpack/webpack.lib.base.babel.d.ts.map +1 -1
- package/dist/types/lib/webpack/webpack.lib.prod.babel.d.ts.map +1 -1
- package/dist/types/lib/webpack/webpack.prod.babel.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/library.tsconfig.json +8 -5
- package/package.json +146 -142
- package/dist/cjs/lint-config/eslint/config.js +0 -186
- package/dist/cjs/lint-config/eslint/react.js +0 -107
- package/dist/cjs/lint-config/eslint/typescript/config.js +0 -97
- package/dist/cjs/testing/setup-test-env.js +0 -6
- package/dist/esm/lint-config/eslint/config.js +0 -155
- package/dist/esm/lint-config/eslint/react.js +0 -76
- package/dist/esm/lint-config/eslint/typescript/config.js +0 -67
- package/dist/esm/testing/setup-test-env.js +0 -5
- package/dist/types/lib/lint-config/eslint/config.d.ts +0 -2
- package/dist/types/lib/lint-config/eslint/config.d.ts.map +0 -1
- package/dist/types/lib/lint-config/eslint/react.d.ts +0 -2
- package/dist/types/lib/lint-config/eslint/react.d.ts.map +0 -1
- package/dist/types/lib/lint-config/eslint/typescript/config.d.ts +0 -2
- package/dist/types/lib/lint-config/eslint/typescript/config.d.ts.map +0 -1
- package/dist/types/lib/testing/setup-test-env.d.ts +0 -2
- package/dist/types/lib/testing/setup-test-env.d.ts.map +0 -1
- package/dist/types/lib/tests/basic.test.d.ts +0 -1
- package/dist/types/lib/tests/basic.test.d.ts.map +0 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
2
|
import ip from "ip";
|
|
3
|
+
import { isHttps } from "../utils.js";
|
|
3
4
|
const divider = chalk.gray("\n-----------------------------------");
|
|
4
5
|
const logger = {
|
|
5
6
|
// Called whenever there's an error on the server we want to print
|
|
@@ -15,10 +16,12 @@ const logger = {
|
|
|
15
16
|
const accessUrls = `${chalk.bold("Access URLs:")}${divider}
|
|
16
17
|
`;
|
|
17
18
|
const localHostUrl = `Localhost: ${chalk.magenta(
|
|
18
|
-
|
|
19
|
+
`${isHttps() ? "https" : "http"}://${host}:${port}`
|
|
19
20
|
)}
|
|
20
21
|
`;
|
|
21
|
-
const lanUrl = `LAN: ${chalk.magenta(
|
|
22
|
+
const lanUrl = `LAN: ${chalk.magenta(
|
|
23
|
+
`${isHttps() ? "https" : "http"}://${ip.address()}:${port}`
|
|
24
|
+
)}
|
|
22
25
|
`;
|
|
23
26
|
const proxy = tunnelStarted ? `
|
|
24
27
|
Proxy: ${chalk.magenta(tunnelStarted)}` : "";
|
|
@@ -27,7 +27,7 @@ const setupDefaultMiddlewares = (app) => {
|
|
|
27
27
|
const setupAdditionalMiddlewars = (app, options = {}) => {
|
|
28
28
|
const { buildPath = paths.buildPath, basePath = paths.basePath } = options;
|
|
29
29
|
app.use(compression());
|
|
30
|
-
app.get(basePath, (
|
|
30
|
+
app.get(basePath, (_req, res) => {
|
|
31
31
|
sendFileWithCSPNonce({ buildPath, res });
|
|
32
32
|
});
|
|
33
33
|
app.use(
|
|
@@ -41,7 +41,7 @@ const setupAdditionalMiddlewars = (app, options = {}) => {
|
|
|
41
41
|
app.use(expressStaticGzip("cdn", {}));
|
|
42
42
|
app.get(
|
|
43
43
|
"*",
|
|
44
|
-
(
|
|
44
|
+
(_req, res) => sendFileWithCSPNonce({ buildPath, res })
|
|
45
45
|
);
|
|
46
46
|
return app;
|
|
47
47
|
};
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
import https from "node:https";
|
|
1
2
|
import http from "node:http";
|
|
2
3
|
import * as wsLib from "ws";
|
|
4
|
+
import { getCertOptions } from "./cert.js";
|
|
5
|
+
import { isHttps } from "../utils.js";
|
|
3
6
|
const PING_INTERVAL = 3e4;
|
|
4
7
|
const DEFAULT_PORT = 5001;
|
|
5
8
|
const onSocketError = (err) => {
|
|
@@ -25,9 +28,9 @@ const createWSServer = ({
|
|
|
25
28
|
const heartbeat = () => {
|
|
26
29
|
isAlive = true;
|
|
27
30
|
};
|
|
28
|
-
const
|
|
31
|
+
const server = isHttps() ? https.createServer(getCertOptions()) : http.createServer();
|
|
29
32
|
const wsServer = new wsLib.WebSocketServer({ noServer: true });
|
|
30
|
-
|
|
33
|
+
server.on("upgrade", (req, socket, head) => {
|
|
31
34
|
socket.on("error", onSocketError);
|
|
32
35
|
wsServer.handleUpgrade(req, socket, head, (ws) => {
|
|
33
36
|
const protocols = req.headers["sec-websocket-protocol"]?.split(",");
|
|
@@ -84,9 +87,9 @@ const createWSServer = ({
|
|
|
84
87
|
clearInterval(interval);
|
|
85
88
|
});
|
|
86
89
|
return new Promise((resolve) => {
|
|
87
|
-
|
|
90
|
+
server.listen(port, () => {
|
|
88
91
|
console.log(`Websocket server listening on port ${port}`);
|
|
89
|
-
return resolve({
|
|
92
|
+
return resolve({ server, wsServer });
|
|
90
93
|
});
|
|
91
94
|
});
|
|
92
95
|
};
|
|
@@ -6,10 +6,8 @@ const { findMonoRepoRoot } = require('../monorepo/utils.cjs');
|
|
|
6
6
|
|
|
7
7
|
let isReactModule = true;
|
|
8
8
|
try {
|
|
9
|
-
/* eslint-disable global-require, import/no-unresolved */
|
|
10
9
|
require('react');
|
|
11
10
|
require('styled-components');
|
|
12
|
-
/* eslint-enable global-require, import/no-unresolved */
|
|
13
11
|
} catch (err) {
|
|
14
12
|
isReactModule = false;
|
|
15
13
|
}
|
|
@@ -64,6 +62,7 @@ const jestConfig = {
|
|
|
64
62
|
'.*\\.(jpg|jpeg|png|gif|eot|otf|webp|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga|ico)$':
|
|
65
63
|
getMockFilePath('image.js'),
|
|
66
64
|
'.*\\.svg(?:\\?[a-zA-Z]+)?$': getMockFilePath('svg.js'),
|
|
65
|
+
'.*iframe\\.html(?:\\?[a-zA-Z]+)?$': getMockFilePath('iframe.js'),
|
|
67
66
|
'.*\\.html(?:\\?[a-zA-Z]+)?$': getMockFilePath('html.js'),
|
|
68
67
|
'@elliemae/pui-user-monitoring': getMockFilePath('pui-user-monitoring.js'),
|
|
69
68
|
'@elliemae/pui-app-loader': getMockFilePath('pui-app-loader.js'),
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* The block below contains polyfills for Node.js globals
|
|
3
3
|
* required for Jest to function when running JSDOM tests.
|
|
4
4
|
* These HAVE to be require's and HAVE to be in this exact
|
|
5
5
|
* order, since "undici" depends on the "TextEncoder" global API.
|
|
@@ -32,7 +32,6 @@ module.exports = (request, options) => {
|
|
|
32
32
|
}
|
|
33
33
|
const resolution = resolutions.find(({ matcher }) => matcher.test(request));
|
|
34
34
|
if (resolution) {
|
|
35
|
-
// eslint-disable-next-line no-restricted-syntax
|
|
36
35
|
for (const extension of resolution.extensions) {
|
|
37
36
|
try {
|
|
38
37
|
return resolver(
|
|
@@ -1,16 +1,13 @@
|
|
|
1
|
-
import path from "node:path";
|
|
2
|
-
import { fileURLToPath } from "node:url";
|
|
3
1
|
import { defineConfig, configDefaults } from "vitest/config";
|
|
4
2
|
import react from "@vitejs/plugin-react";
|
|
5
3
|
import tsconfigPaths from "vite-tsconfig-paths";
|
|
6
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
7
4
|
const vitestConfig = defineConfig({
|
|
5
|
+
// @ts-expect-error - `react` and `tsconfigPaths` are not part of the default config
|
|
8
6
|
plugins: [react(), tsconfigPaths()],
|
|
9
7
|
test: {
|
|
10
8
|
globals: true,
|
|
11
9
|
root: process.cwd(),
|
|
12
10
|
environment: "happy-dom",
|
|
13
|
-
setupFiles: [path.resolve(__dirname, "./setup-test-env.js")],
|
|
14
11
|
include: ["./{app,lib}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}"],
|
|
15
12
|
exclude: [...configDefaults.exclude, ".idea", ".git", ".cache", "e2e"],
|
|
16
13
|
coverage: {
|
package/dist/esm/utils.cjs
CHANGED
package/dist/esm/utils.js
CHANGED
|
@@ -2,6 +2,7 @@ import fs from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
const basePath = (process.env.BASE_PATH ?? "/").replace(/\/?$/, "/");
|
|
4
4
|
const isApp = () => fs.existsSync(path.join(process.cwd(), "app"));
|
|
5
|
+
const isHttps = () => process.env.HTTPS === "true";
|
|
5
6
|
const getAppConfig = () => {
|
|
6
7
|
const appConfigPath = path.join(
|
|
7
8
|
process.cwd(),
|
|
@@ -15,5 +16,6 @@ const getAppConfig = () => {
|
|
|
15
16
|
export {
|
|
16
17
|
basePath,
|
|
17
18
|
getAppConfig,
|
|
18
|
-
isApp
|
|
19
|
+
isApp,
|
|
20
|
+
isHttps
|
|
19
21
|
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import HtmlWebpackPlugin from "html-webpack-plugin";
|
|
2
|
+
import { CSP } from "./csp.js";
|
|
3
|
+
const defaultOptions = {
|
|
4
|
+
enableUnsafeEval: false
|
|
5
|
+
};
|
|
6
|
+
class CspPlugin {
|
|
7
|
+
#options = defaultOptions;
|
|
8
|
+
/**
|
|
9
|
+
* @param {object} options Additional options for this module.
|
|
10
|
+
*/
|
|
11
|
+
constructor(options) {
|
|
12
|
+
this.#options = { ...this.#options, ...options ?? {} };
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Processes HtmlWebpackPlugin's html data by adding the CSP
|
|
16
|
+
* @param _compilation
|
|
17
|
+
* @param htmlPluginData
|
|
18
|
+
* @param htmlPluginData.html
|
|
19
|
+
* @param compileCb
|
|
20
|
+
* @returns {*}
|
|
21
|
+
*/
|
|
22
|
+
processCsp(_compilation, htmlPluginData, compileCb) {
|
|
23
|
+
const cspModule = new CSP(htmlPluginData.html);
|
|
24
|
+
cspModule.refactorSourcedScriptsForHashBasedCsp();
|
|
25
|
+
const scriptHashes = cspModule.hashAllInlineScripts();
|
|
26
|
+
const { enableUnsafeEval } = this.#options;
|
|
27
|
+
const strictCsp = CSP.getStrictCsp(scriptHashes, {
|
|
28
|
+
enableUnsafeEval
|
|
29
|
+
});
|
|
30
|
+
cspModule.addMetaTag(strictCsp);
|
|
31
|
+
htmlPluginData.html = cspModule.serializeDom();
|
|
32
|
+
return compileCb(null, htmlPluginData);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Hooks into webpack to collect assets and hash them, build the policy, and add it into our HTML template
|
|
36
|
+
* @param compiler
|
|
37
|
+
*/
|
|
38
|
+
apply(compiler) {
|
|
39
|
+
compiler.hooks.compilation.tap("CspPlugin", (compilation) => {
|
|
40
|
+
HtmlWebpackPlugin.getCompilationHooks(compilation).beforeEmit.tapAsync(
|
|
41
|
+
"CspPlugin",
|
|
42
|
+
this.processCsp.bind(this, compilation)
|
|
43
|
+
);
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
export {
|
|
48
|
+
CspPlugin
|
|
49
|
+
};
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import * as crypto from "node:crypto";
|
|
2
|
+
import * as cheerio from "cheerio";
|
|
3
|
+
class CSP {
|
|
4
|
+
static HASH_FUNCTION = "sha256";
|
|
5
|
+
static INLINE_SCRIPT_SELECTOR = "script:not([src])";
|
|
6
|
+
static SOURCED_SCRIPT_SELECTOR = "script[src]";
|
|
7
|
+
$;
|
|
8
|
+
constructor(html) {
|
|
9
|
+
this.$ = cheerio.load(html);
|
|
10
|
+
}
|
|
11
|
+
serializeDom() {
|
|
12
|
+
return this.$.root().html() ?? "";
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Returns a strict Content Security Policy for mittigating XSS.
|
|
16
|
+
* For more details read csp.withgoogle.com.
|
|
17
|
+
* If you modify this CSP, make sure it has not become trivially bypassable by
|
|
18
|
+
* checking the policy using csp-evaluator.withgoogle.com.
|
|
19
|
+
* @param hashes A list of sha-256 hashes of trusted inline scripts.
|
|
20
|
+
* @param cspOptions
|
|
21
|
+
* @param enableTrustedTypes If Trusted Types should be enabled for scripts.
|
|
22
|
+
* @param enableBrowserFallbacks If fallbacks for older browsers should be
|
|
23
|
+
* added. This is will not weaken the policy as modern browsers will ignore
|
|
24
|
+
* the fallbacks.
|
|
25
|
+
* @param enableUnsafeEval If you cannot remove all uses of eval(), you can
|
|
26
|
+
* still set a strict CSP, but you will have to use the 'unsafe-eval'
|
|
27
|
+
* keyword which will make your policy slightly less secure.
|
|
28
|
+
* @param cspOptions.enableBrowserFallbacks
|
|
29
|
+
* @param cspOptions.enableTrustedTypes
|
|
30
|
+
* @param cspOptions.enableUnsafeEval
|
|
31
|
+
* @returns A strict Content Security Policy string.
|
|
32
|
+
*/
|
|
33
|
+
static getStrictCsp(hashes, cspOptions = {
|
|
34
|
+
enableUnsafeEval: false
|
|
35
|
+
}) {
|
|
36
|
+
const strictCspTemplate = {
|
|
37
|
+
// 'strict-dynamic' allows hashed scripts to create new scripts.
|
|
38
|
+
"script-src": [`'strict-dynamic'`, ...hashes ?? []],
|
|
39
|
+
// Restricts `object-src` to disable dangerous plugins like Flash.
|
|
40
|
+
"object-src": [`'none'`],
|
|
41
|
+
// Restricts `base-uri` to block the injection of `<base>` tags. This
|
|
42
|
+
// prevents attackers from changing the locations of scripts loaded from
|
|
43
|
+
// relative URLs.
|
|
44
|
+
"base-uri": [`'self'`]
|
|
45
|
+
};
|
|
46
|
+
if (cspOptions.enableUnsafeEval) {
|
|
47
|
+
strictCspTemplate["script-src"].push(`'unsafe-eval'`);
|
|
48
|
+
}
|
|
49
|
+
return Object.entries(strictCspTemplate).map(([directive, values]) => `${directive} ${values.join(" ")};`).join("");
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Enables a CSP via a meta tag at the beginning of the document.
|
|
53
|
+
* Warning: It's recommended to set CSP as HTTP response header instead of
|
|
54
|
+
* using a meta tag. Injections before the meta tag will not be covered by CSP
|
|
55
|
+
* and meta tags don't support CSP in report-only mode.
|
|
56
|
+
* @param csp A Content Security Policy string.
|
|
57
|
+
*/
|
|
58
|
+
addMetaTag(csp) {
|
|
59
|
+
let metaTag = this.$('meta[http-equiv="Content-Security-Policy"]');
|
|
60
|
+
if (!metaTag.length) {
|
|
61
|
+
metaTag = cheerio.load('<meta http-equiv="Content-Security-Policy">')(
|
|
62
|
+
"meta"
|
|
63
|
+
);
|
|
64
|
+
metaTag.prependTo(this.$("head"));
|
|
65
|
+
}
|
|
66
|
+
metaTag.attr("content", csp);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Replaces all sourced scripts with a single inline script that can be hashed
|
|
70
|
+
*/
|
|
71
|
+
refactorSourcedScriptsForHashBasedCsp() {
|
|
72
|
+
const scriptInfoList = this.$(CSP.SOURCED_SCRIPT_SELECTOR).map((_i, script) => {
|
|
73
|
+
const src = this.$(script).attr("src") ?? "";
|
|
74
|
+
const type = this.$(script).attr("type") ?? "";
|
|
75
|
+
this.$(script).remove();
|
|
76
|
+
return { src, type };
|
|
77
|
+
}).toArray().filter((info) => info.src);
|
|
78
|
+
const loaderScript = CSP.createLoaderScript(scriptInfoList);
|
|
79
|
+
if (!loaderScript) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const newScript = cheerio.load("<script>")("script");
|
|
83
|
+
newScript.text(loaderScript);
|
|
84
|
+
newScript.appendTo(this.$("body"));
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Returns a list of hashes of all inline scripts found in the HTML document.
|
|
88
|
+
* @returns A list of sha-256 hashes of inline scripts.
|
|
89
|
+
*/
|
|
90
|
+
hashAllInlineScripts() {
|
|
91
|
+
return this.$(CSP.INLINE_SCRIPT_SELECTOR).map((_i, elem) => CSP.hashInlineScript(this.$(elem).html() ?? "")).get();
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Returns JS code for dynamically loading sourced (external) scripts.
|
|
95
|
+
* @param scriptInfoList A list of objects containing src and type for scripts that should be loaded
|
|
96
|
+
* @returns JS code for loading scripts.
|
|
97
|
+
*/
|
|
98
|
+
static createLoaderScript(scriptInfoList) {
|
|
99
|
+
if (!scriptInfoList.length) {
|
|
100
|
+
return void 0;
|
|
101
|
+
}
|
|
102
|
+
return `
|
|
103
|
+
var scripts = ${JSON.stringify(scriptInfoList)};
|
|
104
|
+
scripts.forEach(function(scriptInfo) {
|
|
105
|
+
var s = document.createElement('script');
|
|
106
|
+
s.src = scriptInfo.src;
|
|
107
|
+
if (scriptInfo.type) {
|
|
108
|
+
s.type = scriptInfo.type;
|
|
109
|
+
}
|
|
110
|
+
s.async = false; // preserve execution order.
|
|
111
|
+
document.body.appendChild(s);
|
|
112
|
+
});
|
|
113
|
+
`;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Calculates a CSP compatible hash of an inline script.
|
|
117
|
+
* @param scriptText Text between opening and closing script tag. Has to
|
|
118
|
+
* include whitespaces and newlines!
|
|
119
|
+
* @returns A sha-256 hash of the script.
|
|
120
|
+
*/
|
|
121
|
+
static hashInlineScript(scriptText) {
|
|
122
|
+
const hash = crypto.createHash(CSP.HASH_FUNCTION).update(scriptText, "utf-8").digest("base64");
|
|
123
|
+
return `'${CSP.HASH_FUNCTION}-${hash}'`;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
export {
|
|
127
|
+
CSP
|
|
128
|
+
};
|
|
@@ -15,7 +15,7 @@ const getNodeModulesRegEx = (modules) => modules.map(
|
|
|
15
15
|
const excludeNodeModulesExcept = (modules) => {
|
|
16
16
|
const moduleRegExps = getNodeModulesRegEx(modules);
|
|
17
17
|
return function(modulePath) {
|
|
18
|
-
if (
|
|
18
|
+
if (modulePath.includes("node_modules")) {
|
|
19
19
|
for (const moduleRegExp of moduleRegExps)
|
|
20
20
|
if (moduleRegExp.test(modulePath)) return false;
|
|
21
21
|
return true;
|
|
@@ -87,9 +87,9 @@ const getUserMonitoringFileName = () => {
|
|
|
87
87
|
);
|
|
88
88
|
if (!fs.existsSync(userMonLibPath)) return `${libName}.js`;
|
|
89
89
|
const distJSFiles = fs.readdirSync(userMonLibPath);
|
|
90
|
-
return distJSFiles.
|
|
91
|
-
(fName) =>
|
|
92
|
-
)
|
|
90
|
+
return distJSFiles.find(
|
|
91
|
+
(fName) => /emuiUserMonitoring+((?!chunk).)*\.js$/.exec(fName)
|
|
92
|
+
);
|
|
93
93
|
};
|
|
94
94
|
const getAppLoaderFileName = () => {
|
|
95
95
|
const libName = "emuiAppLoader";
|
|
@@ -99,9 +99,9 @@ const getAppLoaderFileName = () => {
|
|
|
99
99
|
);
|
|
100
100
|
if (!fs.existsSync(appLoaderLibPath)) return `${libName}.js`;
|
|
101
101
|
const distJSFiles = fs.readdirSync(appLoaderLibPath);
|
|
102
|
-
return distJSFiles.
|
|
103
|
-
(fName) =>
|
|
104
|
-
)
|
|
102
|
+
return distJSFiles.find(
|
|
103
|
+
(fName) => /emuiAppLoader+((?!chunk).)*\.js$/.exec(fName)
|
|
104
|
+
);
|
|
105
105
|
};
|
|
106
106
|
const getDiagnosticsFileName = () => {
|
|
107
107
|
const libName = "emuiDiagnostics";
|
|
@@ -111,9 +111,9 @@ const getDiagnosticsFileName = () => {
|
|
|
111
111
|
);
|
|
112
112
|
if (!fs.existsSync(libPath)) return `${libName}.js`;
|
|
113
113
|
const distJSFiles = fs.readdirSync(libPath);
|
|
114
|
-
return distJSFiles.
|
|
115
|
-
(fName) =>
|
|
116
|
-
)
|
|
114
|
+
return distJSFiles.find(
|
|
115
|
+
(fName) => /emuiDiagnostics+((?!chunk).)*\.js$/.exec(fName)
|
|
116
|
+
);
|
|
117
117
|
};
|
|
118
118
|
const getENCWLoaderFileName = () => {
|
|
119
119
|
const libName = "emuiEncwLoader";
|
|
@@ -123,13 +123,13 @@ const getENCWLoaderFileName = () => {
|
|
|
123
123
|
);
|
|
124
124
|
if (!fs.existsSync(appLoaderLibPath)) return `${libName}.js`;
|
|
125
125
|
const distJSFiles = fs.readdirSync(appLoaderLibPath);
|
|
126
|
-
return distJSFiles.
|
|
127
|
-
(fName) =>
|
|
128
|
-
)
|
|
126
|
+
return distJSFiles.find(
|
|
127
|
+
(fName) => /emuiEncwLoader+((?!chunk).)*\.js$/.exec(fName)
|
|
128
|
+
);
|
|
129
129
|
};
|
|
130
130
|
const getAppVersion = () => {
|
|
131
131
|
if (!process.env.APP_VERSION) return LATEST_VERSION;
|
|
132
|
-
const match =
|
|
132
|
+
const match = /^v?(\d+\.\d+)\..*$/.exec(process.env.APP_VERSION);
|
|
133
133
|
return match?.[1] ?? LATEST_VERSION;
|
|
134
134
|
};
|
|
135
135
|
const getPaths = (latestVersion = true) => {
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
const VALID_PARAMS = ["isInterceptable", "intercept", "afterSend"];
|
|
2
|
+
const validateParams = (methods) => {
|
|
3
|
+
Object.keys(methods).forEach((k) => {
|
|
4
|
+
if (!VALID_PARAMS.includes(k)) {
|
|
5
|
+
throw new Error(`${k} isn't a valid param (${VALID_PARAMS.join(", ")})`);
|
|
6
|
+
}
|
|
7
|
+
});
|
|
8
|
+
if (!("isInterceptable" in methods)) {
|
|
9
|
+
throw new Error("isInterceptable is a required param (function)");
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
const interceptorMiddleware = (fn) => (req, res, next) => {
|
|
13
|
+
const methods = fn(req, res);
|
|
14
|
+
validateParams(methods);
|
|
15
|
+
const originalEnd = res.end;
|
|
16
|
+
const originalWrite = res.write;
|
|
17
|
+
const chunks = [];
|
|
18
|
+
let isIntercepting;
|
|
19
|
+
let isFirstWrite = true;
|
|
20
|
+
function intercept(rawChunk, encoding) {
|
|
21
|
+
if (isFirstWrite) {
|
|
22
|
+
isFirstWrite = false;
|
|
23
|
+
isIntercepting = methods.isInterceptable();
|
|
24
|
+
}
|
|
25
|
+
if (isIntercepting) {
|
|
26
|
+
if (rawChunk) {
|
|
27
|
+
let tempChunk = rawChunk;
|
|
28
|
+
if (rawChunk !== null && !Buffer.isBuffer(tempChunk)) {
|
|
29
|
+
if (!encoding) {
|
|
30
|
+
tempChunk = Buffer.from(rawChunk);
|
|
31
|
+
} else {
|
|
32
|
+
tempChunk = Buffer.from(
|
|
33
|
+
rawChunk,
|
|
34
|
+
encoding
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
chunks.push(tempChunk);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return isIntercepting;
|
|
42
|
+
}
|
|
43
|
+
const newResWrite = (...args) => {
|
|
44
|
+
if (!intercept(args[0], args[1])) {
|
|
45
|
+
return originalWrite.apply(res, args);
|
|
46
|
+
}
|
|
47
|
+
return true;
|
|
48
|
+
};
|
|
49
|
+
res.write = newResWrite;
|
|
50
|
+
const afterSend = (oldBody, newBody) => {
|
|
51
|
+
if (typeof methods.afterSend === "function") {
|
|
52
|
+
process.nextTick(() => {
|
|
53
|
+
methods.afterSend?.(oldBody, newBody);
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
const newResEnd = (...args) => {
|
|
58
|
+
if (intercept(args[0], args[1])) {
|
|
59
|
+
isIntercepting = false;
|
|
60
|
+
const oldBody = Buffer.concat(chunks).toString("utf-8");
|
|
61
|
+
if (methods.intercept) {
|
|
62
|
+
if (typeof methods.intercept !== "function") {
|
|
63
|
+
throw new Error(
|
|
64
|
+
"`send` must be a function with the body to be sent as the only param"
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
res.removeHeader("Content-Length");
|
|
68
|
+
methods.intercept(oldBody, (newBody) => {
|
|
69
|
+
args[0] = newBody;
|
|
70
|
+
originalEnd.apply(res, args);
|
|
71
|
+
afterSend(oldBody, newBody);
|
|
72
|
+
});
|
|
73
|
+
} else {
|
|
74
|
+
afterSend(oldBody, oldBody);
|
|
75
|
+
originalEnd.apply(res, args);
|
|
76
|
+
}
|
|
77
|
+
} else {
|
|
78
|
+
originalEnd.apply(res, args);
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
res.end = newResEnd;
|
|
82
|
+
next();
|
|
83
|
+
};
|
|
84
|
+
const addCSPNonceMiddleware = (middlewares) => {
|
|
85
|
+
const index = middlewares.findIndex(
|
|
86
|
+
(middleware) => middleware.name === "webpack-dev-middleware"
|
|
87
|
+
);
|
|
88
|
+
middlewares.splice(index, 0, {
|
|
89
|
+
name: "csp-nonce-middleware",
|
|
90
|
+
middleware: interceptorMiddleware((_req, res) => ({
|
|
91
|
+
// Only HTML responses will be intercepted
|
|
92
|
+
isInterceptable() {
|
|
93
|
+
return (res.get("Content-Type") ?? "").includes("text/html");
|
|
94
|
+
},
|
|
95
|
+
// Appends a paragraph at the end of the response body
|
|
96
|
+
intercept(body, send) {
|
|
97
|
+
send(body.replace(/__CSP_NONCE__/g, res.locals.cspNonce));
|
|
98
|
+
}
|
|
99
|
+
}))
|
|
100
|
+
});
|
|
101
|
+
};
|
|
102
|
+
export {
|
|
103
|
+
addCSPNonceMiddleware,
|
|
104
|
+
interceptorMiddleware
|
|
105
|
+
};
|
|
@@ -6,6 +6,7 @@ import CircularDependencyPlugin from "circular-dependency-plugin";
|
|
|
6
6
|
import MiniCssExtractPlugin from "mini-css-extract-plugin";
|
|
7
7
|
import ReactRefreshWebpackPlugin from "@pmmmwh/react-refresh-webpack-plugin";
|
|
8
8
|
import expressStaticGzip from "express-static-gzip";
|
|
9
|
+
import { addCSPNonceMiddleware } from "./interceptor-middleware.js";
|
|
9
10
|
import { setupDefaultMiddlewares } from "../server/middlewares.js";
|
|
10
11
|
import { loadRoutes } from "../server/appRoutes.js";
|
|
11
12
|
import {
|
|
@@ -16,6 +17,8 @@ import {
|
|
|
16
17
|
import { baseConfig } from "./webpack.base.babel.js";
|
|
17
18
|
import { wsPort } from "../server/utils.js";
|
|
18
19
|
import { createWSServer } from "../server/wsServer.js";
|
|
20
|
+
import { getCertOptions } from "../server/cert.js";
|
|
21
|
+
import { isHttps } from "../utils.js";
|
|
19
22
|
const __filename = fileURLToPath(import.meta.url);
|
|
20
23
|
const {
|
|
21
24
|
appVersion,
|
|
@@ -27,6 +30,10 @@ const {
|
|
|
27
30
|
diagnosticsScriptPath,
|
|
28
31
|
encwLoaderScriptPath
|
|
29
32
|
} = getPaths();
|
|
33
|
+
const serverOptions = {
|
|
34
|
+
type: "https",
|
|
35
|
+
options: getCertOptions()
|
|
36
|
+
};
|
|
30
37
|
const devConfig = {
|
|
31
38
|
mode: "development",
|
|
32
39
|
cache: {
|
|
@@ -70,7 +77,7 @@ const devConfig = {
|
|
|
70
77
|
// Add development plugins
|
|
71
78
|
plugins: [
|
|
72
79
|
new HtmlWebpackPlugin({
|
|
73
|
-
inject: !isAppLoaderEnabled()
|
|
80
|
+
inject: !isAppLoaderEnabled() && process.env.CSP !== "true",
|
|
74
81
|
// Inject all files that are generated by webpack, e.g. bundle.js
|
|
75
82
|
template: !isAppLoaderEnabled() ? "app/index.html" : "app/index-app-loader.html",
|
|
76
83
|
emui: {
|
|
@@ -81,10 +88,14 @@ const devConfig = {
|
|
|
81
88
|
appLoaderScriptPath,
|
|
82
89
|
diagnosticsScriptPath,
|
|
83
90
|
encwLoaderScriptPath,
|
|
84
|
-
googleTagManager: isGoogleTagManagerEnabled()
|
|
91
|
+
googleTagManager: isGoogleTagManagerEnabled(),
|
|
92
|
+
csp: process.env.CSP === "true"
|
|
85
93
|
},
|
|
86
94
|
scriptLoading: "defer"
|
|
87
95
|
}),
|
|
96
|
+
// new CspPlugin(HtmlWebpackPlugin, {
|
|
97
|
+
// enableUnsafeEval: true,
|
|
98
|
+
// }),
|
|
88
99
|
new CircularDependencyPlugin({
|
|
89
100
|
exclude: /a\.(js|ts|jsx|tsx)|node_modules/,
|
|
90
101
|
// exclude node_modules
|
|
@@ -117,11 +128,12 @@ const devConfig = {
|
|
|
117
128
|
hot: true,
|
|
118
129
|
open: [basePath],
|
|
119
130
|
port: process.env.PORT ?? "auto",
|
|
131
|
+
server: isHttps() ? serverOptions : {},
|
|
120
132
|
setupMiddlewares: (middlewares, devServer) => {
|
|
133
|
+
addCSPNonceMiddleware(middlewares);
|
|
121
134
|
if (devServer.app) {
|
|
122
135
|
setupDefaultMiddlewares(devServer.app);
|
|
123
|
-
loadRoutes(devServer.app).
|
|
124
|
-
}).catch((err) => {
|
|
136
|
+
loadRoutes(devServer.app).catch((err) => {
|
|
125
137
|
console.error(err);
|
|
126
138
|
});
|
|
127
139
|
const cdnRouter = expressStaticGzip("cdn", {});
|
|
@@ -39,13 +39,11 @@ const plugins = [
|
|
|
39
39
|
new webpack.DefinePlugin({
|
|
40
40
|
APP_CONFIG: getAppConfig()
|
|
41
41
|
}),
|
|
42
|
-
/* eslint-disable indent */
|
|
43
42
|
...copyPluginPatterns.length > 0 ? [
|
|
44
43
|
new CopyWebpackPlugin({
|
|
45
44
|
patterns: copyPluginPatterns
|
|
46
45
|
})
|
|
47
46
|
] : [],
|
|
48
|
-
/* eslint-enable indent */
|
|
49
47
|
new webpack.optimize.LimitChunkCountPlugin({
|
|
50
48
|
maxChunks: 1
|
|
51
49
|
}),
|
|
@@ -29,10 +29,13 @@ const getProdConfig = ({ latestVersion = true } = {}) => {
|
|
|
29
29
|
},
|
|
30
30
|
optimization: {
|
|
31
31
|
moduleIds: "deterministic",
|
|
32
|
+
// realContentHash: false,
|
|
32
33
|
minimizer: [
|
|
33
34
|
new EsbuildPlugin({
|
|
34
35
|
target: browserslistToEsbuild(),
|
|
35
|
-
css: true
|
|
36
|
+
css: true,
|
|
37
|
+
pure: ["console.log", "console.warn"],
|
|
38
|
+
drop: ["debugger"]
|
|
36
39
|
})
|
|
37
40
|
],
|
|
38
41
|
runtimeChunk: true,
|
|
@@ -46,7 +49,8 @@ const getProdConfig = ({ latestVersion = true } = {}) => {
|
|
|
46
49
|
},
|
|
47
50
|
vendors: {
|
|
48
51
|
name: "vendors",
|
|
49
|
-
test: /[\\/]node_modules[\\/]
|
|
52
|
+
test: /[\\/]node_modules[\\/]/,
|
|
53
|
+
enforce: true
|
|
50
54
|
}
|
|
51
55
|
}
|
|
52
56
|
}
|
|
@@ -83,7 +87,7 @@ const {
|
|
|
83
87
|
basePath
|
|
84
88
|
} = getPaths();
|
|
85
89
|
const htmlWebpackPlugin = new HtmlWebpackPlugin({
|
|
86
|
-
inject: !isAppLoaderEnabled()
|
|
90
|
+
inject: !isAppLoaderEnabled() && process.env.CSP !== "true",
|
|
87
91
|
template: !isAppLoaderEnabled() ? "app/index.html" : "app/index-app-loader.html",
|
|
88
92
|
minify: {
|
|
89
93
|
removeComments: true,
|
|
@@ -105,12 +109,15 @@ const htmlWebpackPlugin = new HtmlWebpackPlugin({
|
|
|
105
109
|
appLoaderScriptPath,
|
|
106
110
|
diagnosticsScriptPath,
|
|
107
111
|
encwLoaderScriptPath,
|
|
108
|
-
googleTagManager: isGoogleTagManagerEnabled()
|
|
112
|
+
googleTagManager: isGoogleTagManagerEnabled(),
|
|
113
|
+
csp: process.env.CSP === "true"
|
|
109
114
|
},
|
|
110
115
|
scriptLoading: "defer"
|
|
111
116
|
});
|
|
112
117
|
const config = baseConfig(getProdConfig());
|
|
113
|
-
if (config.plugins)
|
|
118
|
+
if (config.plugins) {
|
|
119
|
+
config.plugins.push(htmlWebpackPlugin);
|
|
120
|
+
}
|
|
114
121
|
var webpack_prod_babel_default = config;
|
|
115
122
|
export {
|
|
116
123
|
webpack_prod_babel_default as default
|