Skip to content

feat(cloud): ✨ Add cloud API integration with authentication and credits#48

Merged
jorben merged 49 commits intomasterfrom
feature/cloud-api-integration
Mar 1, 2026
Merged

feat(cloud): ✨ Add cloud API integration with authentication and credits#48
jorben merged 49 commits intomasterfrom
feature/cloud-api-integration

Conversation

@jorben
Copy link
Collaborator

@jorben jorben commented Mar 1, 2026

Summary

  • Implement full cloud API integration with device flow OAuth authentication
  • Add cloud task management (create, list, delete, detail with page images)
  • Integrate credits system with real backend (free/paid balance, daily limits, history)
  • Add SSE real-time updates for cloud task status with reconnection handling
  • Support Office file uploads and multi-tier cloud model selection
  • Add internationalization for all cloud features across 6 locales

Test Plan

  • Verify device flow login/logout works correctly
  • Test cloud task creation with PDF and Office files
  • Verify credit balance display and history pagination
  • Test SSE reconnection and event deduplication
  • Confirm recharge button is disabled with "Coming soon" tooltip
  • Run full test suite: npm test

🤖 Generated with Claude Code

jorben and others added 30 commits January 29, 2026 16:25
Add CLAUDE.md with project guidance for Claude Code including:
- Common development commands
- Architecture overview (three-process Electron, Clean Architecture)
- Key conventions (ESM modules, path aliases, IPC format)
- Test configuration details
- Links to detailed documentation

Co-Authored-By: Claude (anthropic/claude-opus-4.5) <noreply@anthropic.com>
Integrate MarkPDFDown Cloud API for cloud-based PDF conversion, including
Clerk authentication system for user management. This feature allows users
to convert documents using cloud credits alongside local LLM providers.

New features:
- CloudService for API communication with token-based auth
- CloudContext for managing user state, credits, and cloud operations
- AccountCenter component for account management and credit display
- Cloud model option in UploadPanel with authentication check
- Cloud/local task differentiation in task list view
- IPC handlers for cloud:setToken, cloud:convert, cloud:getTasks

Changes:
- Add @clerk/clerk-react dependency for authentication
- Add Account tab as default in Settings page
- Replace GitHub icon with user profile avatar in sidebar
- Update tests to mock CloudContext

BREAKING CHANGE: Settings page now defaults to Account tab instead of Model Service

Co-Authored-By: Claude (anthropic/claude-opus-4.5) <noreply@anthropic.com>
Add i18n support for cloud-related UI components across all 6 languages:
- AccountCenter: translate account management, credit balance, and history
- UploadPanel: translate cloud provider and model names, status messages
- Layout: translate user profile tooltip
- List: translate cloud/local task type labels
- Settings: translate account tab label

Add new account.json namespace with translations for:
- Account center title and sign in/out buttons
- Credit balance section (monthly free, paid credits)
- Credit history table columns and type labels

Co-Authored-By: Claude (anthropic/claude-opus-4.5) <noreply@anthropic.com>
- Change paid credits description to "$1 USD = 1,500 credits"
- Change free credits description to monthly quota with UTC+0 reset time
- Rename "Monthly Free Credits" to "Free Credits" across all locales
- Remove unused reset_hint and never_expire fields
- Align description layout between free and paid credit cards

Co-Authored-By: Claude (anthropic/claude-opus-4.5) <noreply@anthropic.com>
Remove HomeOutlined icon for local tasks, keeping only cloud icon indicator.

Co-Authored-By: Claude (anthropic/claude-opus-4.5) <noreply@anthropic.com>
- Remove unused imports in CloudService (net, fs, path, FormData)
- Add missing getCreditHistory method to CloudService
- Remove unused imports in Layout (Space, GithubOutlined)
- Remove unused openExternalLink function in Layout
- Add cloud API types to WindowAPI and ElectronAPI interfaces
- Add window, platform, and app types to electron.d.ts
- Add hasRunningTasks type to task API interface
- Create CloudFileInput interface for flexible file type handling
- Fix UploadPanel to properly convert UploadFile to CloudFileInput

Co-Authored-By: Claude (anthropic/claude-opus-4.5) <noreply@anthropic.com>
Implement deep linking support for OAuth authentication flow:
- Register markpdfdown:// custom protocol for all platforms
- Handle OAuth callback URLs in main process
- Add IPC bridge to forward auth events to renderer
- Replace Clerk modal with inline SignIn component
- Configure ClerkProvider with allowedRedirectProtocols
- Add onOAuthCallback mock to test setup

Co-Authored-By: Claude (anthropic/claude-opus-4.5) <noreply@anthropic.com>
# Conflicts:
#	CLAUDE.md
#	package.json
#	src/main/ipc/handlers/index.ts
#	src/preload/electron.d.ts
#	src/preload/index.ts
#	src/renderer/electron.d.ts
Remove @clerk/clerk-react dependency and implement a custom device flow
(RFC 8628) authentication system via AuthManager. The new flow:
- Main process manages tokens (access + refresh) with encrypted storage
- Renderer receives auth state changes via IPC event bridge
- AccountCenter UI shows user code for browser-based authorization
- CloudService retrieves tokens from AuthManager instead of receiving
  them from the renderer

Also updates IPC channels, preload bridge, type definitions, i18n
strings, and test mocks to align with the new auth architecture.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The fetchUserProfile method was assigning the raw API response
directly to userProfile instead of extracting the nested data
field. Since the API returns { success, data: CloudUserProfile },
the user's name, email, and avatar_url were all undefined in the
renderer, causing avatar and username not to display after login.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
在 AuthManager 中添加以下改进:

- 添加 buildUserAgent() 函数,为所有 API 请求设置自定义 User-Agent 头
- 添加 fetchWithAuth() 方法,自动携带认证令牌并在 401 时自动刷新 token
- 所有 API 请求现在都包含 User-Agent 和正确的认证头
- 改进登出流程,使用 fetchWithAuth 处理认证失败情况

此变更提高了 API 调用的可追踪性和认证流程的健壮性。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace mock credit data with actual API calls:
- Add getCredits() endpoint to fetch current balance
- Add type filter support for credit history
- Update UI to show monthly and daily credit balances
- Add 7 credit transaction types (consume, topup, refund, etc.)
- Add TypeScript types for CreditsApiResponse

🤖 Generated with [Claude Code](https://claude.com/claude-code)
Add three cloud model tiers with different credit pricing:
- Fit Lite: ~10 credits/page
- Fit Pro: ~20 credits/page
- Fit Ultra: ~60 credits/page

Changes:
- Update model selector to display 3 options with i18n support
- Pass selected model tier through IPC to cloud conversion
- Add translations for all 6 supported languages

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add i18n text explaining credit consumption rates for different
cloud models (Lite/Pro/Ultra) displayed next to Credit Balance title.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace mock implementation with real API call to create cloud conversion tasks:
- Add CreateTaskResponse and CloudModelTier types
- Use FormData for multipart/form-data file upload
- Support both file path and content as input
- Handle API errors properly with structured response
- Default to 'lite' model tier if not specified

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace mock data with real cloud API calls for complete task
lifecycle management. This covers all 10 API endpoints from the
client integration guide:

Backend integration:
- Replace mock getTasks with GET /api/v1/tasks
- Add getTaskById, getTaskPages, cancelTask, retryTask,
  retryPage, getTaskResult, downloadPdf methods to CloudService
- Create CloudSSEManager for real-time task event streaming
  with auto-reconnect, exponential backoff, and heartbeat

IPC & Preload:
- Add 13 CLOUD IPC channels and CLOUD_TASK_EVENT event
- Register 9 new IPC handlers in cloud.handler.ts
- Expose all new methods and onCloudTaskEvent in preload bridge

Renderer:
- Extend CloudContext with 7 new actions and SSE lifecycle
- Create cloudTaskMapper utility for API response mapping
- Update List page with cloud task actions and SSE live updates
- Create CloudPreview page for viewing cloud task results
- Add cloud-preview i18n translations for all 6 locales

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add page image support to CloudPreview component by:

- Add image_url field to CloudTaskPageResponse type
- Add getPageImage IPC channel and handler for proxying image requests
- Implement dual URL handling:
  - Presigned URLs (https://) - use directly in <img> tag
  - Relative API paths - proxy through main process with auth token
- Update CloudPreview left panel to display page images
- Handle image loading states and errors gracefully

This enables users to view the original PDF page images alongside
their markdown conversion results in cloud task details.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add checkDeviceTokenStatus() method to immediately verify token status
when receiving protocol URL callback, instead of waiting for next polling
interval. This reduces token acquisition latency from ~5s to milliseconds.

The implementation:
- Calls /api/v1/auth/device/token immediately on callback
- Stops polling after successful token acquisition
- Handles 428 (authorization_pending) gracefully by continuing polling
- Thread-safe: concurrent requests don't cause issues

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add model_tier field to CloudTaskResponse type and use it for
displaying the correct model tier (lite/pro/ultra) in the task list.

Previously the code incorrectly used status_name for model tier mapping.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Change task list model name from "Cloud Lite/Pro/Ultra" to
"Fit Lite/Pro/Ultra" to match the upload panel selector.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…rolling

- Add height: 100vh and minHeight: 0 to Layout content area so flex
  children get a constrained height
- Make Settings Tabs a flex column filling its container, with only
  the tab content holder scrolling (tab bar stays fixed)
- Add scrollbar styles for settings-tabs consistent with existing
  model-service-tabs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The cloud task list received each SSE event 3 times because multiple
SSE connections were being established concurrently:

1. Main process auto-connect in initializeBackgroundServices
2. Renderer CloudContext useEffect calling sseConnect on auth
3. React re-renders causing disconnect+reconnect race conditions

Key changes:
- Remove main process SSE auto-connect; let renderer CloudContext
  manage SSE lifecycle exclusively via IPC to avoid dual entry points
- Set connected flag synchronously before any await in connect() to
  prevent concurrent calls from passing the guard
- Abort any lingering stream in startStream() before creating new one
- Use authManager.fetchWithAuth for automatic token refresh on 401
- Filter connected/heartbeat control events from renderer forwarding
- Fix page_completed counting to use page number (idempotent) instead
  of naive increment that double-counts on SSE reconnect replay
- Fix pdf_ready status mapping from SPLITTING(2) to PROCESSING(3)
- Move fetchTasks out of setState callback using queueMicrotask
- Add connected event type to CloudSSEEventType for type safety
- Reset lastEventId on disconnect to prevent cross-session replays
- Add diagnostic logging throughout SSE pipeline

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…odel

- Replace consume_settle/page_retry with pre_auth, settle, pre_auth_release
- Add frozen fields to CreditsApiResponse for bonus and paid credits
- Fix description column to show API description instead of file_name
- Reorder credits column before description in history table
- Style pre_auth/pre_auth_release amounts as secondary (grey)
- Update transaction type translations for all 6 locales

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…vent-ID

- disconnect() no longer resets lastEventId, preserving resumption point
- Add resetAndDisconnect() for explicit logout (clears lastEventId)
- Fix duplicate reconnect by clearing pending reconnectTimer
- Skip redundant reconnect when stream is aborted (not naturally ended)
- Log Last-Event-ID in reconnect/connect for debugging

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add AuthTokenInvalidError for definitive auth failures (401/403)
- Only clear refresh token on permanent failures, keep it for transient errors
- Add retry logic for auto-refresh with exponential backoff
- Schedule init retry on transient failure during session restore
- Fix getAccessToken to attempt refresh when access token is missing

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
jorben and others added 14 commits February 28, 2026 11:58
Active tasks: 10s → 60s, idle: 30s → 120s. Real-time updates
are handled by SSE, polling serves only as a fallback.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Remove Download PDF entry from More Actions menu
- Add retry failed pages action (status=8 with failed pages)
- Add delete action for terminal-state tasks
- Use Dropdown.Button pattern matching local Preview
- Add i18n keys for all 6 locales

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… list

- Fetch up to 100 items from each source, then paginate locally
- Add unified sortTimestamp field to CloudTask for cross-source sorting
- Sort combined list by timestamp (newest first) before pagination
- Fix pagination total to include both local and cloud task counts
Add timeout support to AuthManager.fetchWithAuth() using AbortController.
Both initial requests and 401 retry requests now have 8-second timeout.
SSE heartbeat timeout (90s) remains unchanged.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Append provider name 'Markdown.Fit' to cloud task model_name field
for clearer identification of cloud tasks in the task list.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add support for selecting and uploading Office documents (doc, docx, xls, xlsx, ppt, pptx) when using cloud conversion with authenticated users.

- Add allowOffice parameter to file select dialog
- Add file type detection (pdf, image, office, unsupported)
- Show appropriate hints when Office files are not supported
- Add validation to prevent unsupported file types

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- FormData uploads: 120s timeout
- Download requests (/result, /download): no timeout
- Other API requests: 8s timeout

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Reduce max diff size from 128KB to 64KB to avoid exceeding LLM context limits
- Replace silent curl failure with explicit HTTP status code checking and error output
- Add timeout (120s for LLM API, 30s for GitHub API) to prevent hanging requests

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace printf|head pipe with bash substring expansion to avoid
SIGPIPE under set -o pipefail when diff exceeds max size.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Use --rawfile to pass system prompt and diff content via temp files
instead of --arg CLI arguments, avoiding ARG_MAX limit on Linux.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Write request body to temp file and use curl -d @file syntax
to avoid ARG_MAX limit when sending large diffs to LLM API.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

This PR introduces a substantial cloud integration surface (auth/device flow, cloud task APIs, SSE updates, credits, and renderer wiring) and is generally well-structured for a large feature. However, I found a couple of correctness/security issues that should be fixed before merge, plus a few important contract/perf gaps. As-is, I don’t think it’s fully ready to merge yet.

Critical

  • src/core/infrastructure/services/AuthManager.ts:537Refresh token may be persisted in plaintext fallback: persistRefreshToken writes the refresh token unencrypted when safeStorage.isEncryptionAvailable() is false. This is a credential-at-rest leak risk on some platforms/environments.
    Fix: Do not persist the refresh token unless encryption is available (keep session memory-only in that case), or encrypt with an app-managed key from OS keychain/credential vault. At minimum, gate persistence behind a strict security check and log a warning.

  • src/main/index.ts:114Protocol callback URL is not validated beyond scheme prefix: handleProtocolUrl accepts any markpdfdown://... URL and triggers auth token check. While you’re not parsing params now, this still creates a broad attack surface for external URL triggers and future callback parsing bugs.
    Fix: Strict-parse the URL and only accept expected host/path patterns for OAuth callback (e.g., markpdfdown://auth/callback), reject everything else, and avoid logging full callback URLs if tokens/codes may appear in query params in future.

Important

  • src/preload/electron.d.ts:89IPC type contract mismatch for cloud.convert: d.ts omits page_range but preload implementation includes it (src/preload/index.ts:93). This can cause renderer compile-time mismatch and hidden runtime usage drift.
    Fix: Update electron.d.ts to include page_range?: string in cloud.convert signature (and ideally share a single type from shared/types).

  • src/core/infrastructure/services/CloudSSEManager.ts:228Unbounded verbose logging in stream loop: per-chunk logging (Chunk #...) and frequent event logging can heavily degrade main-process performance and bloat logs during long sessions.
    Fix: Gate chunk/event debug logs behind a debug flag or environment check; keep only warnings/errors in production.

  • src/core/infrastructure/services/CloudService.ts:150Path parameters are not URL-encoded: IDs are interpolated directly in multiple endpoints (/tasks/${id}, /tasks/${taskId}/...). If malformed IDs ever pass through IPC, requests can break or hit unintended routes.
    Fix: Wrap all path params with encodeURIComponent(...) before interpolation.

Suggestion

  • src/main/ipc/handlers/cloud.handler.ts:12Missing input validation at IPC boundary: handlers trust renderer-provided payloads (taskId, pageNumber, fileData.path) without shape/range checks.
    Suggestion: Add lightweight runtime validation (e.g., zod/manual guards) in handlers before calling services to reduce malformed input risks and improve error clarity.

Praise

  • src/core/infrastructure/services/AuthManager.ts:83Good transient-vs-definitive auth failure handling: distinguishing AuthTokenInvalidError from network/server errors and scheduling retry is a solid resilience pattern that avoids unnecessary forced logouts.

  • src/core/infrastructure/services/CloudSSEManager.ts:46Strong reconnection lifecycle control: setting connected before await, aborting old streams, heartbeat timeout, and exponential backoff are well thought out and reduce race/reconnect duplication bugs.

  • src/main/ipc/handlers/file.handler.ts:101Nice UX/security balance on file filters: conditional Office support plus backend-side revalidation in upload flow is a good defense-in-depth approach against file-type bypasses.

- Prevent refresh token plaintext persistence when encryption unavailable
- Strict-validate protocol URL paths (only allow auth/callback)
- Add missing page_range field to cloud.convert type definition
- Gate SSE verbose logging behind isDev check for production perf
- URL-encode all path parameters in CloudService API calls

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

This PR adds substantial cloud integration across main/preload/renderer, including OAuth device flow, authenticated cloud task APIs, and SSE task event streaming. Overall, the architecture is solid and most flows are thoughtfully implemented (especially retries/backoff and state broadcasting), but there are a couple of correctness/security issues that should be fixed before merge. I would not merge yet until the critical items below are addressed.

Critical

  • src/main/ipc/handlers/cloud.handler.ts:13Unbounded IPC input can trigger memory exhaustion via content uploads: The cloud:convert handler accepts arbitrary ArrayBuffer from renderer (fileData.content) and forwards it to FormData without size/type validation. A compromised renderer (or XSS in renderer) can send huge payloads and exhaust main-process memory (DoS).
    Fix: Validate IPC payload in main before passing to CloudService: enforce max byte size, require either path or content (not both), validate filename extension/MIME allowlist, and reject oversized/invalid payloads.

  • src/main/index.ts:133Protocol callback path validation can be bypassed for malformed but accepted URLs: Path allowlisting is based on string concat of host + pathname and only trims trailing slashes. This may accept unexpected variants (markpdfdown://auth//callback, encoded slashes, case variants) depending on URL normalization and upstream callback generation. For auth callbacks, strict parsing is security-sensitive.
    Fix: Normalize and validate components explicitly: lowercase host, decode/normalize pathname, reject duplicate slashes, and match exact allowed tuples like (host==='auth' && pathname==='/callback') || (host==='auth' && pathname==='').

Important

  • src/core/infrastructure/services/AuthManager.ts:308Timeout/retry race can duplicate refresh attempts under concurrent requests: fetchWithAuth and getAccessToken can both call refreshAccessToken() concurrently from different callers when token is near expiry or 401 responses happen in parallel. This can cause redundant refresh calls and state races (especially around token rotation).
    Fix: Add a refresh mutex/promise deduplication (this.refreshInFlight) so concurrent callers await one refresh operation.

  • src/core/infrastructure/services/AuthManager.ts:150shell.openExternal not awaited/checked: Browser open failures are ignored; user can be stuck in polling without browser launched.
    Fix: await shell.openExternal(...) and handle false/rejection by setting deviceFlowStatus='error' with actionable message.

  • src/core/infrastructure/services/CloudSSEManager.ts:223SSE parser only splits on \n\n, misses CRLF framing: Many SSE servers delimit with \r\n\r\n. Current parsing can delay or break event handling.
    Fix: Normalize line endings (buffer.replace(/\r\n/g, '\n')) before split, or split on /\r?\n\r?\n/.

  • src/core/infrastructure/services/CloudService.ts:382Filename from Content-Disposition insufficiently sanitized: Download filename is derived from header and directly used in path.join(downloadsPath, fileName). Even with save dialog, odd names can degrade UX and potentially cause path oddities on some platforms.
    Fix: Sanitize filename to basename-safe characters (path.basename, strip control chars/reserved chars), and fallback to safe default.

  • src/preload/electron.d.ts:85any-heavy API contracts weaken IPC type safety: New auth/cloud APIs and events are all Promise<any>/(event:any), increasing risk of contract drift between main/preload/renderer.
    Fix: Reuse shared cloud/auth response/event types in preload typings and expose strict return types.

Praise

  • src/core/infrastructure/services/AuthManager.ts:90Good transient vs permanent auth failure handling: Distinguishing AuthTokenInvalidError from transient network/server errors avoids unnecessary logout and improves resilience.
  • src/core/infrastructure/services/CloudSSEManager.ts:119Clean reconnect strategy: Backoff + heartbeat timeout + timer cleanup is well-structured and reduces duplicate stream risk.
  • src/main/ipc/handlers/file.handler.ts:101Nice UX/security balance on file filters: Conditional Office support plus dialog filter ordering is a practical improvement and integrates cleanly with existing file flow.

- Add 100MB file size limit validation in cloud:convert IPC handler
- Normalize protocol URL path (lowercase, deduplicate slashes, decode)
- Deduplicate concurrent refresh token calls with in-flight promise
- Await shell.openExternal and handle browser launch failures
- Normalize CRLF to LF in SSE stream parser for server compatibility
- Sanitize Content-Disposition filename with path.basename and char filter

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

This PR adds substantial cloud/auth infrastructure (device-flow OAuth, cloud task CRUD, credits APIs, SSE streaming, protocol callback wiring, and renderer/preload IPC exposure). Overall direction is solid and many reliability concerns were considered, but there are a few correctness/security issues that should be fixed before merge. As-is, I don’t think it is ready to merge yet.

Critical

  • src/main/ipc/handlers/cloud.handler.ts:17IPC handler accepts arbitrary renderer-provided file paths without path trust boundary: cloud:convert will read fileData.path directly in main process (fs.statSync then eventually fs.readFileSync in CloudService.convert). Any renderer compromise (or untrusted web content loaded by mistake) can exfiltrate arbitrary local files by passing crafted absolute paths.
    Fix: Restrict path inputs to files selected via trusted dialog.showOpenDialog flow (e.g., mint short-lived file tokens/handles in main and only accept those), or disallow path input entirely and require content passed from preload-controlled selection flow.

  • src/core/infrastructure/services/CloudService.ts:53Synchronous full-file read in main process can block UI/event loop: fs.readFileSync(fileData.path) is used for uploads. For large files (up to 100MB allowed), this can noticeably freeze Electron main process and impact IPC responsiveness.
    Fix: Use async IO (fs.promises.readFile) or stream/multipart upload to avoid blocking main thread. If full buffering is required, do it async and consider worker/off-main process.

Important

  • src/main/index.ts:121Protocol callback path normalization can throw on malformed percent-encoding: decodeURIComponent(parsed.host) / decodeURIComponent(parsed.pathname) can throw for malformed escape sequences, which is caught, but then valid-but-unusual encoded paths may be rejected inconsistently after decode/recombine.
    Fix: Normalize using URL components without manual decode where possible, and compare against canonical expected forms more defensively (or decode each segment safely).

  • src/preload/electron.d.ts:85Weak API contracts (Promise<any>) for new auth/cloud IPC surface: Most newly added APIs are typed as any, which undermines compile-time contract checks across preload/renderer and can hide breaking changes.
    Fix: Replace any with shared response/request types from shared/types/cloud-api.ts (and equivalent auth types), especially for auth:*, cloud:*, and event callbacks.

  • src/core/infrastructure/services/AuthManager.ts:313Download timeout detection via URL substring is brittle: isDownload = url.includes('/result') || url.includes('/download') can disable timeout for unintended endpoints or miss real download endpoints (/pdf currently not included).
    Fix: Make timeout behavior explicit via options flag (fetchWithAuth(url, { ..., meta: { timeoutMs } }) pattern) or centralize endpoint-specific configuration.

Suggestion

  • src/core/infrastructure/services/CloudSSEManager.ts:155No content-type validation for SSE stream: You log content-type but don’t enforce text/event-stream, so HTML/JSON error bodies might be parsed as stream data and generate noisy reconnect loops.
    Suggestion: Validate content-type includes text/event-stream; otherwise treat as non-retriable (or retry with capped attempts + explicit auth check).

Praise

  • src/core/infrastructure/services/AuthManager.ts:498Refresh de-duplication is well implemented: refreshInFlight avoids concurrent refresh races and token stomp issues; this is a strong correctness safeguard.
  • src/core/infrastructure/services/CloudService.ts:404Safe filename sanitization on PDF download: Using path.basename plus reserved/control character replacement is a good defense against path/header-based filename abuse.
  • src/core/infrastructure/services/CloudSSEManager.ts:118Reconnect cleanup logic prevents duplicate reconnect storms: Clearing existing reconnect and heartbeat timers before scheduling reconnect is a solid resilience pattern.

- Use async fs.readFile instead of sync readFileSync for upload (unblock main)
- Use async fs.promises.stat in cloud handler for file size validation
- Safe decodeURIComponent in protocol URL handler (catch malformed encoding)
- Replace brittle URL-substring timeout detection with explicit timeoutMs option
- Add content-type validation for SSE stream (reject non text/event-stream)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

This PR introduces substantial cloud integration across main/preload/renderer, including OAuth device flow auth, cloud task APIs, credits, and SSE updates. Overall the structure is solid and the implementation shows good attention to retry logic and token lifecycle, but there are a few correctness and contract issues that should be fixed before merge. I would not merge yet until the critical items are addressed.

Critical

  • src/core/infrastructure/services/CloudSSEManager.ts:177SSE abort signal is dropped due to fetchWithAuth overwrite: startStream() passes signal: this.abortController.signal, but AuthManager.fetchWithAuth() always creates its own AbortController and overwrites options.signal. This means disconnect()/reconnect() won’t reliably abort the in-flight SSE fetch, causing leaked/duplicate streams and racey reconnect behavior.
    Fix: Update fetchWithAuth to respect a caller-provided options.signal (compose with timeout signal via AbortSignal.any where available, or skip creating timeout controller when options.signal exists).

  • src/main/index.ts:126OAuth callback trigger can be spoofed by any markpdfdown://auth... URL: handleProtocolUrl validates only scheme/path and then immediately calls authManager.checkDeviceTokenStatus() with no state/nonce validation. Any external app/site can force callback handling and authentication polling transitions (CSRF-like trigger on local app state).
    Fix: Require a short-lived random state generated at login initiation and validate it in protocol callback before invoking token check. Ignore callbacks without matching state.

Important

  • src/preload/electron.d.ts:85Unsafe any API contracts for new IPC surface: all newly added auth/cloud methods and event payloads are typed as Promise<any> / any, which breaks shared contract safety and makes main/renderer drift likely.
    Fix: Reuse shared/types/cloud-api.ts types in preload typings and enforce strongly-typed return envelopes ({ success, data, error }) per method.

  • src/main/ipc/handlers/cloud.handler.ts:16Input validation incomplete for untrusted renderer payload: cloud:convert validates size but not shape/content of fileData.name, model, or page_range. Since IPC is an attack surface, malformed values can still hit backend/API unexpectedly.
    Fix: Add strict runtime validation (type/length/pattern), especially for page_range and model allowlist, and reject invalid payloads early.

  • src/core/infrastructure/services/AuthManager.ts:312Timeout abort error not normalized for UX: fetchWithAuth uses abort for timeout but returns raw AbortError, which will surface as generic failure in many callers.
    Fix: Catch AbortError in fetchWithAuth and throw a normalized timeout error message (e.g., Request timeout) so renderer can show actionable feedback.

Suggestion

  • src/core/infrastructure/services/AuthManager.ts:1Use async fs APIs for token persistence in main process: sync fs operations (readFileSync, writeFileSync, etc.) on auth paths can block the main thread.
    Suggestion: Switch to fs/promises equivalents and keep initialization non-blocking.

  • src/renderer/components/UploadPanel.tsx:119Duplicate comment line: “Inject Cloud Models...” comment is duplicated consecutively, which adds noise.
    Suggestion: Remove redundant line.

Praise

  • src/core/infrastructure/services/AuthManager.ts:483Good refresh deduplication: refreshInFlight guard is a strong concurrency control that prevents token refresh stampedes.

  • src/core/infrastructure/services/CloudService.ts:402Safe filename handling for downloads: sanitizing Content-Disposition filename with basename and reserved/control char replacement is a solid defense against path/file-name abuse.

  • src/main/index.ts:113Protocol path allowlist validation: validating callback URL against explicit allowed paths is a good baseline hardening step for custom protocol handling.

- Compose caller signal with timeout signal in fetchWithAuth using AbortSignal.any
- SSE disconnect/reconnect now reliably aborts in-flight fetch requests
- Normalize timeout AbortError to 'Request timeout' for better UX
- Remove duplicate comment line in UploadPanel

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

This PR adds a substantial cloud integration surface (auth/device flow, cloud task APIs, SSE updates, credits, protocol callback wiring, and renderer IPC/context integration). Overall the implementation is solid and thoughtful (notably around token refresh dedup and SSE reconnect handling), but there are a few correctness and security issues that should be fixed before merge. I would not merge yet until the critical items below are addressed.

Critical

  • src/main/ipc/handlers/cloud.handler.ts:19IPC allows arbitrary filesystem reads via untrusted fileData.path: The renderer can pass any absolute path to cloud:convert, and main will stat it then CloudService.convert() reads it and uploads contents. If renderer is compromised (XSS/dependency compromise), this becomes local file exfiltration. Restrict path inputs to a trusted file-selection flow (e.g., issue opaque file tokens from file:selectDialog and only allow those), or at minimum validate against a recent allowlist of user-selected paths and block everything else.

  • src/main/index.ts:126OAuth callback URL validation can be bypassed with encoded slash in host: In handleProtocolUrl, decodeURIComponent(parsed.host) is applied before path checks. A URL like markpdfdown://auth%2Fcallback can decode host to auth/callback and pass ALLOWED_PROTOCOL_PATHS. Host should not be decoded this way for path authorization logic. Validate using structural URL parts directly (parsed.host exact match + normalized parsed.pathname) and reject percent-encoded / in host.

Important

  • src/core/infrastructure/services/AuthManager.ts:357Potential runtime incompatibility with AbortSignal.any: fetchWithAuth uses AbortSignal.any(signals), which may be unavailable depending on Electron/Node runtime. If unsupported, auth/network calls will throw at runtime. Add a compatibility fallback (manual composite AbortController wiring) when AbortSignal.any is undefined.

  • src/core/infrastructure/services/CloudSSEManager.ts:165Connection lock can deadlock on early startStream errors: connect() sets this.connected = true before await this.startStream(). If startStream() throws before scheduling reconnect (e.g., synchronous runtime failure), connected remains true and future connect() calls no-op with “Already connected”. Wrap startStream() in try/catch in connect() and reset connected=false on unrecoverable startup error.

  • src/main/ipc/handlers/cloud.handler.ts:53Missing input bounds validation for pagination parameters: cloud:getTasks, cloud:getTaskPages, and cloud:getCreditHistory forward renderer-provided page/pageSize directly. A malicious renderer can request huge page sizes causing backend and memory pressure. Clamp and validate numeric ranges in IPC layer (e.g., page >= 1, pageSize 1..100).

  • src/core/infrastructure/config.ts:1Unvalidated API_BASE_URL contract: Base URL is taken from environment without validation. A malformed or non-HTTPS value could break auth flow or downgrade security. Validate at startup (must be https: in production, allow http://localhost only in dev), and fail fast with explicit error.

Praise

  • src/core/infrastructure/services/AuthManager.ts:530Refresh deduplication is well implemented: The refreshInFlight guard prevents concurrent refresh stampedes and racey token overwrites, which is critical under high request concurrency.

  • src/core/infrastructure/services/CloudSSEManager.ts:112Reconnect/backoff and heartbeat management are robust: Cleanup of timers/controllers plus exponential backoff and heartbeat timeout handling demonstrates good resilience design for long-lived SSE streams.

  • src/main/index.ts:157Single-instance + protocol callback handling is thoughtfully integrated: Handling open-url (macOS) and second-instance (Windows/Linux) with window focusing improves cross-platform correctness for OAuth callback UX.

- Validate protocol URL host structurally without decodeURIComponent
- Reject percent-encoded characters in host to prevent bypass
- Add AbortSignal.any fallback for older runtimes
- Reset SSE connected flag on startStream failure to prevent deadlock

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

This PR introduces a substantial cloud integration surface (OAuth device flow auth, cloud task APIs, credits, SSE updates, and renderer wiring) and overall follows a solid architecture split. The core service structure is good, but there are a few correctness/security issues around unvalidated renderer input and protocol URL handling that should be fixed before merge. With those addressed, this is close to merge-ready.

Critical

  • src/main/ipc/handlers/cloud.handler.ts:17Unvalidated fileData.path allows arbitrary main-process file reads/uploads: cloud:convert accepts a raw path from the renderer and reads it (fs.promises.stat + later fs.readFile in CloudService) without constraining source. If renderer-side JS is compromised, this can exfiltrate arbitrary local files to cloud.
    Fix: Only allow uploads from:

    1. paths returned by trusted file dialog in this process (track allowlisted selections), or
    2. direct file bytes from renderer (content) while disabling arbitrary path mode for IPC callers.
      Also validate extension/MIME in main before upload, not only in renderer.
  • src/main/ipc/handlers/cloud.handler.ts:203Unvalidated task/page parameters in privileged IPC endpoints: task IDs/page numbers from renderer are passed directly into authenticated backend requests (getTaskById, retryPage, etc.). While encodeURIComponent helps URL safety, this still exposes a broad authenticated proxy surface to any renderer compromise.
    Fix: Add strict schema validation in each IPC handler (e.g., zod/yup/manual): taskId pattern/length, pageNumber positive integer bounds, pagination limits, and allowed enums (type in history). Reject invalid payloads early.

  • src/main/index.ts:116Custom protocol URL validation bypass risk due to host parsing assumptions: validation relies on parsed.host and pathname equality with limited normalization. markpdfdown:///auth/callback and other URL forms can shift expected components and may be mishandled.
    Fix: Validate against explicit accepted URL shapes using both hostname, pathname, and possibly origin, with a strict parser helper + exhaustive allowed patterns. Reject anything not exactly matching documented callback URL(s).

Important

  • src/core/infrastructure/services/CloudService.ts:53Node/Electron runtime compatibility risk for FormData/Blob: service uses web FormData and Blob in main process. Depending on Electron/Node target, this can fail or behave inconsistently.
    Fix: Ensure runtime support is guaranteed by project engine version, or switch to a Node-safe multipart implementation (form-data/undici-native path) and add an integration test in main context.

  • src/core/infrastructure/services/AuthManager.ts:34Potential credential/user data leakage in logs: multiple logs print auth flow internals and raw error bodies (Token check error: ... body, session restoration errors). Backend error bodies can contain sensitive context.
    Fix: Sanitize/redact logs (status codes + stable error codes only), and avoid printing response bodies from auth/token endpoints in production.

  • src/preload/electron.d.ts:85Type contract too weak (Promise<any>) for new high-impact IPC API: new auth/cloud APIs are all any, which removes compile-time guarantees between renderer/main and increases runtime mismatch risk.
    Fix: Replace with shared response/request types from shared/types/cloud-api.ts and consistent IPC result envelopes.

Suggestion

  • src/core/infrastructure/services/CloudSSEManager.ts:236SSE buffer growth guard: parsing loop accumulates buffer until delimiter appears. Malformed stream/chunk behavior could grow memory.
    Suggestion: Add max buffer size guard and reset/reconnect if exceeded.

  • src/core/infrastructure/services/AuthManager.ts:492Auto-refresh retry scheduling is hard to reason about: recursive scheduleTokenRefresh(retryDelayMs / 1000) during retry path mixes “expiry-driven schedule” and “retry-driven schedule”.
    Suggestion: Split into dedicated scheduleRefreshAtExpiry and scheduleRefreshRetry methods to simplify control flow and avoid accidental timing regressions.

Praise

  • src/core/infrastructure/services/AuthManager.ts:531Good concurrent refresh deduplication: refreshInFlight prevents token refresh stampedes across concurrent requests, which is important for correctness and backend load.
  • src/core/infrastructure/services/CloudSSEManager.ts:140Thoughtful reconnect lifecycle cleanup: aborting streams, clearing heartbeat/reconnect timers, and preserving lastEventId is robust and minimizes duplicate event processing.
  • src/main/index.ts:394Startup sequencing is well integrated: auth restoration is explicitly initialized during background service setup, improving user session continuity and reducing renderer-side complexity.

@jorben jorben merged commit 7cd348f into master Mar 1, 2026
4 of 5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant