feat(cloud): ✨ Add cloud API integration with authentication and credits#48
feat(cloud): ✨ Add cloud API integration with authentication and credits#48
Conversation
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>
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>
There was a problem hiding this comment.
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:537— Refresh token may be persisted in plaintext fallback:persistRefreshTokenwrites the refresh token unencrypted whensafeStorage.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:114— Protocol callback URL is not validated beyond scheme prefix:handleProtocolUrlaccepts anymarkpdfdown://...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:89— IPC type contract mismatch forcloud.convert: d.ts omitspage_rangebut preload implementation includes it (src/preload/index.ts:93). This can cause renderer compile-time mismatch and hidden runtime usage drift.
Fix: Updateelectron.d.tsto includepage_range?: stringincloud.convertsignature (and ideally share a single type fromshared/types). -
src/core/infrastructure/services/CloudSSEManager.ts:228— Unbounded 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:150— Path 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 withencodeURIComponent(...)before interpolation.
Suggestion
src/main/ipc/handlers/cloud.handler.ts:12— Missing 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:83— Good transient-vs-definitive auth failure handling: distinguishingAuthTokenInvalidErrorfrom network/server errors and scheduling retry is a solid resilience pattern that avoids unnecessary forced logouts. -
src/core/infrastructure/services/CloudSSEManager.ts:46— Strong reconnection lifecycle control: settingconnectedbefore 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:101— Nice 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>
There was a problem hiding this comment.
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:13— Unbounded IPC input can trigger memory exhaustion viacontentuploads: Thecloud:converthandler accepts arbitraryArrayBufferfrom renderer (fileData.content) and forwards it toFormDatawithout 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 toCloudService: enforce max byte size, require eitherpathorcontent(not both), validate filename extension/MIME allowlist, and reject oversized/invalid payloads. -
src/main/index.ts:133— Protocol callback path validation can be bypassed for malformed but accepted URLs: Path allowlisting is based on string concat ofhost + pathnameand 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:308— Timeout/retry race can duplicate refresh attempts under concurrent requests:fetchWithAuthandgetAccessTokencan both callrefreshAccessToken()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:150—shell.openExternalnot awaited/checked: Browser open failures are ignored; user can be stuck inpollingwithout browser launched.
Fix:await shell.openExternal(...)and handle false/rejection by settingdeviceFlowStatus='error'with actionable message. -
src/core/infrastructure/services/CloudSSEManager.ts:223— SSE 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:382— Filename fromContent-Dispositioninsufficiently sanitized: Download filename is derived from header and directly used inpath.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:85—any-heavy API contracts weaken IPC type safety: New auth/cloud APIs and events are allPromise<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:90— Good transient vs permanent auth failure handling: DistinguishingAuthTokenInvalidErrorfrom transient network/server errors avoids unnecessary logout and improves resilience.src/core/infrastructure/services/CloudSSEManager.ts:119— Clean reconnect strategy: Backoff + heartbeat timeout + timer cleanup is well-structured and reduces duplicate stream risk.src/main/ipc/handlers/file.handler.ts:101— Nice 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>
There was a problem hiding this comment.
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:17— IPC handler accepts arbitrary renderer-provided file paths without path trust boundary:cloud:convertwill readfileData.pathdirectly in main process (fs.statSyncthen eventuallyfs.readFileSyncinCloudService.convert). Any renderer compromise (or untrusted web content loaded by mistake) can exfiltrate arbitrary local files by passing crafted absolute paths.
Fix: Restrictpathinputs to files selected via trusteddialog.showOpenDialogflow (e.g., mint short-lived file tokens/handles in main and only accept those), or disallow path input entirely and requirecontentpassed from preload-controlled selection flow. -
src/core/infrastructure/services/CloudService.ts:53— Synchronous 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:121— Protocol 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:85— Weak API contracts (Promise<any>) for new auth/cloud IPC surface: Most newly added APIs are typed asany, which undermines compile-time contract checks across preload/renderer and can hide breaking changes.
Fix: Replaceanywith shared response/request types fromshared/types/cloud-api.ts(and equivalent auth types), especially forauth:*,cloud:*, and event callbacks. -
src/core/infrastructure/services/AuthManager.ts:313— Download 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 (/pdfcurrently 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:155— No content-type validation for SSE stream: You log content-type but don’t enforcetext/event-stream, so HTML/JSON error bodies might be parsed as stream data and generate noisy reconnect loops.
Suggestion: Validatecontent-typeincludestext/event-stream; otherwise treat as non-retriable (or retry with capped attempts + explicit auth check).
Praise
src/core/infrastructure/services/AuthManager.ts:498— Refresh de-duplication is well implemented:refreshInFlightavoids concurrent refresh races and token stomp issues; this is a strong correctness safeguard.src/core/infrastructure/services/CloudService.ts:404— Safe filename sanitization on PDF download: Usingpath.basenameplus reserved/control character replacement is a good defense against path/header-based filename abuse.src/core/infrastructure/services/CloudSSEManager.ts:118— Reconnect 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>
There was a problem hiding this comment.
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:177— SSE abort signal is dropped due tofetchWithAuthoverwrite:startStream()passessignal: this.abortController.signal, butAuthManager.fetchWithAuth()always creates its ownAbortControllerand overwritesoptions.signal. This meansdisconnect()/reconnect()won’t reliably abort the in-flight SSE fetch, causing leaked/duplicate streams and racey reconnect behavior.
Fix: UpdatefetchWithAuthto respect a caller-providedoptions.signal(compose with timeout signal viaAbortSignal.anywhere available, or skip creating timeout controller whenoptions.signalexists). -
src/main/index.ts:126— OAuth callback trigger can be spoofed by anymarkpdfdown://auth...URL:handleProtocolUrlvalidates only scheme/path and then immediately callsauthManager.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 randomstategenerated at login initiation and validate it in protocol callback before invoking token check. Ignore callbacks without matching state.
Important
-
src/preload/electron.d.ts:85— UnsafeanyAPI contracts for new IPC surface: all newly addedauth/cloudmethods and event payloads are typed asPromise<any>/any, which breaks shared contract safety and makes main/renderer drift likely.
Fix: Reuseshared/types/cloud-api.tstypes in preload typings and enforce strongly-typed return envelopes ({ success, data, error }) per method. -
src/main/ipc/handlers/cloud.handler.ts:16— Input validation incomplete for untrusted renderer payload:cloud:convertvalidates size but not shape/content offileData.name,model, orpage_range. Since IPC is an attack surface, malformed values can still hit backend/API unexpectedly.
Fix: Add strict runtime validation (type/length/pattern), especially forpage_rangeandmodelallowlist, and reject invalid payloads early. -
src/core/infrastructure/services/AuthManager.ts:312— Timeout abort error not normalized for UX:fetchWithAuthuses abort for timeout but returns rawAbortError, which will surface as generic failure in many callers.
Fix: CatchAbortErrorinfetchWithAuthand throw a normalized timeout error message (e.g.,Request timeout) so renderer can show actionable feedback.
Suggestion
-
src/core/infrastructure/services/AuthManager.ts:1— Use async fs APIs for token persistence in main process: syncfsoperations (readFileSync,writeFileSync, etc.) on auth paths can block the main thread.
Suggestion: Switch tofs/promisesequivalents and keep initialization non-blocking. -
src/renderer/components/UploadPanel.tsx:119— Duplicate comment line: “Inject Cloud Models...” comment is duplicated consecutively, which adds noise.
Suggestion: Remove redundant line.
Praise
-
src/core/infrastructure/services/AuthManager.ts:483— Good refresh deduplication:refreshInFlightguard is a strong concurrency control that prevents token refresh stampedes. -
src/core/infrastructure/services/CloudService.ts:402— Safe filename handling for downloads: sanitizingContent-Dispositionfilename withbasenameand reserved/control char replacement is a solid defense against path/file-name abuse. -
src/main/index.ts:113— Protocol 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>
There was a problem hiding this comment.
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:19— IPC allows arbitrary filesystem reads via untrustedfileData.path: The renderer can pass any absolute path tocloud:convert, and main willstatit thenCloudService.convert()reads it and uploads contents. If renderer is compromised (XSS/dependency compromise), this becomes local file exfiltration. Restrictpathinputs to a trusted file-selection flow (e.g., issue opaque file tokens fromfile:selectDialogand only allow those), or at minimum validate against a recent allowlist of user-selected paths and block everything else. -
src/main/index.ts:126— OAuth callback URL validation can be bypassed with encoded slash in host: InhandleProtocolUrl,decodeURIComponent(parsed.host)is applied before path checks. A URL likemarkpdfdown://auth%2Fcallbackcan decode host toauth/callbackand passALLOWED_PROTOCOL_PATHS. Host should not be decoded this way for path authorization logic. Validate using structural URL parts directly (parsed.hostexact match + normalizedparsed.pathname) and reject percent-encoded/in host.
Important
-
src/core/infrastructure/services/AuthManager.ts:357— Potential runtime incompatibility withAbortSignal.any:fetchWithAuthusesAbortSignal.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) whenAbortSignal.anyis undefined. -
src/core/infrastructure/services/CloudSSEManager.ts:165— Connection lock can deadlock on earlystartStreamerrors:connect()setsthis.connected = truebeforeawait this.startStream(). IfstartStream()throws before scheduling reconnect (e.g., synchronous runtime failure),connectedremains true and futureconnect()calls no-op with “Already connected”. WrapstartStream()in try/catch inconnect()and resetconnected=falseon unrecoverable startup error. -
src/main/ipc/handlers/cloud.handler.ts:53— Missing input bounds validation for pagination parameters:cloud:getTasks,cloud:getTaskPages, andcloud:getCreditHistoryforward 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:1— UnvalidatedAPI_BASE_URLcontract: 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 behttps:in production, allowhttp://localhostonly in dev), and fail fast with explicit error.
Praise
-
src/core/infrastructure/services/AuthManager.ts:530— Refresh deduplication is well implemented: TherefreshInFlightguard prevents concurrent refresh stampedes and racey token overwrites, which is critical under high request concurrency. -
src/core/infrastructure/services/CloudSSEManager.ts:112— Reconnect/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:157— Single-instance + protocol callback handling is thoughtfully integrated: Handlingopen-url(macOS) andsecond-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>
There was a problem hiding this comment.
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:17— UnvalidatedfileData.pathallows arbitrary main-process file reads/uploads:cloud:convertaccepts a raw path from the renderer and reads it (fs.promises.stat+ laterfs.readFileinCloudService) without constraining source. If renderer-side JS is compromised, this can exfiltrate arbitrary local files to cloud.
Fix: Only allow uploads from:- paths returned by trusted file dialog in this process (track allowlisted selections), or
- direct file bytes from renderer (
content) while disabling arbitrarypathmode for IPC callers.
Also validate extension/MIME in main before upload, not only in renderer.
-
src/main/ipc/handlers/cloud.handler.ts:203— Unvalidated task/page parameters in privileged IPC endpoints: task IDs/page numbers from renderer are passed directly into authenticated backend requests (getTaskById,retryPage, etc.). WhileencodeURIComponenthelps 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):taskIdpattern/length,pageNumberpositive integer bounds, pagination limits, and allowed enums (typein history). Reject invalid payloads early. -
src/main/index.ts:116— Custom protocol URL validation bypass risk due to host parsing assumptions: validation relies onparsed.hostandpathnameequality with limited normalization.markpdfdown:///auth/callbackand other URL forms can shift expected components and may be mishandled.
Fix: Validate against explicit accepted URL shapes using bothhostname,pathname, and possiblyorigin, with a strict parser helper + exhaustive allowed patterns. Reject anything not exactly matching documented callback URL(s).
Important
-
src/core/infrastructure/services/CloudService.ts:53— Node/Electron runtime compatibility risk forFormData/Blob: service uses webFormDataandBlobin 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:34— Potential 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:85— Type contract too weak (Promise<any>) for new high-impact IPC API: new auth/cloud APIs are allany, which removes compile-time guarantees between renderer/main and increases runtime mismatch risk.
Fix: Replace with shared response/request types fromshared/types/cloud-api.tsand consistent IPC result envelopes.
Suggestion
-
src/core/infrastructure/services/CloudSSEManager.ts:236— SSE buffer growth guard: parsing loop accumulatesbufferuntil 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:492— Auto-refresh retry scheduling is hard to reason about: recursivescheduleTokenRefresh(retryDelayMs / 1000)during retry path mixes “expiry-driven schedule” and “retry-driven schedule”.
Suggestion: Split into dedicatedscheduleRefreshAtExpiryandscheduleRefreshRetrymethods to simplify control flow and avoid accidental timing regressions.
Praise
src/core/infrastructure/services/AuthManager.ts:531— Good concurrent refresh deduplication:refreshInFlightprevents token refresh stampedes across concurrent requests, which is important for correctness and backend load.src/core/infrastructure/services/CloudSSEManager.ts:140— Thoughtful reconnect lifecycle cleanup: aborting streams, clearing heartbeat/reconnect timers, and preservinglastEventIdis robust and minimizes duplicate event processing.src/main/index.ts:394— Startup sequencing is well integrated: auth restoration is explicitly initialized during background service setup, improving user session continuity and reducing renderer-side complexity.
Summary
Test Plan
npm test🤖 Generated with Claude Code