catalyst-core 0.3.0-beta.2 → 0.3.0-beta.4
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/dist/native/androidSetup.js +8 -6
- package/dist/offline/catalyst-sw.js +1 -0
- package/dist/scripts/build.js +15 -11
- package/dist/scripts/generateOfflineManifest.js +1 -0
- package/dist/scripts/scriptUtils.js +1 -0
- package/dist/scripts/serve.js +24 -22
- package/dist/scripts/start.js +1 -3
- package/dist/server/expressServer.js +8 -0
- package/dist/server/renderer/document/Body.jsx +4 -1
- package/dist/server/renderer/document/Head.jsx +2 -0
- package/dist/server/renderer/extract.js +1 -0
- package/dist/server/renderer/handler.jsx +1 -0
- package/dist/vite/FastRefresh.jsx +3 -1
- package/dist/vite/inject-cache-key-plugin.js +7 -0
- package/dist/vite/loadCustomViteConfig.js +29 -0
- package/dist/vite/manifest-categorization-plugin.js +2 -0
- package/dist/vite/node-loader.mjs +36 -2
- package/dist/vite/resolveDevServerConfig.js +45 -0
- package/dist/vite/vite.config.client.js +1 -1
- package/dist/vite/vite.config.js +37 -9
- package/dist/vite/vite.config.server.js +5 -1
- package/mcp_v2/tools/github.js +11 -0
- package/package.json +3 -2
- package/test/vite/resolveDevServerConfig.test.js +113 -0
|
@@ -209,13 +209,15 @@ async function checkEmulator(ADB_PATH) {
|
|
|
209
209
|
}
|
|
210
210
|
async function startEmulator(EMULATOR_PATH, androidConfig) {
|
|
211
211
|
progress.log(`Starting emulator: ${androidConfig.emulatorName}...`);
|
|
212
|
-
// nosemgrep: javascript.lang.security.detect-child-process.detect-child-process -
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
212
|
+
// nosemgrep: javascript.lang.security.detect-child-process.detect-child-process - spawn with an argv array (no shell), so androidConfig.emulatorName can't be interpreted as shell syntax even though it comes from a locally-editable config file.
|
|
213
|
+
const child = (0, _child_process.spawn)(EMULATOR_PATH, ["-avd", androidConfig.emulatorName, "-read-only"], {
|
|
214
|
+
detached: true,
|
|
215
|
+
stdio: "ignore"
|
|
216
|
+
});
|
|
217
|
+
child.on("error", error => {
|
|
218
|
+
progress.log(`Error starting emulator: ${error}`, "error");
|
|
218
219
|
});
|
|
220
|
+
child.unref();
|
|
219
221
|
}
|
|
220
222
|
async function updateLocalProperties(sdkPath) {
|
|
221
223
|
progress.start("properties");
|
|
@@ -99,6 +99,7 @@ function isOfflineRoute(url, manifest) {
|
|
|
99
99
|
const { pathname } = new URL(url)
|
|
100
100
|
return (manifest.routes || []).some((route) => {
|
|
101
101
|
try {
|
|
102
|
+
// nosemgrep: javascript.lang.security.audit.detect-non-literal-regexp.detect-non-literal-regexp - route.regex is generated at build time by generateOfflineManifest.js's routeRegex(), which escapes every regex metacharacter in literal path segments and only ever emits flat (non-nested, non-repeating) patterns for :param and * segments — it cannot produce a catastrophic-backtracking shape. The manifest itself is same-origin build output, not user-submitted at runtime.
|
|
102
103
|
return new RegExp(route.regex).test(pathname)
|
|
103
104
|
} catch (_) {
|
|
104
105
|
return false
|
package/dist/scripts/build.js
CHANGED
|
@@ -4,6 +4,7 @@ import { arrayToObject } from "./scriptUtils.js"
|
|
|
4
4
|
import { fileURLToPath } from "url"
|
|
5
5
|
import { dirname } from "path"
|
|
6
6
|
import { readFileSync, existsSync, rmSync } from "fs"
|
|
7
|
+
import { createRequire } from "module"
|
|
7
8
|
|
|
8
9
|
const __filename = fileURLToPath(import.meta.url)
|
|
9
10
|
const __dirname = dirname(__filename)
|
|
@@ -11,16 +12,19 @@ const loaderPath = path.resolve(__dirname, "../../dist/vite/node-loader.mjs")
|
|
|
11
12
|
const configPath = path.join(process.env.PWD, "config/config.json")
|
|
12
13
|
const configJSON = JSON.parse(readFileSync(configPath), "utf-8")
|
|
13
14
|
|
|
15
|
+
// Resolve vite's actual JS entry point rather than spawning the "vite" command name,
|
|
16
|
+
// which on Windows only resolves via the npm-installed .cmd shim (i.e. requires a shell).
|
|
17
|
+
// Invoking process.execPath + this path directly works cross-platform with no shell.
|
|
18
|
+
const require = createRequire(import.meta.url)
|
|
19
|
+
const viteBinPath = path.join(path.dirname(require.resolve("vite/package.json")), "bin", "vite.js")
|
|
20
|
+
|
|
14
21
|
/**
|
|
15
|
-
* @param {string}
|
|
22
|
+
* @param {string[]} args
|
|
16
23
|
* @param {import('child_process').SpawnOptions} options
|
|
17
24
|
*/
|
|
18
|
-
function runBuildStep(
|
|
25
|
+
function runBuildStep(args, options) {
|
|
19
26
|
return new Promise((resolve, reject) => {
|
|
20
|
-
const child = spawn(
|
|
21
|
-
...options,
|
|
22
|
-
shell: true,
|
|
23
|
-
})
|
|
27
|
+
const child = spawn(process.execPath, args, options)
|
|
24
28
|
child.on("close", (code) => {
|
|
25
29
|
if (code === 0) {
|
|
26
30
|
resolve()
|
|
@@ -69,8 +73,8 @@ async function build() {
|
|
|
69
73
|
]),
|
|
70
74
|
}
|
|
71
75
|
|
|
72
|
-
const
|
|
73
|
-
const
|
|
76
|
+
const serverBuildArgs = [viteBinPath, "build", "--config", "./dist/vite/vite.config.server.js", "--ssr"]
|
|
77
|
+
const clientBuildArgs = [viteBinPath, "build", "--config", "./dist/vite/vite.config.client.js"]
|
|
74
78
|
const spawnBase = {
|
|
75
79
|
cwd: dirname,
|
|
76
80
|
stdio: "inherit",
|
|
@@ -80,11 +84,11 @@ async function build() {
|
|
|
80
84
|
|
|
81
85
|
try {
|
|
82
86
|
await Promise.all([
|
|
83
|
-
runBuildStep(
|
|
87
|
+
runBuildStep(serverBuildArgs, {
|
|
84
88
|
...spawnBase,
|
|
85
89
|
env: { ...baseEnv, CATALYST_VITE_CACHE_ID: "ssr" },
|
|
86
90
|
}),
|
|
87
|
-
runBuildStep(
|
|
91
|
+
runBuildStep(clientBuildArgs, {
|
|
88
92
|
...spawnBase,
|
|
89
93
|
env: { ...baseEnv, CATALYST_VITE_CACHE_ID: "client" },
|
|
90
94
|
}),
|
|
@@ -96,7 +100,7 @@ async function build() {
|
|
|
96
100
|
|
|
97
101
|
console.log("✅ Server and client builds completed!")
|
|
98
102
|
|
|
99
|
-
await runBuildStep("
|
|
103
|
+
await runBuildStep(["./dist/scripts/generateOfflineManifest.js"], {
|
|
100
104
|
...spawnBase,
|
|
101
105
|
env: baseEnv,
|
|
102
106
|
})
|
|
@@ -68,6 +68,7 @@ async function loadAppRoutes(appRoot) {
|
|
|
68
68
|
})
|
|
69
69
|
|
|
70
70
|
try {
|
|
71
|
+
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - appRoot is the app's own build root and the remainder is a hardcoded literal path, not request input.
|
|
71
72
|
const routesModule = await vite.ssrLoadModule(path.join(appRoot, "src/js/routes/utils.js"))
|
|
72
73
|
return typeof routesModule.getRoutes === "function" ? routesModule.getRoutes() : []
|
|
73
74
|
} finally {
|
|
@@ -23,6 +23,7 @@ export const printBundleInformation = () => {
|
|
|
23
23
|
const files = fs.readdirSync(directoryPath)
|
|
24
24
|
files.forEach((file) => {
|
|
25
25
|
if (!file.includes("txt") && !file.includes("json")) {
|
|
26
|
+
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - file comes from fs.readdirSync(directoryPath), i.e. actual filenames already on disk in the build output, not request input.
|
|
26
27
|
const filePath = path.join(directoryPath, file)
|
|
27
28
|
const fileSize = getFileSizeSync(filePath)
|
|
28
29
|
if (fileSize !== null) {
|
package/dist/scripts/serve.js
CHANGED
|
@@ -24,28 +24,30 @@ function startProd() {
|
|
|
24
24
|
|
|
25
25
|
console.log("🚀 Starting production server...")
|
|
26
26
|
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
27
|
+
const inspectArgs = commandLineArguments.includes("--inspect") ? ["--inspect"] : []
|
|
28
|
+
spawnSync(
|
|
29
|
+
"node",
|
|
30
|
+
[...inspectArgs, "--import", preInitPath, "--loader", loaderPath, "./dist/server/expressServer.js"],
|
|
31
|
+
{
|
|
32
|
+
cwd: dirname,
|
|
33
|
+
stdio: "inherit",
|
|
34
|
+
env: {
|
|
35
|
+
...process.env,
|
|
36
|
+
src_path: process.env.PWD,
|
|
37
|
+
NODE_ENV: "production",
|
|
38
|
+
IS_DEV_COMMAND: false,
|
|
39
|
+
APPLICATION: name || "catalyst_app",
|
|
40
|
+
...argumentsObject,
|
|
41
|
+
filterKeys: JSON.stringify([
|
|
42
|
+
"src_path",
|
|
43
|
+
"NODE_ENV",
|
|
44
|
+
"IS_DEV_COMMAND",
|
|
45
|
+
"APPLICATION",
|
|
46
|
+
...Object.keys(argumentsObject),
|
|
47
|
+
]),
|
|
48
|
+
},
|
|
49
|
+
}
|
|
50
|
+
)
|
|
49
51
|
}
|
|
50
52
|
|
|
51
53
|
startProd()
|
package/dist/scripts/start.js
CHANGED
|
@@ -22,11 +22,9 @@ function start() {
|
|
|
22
22
|
const packageJson = JSON.parse(readFileSync(path.join(process.env.PWD, "package.json"), "utf-8"))
|
|
23
23
|
const { name } = packageJson
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
spawnSync(command, [], {
|
|
25
|
+
spawnSync("node", ["--import", preInitPath, "--loader", loaderPath, "./dist/server/expressServer.js"], {
|
|
27
26
|
cwd: dirname,
|
|
28
27
|
stdio: "inherit",
|
|
29
|
-
shell: true,
|
|
30
28
|
env: {
|
|
31
29
|
...process.env,
|
|
32
30
|
src_path: process.env.PWD,
|
|
@@ -10,6 +10,7 @@ import pc from "picocolors"
|
|
|
10
10
|
import fs from "fs"
|
|
11
11
|
const { cyan, yellow, green } = pc
|
|
12
12
|
|
|
13
|
+
import { toMountPathPrefix } from "../vite/resolveDevServerConfig.js"
|
|
13
14
|
import { validateMiddleware, safeCall } from "./utils/validator.js"
|
|
14
15
|
import { botDetectionMiddleware } from "./utils/botDetectionMiddleware.js"
|
|
15
16
|
import { cjsRequire } from "./utils/cjsRequire.js"
|
|
@@ -126,6 +127,7 @@ const isProduction = process.env.NODE_ENV === "production"
|
|
|
126
127
|
|
|
127
128
|
function serveBuildFile(app, buildPath, urlPath, fileName, headers = {}) {
|
|
128
129
|
app.get(urlPath, (_req, res, next) => {
|
|
130
|
+
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - fileName is always a hardcoded literal passed at each serveBuildFile call site, never derived from the request.
|
|
129
131
|
const filePath = path.join(buildPath, fileName)
|
|
130
132
|
if (!fs.existsSync(filePath)) return next()
|
|
131
133
|
res.set(headers)
|
|
@@ -207,6 +209,12 @@ async function createServer() {
|
|
|
207
209
|
appType: "custom",
|
|
208
210
|
root: process.env.src_path,
|
|
209
211
|
})
|
|
212
|
+
|
|
213
|
+
// Expose Vite's resolved base to the SSR renderer so the URLs it injects
|
|
214
|
+
// (client entry, react-refresh preamble) match the path Vite serves under.
|
|
215
|
+
// Vite normalizes base with a trailing slash; trim it for clean URL joins.
|
|
216
|
+
process.env.APP_MOUNT_PATH = toMountPathPrefix(vite.config.base)
|
|
217
|
+
|
|
210
218
|
app.use(vite.middlewares)
|
|
211
219
|
}
|
|
212
220
|
|
|
@@ -30,7 +30,10 @@ export function Body(props) {
|
|
|
30
30
|
__html: `window.__SAFE_AREA_INITIAL__ = ${JSON.stringify(safeArea)}; window.__CATALYST_NATIVE_WEBVIEW__ = ${nativeWebView ? "true" : "false"}`,
|
|
31
31
|
}}
|
|
32
32
|
/>
|
|
33
|
-
{process.env.NODE_ENV === "development" &&
|
|
33
|
+
{process.env.NODE_ENV === "development" && (
|
|
34
|
+
// Base pinned to Vite's resolved base by server/expressServer.js.
|
|
35
|
+
<script type="module" src={`${process.env.APP_MOUNT_PATH || ""}/client/index.js`}></script>
|
|
36
|
+
)}
|
|
34
37
|
{jsx}
|
|
35
38
|
<script
|
|
36
39
|
/* eslint-disable */
|
|
@@ -34,9 +34,11 @@ export function Head(props) {
|
|
|
34
34
|
{metaTags && metaTags}
|
|
35
35
|
|
|
36
36
|
{/* Inline critical CSS — prevents FOUC/CLS */}
|
|
37
|
+
{/* nosemgrep: typescript.react.security.audit.react-dangerouslysetinnerhtml.react-dangerouslysetinnerhtml - inlineCss is compiled CSS read from disk (readCssFromDisk), sourced from the app's own build output, never from request/user input. */}
|
|
37
38
|
{inlineCss && <style dangerouslySetInnerHTML={{ __html: inlineCss }} />}
|
|
38
39
|
|
|
39
40
|
{/* Deferred CSS from prior SSRs on this route — avoids late layout from post-body <style> */}
|
|
41
|
+
{/* nosemgrep: typescript.react.security.audit.react-dangerouslysetinnerhtml.react-dangerouslysetinnerhtml - deferredRouteInlineCss is compiled CSS read from disk (readCssFromDisk), sourced from the app's own build output, never from request/user input. */}
|
|
40
42
|
{deferredRouteInlineCss && <style dangerouslySetInnerHTML={{ __html: deferredRouteInlineCss }} />}
|
|
41
43
|
|
|
42
44
|
{/* JS modules */}
|
|
@@ -103,6 +103,7 @@ export const readCssFromDisk = (cssPaths = [], basePath) => {
|
|
|
103
103
|
seen.add(asset)
|
|
104
104
|
if (asset.startsWith("http")) continue
|
|
105
105
|
|
|
106
|
+
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - asset paths come from the Vite build manifest generated at build time, never from a request, so there is no user-controlled input here.
|
|
106
107
|
const filePath = path.isAbsolute(asset) ? asset : path.join(basePath, asset.replace(/^\/+/, ""))
|
|
107
108
|
|
|
108
109
|
try {
|
|
@@ -265,6 +265,7 @@ const _renderMarkUp = async (
|
|
|
265
265
|
if (chunkExtractor) {
|
|
266
266
|
const renderedKeys = chunkExtractor.getRenderedComponentKeys()
|
|
267
267
|
this.push(
|
|
268
|
+
// nosemgrep: javascript.lang.security.audit.unknown-value-with-script-tag.unknown-value-with-script-tag - renderedKeys are internal bundler component-module keys tracked by ChunkExtractor, never request/user input, and are JSON.stringify-escaped before embedding.
|
|
268
269
|
`<script>window.__SSR_RENDERED_COMPONENTS__=new Set(${JSON.stringify(renderedKeys)})</script>`
|
|
269
270
|
)
|
|
270
271
|
}
|
|
@@ -6,8 +6,10 @@ const FastRefresh = () => {
|
|
|
6
6
|
type="module"
|
|
7
7
|
// eslint-disable-next-line risxss/catch-potential-xss-react
|
|
8
8
|
dangerouslySetInnerHTML={{
|
|
9
|
+
// Base-prefixed so the preamble resolves under a sub-path; otherwise
|
|
10
|
+
// plugin-react modules throw "$RefreshSig$ is not defined".
|
|
9
11
|
__html: `
|
|
10
|
-
import { injectIntoGlobalHook } from "/@react-refresh";
|
|
12
|
+
import { injectIntoGlobalHook } from "${process.env.APP_MOUNT_PATH || ""}/@react-refresh";
|
|
11
13
|
injectIntoGlobalHook(window);
|
|
12
14
|
window.$RefreshReg$ = () => {};
|
|
13
15
|
window.$RefreshSig$ = () => (type) => type;
|
|
@@ -45,9 +45,11 @@ function loadAliases() {
|
|
|
45
45
|
for (const [alias, aliasPath] of Object.entries(allAliases)) {
|
|
46
46
|
if (aliasPath && typeof aliasPath === "string") {
|
|
47
47
|
try {
|
|
48
|
+
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - appRoot is the app's own build root and aliasPath comes from the developer's package.json config, not request/user input.
|
|
48
49
|
const resolvedPath = path.resolve(appRoot, ...aliasPath.split("/"))
|
|
49
50
|
aliasCache[alias] = resolvedPath
|
|
50
51
|
} catch (error) {
|
|
52
|
+
// nosemgrep: javascript.lang.security.audit.unsafe-formatstring.unsafe-formatstring - alias is a key from the developer's own package.json config, not attacker-controlled; console.warn here does no printf-style substitution.
|
|
51
53
|
console.warn(`Failed to resolve alias ${alias}:`, error.message)
|
|
52
54
|
}
|
|
53
55
|
}
|
|
@@ -87,6 +89,7 @@ function resolveWithExtensions(basePath) {
|
|
|
87
89
|
}
|
|
88
90
|
|
|
89
91
|
for (const ext of extensions) {
|
|
92
|
+
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - basePath is derived from module import specifiers resolved by this plugin's own alias/relative-import logic, never from a request.
|
|
90
93
|
const candidate = path.join(basePath, `index${ext}`)
|
|
91
94
|
if (existsSync(candidate)) {
|
|
92
95
|
return candidate
|
|
@@ -107,9 +110,11 @@ function resolveToAbsolutePath(importPath, importerId) {
|
|
|
107
110
|
for (const [alias, aliasPath] of Object.entries(aliases)) {
|
|
108
111
|
if (importPath === alias || importPath.startsWith(alias + "/")) {
|
|
109
112
|
const remainingPath = importPath === alias ? "" : importPath.slice(alias.length + 1)
|
|
113
|
+
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - importPath comes from the app's own module import graph and aliasPath from the developer's package.json config, never from a request.
|
|
110
114
|
const rawResolved = path.join(aliasPath, remainingPath)
|
|
111
115
|
const finalResolved = resolveWithExtensions(rawResolved)
|
|
112
116
|
if (finalResolved) {
|
|
117
|
+
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - finalResolved is derived from the same alias/import resolution above, not request input.
|
|
113
118
|
return path.resolve(finalResolved)
|
|
114
119
|
}
|
|
115
120
|
}
|
|
@@ -118,6 +123,7 @@ function resolveToAbsolutePath(importPath, importerId) {
|
|
|
118
123
|
// 2. Relative imports — resolve from the importer's directory
|
|
119
124
|
if (importPath.startsWith("./") || importPath.startsWith("../")) {
|
|
120
125
|
const importerDir = path.dirname(importerId)
|
|
126
|
+
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - importPath comes from the app's own module import graph and importerId from Vite's module resolution, never from a request.
|
|
121
127
|
const absolutePath = path.resolve(importerDir, importPath)
|
|
122
128
|
const resolved = resolveWithExtensions(absolutePath)
|
|
123
129
|
if (resolved) {
|
|
@@ -149,6 +155,7 @@ function resolveImportPath(importPath, importerId) {
|
|
|
149
155
|
// Fallback: strip leading ./ and return as-is
|
|
150
156
|
return importPath.replace(/^\.\//, "")
|
|
151
157
|
} catch (error) {
|
|
158
|
+
// nosemgrep: javascript.lang.security.audit.unsafe-formatstring.unsafe-formatstring - importPath comes from the app's own module import graph resolved by Vite at build time, not attacker-controlled; console.warn here does no printf-style substitution.
|
|
152
159
|
console.warn(`Error resolving import path ${importPath}:`, error.message)
|
|
153
160
|
return importPath.replace(/^\.\//, "")
|
|
154
161
|
}
|
|
@@ -5,11 +5,40 @@ import { pathToFileURL } from "url"
|
|
|
5
5
|
const emptyConfig = {
|
|
6
6
|
ssrPlugins: [],
|
|
7
7
|
clientPlugins: [],
|
|
8
|
+
/**
|
|
9
|
+
* devServer lets apps tune the Vite dev server without editing framework
|
|
10
|
+
* internals. All fields are optional and map 1-to-1 onto Vite's `server`
|
|
11
|
+
* config (https://vite.dev/config/server-options).
|
|
12
|
+
*
|
|
13
|
+
* The most common use-case is an app served under a sub-path behind an SSL
|
|
14
|
+
* reverse proxy (nginx/caddy):
|
|
15
|
+
*
|
|
16
|
+
* // buildConfig.js
|
|
17
|
+
* export default {
|
|
18
|
+
* devServer: {
|
|
19
|
+
* // Vite's base path — the single source of truth for the dev mount
|
|
20
|
+
* // path; must match the reverse-proxy location prefix. Defaults to "/".
|
|
21
|
+
* base: "/o-mweb",
|
|
22
|
+
*
|
|
23
|
+
* // HMR WebSocket tuning when the browser connects via the proxy
|
|
24
|
+
* hmr: {
|
|
25
|
+
* clientPort: 443, // port the browser uses (proxy's SSL port)
|
|
26
|
+
* path: "__vite_hmr" // WebSocket pathname (relative to base)
|
|
27
|
+
* },
|
|
28
|
+
*
|
|
29
|
+
* // Vite 6 rejects requests whose Host header isn't localhost/127.0.0.1.
|
|
30
|
+
* // Add your proxy hostname here so the dev server accepts them.
|
|
31
|
+
* allowedHosts: ["local.odinweb.1mg.com"],
|
|
32
|
+
* },
|
|
33
|
+
* }
|
|
34
|
+
*/
|
|
35
|
+
devServer: {},
|
|
8
36
|
}
|
|
9
37
|
|
|
10
38
|
export async function loadCustomViteConfig() {
|
|
11
39
|
const configNames = ["buildConfig.js", "webpackConfig.js"]
|
|
12
40
|
const configPath = configNames
|
|
41
|
+
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - configName is one of two hardcoded literals above, and process.env.src_path is the app root set by the framework's own build tooling, not request/user input.
|
|
13
42
|
.map((configName) => path.join(process.env.src_path, configName))
|
|
14
43
|
.find((candidatePath) => existsSync(candidatePath))
|
|
15
44
|
|
|
@@ -293,6 +293,7 @@ export function manifestCategorizationPlugin(options = {}) {
|
|
|
293
293
|
// branch of the import graph, the affected CSS would vanish from server responses
|
|
294
294
|
// with no runtime error — this check surfaces that as a build-time warning.
|
|
295
295
|
function validateCssCoverage(categorizedManifest, buildDir) {
|
|
296
|
+
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - buildDir is the build tool's own output directory and the remaining segments are hardcoded literals, not request input.
|
|
296
297
|
const cssDir = path.join(buildDir, "client", "assets", "css")
|
|
297
298
|
if (!fs.existsSync(cssDir)) return
|
|
298
299
|
|
|
@@ -403,6 +404,7 @@ export function manifestCategorizationPlugin(options = {}) {
|
|
|
403
404
|
})
|
|
404
405
|
}
|
|
405
406
|
} catch (err) {
|
|
407
|
+
// nosemgrep: javascript.lang.security.audit.unsafe-formatstring.unsafe-formatstring - importPath comes from the app's own module import graph resolved by Vite at build time, not attacker-controlled; console.log here does no printf-style substitution.
|
|
406
408
|
console.log(`❌ Error resolving ${importPath}:`, err.message)
|
|
407
409
|
}
|
|
408
410
|
})
|
|
@@ -56,6 +56,7 @@ function loadAliases() {
|
|
|
56
56
|
const resolvedPath = resolvePath(appRoot, ...aliasPath.split("/"))
|
|
57
57
|
aliasCache[alias] = resolvedPath
|
|
58
58
|
} catch (error) {
|
|
59
|
+
// nosemgrep: javascript.lang.security.audit.unsafe-formatstring.unsafe-formatstring - alias is a key from the developer's own package.json _moduleAliases config, not attacker-controlled; console.warn here does no printf-style substitution.
|
|
59
60
|
console.warn(`Failed to resolve alias ${alias}:`, error.message)
|
|
60
61
|
}
|
|
61
62
|
}
|
|
@@ -100,6 +101,7 @@ function resolveWithExtensions(basePath) {
|
|
|
100
101
|
|
|
101
102
|
// Try index files inside a directory
|
|
102
103
|
for (const ext of extensions) {
|
|
104
|
+
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - basePath is derived from module import specifiers resolved by the Node loader itself, never from a request.
|
|
103
105
|
const candidate = join(basePath, `index${ext}`)
|
|
104
106
|
if (existsSync(candidate)) {
|
|
105
107
|
return candidate
|
|
@@ -119,10 +121,12 @@ function resolveAlias(specifier, parentURL) {
|
|
|
119
121
|
for (const [alias, aliasPath] of Object.entries(aliases)) {
|
|
120
122
|
if (specifier.startsWith(alias + "/") || specifier === alias) {
|
|
121
123
|
const remainingPath = specifier === alias ? "" : specifier.slice(alias.length + 1)
|
|
124
|
+
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - specifier comes from an ES module import statement in the app's own source code, and aliasPath from the developer's package.json config, never from a request.
|
|
122
125
|
const rawResolvedPath = join(aliasPath, remainingPath)
|
|
123
126
|
const finalResolvedPath = resolveWithExtensions(rawResolvedPath)
|
|
124
127
|
|
|
125
128
|
if (finalResolvedPath) {
|
|
129
|
+
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - finalResolvedPath is derived from the same alias/import resolution above, not request input.
|
|
126
130
|
return resolvePath(finalResolvedPath)
|
|
127
131
|
}
|
|
128
132
|
}
|
|
@@ -140,6 +144,7 @@ function resolveRelative(specifier, parentURL) {
|
|
|
140
144
|
try {
|
|
141
145
|
const parentPath = fileURLToPath(parentURL)
|
|
142
146
|
const parentDir = dirname(parentPath)
|
|
147
|
+
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - specifier comes from an ES module import statement in the app's own source code, parentURL from the module loader itself, never from a request.
|
|
143
148
|
const absolutePath = resolvePath(parentDir, specifier)
|
|
144
149
|
return resolveWithExtensions(absolutePath)
|
|
145
150
|
} catch {
|
|
@@ -225,6 +230,7 @@ export async function resolve(specifier, context, defaultResolve) {
|
|
|
225
230
|
*/
|
|
226
231
|
function findNearestPackageType(dir) {
|
|
227
232
|
while (dir !== dirname(dir)) {
|
|
233
|
+
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - dir is walked up from the module's own filesystem location via dirname(), never from a request; "package.json" is a hardcoded literal.
|
|
228
234
|
const pkgPath = join(dir, "package.json")
|
|
229
235
|
if (existsSync(pkgPath)) {
|
|
230
236
|
try {
|
|
@@ -302,9 +308,37 @@ function isValidExportName(name) {
|
|
|
302
308
|
// Loader-scoped require for discovering CJS exports
|
|
303
309
|
const loaderRequire = createRequire(import.meta.url)
|
|
304
310
|
|
|
305
|
-
const CJS_PATTERN = /\b(module\.exports\b|exports\.\w+\s*=)/
|
|
306
311
|
const REQUIRE_CALL_PATTERN = /\brequire\s*\(/
|
|
307
312
|
|
|
313
|
+
/**
|
|
314
|
+
* Decide whether a source file is really CommonJS by parsing it, not by
|
|
315
|
+
* regex-matching the raw text.
|
|
316
|
+
*
|
|
317
|
+
* A regex like /module\.exports\b/ cannot tell code from text, so any file that
|
|
318
|
+
* merely *mentions* module.exports inside a string, comment or template literal
|
|
319
|
+
* was misclassified as CJS. When such a file was real ESM with its own
|
|
320
|
+
* `export default`, the CJS shim appended a second one and Node failed the
|
|
321
|
+
* module with "Duplicate export of 'default'". Docs sites hit this constantly:
|
|
322
|
+
* a Markdown page documenting a CJS config compiles into a string literal in
|
|
323
|
+
* the bundled ESM output. Do not reintroduce source-regex detection.
|
|
324
|
+
*
|
|
325
|
+
* esbuild parses properly and lowers CJS itself, so a file it wraps in its
|
|
326
|
+
* __commonJS helper had no ESM syntax of its own — that is the CJS signal.
|
|
327
|
+
*/
|
|
328
|
+
function isCjsSource(source, filePath) {
|
|
329
|
+
try {
|
|
330
|
+
const { code } = transformSync(source, {
|
|
331
|
+
loader: "jsx",
|
|
332
|
+
format: "esm",
|
|
333
|
+
sourcefile: filePath,
|
|
334
|
+
target: "node20",
|
|
335
|
+
})
|
|
336
|
+
return /\bvar __commonJS =/.test(code)
|
|
337
|
+
} catch {
|
|
338
|
+
return false
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
308
342
|
// Shim __dirname and __filename for ESM app files (these globals only exist in CJS)
|
|
309
343
|
const createDirnameShim = (source) => {
|
|
310
344
|
const hasFilename = /\b(?:const|let|var)\s+__filename\b/.test(source)
|
|
@@ -390,7 +424,7 @@ export async function load(url, context, defaultLoad) {
|
|
|
390
424
|
// We can't use createRequire here because the file lives in a "type":"module"
|
|
391
425
|
// package — Node's CJS require refuses to load it. Instead, we wrap the
|
|
392
426
|
// source inline so that `module.exports` becomes the default export.
|
|
393
|
-
if (
|
|
427
|
+
if (isCjsSource(source, filePath)) {
|
|
394
428
|
const shimmed = [
|
|
395
429
|
createRequireShim(source),
|
|
396
430
|
createDirnameShim(source),
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// Pure resolvers for the Vite dev-server config derived from an app's
|
|
2
|
+
// `buildConfig.devServer`. Side-effect free so the base precedence and the fs
|
|
3
|
+
// allowlist merge can be unit-tested without spinning up Vite.
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Vite `base` for the dev server. `buildConfig.devServer.base` is the single
|
|
7
|
+
* source of truth (default "/"). Normalized to a leading slash and no trailing
|
|
8
|
+
* slash so it doubles as a URL prefix for the SSR renderer.
|
|
9
|
+
*/
|
|
10
|
+
export function resolveDevBase(devServer = {}) {
|
|
11
|
+
const base = devServer?.base
|
|
12
|
+
if (!base || base === "/") return "/"
|
|
13
|
+
return `/${String(base).trim().replace(/^\/+|\/+$/g, "")}`
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* A resolved base as a URL prefix. Root ("/") becomes "" so
|
|
18
|
+
* `${prefix}/client/index.js` stays single-slashed.
|
|
19
|
+
*/
|
|
20
|
+
export function toMountPathPrefix(base) {
|
|
21
|
+
return !base || base === "/" ? "" : String(base).replace(/\/+$/, "")
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* fs.allow for the dev server. Framework paths are always present; apps may
|
|
26
|
+
* extend the list via `devServer.fs.allow` but cannot replace it or relax other
|
|
27
|
+
* fs guards (e.g. `strict`), so buildConfig can't widen filesystem exposure.
|
|
28
|
+
*/
|
|
29
|
+
export function resolveDevFsAllow(devFs, frameworkPaths) {
|
|
30
|
+
const extra = Array.isArray(devFs?.allow) ? devFs.allow : []
|
|
31
|
+
return [...frameworkPaths, ...extra]
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Vite `server` block from `buildConfig.devServer`. Passthrough keys are spread
|
|
36
|
+
* first so the framework-owned `hmr` and `fs` always win.
|
|
37
|
+
*/
|
|
38
|
+
export function buildDevServer(devServer = {}, { frameworkPaths, isProduction = false } = {}) {
|
|
39
|
+
const { base, hmr, fs, ...serverOverrides } = devServer
|
|
40
|
+
return {
|
|
41
|
+
...serverOverrides,
|
|
42
|
+
hmr: isProduction ? false : (hmr ?? true),
|
|
43
|
+
fs: { allow: resolveDevFsAllow(fs, frameworkPaths) },
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import loadEnvironmentVariables from "../scripts/loadEnvironmentVariables.js"
|
|
2
2
|
loadEnvironmentVariables()
|
|
3
3
|
import { defineConfig } from "vite"
|
|
4
|
-
import baseConfig,
|
|
4
|
+
import { sharedViteConfig as baseConfig, getClientEnvVariables } from "./vite.config.js"
|
|
5
5
|
import path from "path"
|
|
6
6
|
import { manifestCategorizationPlugin } from "./manifest-categorization-plugin.js"
|
|
7
7
|
import { injectCacheKeyPlugin } from "./inject-cache-key-plugin.js"
|
package/dist/vite/vite.config.js
CHANGED
|
@@ -2,6 +2,8 @@ import { defineConfig, transformWithEsbuild } from "vite"
|
|
|
2
2
|
import react from "@vitejs/plugin-react"
|
|
3
3
|
import svgr from "vite-plugin-svgr"
|
|
4
4
|
import { builtinModules, createRequire } from "node:module"
|
|
5
|
+
import { loadCustomViteConfig } from "./loadCustomViteConfig.js"
|
|
6
|
+
import { resolveDevBase, buildDevServer } from "./resolveDevServerConfig.js"
|
|
5
7
|
|
|
6
8
|
/**
|
|
7
9
|
* Treat all .scss imports as CSS modules (webpack compat).
|
|
@@ -113,6 +115,7 @@ try {
|
|
|
113
115
|
const packageJson = JSON.parse(packageJsonContent)
|
|
114
116
|
_moduleAliases = packageJson._moduleAliases || {}
|
|
115
117
|
} catch (error) {
|
|
118
|
+
// nosemgrep: javascript.lang.security.audit.unsafe-formatstring.unsafe-formatstring - packageJsonConfig is a build-time-resolved local path, not attacker-controlled; console.warn here does no printf-style substitution.
|
|
116
119
|
console.warn(`Failed to read or parse package.json from ${packageJsonConfig}:`, error.message)
|
|
117
120
|
}
|
|
118
121
|
|
|
@@ -122,6 +125,7 @@ try {
|
|
|
122
125
|
catalyst_moduleAliases = catalystPackageJson._moduleAliases || {}
|
|
123
126
|
} catch (error) {
|
|
124
127
|
console.warn(
|
|
128
|
+
// nosemgrep: javascript.lang.security.audit.unsafe-formatstring.unsafe-formatstring - catalystPackageJsonConfig is a build-time-resolved local path, not attacker-controlled; console.warn here does no printf-style substitution.
|
|
125
129
|
`Failed to read or parse catalyst package.json from ${catalystPackageJsonConfig}:`,
|
|
126
130
|
error.message
|
|
127
131
|
)
|
|
@@ -146,6 +150,7 @@ import { imageUrl, fontUrl } from "./scssParams.js"
|
|
|
146
150
|
// options — so anything else is left alone and Vite falls back to its normal
|
|
147
151
|
// auto-loaded postcss.config.js resolution.
|
|
148
152
|
const resolvePostcssPlugins = () => {
|
|
153
|
+
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - process.env.src_path is the app root set by the framework's own build tooling, not request/user input.
|
|
149
154
|
const configPath = path.join(process.env.src_path, "postcss.config.js")
|
|
150
155
|
if (!fs.existsSync(configPath)) return null
|
|
151
156
|
|
|
@@ -168,6 +173,7 @@ const resolvePostcssPlugins = () => {
|
|
|
168
173
|
return plugin(finalOptions)
|
|
169
174
|
})
|
|
170
175
|
} catch (error) {
|
|
176
|
+
// nosemgrep: javascript.lang.security.audit.unsafe-formatstring.unsafe-formatstring - configPath is a build-time-resolved local path, not attacker-controlled; console.warn here does no printf-style substitution.
|
|
171
177
|
console.warn(`Failed to patch postcss config at ${configPath}:`, error.message)
|
|
172
178
|
return null
|
|
173
179
|
}
|
|
@@ -183,9 +189,11 @@ const alias = () => {
|
|
|
183
189
|
return Object.keys(allAliases).reduce((moduleEnvMap, alias) => {
|
|
184
190
|
if (allAliases[alias] && typeof allAliases[alias] === "string") {
|
|
185
191
|
try {
|
|
192
|
+
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - process.env.src_path is the app root and allAliases comes from the developer's own package.json _moduleAliases config, not request/user input.
|
|
186
193
|
const aliasPath = path.join(process.env.src_path, ...allAliases[alias].split("/"))
|
|
187
194
|
moduleEnvMap[alias] = aliasPath
|
|
188
195
|
} catch (error) {
|
|
196
|
+
// nosemgrep: javascript.lang.security.audit.unsafe-formatstring.unsafe-formatstring - alias is a key from the developer's own package.json _moduleAliases config, not attacker-controlled; console.warn here does no printf-style substitution.
|
|
189
197
|
console.warn(`Failed to configure alias ${alias}:`, error.message)
|
|
190
198
|
}
|
|
191
199
|
}
|
|
@@ -356,7 +364,10 @@ export const isNodeOnlyExternal = (id) =>
|
|
|
356
364
|
id === "@grpc/grpc-js" ||
|
|
357
365
|
id.startsWith("@opentelemetry/")
|
|
358
366
|
|
|
359
|
-
|
|
367
|
+
// ─── Shared config ────────────────────────────────────────────────────────────
|
|
368
|
+
// Exported for the production build (vite.config.client.js spreads this in).
|
|
369
|
+
// Does NOT include `base` or `server` — those differ between dev and prod.
|
|
370
|
+
export const sharedViteConfig = {
|
|
360
371
|
// Parallel `vite build` (SSR + client) must use separate dirs or Vite will block on shared `node_modules/.vite`.
|
|
361
372
|
cacheDir: path.join(
|
|
362
373
|
process.env.src_path,
|
|
@@ -482,17 +493,34 @@ export default defineConfig({
|
|
|
482
493
|
"**/*.woff2",
|
|
483
494
|
],
|
|
484
495
|
|
|
485
|
-
// Server configuration
|
|
486
|
-
server: {
|
|
487
|
-
hmr: !isProduction,
|
|
488
|
-
fs: {
|
|
489
|
-
allow: [process.env.src_path, __dirname],
|
|
490
|
-
},
|
|
491
|
-
},
|
|
492
|
-
|
|
493
496
|
// Preview configuration for production preview
|
|
494
497
|
preview: {
|
|
495
498
|
port: process.env.NODE_SERVER_PORT ? parseInt(process.env.NODE_SERVER_PORT) : 3005,
|
|
496
499
|
host: process.env.NODE_SERVER_HOSTNAME || "localhost",
|
|
497
500
|
},
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// ─── Dev server config ────────────────────────────────────────────────────────
|
|
504
|
+
// Async factory so it can read the app's buildConfig.devServer for `base`, HMR
|
|
505
|
+
// and other Vite server options. `base` comes solely from devServer.base; the
|
|
506
|
+
// resolved value is propagated to the SSR renderer by server/expressServer.js.
|
|
507
|
+
// The production build (vite.config.client.js) ignores this and spreads
|
|
508
|
+
// sharedViteConfig directly.
|
|
509
|
+
export default defineConfig(async () => {
|
|
510
|
+
const { devServer = {}, ssrPlugins = [] } = await loadCustomViteConfig()
|
|
511
|
+
|
|
512
|
+
return {
|
|
513
|
+
...sharedViteConfig,
|
|
514
|
+
base: resolveDevBase(devServer),
|
|
515
|
+
server: buildDevServer(devServer, {
|
|
516
|
+
frameworkPaths: [process.env.src_path, __dirname],
|
|
517
|
+
isProduction,
|
|
518
|
+
}),
|
|
519
|
+
// Apply the app's SSR plugins in dev too — the production build already
|
|
520
|
+
// does this in vite.config.server.js. This lets buildConfig.ssrPlugins
|
|
521
|
+
// inject SSR-only config (e.g. a plugin whose `config()` hook adds
|
|
522
|
+
// ssr.noExternal to bundle React-consuming deps so dev SSR shares the
|
|
523
|
+
// app's single React instance) without hardcoding anything in the framework.
|
|
524
|
+
plugins: [...(sharedViteConfig.plugins || []), ...ssrPlugins],
|
|
525
|
+
}
|
|
498
526
|
})
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import loadEnvironmentVariables from "../scripts/loadEnvironmentVariables.js"
|
|
2
2
|
loadEnvironmentVariables()
|
|
3
3
|
import { defineConfig } from "vite"
|
|
4
|
-
|
|
4
|
+
// Spread the static shared config (a plain object), NOT the default export —
|
|
5
|
+
// the default export is now an async dev-server factory function, and spreading
|
|
6
|
+
// a function would silently drop cacheDir, resolve aliases, css, plugins and
|
|
7
|
+
// build from the production SSR bundle. Mirrors vite.config.client.js.
|
|
8
|
+
import { sharedViteConfig as baseConfig, getClientEnvVariables, isNodeOnlyExternal } from "./vite.config.js"
|
|
5
9
|
import path from "path"
|
|
6
10
|
import { fileURLToPath } from "url"
|
|
7
11
|
import { dirname } from "path"
|
package/mcp_v2/tools/github.js
CHANGED
|
@@ -161,6 +161,7 @@ function safeReadText(filePath, maxBytes = 8000) {
|
|
|
161
161
|
|
|
162
162
|
function runGit(root, command) {
|
|
163
163
|
try {
|
|
164
|
+
// nosemgrep: javascript.lang.security.detect-child-process.detect-child-process - command is always one of the fixed git/node/npm strings hardcoded at call sites, never external input.
|
|
164
165
|
return execSync(command, {
|
|
165
166
|
cwd: root,
|
|
166
167
|
encoding: "utf8",
|
|
@@ -183,6 +184,7 @@ function slugify(input) {
|
|
|
183
184
|
}
|
|
184
185
|
|
|
185
186
|
function ensureFallbackDir(root) {
|
|
187
|
+
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - root is the project_path MCP tool argument, an intentional/documented parameter letting a local developer's coding agent point this tool at any project directory it already has filesystem access to (see mcp.js inputSchema). Not a network-facing input; the caller already has direct fs access via other tools. FALLBACK_DIR is a hardcoded literal.
|
|
186
188
|
const dir = path.join(root, FALLBACK_DIR)
|
|
187
189
|
fs.mkdirSync(dir, { recursive: true })
|
|
188
190
|
return dir
|
|
@@ -406,7 +408,9 @@ function appendSection(sections, heading, content) {
|
|
|
406
408
|
}
|
|
407
409
|
|
|
408
410
|
function isPathInside(root, candidate) {
|
|
411
|
+
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - this is the containment-check helper itself (used by callers below to reject paths outside root); root/candidate here are the values being validated, not yet trusted for file access.
|
|
409
412
|
const resolvedRoot = path.resolve(root)
|
|
413
|
+
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - this is the containment-check helper itself; candidate is the value being validated, not yet trusted for file access.
|
|
410
414
|
const resolvedCandidate = path.resolve(candidate)
|
|
411
415
|
const relative = path.relative(resolvedRoot, resolvedCandidate)
|
|
412
416
|
return relative === "" || (relative && !relative.startsWith("..") && !path.isAbsolute(relative))
|
|
@@ -543,6 +547,7 @@ function normalizeImages(images, root) {
|
|
|
543
547
|
continue
|
|
544
548
|
}
|
|
545
549
|
|
|
550
|
+
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - raw is an MCP tool argument (attachment path) and root the project_path argument; the isPathInside() check on the next line rejects anything that resolves outside root before it is ever read.
|
|
546
551
|
const absolutePath = path.isAbsolute(raw) ? path.resolve(raw) : path.resolve(root, raw)
|
|
547
552
|
if (!isPathInside(root, absolutePath)) {
|
|
548
553
|
localFiles.push({ path: raw, exists: false, rejected: true, reason: "outside_project_root" })
|
|
@@ -789,6 +794,7 @@ function collectRelatedFiles(root, query, explicitFiles) {
|
|
|
789
794
|
const candidates = Array.isArray(explicitFiles) ? explicitFiles : []
|
|
790
795
|
for (const file of candidates) {
|
|
791
796
|
if (typeof file !== "string") continue
|
|
797
|
+
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - file is an MCP tool argument (explicit related-files list) and root the project_path argument; the isPathInside() check on the next line rejects anything that resolves outside root before it is ever read.
|
|
792
798
|
const absolute = path.isAbsolute(file) ? path.resolve(file) : path.resolve(root, file)
|
|
793
799
|
if (!isPathInside(root, absolute)) continue
|
|
794
800
|
const content = safeReadText(absolute, 12000)
|
|
@@ -802,6 +808,7 @@ function collectRelatedFiles(root, query, explicitFiles) {
|
|
|
802
808
|
const words = normalizeSearchText(query).split(/\s+/).filter(Boolean).slice(0, 5)
|
|
803
809
|
if (words.length === 0) return files
|
|
804
810
|
|
|
811
|
+
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - root is the project_path MCP tool argument, an intentional/documented parameter (see mcp.js inputSchema); "src" is a hardcoded literal.
|
|
805
812
|
const srcDir = path.join(root, "src")
|
|
806
813
|
const matches = []
|
|
807
814
|
function walk(dir) {
|
|
@@ -813,6 +820,7 @@ function collectRelatedFiles(root, query, explicitFiles) {
|
|
|
813
820
|
}
|
|
814
821
|
for (const entry of entries) {
|
|
815
822
|
if (entry.name === "node_modules" || entry.name === ".git") continue
|
|
823
|
+
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - entry.name comes from fs.readdirSync(dir) above, i.e. actual filenames already on disk under srcDir, not request input.
|
|
816
824
|
const full = path.join(dir, entry.name)
|
|
817
825
|
if (entry.isDirectory()) {
|
|
818
826
|
walk(full)
|
|
@@ -831,7 +839,9 @@ function collectRelatedFiles(root, query, explicitFiles) {
|
|
|
831
839
|
|
|
832
840
|
function gatherGithubIssueContext(args = {}) {
|
|
833
841
|
const root = getProjectRoot(args)
|
|
842
|
+
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - root is the project_path MCP tool argument, an intentional/documented parameter (see mcp.js inputSchema); "package.json" is a hardcoded literal.
|
|
834
843
|
const pkg = safeReadJson(path.join(root, "package.json"))
|
|
844
|
+
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - root is the project_path MCP tool argument, an intentional/documented parameter (see mcp.js inputSchema); "config/config.json" is a hardcoded literal.
|
|
835
845
|
const configJson = safeReadJson(path.join(root, "config", "config.json"))
|
|
836
846
|
const branch = runGit(root, "git rev-parse --abbrev-ref HEAD")
|
|
837
847
|
const commit = runGit(root, "git rev-parse --short HEAD")
|
|
@@ -874,6 +884,7 @@ function generateIssueMarkdown(args = {}) {
|
|
|
874
884
|
const built = buildIssueBody(args, { enrich: true })
|
|
875
885
|
const labels = resolveIssueLabels(args, built.body)
|
|
876
886
|
const dir = ensureFallbackDir(root)
|
|
887
|
+
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - dir is the FALLBACK_DIR under the project_path MCP argument; the filename is a generated ISO timestamp plus slugify(title), which strips to [a-z0-9-] only, so it cannot contain path separators.
|
|
877
888
|
const filePath = path.join(dir, `${new Date().toISOString().replace(/[:.]/g, "-")}-${slugify(title)}.md`)
|
|
878
889
|
|
|
879
890
|
const markdown = [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "catalyst-core",
|
|
3
|
-
"version": "0.3.0-beta.
|
|
3
|
+
"version": "0.3.0-beta.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.jsx",
|
|
6
6
|
"description": "Web framework that provides great performance out of the box",
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
"scripts": {
|
|
31
31
|
"lint": "eslint .",
|
|
32
32
|
"lint-staged": "lint-staged",
|
|
33
|
+
"test:unit": "node --test \"test/**/*.test.js\"",
|
|
33
34
|
"prettify": "prettier . --write",
|
|
34
35
|
"prepare": "node ../../scripts/safe-rimraf.js ./dist && cp -r ./src ./dist && babel src/native --out-dir dist/native --config-file ./babel.native.config.cjs --ignore '**/.build/**' && babel src/sentry.js --out-file dist/sentry.cjs --config-file ./babel.native.config.cjs",
|
|
35
36
|
"prepublishOnly": "npm run prepare",
|
|
@@ -51,7 +52,7 @@
|
|
|
51
52
|
"react-refresh": "^0.14.0",
|
|
52
53
|
"react-router-dom": "^6.16.0",
|
|
53
54
|
"redux": "^4.2.1",
|
|
54
|
-
"sanitize-html": "^2.
|
|
55
|
+
"sanitize-html": "^2.17.6",
|
|
55
56
|
"sass": "^1.89.2",
|
|
56
57
|
"ua-parser-js": "^1.0.37",
|
|
57
58
|
"vite": "^6.4.3",
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { test } from "node:test"
|
|
2
|
+
import assert from "node:assert/strict"
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
resolveDevBase,
|
|
6
|
+
toMountPathPrefix,
|
|
7
|
+
resolveDevFsAllow,
|
|
8
|
+
buildDevServer,
|
|
9
|
+
} from "../../src/vite/resolveDevServerConfig.js"
|
|
10
|
+
|
|
11
|
+
// ─── resolveDevBase ───────────────────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
test("resolveDevBase: resolves from devServer.base", () => {
|
|
14
|
+
assert.equal(resolveDevBase({ base: "/o-mweb" }), "/o-mweb")
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
test("resolveDevBase: defaults to '/' when base is unset", () => {
|
|
18
|
+
assert.equal(resolveDevBase({}), "/")
|
|
19
|
+
assert.equal(resolveDevBase(undefined), "/")
|
|
20
|
+
assert.equal(resolveDevBase({ base: "/" }), "/")
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
test("resolveDevBase: normalizes to a leading slash, no trailing slash", () => {
|
|
24
|
+
assert.equal(resolveDevBase({ base: "o-mweb" }), "/o-mweb")
|
|
25
|
+
assert.equal(resolveDevBase({ base: "/o-mweb/" }), "/o-mweb")
|
|
26
|
+
assert.equal(resolveDevBase({ base: " /o-mweb// " }), "/o-mweb")
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
// ─── toMountPathPrefix ────────────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
test("toMountPathPrefix: root maps to empty string for clean URL joins", () => {
|
|
32
|
+
assert.equal(toMountPathPrefix("/"), "")
|
|
33
|
+
assert.equal(toMountPathPrefix(""), "")
|
|
34
|
+
assert.equal(toMountPathPrefix(undefined), "")
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
test("toMountPathPrefix: trims Vite's trailing slash", () => {
|
|
38
|
+
assert.equal(toMountPathPrefix("/o-mweb/"), "/o-mweb")
|
|
39
|
+
assert.equal(toMountPathPrefix("/o-mweb"), "/o-mweb")
|
|
40
|
+
// The joined result stays single-slashed.
|
|
41
|
+
assert.equal(`${toMountPathPrefix("/o-mweb/")}/client/index.js`, "/o-mweb/client/index.js")
|
|
42
|
+
assert.equal(`${toMountPathPrefix("/")}/client/index.js`, "/client/index.js")
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
// ─── resolveDevFsAllow: security-sensitive allowlist merge ─────────────────────
|
|
46
|
+
|
|
47
|
+
const FRAMEWORK = ["/app/src", "/framework/dist/vite"]
|
|
48
|
+
|
|
49
|
+
test("resolveDevFsAllow: framework paths always present, none provided", () => {
|
|
50
|
+
assert.deepEqual(resolveDevFsAllow(undefined, FRAMEWORK), FRAMEWORK)
|
|
51
|
+
assert.deepEqual(resolveDevFsAllow({}, FRAMEWORK), FRAMEWORK)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
test("resolveDevFsAllow: apps may EXTEND but never drop framework paths", () => {
|
|
55
|
+
const result = resolveDevFsAllow({ allow: ["/extra/pkg"] }, FRAMEWORK)
|
|
56
|
+
assert.deepEqual(result, ["/app/src", "/framework/dist/vite", "/extra/pkg"])
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
test("resolveDevFsAllow: non-array allow is ignored (no widening via bad input)", () => {
|
|
60
|
+
assert.deepEqual(resolveDevFsAllow({ allow: "/" }, FRAMEWORK), FRAMEWORK)
|
|
61
|
+
assert.deepEqual(resolveDevFsAllow({ allow: null }, FRAMEWORK), FRAMEWORK)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
// ─── buildDevServer: precedence + security ────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
test("buildDevServer: hmr defaults to true in dev", () => {
|
|
67
|
+
const server = buildDevServer({}, { frameworkPaths: FRAMEWORK, isProduction: false })
|
|
68
|
+
assert.equal(server.hmr, true)
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
test("buildDevServer: hmr is false in production", () => {
|
|
72
|
+
const server = buildDevServer({ hmr: { clientPort: 443 } }, { frameworkPaths: FRAMEWORK, isProduction: true })
|
|
73
|
+
assert.equal(server.hmr, false)
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
test("buildDevServer: app hmr overrides are honored in dev", () => {
|
|
77
|
+
const hmr = { clientPort: 443, path: "__vite_hmr" }
|
|
78
|
+
const server = buildDevServer({ hmr }, { frameworkPaths: FRAMEWORK, isProduction: false })
|
|
79
|
+
assert.deepEqual(server.hmr, hmr)
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
test("buildDevServer: passthrough keys (proxy/allowedHosts) are forwarded", () => {
|
|
83
|
+
const server = buildDevServer(
|
|
84
|
+
{ allowedHosts: ["local.example.com"], proxy: { "/api": "http://localhost:9000" } },
|
|
85
|
+
{ frameworkPaths: FRAMEWORK, isProduction: false }
|
|
86
|
+
)
|
|
87
|
+
assert.deepEqual(server.allowedHosts, ["local.example.com"])
|
|
88
|
+
assert.deepEqual(server.proxy, { "/api": "http://localhost:9000" })
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
test("buildDevServer: app fs cannot replace the allowlist, only extend it", () => {
|
|
92
|
+
const server = buildDevServer(
|
|
93
|
+
{ fs: { allow: ["/extra"] } },
|
|
94
|
+
{ frameworkPaths: FRAMEWORK, isProduction: false }
|
|
95
|
+
)
|
|
96
|
+
assert.deepEqual(server.fs.allow, ["/app/src", "/framework/dist/vite", "/extra"])
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
test("buildDevServer: app cannot relax fs.strict or clobber fs via overrides", () => {
|
|
100
|
+
const server = buildDevServer(
|
|
101
|
+
{ fs: { allow: ["/"], strict: false } },
|
|
102
|
+
{ frameworkPaths: FRAMEWORK, isProduction: false }
|
|
103
|
+
)
|
|
104
|
+
// strict is dropped entirely (framework keeps Vite's secure default), and
|
|
105
|
+
// the framework paths remain — "/" is merely appended, not a replacement.
|
|
106
|
+
assert.equal(server.fs.strict, undefined)
|
|
107
|
+
assert.ok(server.fs.allow.includes("/app/src"))
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
test("buildDevServer: 'base' is not leaked into the server block", () => {
|
|
111
|
+
const server = buildDevServer({ base: "/o-mweb" }, { frameworkPaths: FRAMEWORK, isProduction: false })
|
|
112
|
+
assert.equal(server.base, undefined)
|
|
113
|
+
})
|