Follow-up to the push-sweep review: the previous UNSAFE_REGEX used
JS bitwise OR (|) between regex literals, accidentally OR-ing the
RegExp objects instead of combining alternatives into one pattern.
Replace with a single RegExp that catches:
- nested quantifiers (a+)+, [a-z]*+
- class + quantifier
- dot-quantifier followed by dot-quantifier
- lookahead / lookbehind (?=, ?!)
- backrefs \1..\9
- alternation + quantifier
Also add MAX_REGEX_LENGTH=200 hard cap. The denylist is still
defense-in-depth — the length, files-traversed, and per-file-byte caps
are the primary defense.
Address findings from automated security review:
1. Validate rootPath via validatePath/isPathAccessible before recursing,
matching the pattern used by read-file/write-file.
2. Cap query length at 1024 chars to bound regex compilation cost.
3. Reject regexes matching classic ReDoS shapes (nested quantifiers
and similar) before invoking RegExp.
4. Use realpathSync to resolve symlinks and verify containment under
rootPath; drop symlinks that point outside the search root.
5. Cap total files traversed at 10000, in addition to existing
1000-result and 2MB-per-file caps.
Add regression tests for each guard.
Many IPC handlers were returning useful data but the wrappers typed
'void', silently dropping the response. Re-type all affected wrappers:
- file.write: returns { path } (resolved after security validation)
- file.delete/ensureDir/exists/isDirectory/copy/move: full wrappers
(previously only on window.electronAPI.file.*)
- crash.read: typed as Array<{...}> so callers can trust the shape
- crash.delete: returns boolean
- gitStage: returns { files, error? } so callers can detect failure
- gitCommit: returns { summary } | { error }
- updater.check/getState: returns { state }
CrashReportModal crashed on dumps.map because ipc.crash.read returns
{ ok, data } and the consumer was assigning the wrapper to state. Now
unwraps .data and falls back to [] on error.
ipc.file.search was miswired to ipc.file.pickFile — clicking 'Search'
popped a file picker instead of searching. Add real 'search-in-files'
handler in src/main/files/search-in-files.js with regex support,
case-sensitivity toggle, .git/node_modules/dist skip, 2MB file cap, and
1000-result limit. Wire preload bridge + TS declaration + allowlist.
Also:
- Drop dead ipc.file.open wrapper that was miswired to pickFile
- Rename FileEntry field 'modified' to 'modifiedAt' (number) so the
IPC payload matches the declared type
- Update CrashReportModal tests to use the safeCall envelope shape
The list-directory handler returned { path, entries } while the
renderer typed result.data as FileEntry[]; the file-store had to fall
back to raw.entries via Array.isArray. Extract the entries builder into
src/main/files/list-directory.js so it can be unit-tested, return a
plain array, and skip entries that can't be stat'd (broken symlinks,
permission errors) instead of throwing.
Also tighten jest config so dist/*.snap electron-builder artifacts are
not matched as test suites.
GitOperations.getStatus returns { files: [...] } but the renderer's
ipc.file.gitStatus type declares Array<...> and calls result.data.map().
The mismatch made GitStatusPanel.tsx throw 'n.map is not a function'
on mount, which blanked the entire React tree.
Unwrap result.files in the IPC handler so it returns the array the
renderer expects. Add tests covering the success, empty, and non-git
branches so this regression cannot recur.
Refs: blank-screen-on-md-open
- Drop unused imports/vars (withEpubEmbedFontArgs, monoPdfHeaderDir, _e)
- Append CHANGELOG entry summarising the parity work and the
Markdown Converter React rename
- useMonospaceClasses hook calls electronAPI.monospace.getSettings()
and toggles body.mono-{jetbrains-mono,fira-code} and
body.mono-ligatures-{on,off} classes
- Add --font-mono-active and --font-mono-feature tokens in globals.css
- Apply to code/pre/kbd/samp so all code rendering picks up the active
monospace font + ligature settings
- Copy TTFs and LICENSE files from master v4.5.0
- Add assets/fonts/** to electron-builder files + asarUnpack
- Extend download-tools.js with downloadFiraCode() parallel to downloadPandoc()
Port master's v4.5.0 monospace font embedding feature (JetBrains Mono +
Fira Code TTF assets, embedded into PDF/DOCX/EPUB/HTML exports) and
rename branch identity to Markdown Converter React so dev build coexists
with the installed markdown-converter deb without appId/productName
collisions.
Amit Haridas
- batch.showConverter: open BatchMediaConverterDialog for image/audio/video
types (uses existing universal-convert-batch handler with ImageMagick/FFmpeg)
- tools.documentCompare: line-by-line diff of two open tabs with confirmation
dialog, shows diff count in toast and full diff in console
- No more 'coming soon' toasts for any menu item
simple-git returns objects with Map/Set fields that cannot be cloned
over Electron IPC. Flatten all git operation results into plain
JSON-serializable objects to fix 'An object could not be cloned' error.
- App.tsx: call electronAPI.file.rendererReady() on mount so main process
opens pendingFile (files passed via CLI or file associations)
- FindReplaceBar: move updateMatchCount declaration before executeCommand
to fix 'Cannot access d before initialization' crash in minified build
(executeCommand referenced updateMatchCount via closure before its
const declaration in the same scope)
The main process list-directory handler returns {path, entries} but
the file store was calling .map() directly on result.data. Since
arrays have a .entries iterator method, the nullish coalescing
fallback didn't work. Now properly extracts the entries array with
Array.isArray check.
Two related bugs surfaced during end-to-end verification:
1. The 'isAlreadyV5' short-circuit in both the main-side and renderer-side
v4-to-v5 transforms returned the persisted object as-is when a
migration.version=5 marker was present. A previously-shipped v4 build
had written theme: 'ayu-light' (and similar) under the v5 marker;
the transform trusted the marker and passed the invalid theme through,
which then failed the renderer's zod schema on every launch and
reset user settings to defaults.
Fix: in both transforms, when isAlreadyV5 matches, normalize theme
against the v5 enum (light/dark/system) and validate the full result
with the v5 schema before returning. Out-of-range values are replaced
with the v5 default ('system').
2. The renderer's settings-store onRehydrateStorage callback called
useSettingsStore.setState() to reset the store on validation failure.
At that moment the store is still being constructed, and setState
could hit a TDZ ReferenceError (the one we already wrapped in a
try/catch in v5.0.0, which only hid the symptom).
Fix: return the normalized state object from onRehydrateStorage
instead. Zustand's persist middleware applies the returned value
*after* construction completes, so there is no TDZ.
Tests: 334 vitest + 208 jest = 542 passing.
E2E: 12/12 verify-features.mjs steps green; no console errors.
Amit Haridas
1. ThemeSettings: change 'auto' radio value to 'system' to match
the v5 settingsSchema enum. Storing 'auto' silently wipes all
settings on next launch.
2. UpdateBanner: render an 'available' branch so users see a CTA
when a new version is detected (the store already had this state,
but the banner was silent). Added a test.
3. feed-config: remove the unused resolveFeedUrl / FEEDS exports.
feedConfigFor is the actual implementation used by the IPC
handler. Updated tests to match.
4. main/index.js: tighten app:open-external regex to https:// only.
- verify-features.mjs: dismiss FirstRunWizard if visible before clicking
toolbar (wizard overlay blocked previous toolbar clicks); write a
firstRun=true settings.json to test wizard visibility; emit
'updater:status' from main via Playwright IPC.
- package.json: dev:electron now passes --no-sandbox to match the
.claude/skills/run-desktop driver scripts.