Skip to content

Latest commit

 

History

History
152 lines (116 loc) · 51.7 KB

File metadata and controls

152 lines (116 loc) · 51.7 KB

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Docker-only policy

NEVER run npm/yarn/electron/grunt/node on the host. Local node_modules/ is for the Linux Docker build only. Allowed host commands: git, docker, codesign.

Commands

Task Command
Build (Intel/amd64) docker build --build-arg GIT_COMMIT=$(git rev-parse --short HEAD) -t boostnote-legacy .
Build (Apple Silicon/arm64) docker build --platform linux/arm64 --build-arg GIT_COMMIT=$(git rev-parse --short HEAD) --build-arg BUILDARCH=arm64 -t boostnote-legacy-arm64 .
Test all docker run --rm boostnote-legacy npm test
AVA tests only docker run --rm boostnote-legacy npm run ava
Jest tests only docker run --rm boostnote-legacy npm run jest
Lint docker run --rm boostnote-legacy npm run lint
Lint fix docker run --rm boostnote-legacy npm run fix
Compile (webpack) docker run --rm boostnote-legacy npm run compile
Dev (HMR) docker run --rm boostnote-legacy npm run dev
Export .app (Intel) docker cp $(docker create --rm boostnote-legacy):/app/dist/Boostnote-darwin-x64 ./dist/
Export .app (arm64) docker cp $(docker create --rm boostnote-legacy-arm64):/app/dist/Boostnote-darwin-arm64 ./dist/

Omitting GIT_COMMIT build-arg → About dialog shows "unknown".

Running a single test

AVA picks tests/**/*-test.js; run one file:

docker run --rm boostnote-legacy npx ava tests/dataApi/createNote-test.js

Jest picks everything else under tests/:

docker run --rm boostnote-legacy npx jest tests/components/MyComponent.test.js

Architecture

index.js → Squirrel lifecycle → lib/main-app.js
    ├── lib/main-window.js     BrowserWindow creation
    ├── lib/main-menu.js       native app menu
    ├── lib/ipcServer.js       node-ipc server (main process)
    └── lib/touchbar-menu.js

browser/main/index.js  (webpack entry → compiled/main.js)
    ├── store.js               Redux store + Immutable.js via Mutable.js wrappers
    ├── Main.js                root component → SideNav | NoteList | Detail
    ├── lib/dataApi/           CRUD operations on .cson note files
    ├── lib/ConfigManager.js   electron-config wrapper
    ├── lib/shortcutManager.js keyboard shortcut registry
    └── lib/ThemeManager.js

Notes are stored as .cson files in user-defined storage directories on disk. browser/main/lib/dataApi/ contains all read/write operations against those files.

Webpack aliases: lib./lib, browser./browser. These are used throughout import paths.

Toolchain quirks

  • Webpack 1 + Babel 6 — loader chains use ! syntax (e.g. style!css?modules!stylus), not the modern use:[] form.
  • Externals: electron, react, redux, codemirror, lodash, moment, prettier are loaded via <script> tags in the HTML skeleton (webpack-skeleton.js), not bundled. Do not attempt to import them as if they were bundled.
  • process shim: Webpack 1 injects process.versions = {}. Any dep reading process.versions.node at module load (e.g. fs-extra@7+) crashes. Pin such deps or external them.
  • CSS Modules via react-css-modules + Stylus. Class name pattern: [name]__[local]___[path].
  • HMR dev: Manual refresh needed when editing constructors or adding new CSS classes (registered at construction time).

Electron quirks

  • Dialog API: Electron 9+ removed sync/callback forms. Use showMessageBoxSync and Promise-based showOpenDialog/showSaveDialog.
  • webPreferences: enableRemoteModule: true, nodeIntegration: true, contextIsolation: false are required.
  • Node 22 (Debian bookworm) inside Docker for the build toolchain; Electron 11.5.0 (Chrome 87, Node 12) at runtime. Anything that runs in the renderer must stay compatible with Node 12 / Chrome 87, even though Docker has a newer Node.

Dependency policy

  • resolutions block in package.json is the canonical place to force-upgrade vulnerable transitive deps. Adding a new entry there + running yarn install regenerates yarn.lock with a single hoisted version. Use it whenever the parent package cannot be bumped (most of the Webpack 1 / Babel 6 stack).
  • Current entries (CVE-driven). Group by reason so the next maintainer doesn't have to rediscover the why:
    • Renderer / runtime-touching: lodash ^4.17.21, moment ^2.30.1, highlight.js ^10.4.1, codemirror ^5.58.2, dompurify ^2.5.4, set-getter ^0.1.1 (resolved at renderer runtime via markdown-toclazy-cache; the package is in webpack-skeleton.js#externals but Electron's nodeIntegration: true loads it through Node's require), dot-prop ^4.2.1 (renderer runtime via electron-configconfdot-prop; used by browser/main/lib/ConfigManager.js to persist user preferences), decode-uri-component ^0.2.1 (renderer runtime via query-string@^6.13.8decode-uri-component; query-string is imported by browser/lib/newNote.js, browser/main/Detail/*, browser/main/modals/NewNoteModal.js, browser/main/NoteList/index.js). All loaded via <script> externals or bundled into compiled/main.js. For dot-prop, the CVE-2020-8116 sink is unreachable because every preference path argument is a hard-coded string ('keyMap', 'theme', 'editor.fontSize', …) — the bump is defence-in-depth. For decode-uri-component, CVE-2022-38900 is a DoS via repeated malformed %-encoded sequences (pre-0.2.1 looped instead of throwing) — Boostnote's only inputs to query-string are note-creation URL parameters constructed by the app itself, but the upgrade also collapses the build-time consumer (source-map-resolve@^0.5.x) to the patched 0.2.2. remarkable ^1.7.2 (resolves to 1.7.4) is consumed only by markdown-toc@^1.2.0, which is renderer-bundled — markdown-toc/index.js:29 instantiates new Remarkable() (no-args, default schema) to parse user-authored note markdown into a token stream for TOC extraction. Reached from browser/lib/markdown-toc-generator.js via CodeEditor.js, SnippetNoteDetail.js, and MarkdownNoteDetail.js. Patches GHSA-2gjj-79c4-c645 (ReDoS in remarkable's inline parser) and GHSA-jhcr-3vfp-qx79 (XSS via HTML escaping bypass); both affect <1.7.2. ReDoS reachable in this project: an attacker-crafted note triggers the TOC generator and hangs the renderer process. XSS less reachable since markdown-toc consumes the token stream and not the rendered HTML directly, but the bump covers it as defence-in-depth. API surface stable 1.7.1 → 1.7.4 — new Remarkable() + .render() + token-stream access are all unchanged, so markdown-toc's call site works without modification. codemirror ^5.58.2 collapses two coexisting copies — the direct dep ^5.65.0 (already at 5.65.21) and the nested [email protected] installed under node_modules/react-codemirror/node_modules/ (^5.18.2) and node_modules/codemirror-mode-elixir/node_modules/ (^5.20.2) — to a single hoisted [email protected] install (^5.58.2 resolves to the same 5.65.21 yarn already had at top level, and the nested 5.38.0 directories are removed from node_modules on disk). Patches GHSA-3p3p-7p4f-r28x (ReDoS in codemirror HTML/JS/CSS-mode regular expressions, affects <5.58.2). CVE sink was not exploitable in the shipped binary even before this bump: webpack 1's webpack-skeleton.js#externals maps codemirror: 'var CodeMirror', so every renderer require('codemirror') is bundle-replaced with a global lookup, and lib/main.production.html:117 establishes that global by loading <script src="../node_modules/codemirror/lib/codemirror.js"> — the hoisted top-level path, which was already on 5.65.21. The nested 5.38.0 copies under react-codemirror and codemirror-mode-elixir were dead weight on disk and never executed (codemirror-mode-elixir ships a self-contained dist/elixir.js UMD that only calls CodeMirror.defineMode(...) on the global; react-codemirror's require('codemirror') is intercepted by the webpack externals map). Bump removes the lock duplicates + the on-disk dead-weight copies and clears the Dependabot / npm audit flag. Verified: npm run compile produces an unchanged 9.21 MB / 1217 module bundle (renderer already loaded 5.65.21 — no behavior delta), and disk spot-check confirms a single node_modules/codemirror @ 5.65.21 with no nested directories. dompurify ^2.5.4 bumps the single [email protected] exact pin (consumed by mermaid@~9.1.7, renderer-bundled) up to 2.5.9 (latest within ^2.5.4). Patches GHSA-mmhx-hmjr-r674 (tampering by prototype pollution, affects <2.5.4 on the 2.x line). CVE sink is reachable in this project: mermaid passes user-authored diagram label content through DOMPurify.sanitize(html, config) at render time — a crafted mermaid block in a note can reach the proto-pollution path. Bump is minor-within-major (2.4.0 → 2.5.9); the dompurify 2.x API (sanitize(), addHook(), removeHook(), isValidAttribute()) is stable across the patch line, so mermaid 9.1.7's call sites (DOMPurify.sanitize(value, config) for label sanitization and DOMPurify.addHook('uponSanitizeElement', cb) for the custom mermaid hook) work unchanged. [email protected] ships CJS (main: dist/purify.cjs.js, optional module: dist/purify.es.js for bundlers that honour it, no type: module) — clean ESM cliff. Verified: npm run compile produces an unchanged 9.21 MB / 1217 module bundle. js-yaml ^3.14.2 collapses eight coexisting ranges (^3.7.0, ^3.8.1, ^3.9.1, ^3.10.0, ^3.14.1, ~3.14.0, ~3.7.0, ^3.14.2) to a single 3.14.2 entry — the direct dep is already on 3.14.2 (renderer-bundled, called from browser/components/MarkdownPreview.js and browser/main/lib/dataApi/formatMarkdown.js via yaml.load() / yaml.dump() for frontmatter parsing), but markdown-toc@^1.2.0 (also renderer-bundled — imported by browser/lib/markdown-toc-generator.js and reached from CodeEditor.js, SnippetNoteDetail.js, MarkdownNoteDetail.js) lazy-loads gray-matter@^2.1.0 which pulled in js-yaml@^3.8.1 resolving to the vulnerable 3.13.1; [email protected] (build-time CSS minify via postcss-svgo ← cssnano) pulled in js-yaml@~3.7.0 resolving to 3.7.0. Patches the code-injection class fixed in 3.14.2. API surface is identical 3.7 → 3.14 (load / safeLoad / dump / safeDump signatures unchanged; 3.13 added load as alias for safeLoad, 3.14 deprecated safeLoad but kept it working), so the markdown-toc and postcss-svgo call sites are unaffected.
    • Build-time only (loader / packager chain): json5 ^1.0.2, word-wrap ^1.2.4, y18n ^3.2.2, minimist ^1.2.8, qs ^6.5.3, json-schema ^0.4.0, tmp ^0.2.6, brace-expansion ^1.1.15, node-fetch ^2.6.7, tough-cookie ^4.1.3, underscore ^1.12.1, async ^2.6.4, sha.js ^2.4.12, ua-parser-js ^0.7.22, ws ^8.17.1, got ^11.8.5, glob-parent ^5.1.2, set-value ^2.0.1, ansi-regex ^3.0.1, ajv ^6.12.3, acorn ^5.7.4, decompress-zip ^0.3.2, form-data ^2.5.4, kind-of ^6.0.3, lodash.merge ^4.6.2, handlebars ^4.7.7. The ws resolution collapses ws@^4.0.0 + ws@^5.2.0 (both consumed by [email protected], which is a devDep used only by AVA/Jest browser-env helpers in tests/helpers/setup-browser-env.js) to a single [email protected]+ — patches CVE-2024-37890 (DoS via excessive request headers, <7.5.10 / <8.17.1) and CVE-2021-32640 (ReDoS in Sec-WebSocket-Protocol header, <5.2.4). jsdom's WebSocket-impl.js uses the stable new WebSocket(url) client API which is unchanged ws 4 → 8. The got resolution collapses got@^6.7.1 (ava → update-notifier → package-json → got, an auto-update check that's a no-op under CI) and got@^9.6.0 (@electron/get → got, used by electron-packager at build time to fetch the Electron binary) to a single [email protected]+ — patches CVE-2022-33987 (UNIX-socket redirect bypass, <11.8.5 / <12.1.0). Verified end-to-end by running the full docker build .: @electron/get successfully downloaded Electron 11.5.0 binaries through the bumped got, and electron-packager produced both Boostnote-darwin-x64 and Boostnote-linux-x64 artifacts. The glob-parent resolution forces the legacy glob-parent@^2.0.0 consumers (chokidar@^1.x via [email protected] test runner and watchpack@^0.2.x webpack-1 file watcher; glob-base@^0.3.0 via parse-glob) up to 5.1.2+, patching CVE-2020-28469 (ReDoS in the is-glob-style regex). API is stable across glob-parent majors — single globParent(pattern) function with identical return surface, so the chokidar/watchpack call sites are unaffected. The set-value resolution collapses two coexisting majors (^0.4.3 consumed by union-value@^1.0.0cache-base@^1.0.1, and ^2.0.0 consumed by cache-base@^1.0.1 directly) — both upstream of snapdragon@^0.8.1base@^0.11.1, which is reached only through micromatch@^3.1.4/^3.1.8 (anymatch@^2, sane@^2, test-exclude@^4 in the jest@22 / [email protected] / [email protected] dev path). Patches CVE-2019-10747 (proto pollution via attacker-controlled path argument in set(obj, path, value)). API surface stable 0.4 → 2.x (same 3-arg signature; 2.x added an optional options 4th arg), so union-value's internal setValue(target, prop, value) calls work unchanged. The CVE sink is not actually reachable in this project — every consumer passes hardcoded internal strings (snapdragon parser state, cache-base keys, union-value template processing), never user input — but the bump clears the audit complaint and collapses the dual-major entry to a single 2.0.1. The ansi-regex resolution collapses three coexisting ranges (^2.0.0 consumed by chalk@1has-ansi + strip-ansi@^3, plus string-width@^1, webpack-dev-server@1, wrap-ansi@^2; ^3.0.0 consumed by strip-ansi@^4ava, eslint, jest-cli, inquirer, supertap, cliui, string-width@^2, plus pretty-format in jest internals) to a single 3.0.1 entry. Patches CVE-2021-3807 (ReDoS in the ANSI-escape regex, affecting <3.0.1 / <4.1.1 / <5.0.1 / <6.0.1; the lock only carries 2.x and 3.x lines so a single ^3.0.1 resolution covers everything). API is the single function () => RegExp, stable 2 → 5 — chalk@1's has-ansi(str) = ansi-regex().test(str) and strip-ansi@^3's str.replace(ansi-regex(), '') work unchanged. The 2.x range has no patched release (the fix line starts at 3.0.1), so forcing the 2.x consumers up to 3.0.1 is the only way to patch them. Sink not reachable in this project: all consumers process developer-authored CLI output, test stack traces, or webpack-dev-server log lines — never user-controlled ANSI strings. The underscore consumers in the tree (nomnom via jsonlint-mod/lib/cli.js, [email protected] via remarkable's CLI binary, underscore-plus via fs-plus) all live behind CLI entries that Boostnote's renderer never loads — jsonlint-mod is used via web/jsonlint.js, markdown-toc consumes remarkable through its library entry, and js-sequence-diagrams inlines its own underscore copy inside the pre-built UMD bundle. Forcing the bump therefore clears the audit complaint without exercising any of the methods (_.any, _.contains) that were removed in underscore 1.9. The async resolution collapses 5 coexisting majors (0.2 / 0.9 / 1.5 / 2.6 / 3.2 in the lock) to a single 2.6.4 — the CVE-2021-43138 sink (async.mapValues) is not exercised by any consumer (the vulnerable copy was [email protected], which only calls async.map and async.queue); the cross-major coverage holds because every method the actual consumers call is API-stable from 0.9 → 3.x. The sha.js resolution covers the crypto-browserify polyfill graph, which webpack 1's NodeTargetPlugin externalizes at runtime — sha.js never actually loads in the shipped renderer. The ua-parser-js resolution covers fbjs, a React-16-era utility library that is never bundled into the renderer. The ajv resolution collapses three coexisting 5.x ranges (^5.1.0 via request@^2.79.0/^2.83.0/^2.87.0har-validator@~5.0.3, ^5.2.3 via eslint@^4.18.2[email protected], ^5.3.0 via eslint@^4.18.2 directly) to a single [email protected] entry. Patches CVE-2020-15366 (proto pollution in ajv.validate() via deeply-nested schema constructs) and the $data ReDoS advisory; both affect <6.12.3. ajv 5 → 6 is a major bump and [email protected]'s lib/config/config-validator.js was written against the ajv 5 API — [email protected] (consumed via [email protected]) declares peerDependency: ajv@^5.0.0 and yarn now prints a peer-warning, but the actual keyword-registration call sites (require('ajv-keywords')(ajv, ['typeof'])) work unchanged because the keyword DSL is stable 5 → 6. Verified by running npm run lint end-to-end inside the bn-deps image: eslint loaded its rule schemas through ajv 6, validated .eslintrc + every rule's options schema, and emitted only the seven pre-existing prettier/prettier baseline errors (host prettier 1.18 vs Docker prettier 1.19 disagreement documented elsewhere in this file) — zero ajv-derived crashes, zero schema-validation errors. Scope is build-time / dev-only: ajv reaches the tree only through eslint (lint script), har-validatorrequest (used at build time by [email protected] in test helpers and mksnapshot@^0.3.0 in the electron-packager chain, neither of which calls request({har: ...})), and table (eslint CLI formatter). Sink not reachable in this project: every schema fed to ajv is developer-authored (eslint rule option schemas, internal HAR + table specs) — never user-controlled input. Compile bundle unchanged at 9.21 MB / 1217 modules. The acorn resolution collapses eight coexisting ranges (^3.0.0 + ^3.0.4 via [email protected] + acorn-jsx@^3.0.0espree@^3.5.4eslint@^4.18.2; ^4.0.4 via acorn-globals@^3.1.0jsdom@^9.4.2; ^5.0.0 via acorn-globals@^4.1.0[email protected]; ^5.3.0 / ^5.5.0 / ^5.5.3 via jsdom@^11.5.1 / [email protected] / jsdom@^11.9.0 and espree@^3.5.4 directly) to a single hoisted [email protected] install. Patches GHSA-6chw-6frg-f759 (ReDoS in fastRegExpClone, affects >=5.0.0 <5.7.4 / >=6.0.0 <6.4.1 / >=7.0.0 <7.1.1) — only the 5.x line in this tree was vulnerable, but a global resolution to 5.7.4 was the simplest patch that also collapsed the 3.x and 4.x copies. Scope is build-time / dev-only: [email protected] parses developer-authored source files at compile time, eslint lints the same source, [email protected] is consumed by turndown@4 (renderer dep, but turndown uses native DOMParser in Electron and only falls back to jsdom in Node, so the renderer-runtime path doesn't actually load acorn) plus jest-environment-jsdom@^22.4.1 (devDep, test only), jsdom@^9.4.2 is consumed by ava browser-env helpers in tests/helpers/setup-browser-env.js. Sink not reachable: the ReDoS triggers only on attacker-supplied JavaScript source fed to acorn.parse(), which never happens in Boostnote's data flow (all parsed JS is build-time developer source). Surprise result: webpack 1.15.0 works correctly with acorn 5.7.4, contradicting the long-held belief that webpack 1 is hard-locked to acorn 3 — lib/Parser.js only calls the stable acorn.parse(src, opts) surface plus a few node-walker helpers that are unchanged 3 → 5. This does not resolve the uuid 11+ acorn cliff documented under "Pinned direct deps", because that cliff is about ES2020 syntax (?? / ?.) which acorn 5 still cannot tokenize — full ES2020 support arrived in acorn 7+. Verified: full npm run compile (9.21 MB / 1217 modules, no regression) and npm run lint (eslint + espree + acorn-jsx + acorn 5.7.4 produce only the 7 pre-existing prettier/prettier baseline errors). All 8 previously-coexisting acorn ranges collapse to a single hoisted node_modules/acorn entry on disk — no nested copies. The decompress-zip resolution bumps the single exact-pinned [email protected] (consumed by mksnapshot@^0.3.0asar@^0.11.0electron-winstaller@^2.2.0[email protected]) up to 0.3.3 (latest within ^0.3.2). Patches GHSA-3wq5-r5w8-5xhf (arbitrary file overwrite via path traversal during ZIP extraction, affects <0.3.2). Consumer chain is the Squirrel.Windows installer toolchain only — electron-packager@^15.4.0 itself uses the modern [email protected], which has no mksnapshot/decompress-zip dependency. The vulnerable chain is reached only by grunt build win (the create-windows-installer task in gruntfile.js), which the GitHub Actions release workflow does not run — CI ships .zip / .tar.gz only, same reason the Linux electron-installer-* task blocks were already removed. CVE sink is path traversal during extraction of an attacker-controlled ZIP, but mksnapshot's ZIP is the precompiled mksnapshot.zip fetched over HTTPS from github.com/electron/electron/releases (not attacker-controlled). Bump is patch-within-minor (0.3.0 → 0.3.3) and the API surface (new DecompressZip(file).extract({path})) is unchanged between releases. Compile bundle unchanged at 9.21 MB / 1217 modules. The form-data resolution bumps the single [email protected] install (consumed by request@^2.79.0/^2.83.0/^2.87.0[email protected] + [email protected] + mksnapshot@^0.3.0) up to 2.5.5 (latest within ^2.5.4). Patches GHSA-fjxv-7rqg-78g4 — form-data used Math.random() to choose multipart boundary strings, which is predictable and could enable an attacker controlling adjacent multipart fields to inject content (affects <2.5.4 / <3.0.4 / <4.0.4). Scope is build-time / test / dead-path only: turndown@4 (renderer dep) uses native DOMParser in Electron and only falls back to jsdom in Node, so request → form-data is not invoked at renderer runtime; jest-environment-jsdom and the ava browser-env helpers are test-only; mksnapshot's request usage is the Windows-installer chain grunt build win, which CI does not run. Sink not reachable: none of these consumers issue multipart POSTs in Boostnote's data flow — mksnapshot only GETs precompiled binaries from github.com/electron/electron/releases, jsdom is the offline DOM parser, ava/jest exercise local-only resources. Bump is minor-within-major (2.3.2 → 2.5.5); the public API (new FormData() + .append(field, value) + .pipe(req) + .getHeaders()) is stable across the 2.x line, so [email protected]'s multipart builder works unchanged. Compile bundle unchanged at 9.21 MB / 1217 modules. The kind-of resolution collapses four coexisting majors (^3.0.2/^3.0.3/^3.2.0 via align-text + is-accessor-descriptor@^0.1.6 + is-data-descriptor@^0.1.4 + is-number@^2/^3 + micromatch@^2 + object-copy + snapdragon-util + to-object-path; ^4.0.0 via has-values; ^5.0.0 via is-descriptor@^0.1.0; ^6.0.0/^6.0.2 via is-accessor-descriptor@^1.0.0 + is-data-descriptor@^1.0.0 + is-descriptor@^1.0.x + make-iterator + micromatch@^3.1.x + nanomatch + randomatic@^3 + use@^3.1.0) to a single hoisted [email protected]. Patches CVE-2019-20149 / GHSA-6c8f-qphg-qjgp — validation bypass via attacker-controlled constructor.name property on an object passed to kindOf(value); only the 6.x line (<6.0.3) is in the advisory scope, but the global resolution also lifts the 3.x / 4.x / 5.x copies and unifies the on-disk tree. Scope is build-time / dev-tool / test only — every consumer chain roots at jest@22, [email protected], [email protected], or eslint@4 and runs against developer-authored glob patterns (test paths, watch globs, jest config), never against attacker input. Sink not reachable: the CVE requires an attacker-supplied object with a crafted constructor.name, which never reaches micromatch / snapdragon / descriptor-helpers in this project's data flow. API is stable across 3 → 6 — kindOf(value) returns a type-string and consumers only === against the common cases ('object', 'string', 'array', 'number'); 4.x added Symbol detection, 5.x dropped is-buffer, 6.x added the constructor.name validation that the CVE patches, but the call sites in this tree all check the same set of legacy type strings. Verified: full npm run compile (9.21 MB / 1217 modules, no regression) and npm run lint (eslint + table + jest-cli's kind-of consumers run cleanly, producing only the 7 pre-existing prettier/prettier baseline errors). The lodash.merge resolution bumps the single [email protected] install (consumed by concordance@^3.0.0[email protected] test runner, and hullabaloo-config-manager@^1.1.0babel-register/babel-jest config loader) up to 4.6.2. Patches CVE-2018-16469 / GHSA-4xc9-xhrj-v574 (proto pollution via attacker-controlled source object in lodash.merge(dst, src)). Scope is dev/test only: concordance is exercised when ava prints diff output for failing test assertions; hullabaloo-config-manager is the cache layer that loads .babelrc for babel-register. Both merge developer-authored config objects (ava test specs, babel config) — sink not reachable in this project. Bump is patch-within-minor (4.6.1 → 4.6.2); the package ships a single _.merge(dst, src) function whose signature is unchanged across the patch line. Compile bundle unchanged at 9.21 MB / 1217 modules. The handlebars resolution bumps the single [email protected] install (consumed by istanbul-reports@^1.3.0istanbul-api@^1.1.14jest-cli@^22.4.4) up to 4.7.9 (latest within ^4.7.7). Patches GHSA-q2c6-c6pm-g3gh (proto pollution via template compile, affects <4.7.7). Scope is dev/test only: handlebars is exercised solely by istanbul-reports when rendering HTML coverage report templates, which only runs under jest --coverage; Boostnote's npm test never passes --coverage. Sink not reachable: the templates are hardcoded in istanbul-reports/lib/html/*.html.hbs and the compile inputs are developer-authored coverage data, never user input. Bump is minor-within-major (4.5.3 → 4.7.9). API surface (Handlebars.compile(), Handlebars.registerHelper(), Handlebars.SafeString) is stable across the 4.x line. Compile bundle unchanged at 9.21 MB / 1217 modules.
    • Dev-server (HMR) only: cookie ^0.7.0, serve-static ^1.16.0, sockjs ^0.3.20, on-headers ^1.1.0, express ^4.20.0, url-parse ^1.5.8, min-document ^2.19.1, eventsource ^1.1.1, follow-redirects ^1.14.7. The first six load through [email protected]cookie / serve-static / on-headers / express are middleware in the dev HTTP server, sockjs is the WebSocket HMR transport, and url-parse is consumed by sockjs-client for parsing the HMR socket URL. min-document reaches the renderer only via the babel-preset-react-hmre toolchain — react-transform-hmrglobalmin-document — which is gated on .babelrc#env.development.presets. The eventsource resolution bumps the single [email protected] exact pin (consumed by sockjs-client@^1.0.3webpack-dev-server@^1.12.0) up to 1.1.2 (latest within ^1.1.1), patching GHSA-6h5x-7c5m-7cr5 (Authorization header exposure across cross-origin redirects, affects <1.1.1). sockjs-client uses the W3C EventSource surface (new EventSourceDriver(url), onmessage, onerror, readyState, close()), stable across the 0.x → 1.x jump — the eventsource 1.x rewrite changed only internal redirect handling. CVE sink is not reachable in this project: HMR clients connect only to http://localhost:webpack-dev-server, no cross-origin redirects, and the EventSource is never exercised in any context other than npm run dev. The follow-redirects resolution bumps the single [email protected] install (consumed by http-proxy@^1.16.2http-proxy-middleware@~0.17.1webpack-dev-server@^1.12.0) up to 1.16.0 (latest within ^1.14.7). Patches GHSA-74fj-2j2h-c42q (Authorization + Cookie header exposure on cross-domain redirects, affects <1.14.7). API surface is the {http, https} wrapper objects with Node-style .request() / .get() signatures, stable across the 1.x line, so [email protected]'s use of followRedirects.http.request(opts, cb) works unchanged. Sink unreachable: http-proxy-middleware proxies dev HMR requests to localhost backends configured statically in gruntfile.js / webpack.config.js, never to attacker-controlled endpoints that could issue a cross-domain redirect. Webpack 1's DefinePlugin constant-folds the dev branch out of the production compile, so none of these touch the shipped Electron binary.
  • Pinned direct deps with a documented ceiling:
    • uuid is pinned to ^9.0.1. Bumping past 9.x is BLOCKED by webpack 1's acorn parser, not by the ESM cliff. uuid 10.x and 11.x still ship a CJS main (./dist/cjs/index.js), so they look bumpable, but the files under dist/cjs/ use ES2020 nullish coalescing (??) and optional chaining (?.) — webpack 1's bundled acorn (v3.x) cannot tokenize either operator and the compile aborts with Module parse failed … Unexpected token at every v1.js / v4.js / v6.js / v7.js / v35.js entrypoint. Tried 9.0.1 → 11.1.1 empirically; rolled back. uuid 13.0.0+ adds the ESM-only cliff on top (no main field at all). Bumping requires either the webpack 1 → 5 migration (acorn 8 supports ES2020), or a babel-loader exception that transpiles node_modules/uuid through @babel/plugin-proposal-nullish-coalescing-operator + @babel/plugin-proposal-optional-chaining (babel 7-only — would mean dragging a parallel babel 7 install in for one dep). Both are out of scope for incremental work. Production consumer: browser/lib/keygen.jsconst { v4: uuidv4 } = require('uuid').
    • mermaid is pinned to ~9.1.7 (tier-A target chosen during the 8 → 9 investigation — last 9.x release before the v9.2 monorepo + lazy-load import() rewrite that Webpack 1 cannot resolve). Tiers B (~9.3.0) and C (^9.4.3) are still upgrade candidates if a tier-A → tier-B smoke pass is performed; v10 is blocked by the same ESM-only / pure-exports cliff that blocks uuid 13+.
    • highlight.js is pinned to ^10.4.1 (9.x is EOL). 11.x is ESM-only and blocked by the Webpack 1 cliff.
    • raphael is pinned to exactly 2.2.7 (no caret). 2.3.0 produces NaN SVG dimensions where 2.2.7 silently coerced undefined width/height inputs to 0. Any flowchart or sequence-diagram code block in a note triggers it: rewriteIframe (browser/components/MarkdownPreview.js) calls diagram.drawSVG(el, opts) on each .flowchart / .sequence block, which routes through Raphael's geometry math — <svg> attributes come out as "NaN" and React's reconciler throws Error: <svg> attribute height: Expected length, "NaN". raphael is external-loaded (webpack-skeleton.js:53 maps raphael: 'var Raphael' and lib/main.production.html:161 loads <script src="../node_modules/raphael/raphael.min.js">), so the hoisted top-level version controls the actual runtime — the direct-dep pin is the source of truth. [email protected] itself declares raphael: "2.2.7" exact in its own dependencies, which is the upstream contract this pin matches. Caret is forbidden: "raphael": "^2.2.7" resolves to 2.3.0 under yarn install --force, which is the bug this pin prevents. Production consumers: browser/components/MarkdownPreview.js (flowchart + sequence-diagram drawSVG paths) via the runtime global.
    • codemirror-mode-elixir is pinned to exactly 1.1.1 (no caret). Same dist-layout regression as flowchart.js below: 1.1.2 renamed the published dist file from dist/elixir.js to dist/codemirror-mode-elixir.{js,m.js,umd.js} and ships nothing at the old path. lib/main.production.html:119 and lib/main.development.html:123 load the mode through <script src="../node_modules/codemirror-mode-elixir/dist/elixir.js">, which 404s on 1.1.2 with Failed to load resource: net::ERR_FILE_NOT_FOUND elixir.js:1. The mode never registers and Elixir syntax highlighting silently fails in code blocks. Pinning to 1.1.1 (bare, no caret) restores the original file. Moving forward to 1.1.2+ requires editing both HTML files to point at dist/codemirror-mode-elixir.umd.js — a small HTML edit but out of scope for incremental dep work and not strictly necessary since 1.1.2 carries no CVE.
    • flowchart.js is pinned to exactly 1.12.0 (no caret). 1.12.0 is the last release that ships the pre-built UMD bundle at release/flowchart.min.js; the maintainer deleted the release/ directory entirely in 1.12.1+ and now only publishes src/ source. lib/main.production.html:162 and lib/main.development.html:166 load flowchart through a <script src="../node_modules/flowchart.js/release/flowchart.min.js"> tag, and webpack-skeleton.js:54 resolves require('flowchart') to a var flowchart external — so the renderer expects the pre-built UMD to populate the global at runtime. Bumping past 1.12.0 makes the <script> tag 404 (net::ERR_FILE_NOT_FOUND) and the next require('flowchart') throws ReferenceError: flowchart is not defined at the first diagram. Bumping forward to 1.13+/1.18.x requires moving from the <script>-tag/external pattern to webpack-bundling flowchart.js as a normal import — a renderer integration change, not a dep bump — and is out of scope for incremental work. Production consumers: browser/lib/markdown.js (flowchart code-block rendering) via the runtime global. Caret is forbidden: "flowchart.js": "^1.6.5" resolves to 1.18.0 under yarn install --force, which is the bug this pin prevents.
  • Skipped CVE bumps (documented "do nothing"):
    • loader-utils ^1.4.1 (CVE-2022-37601 prototype pollution in parseQuery()) — no patched 0.2.x release exists; the 0.2 line is EOL. Empirically retried with global "loader-utils": "^1.4.1" resolution; npm run compile fails at the very first module (./browser/main/index.js) with Error: A valid query string passed to parseQuery should begin with '?' at Object.parseQuery (loader-utils/lib/parseQuery.js:13:11) at module.exports (babel-loader/lib/index.js:104:35). Root cause: loader-utils 1.x tightened parseQuery to require a leading ? on its input, but [email protected] calls loaderUtils.parseQuery(this.query) where this.query is the raw query string without the leading ?. Same call pattern in [email protected], [email protected], [email protected], and [email protected]'s own internal loader-resolver — bumping the loader-utils version means bumping every loader that uses it (babel-loader 7+, css-loader 1+, style-loader 1+, stylus-loader 3+), i.e. the webpack 1 → 2 migration. The CVE sink is not exploitable in this project: query strings are developer-authored in webpack-config loader chains (style!css?modules&sourceMap!stylus) and never user-controlled. Skipped — defer to the webpack 1 → 5 migration tracked under "Outstanding security work" item #3.
    • electron 11.5.0 → 13.x / 14.x — GHSA-3p22-ghq8-v749 (renderer accesses random Bluetooth device without permission) requires the renderer to call navigator.bluetooth.requestDevice(...). Boostnote source has zero navigator.bluetooth references; the CVE is not exploitable. Upgrading to Electron 13+ deletes the remote module entirely (replaced by the separate @electron/remote npm package) and flips enableRemoteModule to false by default — both require a code migration touching every remote.* call site (24 source files: browser/components/MarkdownPreview.js, browser/lib/contextMenuBuilder.js, browser/lib/context.js, browser/main/Main.js, browser/main/SideNav/*, browser/main/Detail/*, browser/main/modals/*, etc.) plus lib/main-window.js (drop enableRemoteModule: true, add @electron/remote/main.initialize() + .enable(webContents)), plus 2 HTML inline-script edits in lib/main.{production,development}.html (the electron.remote.process.argv HMR --hot detection) and 1 shadow-import delete in browser/main/NoteList/index.js:1029. Tracked migration plan: UpgradePlan_Electron11_to_Electron14.md — phased into Commit A (@electron/remote@^1.2.2 swap, still on Electron 11, zero runtime change, fully reversible) and Commit B (Electron 14.2.9 runtime bump + menu.popup(win)menu.popup({window}) × 3 sites + printToPDF({}, cb) → Promise form). contextIsolation: true (Phase 3) deferred — separate ~1-2 day refactor. Status: still skipped until someone executes the plan. When executed, also update the "Pinned direct deps" section to add an entry for @electron/remote documenting the 1.x vs 2.x peer-dep cliff (1.x supports Electron ≥10; 2.x requires ≥13).
    • minimatch ^3.0.2^3.x consumers already resolve to 3.1.5 via hoist; the only sub-target version is 0.3.0 under [email protected] > [email protected], which would break stylus's build if forced. ReDoS not exploitable on stylus's hard-coded internal patterns. Skipped. Applied 2026-05-27 as "minimatch": "^3.1.4" in the resolutions block. Empirical test passed — [email protected] uses only minimatch.Minimatch class + .set + GLOBSTAR constant, all stable across the 0.3 → 3.x bump, so stylus still compiles cleanly. Forcing the resolution collapses three previously-coexisting entries ([email protected] → 0.3.0 under stylus's glob chain; minimatch@^3.0.4 → 3.1.5 under electron-packager > @electron/asar; minimatch@* → 10.2.5 under @types/minimatch, a TypeScript-only type stub never executed by the Babel 6 build) to a single [email protected]. Patches CVE-2022-3517 (ReDoS in braceExpand via attacker-controlled brace patterns). Sink remained unreachable in this project's data flow — stylus feeds developer-authored glob patterns (browser/**/*.styl, etc.) — but the bump clears the Dependabot complaint and removes the on-disk dead-weight copies.
    • braces ^3.0.3 — lock has three coexisting majors: [email protected] (consumed by micromatch@^2 via jest@22, [email protected] chokidar, http-proxy-middleware@~0.17.1, anymatch@^1), [email protected] (consumed by micromatch@^3 via newer jest, sane@^2, test-exclude@^4), and [email protected] (consumed by micromatch@^4). CVE-2024-4068 (ReDoS via attacker-controlled brace-pattern string) affects 1.x and 2.x. A global "braces": "^3.0.3" resolution would break every micromatch@^2/^3 consumer because braces 3 changed the public API (returns an array by default, different option keys). Bumping micromatch itself to ^4.x would propagate through deeply-pinned dev tools (jest@22, [email protected], http-proxy-middleware@~0.17.1) which are not API-compatible with micromatch 4. The CVE sink is not reachable in this project: every micromatch consumer feeds developer-authored glob patterns (test paths, watch globs, webpack-dev-server proxy maps) — never user input. Skipped, same reasoning as loader-utils / minimatch.
    • postcss 5.x → 8.4.31+ (GHSA-7fh5-64p2-3v2j XSS via unescaped </style> in stringify output; CVE-2023-44270 line-return parsing error) — lock already collapses 12 coexisting postcss@^5.0.x/^5.2.16 ranges to a single [email protected] (the 5.x ceiling). Neither CVE is patched on the 5.x line — postcss 5 is EOL: GHSA-7fh5-64p2-3v2j was fixed only in 8.2.10 and CVE-2023-44270 only in 8.4.31, with no backport to 5.x or 6.x. The Dependabot-suggested "5.2.18" is simply the latest 5.x release and is inert as a fix. Consumer chain is the build-time CSS minify pipeline: css-loader@^0.19.0, autoprefixer@^6.3.1, and ~30 postcss-* plugins from cssnano@^3.x (postcss-calc, postcss-colormin, postcss-discard-, postcss-merge-, postcss-minify-, postcss-modules-, postcss-normalize-, postcss-reduce-, postcss-svgo, postcss-unique-selectors, postcss-zindex, etc.). All run at webpack-1 compile time via ExtractTextPlugin; minified CSS is written to disk, never re-parsed at Electron runtime. Bumping postcss 5 → 8 requires css-loader 0.19 → 4+ (postcss 8 peer), autoprefixer 6 → 10+ (postcss 8 peer), and a full cssnano 3 → 6 plugin-API rewrite (postcss 6 changed plugin(name, fn) to the async visitor pattern; postcss 8 dropped the process() callback form entirely). All three are gated on the webpack 1 → 2 → 5 migration tracked under "Outstanding security work" item #3. CVE sinks are not reachable in this project: every CSS input fed to the build-time postcss is developer-authored browser/**/*.styl compiled through Stylus, never user-controlled content — the stringify XSS sink would require an attacker to place a </style> substring inside CSS that round-trips through postcss.parse(...).toString() at build time, and the line-return sink similarly requires attacker-controlled CSS source. Renderer-bundled postcss path was previously reached via sanitize-html@^2.7.1postcss@^8.3.11, force-resolved through the selective "sanitize-html/postcss": "^7.0.39" entry; that selective entry was removed along with the sanitize-html direct dep (see "Removed prod-only deps" below) once the custom browser/lib/markdown-it-sanitize-html.js plugin became the sole sanitizer path and no longer imported the lib. The renderer no longer bundles any runtime postcss copy; only the build-time [email protected] chain remains. Skipped — same reasoning class as loader-utils / minimatch / braces / @babel/traverse. Track with the webpack 1 → 5 migration.
    • @babel/traverse 7.23.2 (CVE-2023-45133) — the patched version is the scoped @babel/[email protected], which is not in this tree at all. The repo carries the legacy unscoped [email protected] (consumed by every babel-core@6, babel-helper-*@6, babel-plugin-transform-*@6, babel-template@6 entry, i.e. the entire babel 6 transpile chain). The babel team's security policy explicitly states that babel 6 receives no further security fixes — there is no patched 6.x release to bump to. Applying the v7 fix means a full babel 6 → 7 migration: every babel-* direct devDep (babel-core, babel-loader, babel-jest, babel-register, babel-preset-env, babel-preset-es2015, babel-preset-react, babel-preset-react-hmre, babel-plugin-react-transform, babel-plugin-webpack-alias) plus .babelrc plus babel-loader 6 → 8 which is itself gated by the webpack 1 → 2+ migration. The CVE sink is not reachable in this project's threat model: babel only runs at build time (webpack-loader, jest's babel-jest, ava's babel-register) against developer-authored source in browser/ / lib/ / tests/. Note content reaches the renderer through markdown-it → the custom browser/lib/markdown-it-sanitize-html.js plugin, never through babel. There are zero programmatic babel.transform(...) or require('babel-core') call sites in browser/ or lib/. Skipped — defer to the webpack 1 → 5 / babel 6 → 7 migration tracked under "Outstanding security work" item #3.
  • Webpack 1 dep ceilings (general rule): if a dep ships pure ESM ("type": "module" and no main), it will fail to resolve. Always check the candidate's package.json before accepting a major bump. Known blocked majors: uuid 12+, mermaid 10+, json5 2+.
  • optionalDependencies is intentionally absent. The previously-listed grunt-electron-installer-debian and grunt-electron-installer-redhat (plus their electron-installer-* task blocks in gruntfile.js) were removed once it was confirmed that the GH Actions release workflow only ships .zip / .tar.gz of the packaged app — no .deb or .rpm was ever built in CI. Restore the two devDeps, the task config blocks, and the entries in the build:linux task chain if you ever want to resume packaging Linux installers.
  • Removed dev-only deps (dead code):
    • devtron — deprecated Electron DevTools extension (removed from official Electron docs in v10+; Boostnote ships on v11). No source file ever imported it; only references were a stale 'devtron' entry in webpack-skeleton.js#externals and the devDep declaration. Removed; freed ~32 MB of dev node_modules.
    • redux-devtools, redux-devtools-dock-monitor, redux-devtools-log-monitor — powered an in-app Redux debug overlay loaded only when NODE_ENV === 'development'. CI always builds production, so the dev branch was dead code in every shipped binary. Removed alongside browser/main/DevTools/index.dev.js and index.prod.js; browser/main/DevTools/index.js is now the single no-op stub (() => <div /> plus instrument: () => {}) — keeps store.js and browser/main/index.js call sites unchanged. To restore the overlay, recreate index.dev.js with createDevTools(...) and add the env-switch back to index.js.
    • standard — the standalone "StandardJS" CLI. Never invoked (no npm run standard script, no grunt task). .eslintrc#extends: ["standard"] resolves to eslint-config-standard (a separate devDep), not the standard package itself. Removed; freed ~33 MB of transitive closure (its own nested eslint, eslint-plugin-import, eslint-plugin-node, etc.). Side effect: eslint-plugin-promise@^3.4.2 is now declared as a direct devDep — it was previously hoisted out of standard's nested copy and is required as a peer of [email protected]. Without that explicit pin, ESLint refuses to load the config.
    • concurrently — never invoked (no npm script, no grunt task, no source reference). The 100-ish transitive descendants (yargs 17, chalk 4, supports-color 8, modern lodash variants) were pure build-time bloat. Removed; freed ~13 MB and ~110 lockfile lines. To restore for a future parallel dev workflow, re-add concurrently@^9.x and wire it into a new npm run dev script.
    • react-input-autosize — never imported anywhere in browser/ or lib/. Likely leftover from an early autosizing tag-input component that was replaced. Removed; freed a small (~25 KB) entry plus its react-prop-types transitive.
    • immutable (production dep, not dev) — never imported in source. browser/lib/Mutable.js is a thin wrapper around the native ES Map/Set, unrelated to immutable.js despite the name. The only other consumer is [email protected], which lists immutable as an optional peer — CRR keeps working without it for the standard ConnectedRouter / connectRouter / routerMiddleware / push API surface that Boostnote uses. Dropping the direct dep also resolved the CVE proto-pollution advisory: yarn now installs [email protected] through CRR's optional-dep range (^3.8.1 || ^4.0.0), well past the patched 3.8.3. To restore for a future immutable-keyed router variant, re-add immutable as a direct dep at the version of choice.
    • sanitize-html (production dep, not dev) — last call site was browser/lib/markdown-it-sanitize-html.js's sanitizeHtml() invocation on html_block tokens. Commit abf79e43 replaced that call with the in-file sanitizeBlock() helper, which reuses the existing regex-based sanitizeInline() validator. The lib became unreachable from the renderer bundle. Removed the direct dep along with the selective "sanitize-html/postcss": "^7.0.39" resolution (the only reason that resolution existed was to pin sanitize-html's postcss@^8.3.11 peer to the patched 7.x line; with sanitize-html gone, no renderer-bundled postcss copy exists at all). Verified by npm run compile: bundle shrank from 9.21 MB / 1217 modules to 8.3 MB / 1146 modules — sanitize-html plus its htmlparser2, parse-srcset, deepmerge, is-plain-object, [email protected] + ~30 postcss helper transitives dropped out. To restore (e.g., to harden the regex-based validator against attribute-value edge cases like <a title="x > y"> that the naive /<[^>]*>/g tokenizer splits wrong), re-add sanitize-html@^2.7.x as a direct dep, re-add the "sanitize-html/postcss": "^7.0.39" resolution to keep postcss on the patched 7.x line, and revert browser/lib/markdown-it-sanitize-html.js#sanitizeBlock back to the sanitizeHtml() call.

Quick verify loop for dependency changes

Full docker build . takes ~5 min (compile + electron-packager + grunt pack). For iterative dependency work, build the deps stage once and reuse it:

# One-time: build deps-only image
docker build --target deps -t bn-deps .

# Per iteration: edit package.json resolutions, then:
docker run --rm -v "$(pwd)":/app -v /app/node_modules -w /app bn-deps \
  sh -c 'yarn install --ignore-engines && npm run compile'

The -v /app/node_modules anonymous volume preserves the container's node_modules while bind-mounting the host source over /app. npm run compile reuses the cached deps and finishes in ~5s; it is the fastest reliable signal that a dep change has not broken the webpack bundle. Run the full docker build . once at the end to validate electron-packager.

Stale-deps trap: the anonymous volume holds whatever node_modules was baked into the bn-deps image at build time. If you have edited package.json resolutions since then, plain yarn install --ignore-engines may decide the lockfile is satisfied and not actually rewrite node_modules. Use yarn install --ignore-engines --force to force a relink, or rebuild the image with docker build --target deps -t bn-deps . when the resolution churn is non-trivial.

Pre-commit hook: husky's pre-commit runs npm run lint. Because the docker-only policy keeps npm / yarn off the host, the hook prints Can't find yarn in PATH and reports Skipping pre-commit hook. The commit proceeds. This is expected — lint runs inside the image when you want it (docker run --rm boostnote-legacy npm run lint), not on the host.

Test quirks (pre-existing failures — do not fix)

  • npm test = npm run ava && npm run jest (sequential).
  • AVA runs serially (--serial).
  • Jest picks up test files inside dist/Boostnote-darwin-*/ → fail with environment mismatch. Ignore.
  • createNote/createNoteFromUrl Jest tests fail with "Target folder doesn't exist" (test-data issue). Ignore.

Lint / Prettier

  • ESLint: standard + standard-jsx + plugin:react/recommended + prettier.
  • Prettier config: singleQuote: true, semi: false, jsxSingleQuote: true.
  • Unused vars/undef are warnings, not errors.
  • Do NOT fix the 6 pre-existing prettier/prettier errors in MarkdownPreview.js, markdown.js, store.js — host prettier (1.18) and Docker prettier (1.19) disagree; fixing one breaks the other.
  • Pre-commit hook runs npm run lint (husky).

CodeQL / security history

  • GitHub Advanced Security CodeQL scans land on main as commits titled Potential fix for code scanning alert no. N (Copilot Autofix). These are usually safe ReDoS / regex tightenings, but each one mutates a parser/sanitizer regex — review before assuming the diff is harmless, and run the full docker build afterwards. Recent sweep: alerts 14, 15, 16, 20, 24 (May 2026).
  • js/incomplete-sanitization was hit in browser/components/CodeEditor.js (escapePipe). The fix lives in browser/lib/utils.js as escapeMarkdownPipe(str) — it escapes backslashes before pipes so the encoding is reversible. Unit-tested in tests/lib/escapeMarkdownPipe.test.js. Reuse that helper rather than inlining new escape logic.

Outstanding security work (next priorities)

Ordered by runtime impact (renderer-bundled first, build-only / blocked last). Items already applied or explicitly skipped live in the Dependency policy section above.

  1. markdown-it 5.1.0 and 8.4.2 still pinned by transitive consumers (@enyaxu/markdown-it-anchor@5, etc.); resolve to the already-locked 12.3.2 via the resolutions block once the parser-plugin chain (footnote, kbd, anchor) is verified.
  2. Mermaid tier B / C (~9.3.0 then ^9.4.3) — escalate only after manually rendering one of each diagram type from a packaged build and confirming no ChunkLoadError from the v9.2+ lazy-load chunks.
  3. Webpack 1 ceiling — deferred until webpack 1 → 5 migration: [email protected], [email protected], and uuid >= 10 (see Pinned direct deps above for the acorn-cliff post-mortem). Both EOL and CVE-flagged, but no patched version exists within their pinned line. Bumping requires the full toolchain migration (webpack 1 → 2 → 5, babel 6 → 7, css-loader / style-loader / stylus-loader majors). Out of scope for incremental work — see the prior investigation summary in the git log around the webpack-dev-server / loader-utils "do nothing" decisions.