Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
40c76d5
fix(update): return an unsubscribe handle for update notifications
Ashminita28 Jun 1, 2026
8c22700
fix(search): add cleanup on unmount to prevent state updates
Ashminita28 Jun 1, 2026
6b0dc4d
fix(export): make sanitizeCss fixed point to prevent reconstitution o…
Ashminita28 Jun 1, 2026
64fefc6
fix(export): add inline images before DOCX conversion
Ashminita28 Jun 1, 2026
11d7cbf
fix(heading): fix marked heading to render inline markdown and keep T…
Ashminita28 Jun 1, 2026
6dc7123
Merge branch 'dev' into fix/system-hardening
Ashminita28 Jun 1, 2026
391137b
fix: ipc handler clean up & package override issue
Ashminita28 Jun 1, 2026
6034207
fix(settings): add window api mock and fix settings test
Ashminita28 Jun 1, 2026
f7e84b8
chore: update pnpm lock file
Ashminita28 Jun 1, 2026
c53bf0f
fix: add ipc gaurd,and return promise
Ashminita28 Jun 1, 2026
60b5a47
fix: remove husky run in release
Ashminita28 Jun 1, 2026
1d6711a
fix(shortcuts): fix key bord shortcut
Ashminita28 Jun 1, 2026
790f60b
fix(security): allow watching explicitly opened markdown file
Ashminita28 Jun 1, 2026
7d8315e
feat(tab-bar): add plus button to open multiple file from tab bar
Ashminita28 Jun 2, 2026
51024b2
fix(watcher): add timer in watcher ready and error
Ashminita28 Jun 2, 2026
d9cbfe7
feat(renderer): add aria labels to components
Ashminita28 Jun 2, 2026
f915df4
test(tab-bar): fix tab bar component test
Ashminita28 Jun 2, 2026
0798859
feat(renderer): add error boundary for app crash fallback
Ashminita28 Jun 3, 2026
a70fb17
fix(renderer): add missing id and fix broken tailwind class
Ashminita28 Jun 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions .github/workflows/production.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,6 @@ jobs:

- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 10

- name: Install dependencies
run: pnpm install --frozen-lockfile
Expand Down Expand Up @@ -51,8 +49,6 @@ jobs:

- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 10

- name: Setup Node
uses: actions/setup-node@v4
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ on:

permissions:
contents: write

env:
HUSKY: 0

jobs:
# add change set
Expand Down
4 changes: 3 additions & 1 deletion apps/main-processor/src/export/exportDocx.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import HTMLtoDOCX from 'html-to-docx';
import { writeFile } from 'node:fs/promises';
import { buildDocument } from './buildDocument';
import { inlineImages } from './inlineImage';
import { sanitizeCss } from './sanitizeCss';

export async function exportDOCX(bodyHtml: string, css: string, outputPath: string): Promise<void> {
const html = buildDocument(bodyHtml, sanitizeCss(css));
const htmlWithInlineImages = await inlineImages(bodyHtml);
const html = buildDocument(htmlWithInlineImages, sanitizeCss(css));
Comment thread
Ashminita28 marked this conversation as resolved.

const result = await HTMLtoDOCX(html, null, {
table: { row: { cantSplit: true } },
Expand Down
2 changes: 1 addition & 1 deletion apps/main-processor/src/export/exportPdf.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export async function exportPDF(bodyHtml: string, css: string, outputPath: strin
const pdfWindow = new BrowserWindow({
show: false,
webPreferences: {
sandbox: false,
sandbox: true,
},
});

Expand Down
13 changes: 9 additions & 4 deletions apps/main-processor/src/export/sanitizeCss.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,14 @@ import { EXPORT_CONST } from '../utils/constants/export-constants';

export function sanitizeCss(css: string): string {
if (!css || typeof css !== 'string') return '';
const sanitized = EXPORT_CONST.DANGEROUS_CSS_PATTERNS.reduce(
(safeCss, pattern) => safeCss.replace(pattern, ''),
css
);
let sanitized = css;
let previous: string;
do {
previous = sanitized;
sanitized = EXPORT_CONST.DANGEROUS_CSS_PATTERNS.reduce(
(safeCss, pattern) => safeCss.replace(pattern, ''),
sanitized
);
} while (sanitized !== previous);
return sanitized;
}
31 changes: 23 additions & 8 deletions apps/main-processor/src/file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,26 @@ export async function watchFile(
pollInterval: 50,
},
});
await new Promise<void>((resolve) => {
watcher.on('ready', resolve);
currentWatchers.set(filePath, watcher);
await new Promise<void>((resolve, reject) => {
const timeout = setTimeout(() => {
resolve();
}, 1000);
watcher.once('ready', () => {
clearTimeout(timeout);
resolve();
});

watcher.once('error', (error) => {
clearTimeout(timeout);
const watcherError = error instanceof Error ? error : new Error(String(error));

void unWatchFile(filePath)
.catch(() => {})
.finally(() => onError?.(watcherError));

reject(watcherError);
});
});
watcher.on('change', () => {
const existingTimer = debounceTimers.get(filePath);
Expand All @@ -54,13 +72,10 @@ export async function watchFile(
debounceTimers.set(filePath, timer);
});
watcher.on('unlink', () => {
void unWatchFile(filePath).then(() => onDeleted?.());
});
watcher.on('error', (error) => {
const watcherError = error instanceof Error ? error : new Error(String(error));
void unWatchFile(filePath).then(() => onError?.(watcherError));
void unWatchFile(filePath)
.catch(() => {})
.finally(() => onDeleted?.());
});
currentWatchers.set(filePath, watcher);
}

//unwatch file
Expand Down
14 changes: 10 additions & 4 deletions apps/main-processor/src/ipc.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import { app, ipcMain, dialog } from 'electron';
import { readFile, unWatchFile, watchFile } from './file';
import { getFolder } from './folder';
import { validatePath, validateSender, allowedFolderRoots } from './utils/constants/ipc-validation';
import {
validatePath,
validateSender,
allowedFolderRoots,
allowedMarkdownFiles,
} from './utils/constants/ipc-validation';
import { IPC_CONSTANTS } from '@package/shared-constants';
import { getRecentFiles } from './recent/getRecentFile';
import { addRecentFile } from './recent/addRecentFile';
Expand All @@ -26,6 +31,7 @@ export function registerIPCHandlers(): void {
throw new Error('Untrusted sender');
}
const safeFilePath = await resolveMarkdownFilePath(filePath);
allowedMarkdownFiles.add(safeFilePath);
return await readFile(safeFilePath);
});

Expand All @@ -47,7 +53,9 @@ export function registerIPCHandlers(): void {
}
const selected = result.filePaths[0];
if (!selected) return null;
return await resolveMarkdownFilePath(selected);
const safeFilePath = await resolveMarkdownFilePath(selected);
allowedMarkdownFiles.add(safeFilePath);
return safeFilePath;
});

// watches a file
Expand Down Expand Up @@ -199,9 +207,7 @@ export function registerIPCHandlers(): void {
if (!validateSender(event)) {
throw new Error('Untrusted sender');
}

const safeFolderPath = await resolveDirectoryPath(folderPath);
allowedFolderRoots.add(safeFolderPath);
const isAllowed = Array.from(allowedFolderRoots).some(
(root) => safeFolderPath === root || safeFolderPath.startsWith(`${root}/`)
);
Expand Down
19 changes: 16 additions & 3 deletions apps/main-processor/src/utils/constants/ipc-validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,27 @@ import path from 'path';
import { IpcMainInvokeEvent } from 'electron';

// production and dev urls
const allowedOrigin = ['file://', 'http://localhost'];
export const ALLOWED_MARKDOWN_EXTENSIONS = new Set(['.md', '.markdown']);
export const allowedFolderRoots = new Set<string>();
export const allowedMarkdownFiles = new Set<string>();

//validate the sender
export function validateSender(event: IpcMainInvokeEvent): boolean {
const url = event.senderFrame?.url || '';
return allowedOrigin.some((origin) => url.startsWith(origin));
const url = event.senderFrame?.url;

if (!url) return false;

try {
const parsedUrl = new URL(url);

if (parsedUrl.protocol === 'file:') {
return true;
}

return parsedUrl.protocol === 'http:' && parsedUrl.hostname === 'localhost';
} catch {
return false;
}
}

//validate path type
Expand Down
16 changes: 9 additions & 7 deletions apps/main-processor/src/utils/helper/ipc-path-resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
isPathInside,
ALLOWED_MARKDOWN_EXTENSIONS,
allowedFolderRoots,
allowedMarkdownFiles,
} from '../constants/ipc-validation';

/* Validates, resolves symlink and ensures a path is a valid Markdown file inside an optional root directory */
Expand Down Expand Up @@ -54,16 +55,17 @@ export async function resolveDirectoryPath(folderPath: string): Promise<string>

/*Resolves a Markdown file path against a dynamic set of allowed root folders throwing an error if it matches none*/
export async function resolveWatchedMarkdownPath(filePath: string): Promise<string> {
let lastError: unknown;
const safeFilePath = await resolveMarkdownFilePath(filePath);

if (allowedMarkdownFiles.has(safeFilePath)) {
return safeFilePath;
}
for (const allowedRoot of allowedFolderRoots) {
try {
return await resolveMarkdownFilePath(filePath, allowedRoot);
} catch (error) {
lastError = error;
} catch {
continue;
}
}
if (allowedFolderRoots.size > 0) {
throw lastError instanceof Error ? lastError : new Error('Path escapes allowed directory');
}
return await resolveMarkdownFilePath(filePath);
throw new Error('Path escapes allowed directory');
}
8 changes: 5 additions & 3 deletions apps/preload/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,11 @@ const apiContract: MarkdownReaderAPI = {
getPathForFile: (file: File) => webUtils.getPathForFile(file),

onUpdateAvailable: (callback: (version: string) => void) => {
ipcRenderer.on(IPC_CONSTANTS.UPDATE_AVAILABLE, (_event, version: string) => {
callback(version);
});
const handler = (_event: IpcRendererEvent, version: string) => callback(version);
ipcRenderer.on(IPC_CONSTANTS.UPDATE_AVAILABLE, handler);
return () => {
ipcRenderer.removeListener(IPC_CONSTANTS.UPDATE_AVAILABLE, handler);
};
},
downloadUpdate: () => ipcRenderer.send(IPC_CONSTANTS.DOWNLOAD_UPDATE),
};
Expand Down
4 changes: 2 additions & 2 deletions apps/renderer/__tests__/components/TabBar.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ describe('TabBar', () => {

it('marks the active tab', () => {
render(<TabBar tabs={tabs} activeTabId="tab-1" onSwitch={() => {}} onClose={() => {}} />);
expect(screen.getByRole('tab', { name: /README/i })).toHaveAttribute('aria-current', 'true');
expect(screen.getByRole('tab', { name: /README/i })).toHaveAttribute('aria-selected', 'true');
});

it('switches tab when a tab is clicked', () => {
Expand All @@ -49,7 +49,7 @@ describe('TabBar', () => {

render(<TabBar tabs={tabs} activeTabId="tab-1" onSwitch={() => {}} onClose={onClose} />);

fireEvent.click(screen.getAllByRole('button', { name: /close tab/i })[0]);
fireEvent.click(screen.getAllByRole('button', { name: /close .* tab/i })[0]);

expect(onClose).toHaveBeenCalledWith('tab-1');
});
Expand Down
33 changes: 22 additions & 11 deletions apps/renderer/__tests__/hooks/useSettings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,34 +12,45 @@ describe('useSettings font size', () => {
expect(result.current.fontSize).toBe(16);
});

it('should increase font size by adding 2px', () => {
it('should increase font size by adding 2px', async () => {
const { result } = renderHook(() => useSettings());
act(() => result.current.increaseFontSize());
await act(async () => {
result.current.increaseFontSize();
Comment thread
Ashminita28 marked this conversation as resolved.
});
expect(result.current.fontSize).toBe(18);
});

it('should decrease font size by subracting 2px', () => {
it('should decrease font size by subracting 2px', async () => {
const { result } = renderHook(() => useSettings());
act(() => result.current.decreaseFontSize());
await act(async () => {
result.current.decreaseFontSize();
});
expect(result.current.fontSize).toBe(14);
});

it('should not exceed 24px', () => {
it('should not exceed 24px', async () => {
const { result } = renderHook(() => useSettings());
act(() => {
for (let i = 0; i < 10; i++) {

for (let i = 0; i < 10; i++) {
await act(async () => {
result.current.increaseFontSize();
}
});
});
}

expect(result.current.fontSize).toBe(24);
});

it('should return recent font size as 16', () => {
it('should return recent font size as 16', async () => {
const { result } = renderHook(() => useSettings());
act(() => {

await act(async () => {
result.current.increaseFontSize();
});

await act(async () => {
result.current.resetFontSize();
});

expect(result.current.fontSize).toBe(16);
});
Comment thread
Ashminita28 marked this conversation as resolved.
});
18 changes: 18 additions & 0 deletions apps/renderer/setup.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import '@testing-library/jest-dom';
import { vi } from 'vitest';

// mock svg measurments for jsdom environment for mermaid testing
if (typeof window !== 'undefined' && window.SVGElement) {
Expand All @@ -20,3 +21,20 @@ if (typeof window !== 'undefined' && window.SVGElement) {
proto.getComputedTextLength = () => 100;
}
}

let mockSettings = {
fontSize: 16,
readingWidth: 'default',
customCss: '',
};

Object.defineProperty(window, 'api', {
value: {
getSettings: vi.fn(async () => mockSettings),
saveSettings: vi.fn(async (partial) => {
mockSettings = { ...mockSettings, ...partial };
return mockSettings;
}),
},
writable: true,
});
8 changes: 6 additions & 2 deletions apps/renderer/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React,{ useEffect, useState, useRef } from 'react';
import { useEffect, useState, useRef } from 'react';
import { useFile } from './hooks/useFile';
import { Welcome } from './components/Welcome';
import { Reader } from './components/Reader';
Expand Down Expand Up @@ -31,6 +31,7 @@ import { useFilePersistence } from './hooks/useFilePersistence';
import { ReaderToolbar } from './components/ReaderToolbar';
import { useFolderSearch } from './hooks/useFolderSearch';
import { SettingsPanel } from './components/SettingsPanel';
import { ErrorBoundary } from './components/ErrorBoundary';

export default function App() {
const { error, isLoading, openFile, toc,recentFiles,loadFile } =useFile();
Expand Down Expand Up @@ -162,6 +163,7 @@ useShortcuts({
activeTabId={state.activeTabId}
onSwitch={(id) => dispatch({ type: 'SWITCH_TAB', payload: { tabId: id } })}
onClose={(id) => dispatch({ type: 'CLOSE_TAB', payload: { tabId: id } })}
plusOpen={openFileDialog}
/>
)}
<UpdateBanner/>
Expand Down Expand Up @@ -211,7 +213,9 @@ useShortcuts({
className="flex-1 overflow-y-auto"
onScroll={scroll}
>
<Reader html={activeTab.html} getHiglightedHtml={getHiglightedHtml} />
<ErrorBoundary>
<Reader html={activeTab.html} getHiglightedHtml={getHiglightedHtml} />
</ErrorBoundary>
</main>
</div>
)}
Expand Down
2 changes: 1 addition & 1 deletion apps/renderer/src/components/DragDrop.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ when a file is dragged and dropped on the application */

export function DragDrop() {
return (
<div className='pointer-events-none fixed inset-0 z-50 flex items-center justify-center border-2 border-dashed border-accent bg-bg/80ntext-text-base'>
<div role="status" aria-live="polite" className='pointer-events-none fixed inset-0 z-50 flex items-center justify-center border-2 border-dashed border-accent bg-bg/80 text-text-base'>
<div className='rounded-xl border border-border-theme bg-surface px-6 py-4 text-sm font-medium shadow-lg'>
Drop Markdown file to open
</div>
Expand Down
3 changes: 2 additions & 1 deletion apps/renderer/src/components/Error.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ import { ErrorProps } from "../types/component-types"
//error message display function
export function Error({message,onRetry}:ErrorProps){
return (
<div className="m-6 p-4 rounded-lg bg-error-bg text-error border border-error-border">
<div role="alert" className="m-6 p-4 rounded-lg bg-error-bg text-error border border-error-border">
<p className="mb-3 font-medium text-sm">Error: {message}</p>
<button
type="button"
onClick={onRetry}
className="px-3 py-1.5 text-sm rounded bg-error-border text-white hover:opacity-90 transition-opacity"
>
Expand Down
Loading
Loading