-
Notifications
You must be signed in to change notification settings - Fork 51
feat(desktop): add Tauri desktop app POC with OAuth authentication #140
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
- Add Tauri v2 desktop application scaffold - Implement OAuth authentication flow with custom protocol handler - Add Docker integration commands for container management - Create desktop-auth API routes for token exchange - Add GitHub Actions workflow for cross-platform builds - Configure pnpm workspace for desktop package 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Add tauri.dev.conf.json to enable development against localhost:3000 instead of production. Updates CSP to allow localhost HTTP/WebSocket connections for Next.js hot reload. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthroughThis pull request introduces a complete desktop application framework using Tauri. It adds a new desktop package with Tauri configuration, Rust implementation for deep-link handling and navigation, GitHub Actions CI/CD for cross-platform builds, desktop-specific OAuth callback routes, frontend utilities for Tauri integration, and updates existing components to support desktop navigation flows. Changes
Sequence Diagram(s)sequenceDiagram
participant DA as Desktop App
participant DL as /desktop-login
participant WOS as WorkOS
participant DC as /api/auth/desktop-callback
participant APP as Main App
participant DLC as /desktop-callback
DA->>DL: Open deep-link (desktop-login)
DL->>DL: Generate OAuth state, store in Redis
DL->>WOS: Redirect to OAuth authorization URL
WOS->>DC: POST code & state callback
DC->>DC: Verify & consume OAuth state
DC->>WOS: Exchange code for tokens & user info
DC->>DC: Create desktop transfer token (Redis)
DC->>APP: Redirect to hackerai://auth?token=X&origin=Y
APP->>DLC: Open deep-link redirect to /desktop-callback?token=X
DLC->>DLC: Exchange transfer token for sealedSession
DLC->>DLC: Set wos-session cookie
DLC->>DA: Redirect to main app
DA->>APP: Load authenticated app
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 13
🧹 Nitpick comments (11)
app/api/desktop-auth/refresh/route.ts (1)
25-28: Add runtime validation forWORKOS_CLIENT_ID.The non-null assertion (
!) onprocess.env.WORKOS_CLIENT_IDwill cause a runtime error if the environment variable is missing. Consider validating its presence at startup or returning a 500 error with an appropriate message.🔎 Proposed fix
+ const clientId = process.env.WORKOS_CLIENT_ID; + if (!clientId) { + console.error("WORKOS_CLIENT_ID is not configured"); + return NextResponse.json( + { error: "Server configuration error" }, + { status: 500 } + ); + } + const result = await workos.userManagement.authenticateWithRefreshToken({ - clientId: process.env.WORKOS_CLIENT_ID!, + clientId, refreshToken: body.refresh_token, });app/api/desktop-auth/login/route.ts (1)
34-40: Consider structured logging for production.Using
console.erroris acceptable for development but consider implementing structured logging for production environments to improve observability and debugging capabilities.packages/desktop/src-tauri/tauri.conf.json (1)
28-30: CSP includes'unsafe-eval'which weakens XSS protection.The script-src directive includes
'unsafe-eval', which is often required by JavaScript bundlers but weakens CSP protection. Consider whether this can be removed for production builds, or document why it's necessary for the framework in use.packages/desktop/scripts/generate-icons.mjs (1)
12-41: Extract duplicated SVG generation logic.The SVG template is duplicated across
createIcon,createIcns, andcreateIcofunctions. Consider extracting a shared helper:🔎 Proposed refactor
+function generateSvg(size) { + return ` + <svg width="${size}" height="${size}" xmlns="http://www.w3.org/2000/svg"> + <defs> + <linearGradient id="grad" x1="0%" y1="0%" x2="100%" y2="100%"> + <stop offset="0%" style="stop-color:#10b981;stop-opacity:1" /> + <stop offset="100%" style="stop-color:#059669;stop-opacity:1" /> + </linearGradient> + </defs> + <rect width="${size}" height="${size}" rx="${size * 0.2}" fill="url(#grad)"/> + <text + x="50%" + y="50%" + dominant-baseline="central" + text-anchor="middle" + font-family="system-ui, -apple-system, sans-serif" + font-weight="bold" + font-size="${size * 0.6}" + fill="white" + >H</text> + </svg> + `; +} + async function createIcon(size, filename) { - // Create a simple icon with the brand color and "H" letter - const svg = `...`; + const svg = generateSvg(size); // ... rest of function }Also applies to: 43-91, 93-125
packages/desktop/README.md (2)
102-117: Add language specifier to code fence.The ASCII architecture diagram should have a language specifier for the code fence to satisfy markdown linters. Use
textorplaintext:🔎 Proposed fix
-``` +```text ┌─────────────────────────────────────────────────────────────┐
233-233: Use markdown link syntax for URL.Convert the bare URL to proper markdown link format:
🔎 Proposed fix
-Install WebView2 from Microsoft: https://developer.microsoft.com/en-us/microsoft-edge/webview2/ +Install WebView2 from Microsoft: [WebView2 Runtime](https://developer.microsoft.com/en-us/microsoft-edge/webview2/)packages/desktop/src-tauri/src/lib.rs (1)
4-5: Remove unused import or the allow attribute.The
#[allow(unused_imports)]suppresses the warning fortauri::Manager. Either remove the import if it's not used, or remove the allow attribute if it is used elsewhere.🔎 Proposed fix
-#[allow(unused_imports)] -use tauri::Manager;Or if
Managertrait is needed for method calls onAppHandle:-#[allow(unused_imports)] use tauri::Manager;.github/workflows/desktop-build.yml (1)
177-182: Addretention-daysfor consistency.The universal artifact upload is missing
retention-days, unlike other artifact uploads in this workflow:🔎 Proposed fix
- name: Upload universal artifact uses: actions/upload-artifact@v4 with: name: desktop-macOS-universal path: HackerAI-universal.dmg if-no-files-found: error + retention-days: 7packages/desktop/src-tauri/src/docker.rs (2)
48-52: BlockingCommand::output()in async function.
std::process::Command::output()blocks the thread. While Tauri commands run on a thread pool, consider usingtokio::process::Commandfor true async execution, especially if this is called frequently.🔎 Async alternative using tokio::process
-use std::process::{Child, Command, Stdio}; +use std::process::{Child, Stdio}; +use std::process::Command as StdCommand; +use tokio::process::Command; // In check_docker(): - let output = Command::new("docker") + let output = Command::new("docker") .arg("--version") .stdout(Stdio::piped()) .stderr(Stdio::piped()) - .output(); + .output() + .await;
242-254: Status returns hardcoded image/name instead of actual values.
get_sandbox_statusreturns"hackerai/sandbox"andNonefor name regardless of what was actually started. Consider storing theSandboxConfigalongside theChildinSandboxStateto preserve this metadata.🔎 Proposed refactor to preserve sandbox metadata
+pub struct RunningProcess { + pub child: Child, + pub image: String, + pub name: String, +} pub struct SandboxState { - pub process: Mutex<Option<Child>>, + pub process: Mutex<Option<RunningProcess>>, }Then update
start_sandboxto store the metadata andget_sandbox_statusto return actual values.packages/desktop/src-tauri/src/auth.rs (1)
145-153:authenticatedandhas_tokensare redundant.Both fields are set to the same value. For a more accurate status,
authenticatedcould check token validity (e.g., expiry) whilehas_tokensindicates presence. Consider if both fields are needed or if the distinction will be added later.
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (24)
packages/desktop/src-tauri/Cargo.lockis excluded by!**/*.lockpackages/desktop/src-tauri/gen/schemas/acl-manifests.jsonis excluded by!**/gen/**packages/desktop/src-tauri/gen/schemas/capabilities.jsonis excluded by!**/gen/**packages/desktop/src-tauri/gen/schemas/desktop-schema.jsonis excluded by!**/gen/**packages/desktop/src-tauri/gen/schemas/macOS-schema.jsonis excluded by!**/gen/**packages/desktop/src-tauri/icons/128x128.pngis excluded by!**/*.pngpackages/desktop/src-tauri/icons/128x128@2x.pngis excluded by!**/*.pngpackages/desktop/src-tauri/icons/32x32.pngis excluded by!**/*.pngpackages/desktop/src-tauri/icons/icon.icois excluded by!**/*.icopackages/desktop/src-tauri/icons/icon.iconset/icon_1024x1024.pngis excluded by!**/*.pngpackages/desktop/src-tauri/icons/icon.iconset/icon_128x128.pngis excluded by!**/*.pngpackages/desktop/src-tauri/icons/icon.iconset/icon_128x128@2x.pngis excluded by!**/*.pngpackages/desktop/src-tauri/icons/icon.iconset/icon_16x16.pngis excluded by!**/*.pngpackages/desktop/src-tauri/icons/icon.iconset/icon_16x16@2x.pngis excluded by!**/*.pngpackages/desktop/src-tauri/icons/icon.iconset/icon_256x256.pngis excluded by!**/*.pngpackages/desktop/src-tauri/icons/icon.iconset/icon_256x256@2x.pngis excluded by!**/*.pngpackages/desktop/src-tauri/icons/icon.iconset/icon_32x32.pngis excluded by!**/*.pngpackages/desktop/src-tauri/icons/icon.iconset/icon_32x32@2x.pngis excluded by!**/*.pngpackages/desktop/src-tauri/icons/icon.iconset/icon_512x512.pngis excluded by!**/*.pngpackages/desktop/src-tauri/icons/icon.iconset/icon_512x512@2x.pngis excluded by!**/*.pngpackages/desktop/src-tauri/icons/icon.iconset/icon_64x64.pngis excluded by!**/*.pngpackages/desktop/src-tauri/icons/icon.iconset/icon_64x64@2x.pngis excluded by!**/*.pngpackages/desktop/src-tauri/icons/icon.pngis excluded by!**/*.pngpnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (20)
.github/workflows/desktop-build.yml(1 hunks).gitignore(1 hunks)app/api/desktop-auth/callback/route.ts(1 hunks)app/api/desktop-auth/login/route.ts(1 hunks)app/api/desktop-auth/refresh/route.ts(1 hunks)package.json(1 hunks)packages/desktop/README.md(1 hunks)packages/desktop/package.json(1 hunks)packages/desktop/scripts/generate-icons.mjs(1 hunks)packages/desktop/src-tauri/Cargo.toml(1 hunks)packages/desktop/src-tauri/build.rs(1 hunks)packages/desktop/src-tauri/src/auth.rs(1 hunks)packages/desktop/src-tauri/src/docker.rs(1 hunks)packages/desktop/src-tauri/src/lib.rs(1 hunks)packages/desktop/src-tauri/src/main.rs(1 hunks)packages/desktop/src-tauri/tauri.conf.json(1 hunks)packages/desktop/src-tauri/tauri.dev.conf.json(1 hunks)packages/desktop/src/index.html(1 hunks)packages/desktop/tsconfig.json(1 hunks)pnpm-workspace.yaml(1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
package.json
📄 CodeRabbit inference engine (.cursor/rules/convex_rules.mdc)
Add
@types/nodetopackage.jsonwhen using any Node.js built-in modules in Convex functions
Files:
package.json
🧠 Learnings (1)
📚 Learning: 2025-11-28T01:16:26.536Z
Learnt from: fkesheh
Repo: hackerai-tech/hackerai PR: 114
File: lib/ai/tools/utils/convex-sandbox.ts:95-145
Timestamp: 2025-11-28T01:16:26.536Z
Learning: In the local sandbox architecture (lib/ai/tools/utils/convex-sandbox.ts), background command execution semantics are handled by the local sandbox client (packages/local/src/index.ts), not by the ConvexSandbox class. When background: true, the local client immediately submits a result with empty output and PID after spawning the process, causing waitForResult() to resolve immediately and maintaining E2B-compatible non-blocking behavior.
Applied to files:
packages/desktop/src-tauri/src/docker.rs
🧬 Code graph analysis (7)
packages/desktop/src-tauri/src/main.rs (3)
packages/desktop/src-tauri/build.rs (1)
main(1-3)packages/desktop/src-tauri/src/docker.rs (1)
default(12-16)packages/desktop/src-tauri/src/lib.rs (1)
run(8-51)
app/api/desktop-auth/login/route.ts (1)
app/api/workos.ts (1)
workos(7-7)
packages/desktop/src-tauri/src/lib.rs (2)
packages/desktop/src-tauri/src/docker.rs (7)
default(12-16)check_docker(45-85)check_sandbox_image(89-105)pull_sandbox_image(109-128)start_sandbox(133-191)stop_sandbox(195-230)get_sandbox_status(234-269)packages/desktop/src-tauri/src/auth.rs (7)
start_login(30-38)get_stored_tokens(42-66)store_tokens(70-88)refresh_tokens(92-129)logout(133-143)get_auth_status(147-153)handle_deep_link(157-167)
packages/desktop/scripts/generate-icons.mjs (2)
packages/desktop/src-tauri/build.rs (1)
main(1-3)packages/desktop/src-tauri/src/main.rs (1)
main(7-12)
app/api/desktop-auth/refresh/route.ts (1)
app/api/workos.ts (1)
workos(7-7)
app/api/desktop-auth/callback/route.ts (2)
app/api/desktop-auth/login/route.ts (1)
GET(11-41)app/api/workos.ts (1)
workos(7-7)
packages/desktop/src-tauri/build.rs (1)
packages/desktop/src-tauri/src/main.rs (1)
main(7-12)
🪛 actionlint (1.7.9)
.github/workflows/desktop-build.yml
116-116: the runner of "softprops/action-gh-release@v1" action is too old to run on GitHub Actions. update the action's version to fix this issue
(action)
🪛 LanguageTool
packages/desktop/README.md
[uncategorized] ~169-~169: The official name of this software platform is spelled with a capital “H”.
Context: ...ror ## CI/CD GitHub Actions workflow (.github/workflows/desktop-build.yml) builds fo...
(GITHUB)
🪛 markdownlint-cli2 (0.18.1)
packages/desktop/README.md
102-102: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
233-233: Bare URL used
(MD034, no-bare-urls)
🔇 Additional comments (24)
.gitignore (1)
57-60: LGTM!The ignore patterns correctly exclude the desktop package's
node_modulesand Rust build artifacts (target/), which are essential for keeping the repository clean.packages/desktop/src-tauri/tauri.dev.conf.json (1)
1-11: LGTM for development configuration.The CSP includes
'unsafe-inline'and'unsafe-eval'inscript-src, which is acceptable for this development configuration. Ensure the productiontauri.conf.jsonhas a stricter CSP that removes these unsafe directives where possible.packages/desktop/tsconfig.json (1)
1-17: LGTM!The TypeScript configuration is well-suited for a Tauri desktop application with strict mode enabled, appropriate ES2021 target for modern WebViews, and correct exclusion of the Rust source directory.
app/api/desktop-auth/callback/route.ts (2)
31-46: Consider state validation to prevent CSRF attacks.The
stateparameter is forwarded to the deep link but not validated against the value originally generated by the desktop app. While the desktop client may perform this validation upon receiving the callback, server-side validation provides defense in depth. If state validation is intentionally deferred to the client, consider adding a comment to clarify.Additionally, the same non-null assertion issue exists for
WORKOS_CLIENT_IDas in the refresh route.
41-50: Tokens in deep link URLs are acceptable for native OAuth flows.Passing tokens via query parameters in custom scheme redirects (
hackerai://) is a standard pattern for native app OAuth (similar to AppAuth). The custom scheme is handled directly by the OS and native app, avoiding typical URL logging concerns.packages/desktop/src-tauri/build.rs (1)
1-3: LGTM!Standard Tauri build script implementation that correctly delegates to
tauri_build::build().app/api/desktop-auth/login/route.ts (1)
11-20: LGTM!Proper validation of the OAuth state parameter with appropriate minimum length check for CSRF protection.
package.json (1)
41-43: LGTM!Desktop scripts properly delegate to the desktop package workspace following standard monorepo conventions.
pnpm-workspace.yaml (1)
1-2: LGTM!Standard pnpm workspace configuration that properly includes all packages.
packages/desktop/package.json (2)
12-24: LGTM!Dependencies are well-structured with consistent Tauri 2.x versions and appropriate plugins for the desktop app functionality.
6-11: The development configuration filesrc-tauri/tauri.dev.conf.jsonexists and is properly referenced in the script.packages/desktop/src-tauri/src/main.rs (2)
1-5: LGTM!Correct Windows subsystem configuration that prevents console window from appearing in release builds on Windows.
7-12: LGTM!Clean main entry point with proper logging initialization and delegation to the library entry point.
packages/desktop/src-tauri/Cargo.toml (1)
7-7: Verify Tauri 2.x Rust version compatibility. The specifiedrust-version = "1.70"may not align with Tauri 2.x requirements, particularly given that some Tauri plugins (like the updater) require newer versions. Confirm the minimum Rust version needed for your Tauri 2.x dependencies.packages/desktop/src-tauri/tauri.conf.json (1)
1-11: Configuration structure looks good.The Tauri configuration is well-organized with appropriate window dimensions, bundle settings for all platforms, and proper deep-link registration. The macOS minimum version (10.15) and NSIS installer configuration are reasonable defaults.
Also applies to: 12-31, 32-74
packages/desktop/scripts/generate-icons.mjs (1)
127-148: Font rendering may vary across build environments.The SVG uses
system-uifont which renders differently on macOS, Windows, and Linux. For production icons, consider using an embedded font or pre-rendered PNG assets to ensure consistency.The script is functional for POC/development purposes.
packages/desktop/README.md (1)
1-259: Documentation is comprehensive and well-structured.The README thoroughly covers prerequisites, development setup, build processes, architecture, authentication flow, configuration, CI/CD, code signing, and troubleshooting. This is excellent documentation for the desktop app POC.
packages/desktop/src-tauri/src/lib.rs (1)
8-51: Tauri application setup is well-structured.The plugin registration, state management, command handlers, and deep link setup are properly configured. The
#[cfg(desktop)]conditional compilation correctly gates desktop-specific functionality. Using.expect()for fatal startup errors is idiomatic for Tauri applications..github/workflows/desktop-build.yml (2)
1-98: Build workflow is well-structured.The workflow correctly sets up a build matrix for cross-platform compilation, handles platform-specific dependencies, caches appropriately, and uses the official Tauri action. The artifact upload strategy is sound.
155-175: Binary name is correctly configured.The binary name
hackerai-desktopis explicitly defined inpackages/desktop/src-tauri/Cargo.tomlunder the[[bin]]section (name = "hackerai-desktop"), not derived from theproductName. Tauri usesproductNamefor display/UI purposes while the actual binary name is configured separately in Cargo.toml. The workflow correctly references the configured binary name.Likely an incorrect or invalid review comment.
packages/desktop/src-tauri/src/docker.rs (2)
6-17: LGTM onSandboxStatestructure.The use of
Mutex<Option<Child>>is appropriate for managing a single child process across async Tauri commands. TheDefaultimplementation is clean.
34-41: Sensitive token inSandboxConfig.The
tokenfield contains authentication credentials. This is acceptable for Tauri IPC, but ensure the frontend doesn't log or persist this config object unnecessarily.packages/desktop/src-tauri/src/auth.rs (2)
40-66: LGTM on token retrieval.Proper handling of the
NoEntrycase and appropriate error logging. The keychain integration follows best practices.
155-167: Good URL truncation in logs.Truncating the deep link URL to 50 characters in logs prevents accidental token leakage. The routing logic is clear and handles unknown schemes appropriately.
| <!DOCTYPE html> | ||
| <html lang="en"> | ||
| <head> | ||
| <meta charset="UTF-8" /> | ||
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||
| <title>HackerAI</title> | ||
| <style> | ||
| * { | ||
| margin: 0; | ||
| padding: 0; | ||
| box-sizing: border-box; | ||
| } | ||
| body { | ||
| font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, sans-serif; | ||
| background: #0a0a0a; | ||
| color: #fafafa; | ||
| display: flex; | ||
| align-items: center; | ||
| justify-content: center; | ||
| min-height: 100vh; | ||
| } | ||
| .loader { | ||
| text-align: center; | ||
| } | ||
| .spinner { | ||
| width: 40px; | ||
| height: 40px; | ||
| border: 3px solid #333; | ||
| border-top-color: #10b981; | ||
| border-radius: 50%; | ||
| animation: spin 1s linear infinite; | ||
| margin: 0 auto 16px; | ||
| } | ||
| @keyframes spin { | ||
| to { transform: rotate(360deg); } | ||
| } | ||
| </style> | ||
| </head> | ||
| <body> | ||
| <div class="loader"> | ||
| <div class="spinner"></div> | ||
| <p>Loading HackerAI...</p> | ||
| </div> | ||
| <script> | ||
| // Redirect to production site | ||
| // This page is shown briefly during app startup | ||
| window.location.href = "https://hackerai.co"; | ||
| </script> | ||
| </body> | ||
| </html> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# First, let's find the file and explore the directory structure
git ls-files packages/desktop/src/ | head -20Repository: hackerai-tech/hackerai
Length of output: 97
🏁 Script executed:
#!/bin/bash
# Read the index.html file
cat -n packages/desktop/src/index.htmlRepository: hackerai-tech/hackerai
Length of output: 1668
🏁 Script executed:
#!/bin/bash
# Check for Tauri configuration files
find . -name "tauri.conf.json" -o -name "tauri.conf.toml" | head -5Repository: hackerai-tech/hackerai
Length of output: 110
🏁 Script executed:
#!/bin/bash
# Read the Tauri configuration
cat -n ./packages/desktop/src-tauri/tauri.conf.jsonRepository: hackerai-tech/hackerai
Length of output: 3470
🏁 Script executed:
#!/bin/bash
# Check if there are other HTML files or entry points
find packages/desktop/src -name "*.html" -o -name "*.jsx" -o -name "*.tsx" | head -10Repository: hackerai-tech/hackerai
Length of output: 97
Remove or reconfigure this redirect with environment-based URL handling.
All concerns in the original review are confirmed:
-
Immediate redirect negates loading screen: The script executes immediately without delay, making the loading spinner invisible or only briefly visible.
-
Hardcoded production URL: The URL
https://hackerai.cois hardcoded and will break in development/staging environments. Thetauri.conf.jsonuses the same hardcoded URL fordevUrl, with no environment-based configuration. -
Unclear purpose in Tauri context: Tauri apps serve static files from
frontendDist, but having an index.html that simply redirects to an external URL defeats the purpose of building a desktop application. If this is meant as a loading screen, remove the immediate redirect and add a delay. If it's not needed, remove the file entirely.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
🧹 Nitpick comments (3)
packages/desktop/README.md (3)
91-99: Specify language for fenced code block.The ASCII diagram lacks a language specification. Per markdown best practices, add a language identifier (e.g.,
textordiagram).-``` +```text ┌─────────────────────────────────────────────────────────────┐ │ Tauri Desktop App │
105-105: Capitalize "GitHub" in official name.Line 105 uses lowercase "github" in the official product name.
-GitHub Actions workflow (`.github/workflows/desktop-build.yml`) builds for: +GitHub Actions workflow (`.github/workflows/desktop-build.yml`) builds for:
169-169: Wrap bare URL in markdown link syntax.Per markdown conventions, bare URLs should be wrapped in link format.
-Install WebView2 from Microsoft: https://developer.microsoft.com/en-us/microsoft-edge/webview2/ +Install WebView2 from Microsoft: [WebView2](https://developer.microsoft.com/en-us/microsoft-edge/webview2/)
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (4)
packages/desktop/src-tauri/Cargo.lockis excluded by!**/*.lockpackages/desktop/src-tauri/gen/schemas/acl-manifests.jsonis excluded by!**/gen/**packages/desktop/src-tauri/gen/schemas/desktop-schema.jsonis excluded by!**/gen/**packages/desktop/src-tauri/gen/schemas/macOS-schema.jsonis excluded by!**/gen/**
📒 Files selected for processing (4)
packages/desktop/README.mdpackages/desktop/src-tauri/Cargo.tomlpackages/desktop/src-tauri/src/lib.rspackages/desktop/src-tauri/tauri.conf.json
🚧 Files skipped from review as they are similar to previous changes (3)
- packages/desktop/src-tauri/src/lib.rs
- packages/desktop/src-tauri/tauri.conf.json
- packages/desktop/src-tauri/Cargo.toml
🧰 Additional context used
🪛 LanguageTool
packages/desktop/README.md
[uncategorized] ~105-~105: The official name of this software platform is spelled with a capital “H”.
Context: ...pp. ## CI/CD GitHub Actions workflow (.github/workflows/desktop-build.yml) builds fo...
(GITHUB)
🪛 markdownlint-cli2 (0.18.1)
packages/desktop/README.md
91-91: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
169-169: Bare URL used
(MD034, no-bare-urls)
🔇 Additional comments (1)
packages/desktop/README.md (1)
38-101: Excellent documentation of development, build, and architecture.The README clearly documents the simplified webview-wrapper architecture, dual development workflows (production vs. local), and build process. The architecture diagram effectively communicates that authentication is delegated to the web app—consistent with the refactor to remove unused native implementations mentioned in the commit message.
- Remove /api/desktop-auth/* routes (never called by Tauri app) - Remove auth.rs (native OAuth + keychain - never invoked) - Remove docker.rs (sandbox management - never invoked) - Remove deep-link plugin config (no longer needed) - Remove unused dependencies (keyring, reqwest, tokio, uuid, url, etc.) - Simplify lib.rs to minimal webview wrapper - Update README to reflect simplified architecture The Tauri app now loads hackerai.co directly and uses web auth flow.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
♻️ Duplicate comments (2)
.github/workflows/desktop-build.yml (1)
115-130: Previous concerns resolved.The release step has been updated to use
softprops/action-gh-release@v2and references${{ github.ref_name }}directly, addressing the issues raised in the previous review.packages/desktop/src-tauri/tauri.conf.json (1)
75-84: Empty updater public key disables signature verification.The updater configuration has
"pubkey": "", which disables update signature verification. This is a security risk that allows unsigned or malicious updates to be installed.Before releasing, generate a signing key pair and populate the public key as documented in the README (lines 145-163).
🧹 Nitpick comments (1)
packages/desktop/README.md (1)
91-99: Minor documentation style improvements.Static analysis tools flagged a few minor style issues:
- Line 91: The architecture diagram should specify a language (or use
textfor ASCII art).- Line 105: "github" should be capitalized as "GitHub".
- Line 169: The bare URL could be wrapped in a markdown link for consistency.
🔎 Proposed improvements
1. Add language identifier to code block:
## Architecture -``` +```text ┌─────────────────────────────────────────────────────────────┐ │ Tauri Desktop App │2. Capitalize GitHub:
-GitHub Actions workflow (`.github/workflows/desktop-build.yml`) builds for: +GitHub Actions workflow (`.github/workflows/desktop-build.yml`) builds for:3. Wrap bare URL:
-Install WebView2 from Microsoft: https://developer.microsoft.com/en-us/microsoft-edge/webview2/ +Install WebView2 from Microsoft: [https://developer.microsoft.com/en-us/microsoft-edge/webview2/](https://developer.microsoft.com/en-us/microsoft-edge/webview2/)Also applies to: 105-105, 169-169
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (4)
packages/desktop/src-tauri/Cargo.lockis excluded by!**/*.lockpackages/desktop/src-tauri/gen/schemas/acl-manifests.jsonis excluded by!**/gen/**packages/desktop/src-tauri/gen/schemas/desktop-schema.jsonis excluded by!**/gen/**packages/desktop/src-tauri/gen/schemas/macOS-schema.jsonis excluded by!**/gen/**
📒 Files selected for processing (5)
.github/workflows/desktop-build.ymlpackages/desktop/README.mdpackages/desktop/src-tauri/Cargo.tomlpackages/desktop/src-tauri/src/lib.rspackages/desktop/src-tauri/tauri.conf.json
🚧 Files skipped from review as they are similar to previous changes (1)
- packages/desktop/src-tauri/Cargo.toml
🧰 Additional context used
🪛 LanguageTool
packages/desktop/README.md
[uncategorized] ~105-~105: The official name of this software platform is spelled with a capital “H”.
Context: ...pp. ## CI/CD GitHub Actions workflow (.github/workflows/desktop-build.yml) builds fo...
(GITHUB)
🪛 markdownlint-cli2 (0.18.1)
packages/desktop/README.md
91-91: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
169-169: Bare URL used
(MD034, no-bare-urls)
🔇 Additional comments (3)
packages/desktop/src-tauri/src/lib.rs (1)
1-14: LGTM!The Tauri app initialization is clean and straightforward. The registered plugins (os, process, shell, updater) align with typical desktop app requirements, and using
expect()for application startup is appropriate since the app cannot function if initialization fails..github/workflows/desktop-build.yml (2)
79-89: No changes needed to tauri-apps/tauri-action version.The workflow correctly uses
tauri-apps/tauri-action@v0, which is the version documented in the official Tauri v2 distribution guides. This action version is actively maintained and appropriate for the build configuration.
154-175: The hardcoded binary namehackerai-desktopis correct and matches the Cargo.toml[package] namefield. The Tauri binary name derives from the Cargo.toml package name by default, and your configuration has no override in tauri.conf.json. The bundle folder will be namedHackerAI.app(from productName), but the binary inside will behackerai-desktop, which is what the script correctly references.
| "security": { | ||
| "csp": "default-src 'self' https://hackerai.co https://*.hackerai.co https://*.convex.cloud https://*.convex.dev https://auth.hackerai.co https://api.workos.com; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://hackerai.co https://*.hackerai.co https://*.convex.cloud; style-src 'self' 'unsafe-inline' https://hackerai.co https://*.hackerai.co; img-src 'self' data: https: blob:; font-src 'self' data: https:; connect-src 'self' https://hackerai.co https://*.hackerai.co https://*.convex.cloud wss://*.convex.cloud https://auth.hackerai.co https://api.workos.com https://*.posthog.com https://*.stripe.com; frame-src 'self' https://*.stripe.com; media-src 'self' blob:;" | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove 'unsafe-eval' from production CSP; 'unsafe-inline' is less critical in Tauri due to auto-hashing.
Tauri automatically injects nonces and hashes to local scripts and styles at compile time, which reduces the impact of 'unsafe-inline'. However, 'unsafe-eval' should only be used if you cannot remove all uses of eval(). Since this is a production configuration, 'unsafe-eval' should be removed unless the application has a documented dependency on it (e.g., development-mode React Refresh). If the frontend requires eval() in production, document why and consider alternatives using nonce + strict-dynamic instead.
🤖 Prompt for AI Agents
In packages/desktop/src-tauri/tauri.conf.json around lines 28 to 30, the CSP
includes 'unsafe-eval' which should be removed for production; edit the csp
string to remove 'unsafe-eval' from script-src, ensure only allowed sources
remain, and if eval is required in development wrap the dev-only CSP change
behind a build/dev flag or document the dependency and rationale in the repo (or
add a clear comment pointing to the justification and any mitigation like
nonce/strict-dynamic).
Replace dynamically generated icon with custom HackerAI SVG logo. Add dark background (rgb(20,20,20)) and 15% padding around the icon. Simplify icon generation script to read from source SVG file. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Show spinner and disable buttons when clicked to provide immediate visual feedback during page navigation to login/signup routes. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Show spinner on logout button and billing portal redirect buttons to provide visual feedback during navigation. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
- Add Navigation menu with Go Back item (Cmd+[) for desktop app - Use native WKWebView goBack() on macOS for proper history navigation - Add useBfcacheReset hook to reset loading states when page restored - Apply hook to Header, SidebarUserNav, and AccountTab components 🤖 Generated with [Claude Code](https://claude.com/claude-code)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 5
🧹 Nitpick comments (2)
packages/desktop/src-tauri/src/lib.rs (1)
8-13: Document safety assumptions for the unsafe block.The unsafe block performs FFI operations and pointer casting without explaining why these operations are safe. Add a SAFETY comment documenting the invariants that make this code sound (e.g., webview.inner() validity, WKWebView type guarantee).
📝 Suggested documentation addition
let _ = window.with_webview(|webview| unsafe { + // SAFETY: On macOS, Tauri's webview.inner() returns a valid WKWebView pointer. + // The cast is safe because we're on macOS (guarded by cfg) and Tauri guarantees + // the underlying webview is WKWebView on this platform. let wk_webview: &WKWebView = &*webview.inner().cast(); if wk_webview.canGoBack() { wk_webview.goBack(); } });app/hooks/useBfcacheReset.ts (1)
9-20: Document the stability requirement forresetFn.The hook includes
resetFnin the dependency array, which means it will re-subscribe to thepageshowevent wheneverresetFnchanges. If callers don't wrapresetFninuseCallback, the listener will be added/removed on every render, causing unnecessary overhead.All current usages correctly use
useCallback, but future developers may not be aware of this requirement.🔎 Recommended documentation update
/** * Hook that resets state when page is restored from bfcache (back-forward cache). * Use this for loading states that should reset when user navigates back. * - * @param resetFn - Function to call when page is restored from bfcache + * @param resetFn - Function to call when page is restored from bfcache. + * Must be wrapped in useCallback to avoid unnecessary re-subscriptions. + * + * @example + * useBfcacheReset(useCallback(() => setLoading(false), [])); */ export function useBfcacheReset(resetFn: () => void) {
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
packages/desktop/src-tauri/Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (6)
app/components/AccountTab.tsxapp/components/Header.tsxapp/components/SidebarUserNav.tsxapp/hooks/useBfcacheReset.tspackages/desktop/src-tauri/Cargo.tomlpackages/desktop/src-tauri/src/lib.rs
🚧 Files skipped from review as they are similar to previous changes (1)
- packages/desktop/src-tauri/Cargo.toml
🧰 Additional context used
🧬 Code graph analysis (2)
app/components/Header.tsx (1)
app/hooks/useBfcacheReset.ts (1)
useBfcacheReset(9-20)
app/components/SidebarUserNav.tsx (1)
app/hooks/useBfcacheReset.ts (1)
useBfcacheReset(9-20)
🔇 Additional comments (5)
packages/desktop/src-tauri/src/lib.rs (1)
47-47: No action required—window identifier is correct.The window configuration in
tauri.conf.jsoncontains a single window without an explicit label field. In Tauri 2.x, windows without a label default to"main", which matches the identifier used in the code. The navigation feature will work as intended.app/components/Header.tsx (1)
51-78: LGTM! Loading states provide good UX feedback.The implementation correctly:
- Disables both buttons during navigation to prevent double-clicks
- Provides visual feedback with the spinner
- Uses the BFCache reset hook to clear state on back navigation
- Applies consistent behavior to both desktop and mobile variants
app/components/SidebarUserNav.tsx (2)
338-350: LGTM! Logout loading state follows consistent pattern.The implementation correctly:
- Disables the menu item during logout to prevent duplicate requests
- Replaces the icon with a spinner for visual feedback
- Updates the text to indicate the ongoing operation
- Uses the BFCache reset hook to clear state on back navigation
108-111: No changes needed. TheclientLogout()function is synchronous and always navigates away viawindow.location.href, so the loading state doesn't persist—the page unloads. Error handling inhandleLogOutis unnecessary sinceclientLogout()handles its own errors internally and guarantees navigation.Likely an incorrect or invalid review comment.
app/components/AccountTab.tsx (1)
88-114: LGTM! Billing redirect states follow consistent pattern.The implementation correctly:
- Disables all billing controls during redirect to prevent duplicate requests
- Shows "Redirecting..." text with spinner in the dropdown trigger
- Renders spinner icon in the standalone manage button
- Disables the cancel subscription option during redirect
- Uses the BFCache reset hook to clear state on back navigation
- Maintains consistent UX across all billing-related actions
Also applies to: 185-197
| pub fn run() { | ||
| tauri::Builder::default() | ||
| .plugin(tauri_plugin_os::init()) | ||
| .plugin(tauri_plugin_process::init()) | ||
| .plugin(tauri_plugin_shell::init()) | ||
| .plugin(tauri_plugin_updater::Builder::new().build()) | ||
| .setup(|app| { | ||
| let go_back_item = MenuItemBuilder::new("Go Back") | ||
| .id("go_back") | ||
| .accelerator("CmdOrCtrl+[") | ||
| .build(app)?; | ||
|
|
||
| let navigation_menu = SubmenuBuilder::new(app, "Navigation") | ||
| .item(&go_back_item) | ||
| .build()?; | ||
|
|
||
| let menu = MenuBuilder::new(app) | ||
| .items(&[&navigation_menu]) | ||
| .build()?; | ||
|
|
||
| app.set_menu(menu)?; | ||
|
|
||
| log::info!("HackerAI Desktop initialized with navigation menu"); | ||
| Ok(()) | ||
| }) | ||
| .on_menu_event(|app, event| { | ||
| if event.id().as_ref() == "go_back" { | ||
| if let Some(window) = app.get_webview_window("main") { | ||
| navigate_back(&window); | ||
| } | ||
| } | ||
| }) | ||
| .run(tauri::generate_context!()) | ||
| .expect("error while running tauri application"); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Search for OAuth protocol handler registration
echo "=== Checking for tauri.conf.json files ==="
find packages/desktop -name "tauri.conf.json" -type f
echo -e "\n=== Searching for hackerai:// protocol references ==="
rg -n "hackerai://" packages/desktop/ 2>/dev/null || echo "No matches found"
echo -e "\n=== Searching for OAuth/deep link handlers ==="
rg -n -i "oauth|deep.?link|protocol.*handler" packages/desktop/ -g '*.{rs,ts,tsx}' 2>/dev/null || echo "No matches found"
echo -e "\n=== Checking tauri config for protocol registration ==="
if [ -f packages/desktop/tauri.conf.json ]; then
cat packages/desktop/tauri.conf.json | jq '.tauri // .identifier // empty' 2>/dev/null || cat packages/desktop/tauri.conf.json
fi
echo -e "\n=== Searching for assetProtocol or scheme in config ==="
rg -n "assetProtocol|scheme|protocol" packages/desktop/*.json 2>/dev/null || echo "No matches found"Repository: hackerai-tech/hackerai
Length of output: 419
🏁 Script executed:
#!/bin/bash
echo "=== Reading tauri.conf.json ==="
cat packages/desktop/src-tauri/tauri.conf.json
echo -e "\n=== Searching for auth or callback references ==="
rg -n "auth|callback|redirect" packages/desktop/ -g '*.{rs,ts,tsx,json}' --max-count 20
echo -e "\n=== Checking for OAuth in frontend code ==="
rg -n "oauth|auth" packages/desktop/src/ -g '*.{ts,tsx,js,jsx}' --max-count 20
echo -e "\n=== Checking for environment or config variables ==="
rg -n "OAUTH|PROTOCOL|DEEP" packages/desktop/ -g '*.{rs,ts,tsx,json,env}' --max-count 20Repository: hackerai-tech/hackerai
Length of output: 4520
🏁 Script executed:
#!/bin/bash
echo "=== Searching for TODO/FIXME related to protocol or OAuth ==="
rg -n "todo|fixme" packages/desktop/src-tauri/ -i --max-count 20
echo -e "\n=== Searching for any protocol handler setup ==="
rg -n "protocol|handler|register" packages/desktop/src-tauri/src/ -i --max-count 20
echo -e "\n=== Reading entire lib.rs to see full OAuth context ==="
wc -l packages/desktop/src-tauri/src/lib.rs && cat -n packages/desktop/src-tauri/src/lib.rs
echo -e "\n=== Checking for main.rs or other entry points ==="
find packages/desktop/src-tauri/src -name "*.rs" -type fRepository: hackerai-tech/hackerai
Length of output: 2491
🏁 Script executed:
cat -n packages/desktop/src-tauri/src/main.rsRepository: hackerai-tech/hackerai
Length of output: 484
🏁 Script executed:
echo "=== Final check: searching for any WorkOS/OAuth setup in entire src-tauri ==="
find packages/desktop/src-tauri/src -type f -name "*.rs" | xargs wc -l
rg -n "workos|oauth" packages/desktop/src-tauri/ -iRepository: hackerai-tech/hackerai
Length of output: 1824
Implement OAuth deep link protocol handler for the hackerai:// custom protocol.
The Rust backend code and tauri.conf.json have no protocol handler registration for deep link callbacks. The codebase allows connections to auth.hackerai.co and api.workos.com in the CSP, indicating OAuth via web flow only. If the PR requires OAuth authentication with deep link callbacks using the hackerai:// protocol, you need to:
- Register the protocol in
tauri.conf.jsonundertauri.macOS.protocolsor equivalent platform config - Implement a handler in the Rust backend to process incoming
hackerai://callbacks - Pass callback data (auth tokens, codes, etc.) to the frontend for OAuth completion
- Add deep link support (hackerai://) for auth callback - Create /desktop-login route that initiates OAuth flow - Create /api/auth/desktop-callback for WorkOS OAuth handling - Create /desktop-callback for session token exchange - Add Redis-based transfer token system for secure session handoff - Add useTauri hook for Tauri environment detection - Configure Tauri capabilities for shell.open from remote URLs - Add configurable build script (APP_URL env var) - Remove loading spinners from auth buttons
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 8
🤖 Fix all issues with AI agents
In @app/api/auth/desktop-callback/route.ts:
- Around line 100-104: The deepLinkUrl is being interpolated directly into HTML
and a script (the anchor href and window.location.href) which is a fragile XSS
vector; update the code that builds/embeds deepLinkUrl so it is safely encoded
for each context — HTML-escape or use a safe serializer for the href and
URL-encode or JSON.stringify the value when embedding into the inline script;
ensure the code path that constructs deepLinkUrl (where transferToken and origin
are combined) validates/whitelists origin and uses encodeURIComponent for query
parts and a proper HTML-escape for attributes before assigning to deepLinkUrl or
before embedding it into window.location.href to eliminate potential injection.
In @app/components/Header.tsx:
- Around line 7-27: An exception thrown by openInBrowser (via useTauri) can
abort handleSignIn and prevent the fallback navigation; wrap the call to
openInBrowser in a try/catch inside handleSignIn (and the analogous sign-up
handler at lines 29-39) so any error is caught, log or ignore the error, and
always fall back to setting window.location.href = "/login" (or the sign-up
route) when openInBrowser fails or returns falsy. Ensure you reference the
isTauri check first, call openInBrowser inside try, and in the catch block
proceed with the fallback navigation.
In @app/hooks/useTauri.ts:
- Around line 14-37: detectTauri/useTauri are fine but openInBrowser is calling
non‑existent shell.open; switch to Tauri v2 Opener plugin: check for
window.__TAURI__?.opener?.openUrl and call await
window.__TAURI__.opener.openUrl(url) inside a try/catch to return true on
success and false on failure, and ensure the desktop package installs
@tauri-apps/plugin-opener so the opener API is available.
In @lib/desktop-auth.ts:
- Around line 25-31: The generateTransferToken function uses
crypto.getRandomValues without an explicit import or safe fallback; update the
module to either import Node's crypto API (e.g., import { randomFillSync } from
'node:crypto' at top-level) and use it to populate the Uint8Array, or use
globalThis.crypto but add a runtime guard that falls back to the imported Node
crypto implementation when globalThis.crypto is unavailable; ensure you
reference generateTransferToken and replace the direct crypto.getRandomValues
call with the chosen safe approach (top-level import preferred over require).
In @packages/desktop/package.json:
- Around line 14-26: The package.json lists outdated Tauri packages; update the
dependency versions for the Tauri packages in packages/desktop/package.json by
bumping "@tauri-apps/api" to ^2.9.0, "@tauri-apps/plugin-updater" to ^2.9.0, and
all other "@tauri-apps/plugin-*" entries currently ^2.2.0 to ^2.9.0, and update
the devDependency "@tauri-apps/cli" to ^2.8.4; ensure the caret ranges are
preserved (e.g., "^2.9.0") and run your package manager to verify lockfile and
compatibility.
In @packages/desktop/src-tauri/Cargo.toml:
- Around line 1-42: The Cargo.toml sets rust-version = "1.70" which is below the
minimum required by tauri-build = "2"; update the rust-version entry in the
[package] section (symbol: rust-version) to "1.78" or higher so the project
meets tauri-build v2's minimum Rust version requirement and rebuilds cleanly
(verify the change in the existing Cargo.toml where tauri-build is declared
under [build-dependencies]).
In @packages/desktop/src-tauri/src/lib.rs:
- Around line 19-42: handle_auth_deep_link currently trusts an external origin
query and uses callback_url.parse().unwrap(), opening a phishing vector and
risking a panic; instead validate and allowlist the origin before using it
(e.g., accept only known schemes/hosts like https://hackerai.co and any other
explicit trusted hosts), build the callback URL from that trusted base, parse it
with Url::parse (or url::Url::join) and handle the Result (log and return on
Err) rather than unwrap, and only call window.navigate with a safely validated
Url; update references in the function (origin, callback_url, window.navigate)
accordingly.
- Around line 44-94: Wrap desktop-only plugin registrations and the runtime
deep-link setup in run(): gate the tauri_plugin_updater::Builder::new().build()
and any other plugins that are desktop-only with #[cfg(desktop)] so they are not
initialized on mobile, and also gate the runtime deep-link setup block that uses
DeepLinkExt and handle_auth_deep_link with #[cfg(desktop)]; for mobile targets
remove or refactor the app.deep_link().on_open_url(...) registration (or move
deep-link handling to config-time/manifest-level setup for Android/iOS) so
tauri_plugin_updater and the runtime deep-link code are never executed on mobile
builds.
🧹 Nitpick comments (6)
app/desktop-login/route.ts (1)
11-15: Consider validating environment variables at startup.Using non-null assertion (
!) onWORKOS_CLIENT_IDwill cause a runtime crash if the variable is missing. While this may be acceptable for a required config, consider adding explicit validation or using a config module that validates at startup.🔧 Suggested improvement
+const clientId = process.env.WORKOS_CLIENT_ID; +if (!clientId) { + throw new Error("WORKOS_CLIENT_ID environment variable is required"); +} + const authorizationUrl = workos.userManagement.getAuthorizationUrl({ provider: "authkit", - clientId: process.env.WORKOS_CLIENT_ID!, + clientId, redirectUri: desktopCallbackUrl, });packages/desktop/src-tauri/capabilities/default.json (1)
11-17: Consider restrictingshell:allow-openURLs to known domains.The current configuration allows opening any HTTP/HTTPS URL via
shell:allow-open. While this is convenient, it could be a security concern if the app is compromised. Consider restricting to specific domains needed for the OAuth flow.🔒 More restrictive configuration example
{ "identifier": "shell:allow-open", "allow": [ - { "path": "http://**", "scheme": "http" }, - { "path": "https://**", "scheme": "https" } + { "path": "http://localhost:*/**", "scheme": "http" }, + { "path": "https://hackerai.co/**", "scheme": "https" }, + { "path": "https://*.workos.com/**", "scheme": "https" } ] }That said, if the desktop app legitimately needs to open arbitrary user-facing links (e.g., documentation, external resources), the broader permission may be acceptable.
packages/desktop/scripts/build.js (2)
9-22: Consider using a temporary file to avoid corrupted state.If the build process is forcefully terminated (SIGKILL, power loss), the
finallyblock may not execute, leavingindex.htmlin a modified state. Consider writing to a temporary file and using that for the build instead of modifying the source file in-place.🔧 Safer approach using a temp directory
const APP_URL = process.env.APP_URL || "https://hackerai.co"; const srcPath = path.join(__dirname, "../src/index.html"); +const distPath = path.join(__dirname, "../src-dist"); const original = fs.readFileSync(srcPath, "utf-8"); const modified = original.replace(/__APP_URL__/g, APP_URL); -fs.writeFileSync(srcPath, modified); +// Create a dist directory with the modified file +fs.mkdirSync(distPath, { recursive: true }); +fs.writeFileSync(path.join(distPath, "index.html"), modified); console.log(`Building with APP_URL=${APP_URL}`); try { - const args = ["tauri", "build", ...process.argv.slice(2)]; + // Point tauri to use the dist directory (requires tauri config adjustment) + const args = ["tauri", "build", ...process.argv.slice(2)]; execFileSync("pnpm", args, { cwd: path.join(__dirname, ".."), stdio: "inherit", }); } finally { - fs.writeFileSync(srcPath, original); + fs.rmSync(distPath, { recursive: true, force: true }); }Alternatively, this is a minor concern if
.gitignoreincludessrc/index.htmlchanges or if developers are aware they should rungit checkoutafter interrupted builds.
6-6: Consider validating APP_URL format.The
APP_URLis directly injected into HTML without validation. While the risk is low (it's a build-time environment variable controlled by developers/CI), validating it as a proper URL would add a safety layer.🔧 Add URL validation
const APP_URL = process.env.APP_URL || "https://hackerai.co"; + +try { + new URL(APP_URL); +} catch { + console.error(`Invalid APP_URL: ${APP_URL}`); + process.exit(1); +} + const srcPath = path.join(__dirname, "../src/index.html");lib/desktop-auth.ts (1)
11-23: Avoid recreating Redis client per call (and consider lazy singleton).
getRedis()constructs a newRedisinstance every invocation; in serverless this can be noisy/expensive.app/components/ChatHeader.tsx (1)
25-106: Deduplicate desktop-login navigation logic shared withHeader.tsx.
Consider a small helper (e.g.,tryOpenDesktopLogin(): Promise<boolean>) to avoid divergence and keep behavior consistent.
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (6)
packages/desktop/src-tauri/Cargo.lockis excluded by!**/*.lockpackages/desktop/src-tauri/gen/schemas/acl-manifests.jsonis excluded by!**/gen/**packages/desktop/src-tauri/gen/schemas/capabilities.jsonis excluded by!**/gen/**packages/desktop/src-tauri/gen/schemas/desktop-schema.jsonis excluded by!**/gen/**packages/desktop/src-tauri/gen/schemas/macOS-schema.jsonis excluded by!**/gen/**pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (17)
app/api/auth/desktop-callback/route.tsapp/components/ChatHeader.tsxapp/components/Header.tsxapp/components/SidebarUserNav.tsxapp/desktop-callback/route.tsapp/desktop-login/route.tsapp/hooks/useTauri.tslib/desktop-auth.tsmiddleware.tspackage.jsonpackages/desktop/package.jsonpackages/desktop/scripts/build.jspackages/desktop/src-tauri/Cargo.tomlpackages/desktop/src-tauri/capabilities/default.jsonpackages/desktop/src-tauri/src/lib.rspackages/desktop/src-tauri/tauri.conf.jsonpackages/desktop/src/index.html
🚧 Files skipped from review as they are similar to previous changes (3)
- packages/desktop/src-tauri/tauri.conf.json
- packages/desktop/src/index.html
- package.json
🧰 Additional context used
🧬 Code graph analysis (3)
app/desktop-callback/route.ts (3)
app/api/auth/desktop-callback/route.ts (1)
GET(8-60)app/desktop-login/route.ts (1)
GET(6-18)lib/desktop-auth.ts (1)
exchangeDesktopTransferToken(57-83)
app/components/Header.tsx (1)
app/hooks/useTauri.ts (2)
useTauri(21-24)openInBrowser(26-37)
app/components/ChatHeader.tsx (1)
app/hooks/useTauri.ts (2)
useTauri(21-24)openInBrowser(26-37)
🔇 Additional comments (9)
app/components/SidebarUserNav.tsx (1)
103-105: > Likely an incorrect or invalid review comment.app/desktop-login/route.ts (1)
6-17: LGTM!The desktop login route correctly initiates the OAuth flow by constructing the authorization URL and redirecting to WorkOS. The dynamic callback URL construction from
url.originensures proper handling across environments.middleware.ts (1)
21-24: LGTM!The three new unauthenticated paths are correctly configured for the desktop OAuth flow:
/api/auth/desktop-callback- receives OAuth code from WorkOS/desktop-login- initiates the login flow/desktop-callback- exchanges transfer token for sessionAll must be public as authentication has not yet occurred at these stages.
app/desktop-callback/route.ts (2)
4-16: LGTM!The token validation flow correctly handles missing and invalid tokens by redirecting to login. The single-use nature of the transfer token (deleted upon exchange in
exchangeDesktopTransferToken) provides good security against token replay attacks.
20-25: The session-only cookie configuration is consistent throughout the codebase.The
wos-sessioncookie is set withoutmaxAgeorexpiresin bothapp/desktop-callback/route.tsandapp/api/entitlements/route.ts, establishing a consistent pattern of session-only cookies (cleared when the browser closes). This appears to be intentional design rather than an oversight. If session persistence across app restarts is needed in the future, both locations would need to be updated together.packages/desktop/src-tauri/capabilities/default.json (1)
6-8: LGTM!The remote URL configuration correctly scopes access to localhost (for development) and the production domain. This prevents the webview from loading arbitrary remote content.
app/api/auth/desktop-callback/route.ts (2)
20-45: LGTM - solid authentication flow.The authentication flow correctly:
- Validates the OAuth code from WorkOS
- Seals the session data securely with iron-session
- Creates a single-use transfer token via Redis
- Handles errors gracefully without leaking sensitive information
The deep link approach with transfer tokens is a secure pattern for bridging browser-based OAuth to desktop apps.
109-151: LGTM - error page implementation.The error page uses hardcoded messages and a safe error deep link. The styling is consistent with the success page.
lib/desktop-auth.ts (1)
33-55: Use GETDEL to make token exchange atomic and wrap parse/Redis operations in try/catch returning null.The current implementation has two critical issues:
Non-atomic read→delete race condition (lines 70–75): The separate
getanddelcalls create a window where another request can fetch the same token, enabling replay attacks or double-exchange of transfer tokens.Unhandled JSON.parse error (line 78): If Redis returns corrupted data,
JSON.parse()throws an uncaught exception, crashing the route handler with a 500 error. The caller (app/desktop-callback/route.ts:13) has no try/catch.Similarly,
createDesktopTransferToken(line 52) should wrapredis.setin a try/catch for consistency, rather than relying on caller error handling.@upstash/redis supports
GETDEL(atomic read+delete in a single operation). Use it with try/catch:Suggested fix
export async function exchangeDesktopTransferToken( transferToken: string, ): Promise<{ sealedSession: string } | null> { const redis = getRedis(); if (!redis) { console.error( "[Desktop Auth] Redis not configured, cannot exchange transfer token", ); return null; } const key = `${TRANSFER_TOKEN_PREFIX}${transferToken}`; - const rawData = await redis.get<string>(key); - if (!rawData) { - return null; - } - - await redis.del(key); - - const data: TransferTokenData = - typeof rawData === "string" ? JSON.parse(rawData) : rawData; + try { + // Use GETDEL for atomic read+delete to prevent replay races. + const rawData = await (redis as any).getdel<string>(key); + if (!rawData) return null; + + const data: TransferTokenData = + typeof rawData === "string" ? JSON.parse(rawData) : (rawData as any); - return { - sealedSession: data.sealedSession, - }; + return { sealedSession: data.sealedSession }; + } catch (e) { + console.error("[Desktop Auth] Failed to exchange transfer token:", e); + return null; + } }Also wrap
redis.setincreateDesktopTransferTokensimilarly.Likely an incorrect or invalid review comment.
- Fix open redirect vulnerability by validating origin against allowlist - Add try-catch around all Redis operations with proper logging - Add XSS protection with HTML escaping in rendered pages - Add token format validation to prevent injection - Add user feedback with error query params on redirect failures - Add error handling to openInBrowser with window.open fallback - Fix potential panic in Rust URL parsing with proper error handling - Add logging for missing tokens and auth errors in deep links
- Read WORKOS_COOKIE_MAX_AGE from env to match WorkOS AuthKit config - Fall back to 400 days default (WorkOS default, Chrome maximum) - Redact transfer token in Rust logs to first 8 chars only
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In @packages/desktop/src-tauri/src/lib.rs:
- Around line 19-23: The origin check using starts_with is insecure; update
validate_origin to perform exact origin matching instead of prefix matching:
either compare the incoming origin string for equality against entries in
ALLOWED_ORIGINS, or (preferably) parse the origin with a URL parser (e.g.,
url::Url) and compare scheme, host, and port against each allowed origin parsed
the same way; change references in validate_origin and ALLOWED_ORIGINS
accordingly so inputs like "https://hackerai.co.evil.com" or
"http://localhost:3000.attacker.com" no longer match.
🧹 Nitpick comments (3)
app/hooks/useTauri.ts (1)
26-44: Clarify return value semantics and consider URL validation.The function has ambiguous return value semantics:
- Line 39 returns
falseafter thewindow.openfallback, even thoughwindow.openmay have succeeded- Line 43 returns
falsewhen not in Tauri, which doesn't indicate whether any action was takenConsider:
- Returning
truewhenwindow.openis called (since it's a valid fallback)- Adding JSDoc comments to clarify what
true/falserepresent- Adding basic URL validation to reject obviously invalid inputs like empty strings or
javascript:protocol URLs♻️ Example improvement with clearer semantics
+/** + * Opens a URL in the system browser when running in Tauri, + * otherwise falls back to window.open. + * @param url - The URL to open + * @returns true if opened via Tauri shell, false otherwise + */ export async function openInBrowser(url: string): Promise<boolean> { if (typeof window === "undefined") { return false; } + + // Basic URL validation + if (!url || url.trim() === "" || url.startsWith("javascript:")) { + console.error("[Tauri] Invalid URL:", url); + return false; + } if (window.__TAURI__?.shell?.open) { try { await window.__TAURI__.shell.open(url); return true; } catch (err) { console.error("[Tauri] Failed to open URL in browser:", url, err); // Fallback to window.open window.open(url, "_blank"); - return false; + return false; // Tauri failed, used fallback } } return false; }packages/desktop/src-tauri/src/lib.rs (1)
98-125: Menu setup is correct; consider more graceful error handling.The navigation menu and event handling are implemented correctly. The
CmdOrCtrl+[accelerator follows platform conventions.Line 124: Using
.expect()will panic on errors. While acceptable for app initialization (the app cannot proceed without proper setup), consider adding more context to the panic message or using a more graceful shutdown.♻️ Optional: More descriptive error message
.run(tauri::generate_context!()) - .expect("error while running tauri application"); + .expect("Failed to initialize Tauri application. Check logs for details.");app/api/auth/desktop-callback/route.ts (1)
119-162: Consider more flexible error recovery.The error page properly escapes the error message. However, line 158 uses a hardcoded deep link (
hackerai://auth?error=auth_failed) which may not be appropriate for all error scenarios (e.g., if the user isn't actually in the desktop app).♻️ Optional: More flexible error handling
Consider accepting an optional fallback URL parameter or providing multiple recovery options:
-function renderErrorPage(message: string): string { +function renderErrorPage(message: string, fallbackUrl?: string): string { const safeMessage = escapeHtml(message); + const safeUrl = fallbackUrl ? escapeHtml(fallbackUrl) : "hackerai://auth?error=auth_failed"; return ` <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Authentication Error</title> ... <body> <div class="container"> <h1>Authentication Error</h1> <p>${safeMessage}</p> - <a href="hackerai://auth?error=auth_failed">Return to App</a> + <a href="${safeUrl}">Return to App</a> </div> </body> </html>`; }
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (6)
.cursor/rules/convex_rules.mdcapp/api/auth/desktop-callback/route.tsapp/desktop-callback/route.tsapp/hooks/useTauri.tslib/desktop-auth.tspackages/desktop/src-tauri/src/lib.rs
✅ Files skipped from review due to trivial changes (1)
- .cursor/rules/convex_rules.mdc
🚧 Files skipped from review as they are similar to previous changes (2)
- app/desktop-callback/route.ts
- lib/desktop-auth.ts
🔇 Additional comments (9)
app/hooks/useTauri.ts (3)
1-12: LGTM!The type augmentation correctly declares the Tauri global interfaces with proper optional chaining support.
14-19: LGTM!The Tauri detection logic properly guards against SSR and checks both Tauri properties for compatibility.
21-24: LGTM!Clean hook implementation that follows React conventions.
packages/desktop/src-tauri/src/lib.rs (3)
1-18: LGTM!The platform-specific navigation implementation is appropriate. The
unsafeblock is necessary for Objective-C FFI, and thecanGoBackguard prevents invalid navigation attempts.
25-73: Good error handling and defensive coding.The deep link handler includes several good practices:
- Defensive path checking for different auth URL variants (line 30)
- Token redaction in logs for security (line 45)
- Comprehensive error handling with fallback navigation (lines 47-61)
- Logging for debugging without exposing sensitive data
Note: This function relies on
validate_originwhich has a critical security flaw flagged separately.
75-97: LGTM!The plugin initialization and deep link registration are implemented correctly. The desktop-only conditional compilation and handle cloning for the closure follow Tauri best practices.
app/api/auth/desktop-callback/route.ts (3)
1-15: LGTM!The HTML escaping function properly handles the essential characters to prevent XSS attacks in the rendered HTML pages.
17-69: Authentication flow is well-structured with good error handling.The handler properly:
- Validates input parameters before processing
- Handles WorkOS authentication errors gracefully
- Checks for transfer token creation failures
- Returns appropriate HTTP status codes (400 for client errors, 500 for server errors)
The origin at line 56 is derived from
url.origin(the API route's own origin), which is safe and will be validated by the Rust deep link handler.
71-117: LGTM!The success page properly escapes the deep link URL before rendering, includes both automatic redirect and manual fallback, and follows good UX practices.
- Fix XSS vulnerability by using JSON.stringify for JS context instead of HTML escaping - Fix origin validation bypass by properly parsing URLs and validating exact host match - Add OAuth state parameter for CSRF protection with Redis-backed verification - Add error handling to /desktop-login route with graceful fallbacks
- Add success toast when files are downloaded in desktop app - Show "Saved to Downloads folder" description with "Show in folder" action - Add tauri-plugin-opener for opening Downloads folder directly - Add isTauriEnvironment() helper for Tauri detection
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 4
🤖 Fix all issues with AI agents
In @app/api/auth/desktop-callback/route.ts:
- Around line 47-63: The code uses non-null assertions for
process.env.WORKOS_CLIENT_ID and process.env.WORKOS_COOKIE_PASSWORD when calling
workos.userManagement.authenticateWithCode and sealData; add explicit validation
at the start of the desktop callback (before calling authenticateWithCode) that
checks these env vars and returns a clear HTTP error/Response (or throws a
descriptive Error) if missing, then use the validated values (e.g., const
clientId = process.env.WORKOS_CLIENT_ID; if (!clientId) return new
Response("Missing WORKOS_CLIENT_ID", { status: 500 });) so
authenticateWithCode({ code, clientId }) and sealData(..., { password }) do not
rely on '!' assertions.
In @lib/desktop-auth.ts:
- Around line 65-115: The exchangeDesktopTransferToken function currently does a
non-atomic redis.get() followed by redis.del(), allowing a race where multiple
callers can read the token before it's deleted; modify
exchangeDesktopTransferToken to use the atomic redis.getdel(key) (supported by
@upstash/redis v1.35.7) when retrieving the transfer token so the key is removed
as part of the read, keep the existing TOKEN_FORMAT_REGEX validation and error
handling, parse the returned rawData into TransferTokenData and return {
sealedSession: data.sealedSession }, and remove the separate redis.del() block
(or treat any getdel errors the same way you currently handle get errors) so
single-use semantics are guaranteed for TRANSFER_TOKEN_PREFIX tokens obtained
via getRedis().
In @packages/desktop/src-tauri/Cargo.toml:
- Line 7: The Cargo.toml currently sets rust-version = "1.70" which is below
Tauri 2's requirement; update the rust-version field in Cargo.toml to "1.78" so
the manifest's rust-version key reflects the minimum Rust 1.78 required by Tauri
2.
In @packages/desktop/src-tauri/src/lib.rs:
- Line 53: The callback URL currently inserts the raw token into callback_url
causing issues with special characters; URL-encode token before building the
string. In the handler that constructs callback_url (using origin and token),
add URL-encoding for token (e.g., via url::form_urlencoded::byte_serialize or
the urlencoding crate) and replace token with the encoded_token when formatting
callback_url, and add the chosen crate to Cargo.toml dependencies.
🧹 Nitpick comments (6)
packages/desktop/src-tauri/Cargo.toml (2)
9-11: Reconsider the necessity of multiple crate types.Declaring
cdylibandstaticlibcrate types is typically for creating C-compatible libraries or FFI bindings. For a desktop application, you usually only need the binary target. These additional crate types increase compilation time and output size without providing clear benefits.♻️ Simplify to binary-only unless FFI is needed
If you don't need to expose a C-compatible library interface, remove the
[lib]section entirely and keep only the binary:-[lib] -name = "hackerai_desktop_lib" -crate-type = ["lib", "cdylib", "staticlib"] - [[bin]] name = "hackerai-desktop" path = "src/main.rs"If you do need library functionality for internal modules, use only:
[lib] name = "hackerai_desktop_lib" -crate-type = ["lib", "cdylib", "staticlib"] +crate-type = ["lib"]
20-32: Consider using more specific version constraints.All dependencies use major-version-only constraints (e.g.,
"2","1"), which means any minor or patch version within that major version can be selected. While this maximizes compatibility, it can lead to:
- Unexpected breaking changes in minor versions (especially for 0.x versions)
- Non-reproducible builds across environments
- Potential security issues if a compromised minor version is published
Consider using caret requirements (e.g.,
"^2.0") or tilde requirements (e.g.,"~2.0") for more control, or use exact versions in production:-tauri = { version = "2", features = [] } +tauri = { version = "^2.0", features = [] }Or for maximum reproducibility, use a
Cargo.lockfile (which you should be committing for applications) and consider exact versions for critical dependencies.app/hooks/useTauri.ts (1)
25-28: Consider makinguseTaurireactive for SSR hydration edge cases.The hook currently calls
detectTauri()synchronously without React state. During SSR hydration,window.__TAURI__may not be immediately available, potentially causing a hydration mismatch.Optional: Make the hook reactive
+import { useState, useEffect } from "react"; + export function useTauri(): { isTauri: boolean } { - const isTauri = detectTauri(); - return { isTauri }; + const [isTauri, setIsTauri] = useState(false); + + useEffect(() => { + setIsTauri(detectTauri()); + }, []); + + return { isTauri }; }app/desktop-login/route.ts (1)
5-5: Consider validatingWORKOS_API_KEYavailability.The WorkOS client is instantiated at module load time. If
WORKOS_API_KEYis missing, this could throw an unclear error during module initialization rather than a graceful redirect.Optional: Lazy initialization with validation
-const workos = new WorkOS(process.env.WORKOS_API_KEY); +let workos: WorkOS | null = null; +function getWorkOS(): WorkOS { + if (!workos) { + if (!process.env.WORKOS_API_KEY) { + throw new Error("WORKOS_API_KEY not configured"); + } + workos = new WorkOS(process.env.WORKOS_API_KEY); + } + return workos; +} export async function GET(request: Request) { // ... - const authorizationUrl = workos.userManagement.getAuthorizationUrl({ + const authorizationUrl = getWorkOS().userManagement.getAuthorizationUrl({lib/desktop-auth.ts (2)
1-12: Prevent accidental client-side imports (server-only).Given this touches Redis + server secrets, consider marking the module as server-only to avoid accidental imports from client components (which can also bloat client bundles).
Proposed change
+import "server-only"; import { Redis } from "@upstash/redis";
14-26: Memoize the Redis client to avoid re-instantiation per call.
getRedis()constructs a newRedisinstance each time; memoizing reduces overhead and avoids subtle differences across calls.Proposed change
import { Redis } from "@upstash/redis"; +let redisClient: Redis | null | undefined; + function getRedis(): Redis | null { + if (redisClient !== undefined) return redisClient; const redisUrl = process.env.UPSTASH_REDIS_REST_URL; const redisToken = process.env.UPSTASH_REDIS_REST_TOKEN; if (!redisUrl || !redisToken) { - return null; + redisClient = null; + return redisClient; } - return new Redis({ + redisClient = new Redis({ url: redisUrl, token: redisToken, }); + return redisClient; }
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (6)
packages/desktop/src-tauri/Cargo.lockis excluded by!**/*.lockpackages/desktop/src-tauri/gen/schemas/acl-manifests.jsonis excluded by!**/gen/**packages/desktop/src-tauri/gen/schemas/capabilities.jsonis excluded by!**/gen/**packages/desktop/src-tauri/gen/schemas/desktop-schema.jsonis excluded by!**/gen/**packages/desktop/src-tauri/gen/schemas/macOS-schema.jsonis excluded by!**/gen/**pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (11)
app/api/auth/desktop-callback/route.tsapp/components/AllFilesDialog.tsxapp/components/FilePartRenderer.tsxapp/desktop-login/route.tsapp/hooks/useTauri.tslib/desktop-auth.tspackage.jsonpackages/desktop/package.jsonpackages/desktop/src-tauri/Cargo.tomlpackages/desktop/src-tauri/capabilities/default.jsonpackages/desktop/src-tauri/src/lib.rs
🚧 Files skipped from review as they are similar to previous changes (2)
- packages/desktop/package.json
- package.json
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-12-24T18:09:08.574Z
Learnt from: CR
Repo: hackerai-tech/hackerai PR: 0
File: .cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-24T18:09:08.574Z
Learning: Applies to convex/**/*.{ts,js} : Always add `'use node';` to the top of files containing actions that use Node.js built-in modules
Applied to files:
lib/desktop-auth.ts
📚 Learning: 2025-12-24T18:09:08.574Z
Learnt from: CR
Repo: hackerai-tech/hackerai PR: 0
File: .cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-24T18:09:08.574Z
Learning: Applies to convex/**/crons.ts : If a cron calls an internal function, always import the `internal` object from `_generated/api` even if the internal function is defined in the same `crons.ts` file
Applied to files:
lib/desktop-auth.ts
🧬 Code graph analysis (4)
app/components/AllFilesDialog.tsx (1)
app/hooks/useTauri.ts (2)
isTauriEnvironment(21-23)openDownloadsFolder(49-67)
app/desktop-login/route.ts (5)
app/api/workos.ts (1)
workos(7-7)__mocks__/workos-node.ts (1)
WorkOS(21-21)app/api/auth/desktop-callback/route.ts (1)
GET(17-87)app/desktop-callback/route.ts (1)
GET(15-42)lib/desktop-auth.ts (1)
createOAuthState(117-135)
app/api/auth/desktop-callback/route.ts (1)
lib/desktop-auth.ts (2)
verifyAndConsumeOAuthState(137-158)createDesktopTransferToken(36-63)
app/components/FilePartRenderer.tsx (1)
app/hooks/useTauri.ts (2)
isTauriEnvironment(21-23)openDownloadsFolder(49-67)
🔇 Additional comments (20)
packages/desktop/src-tauri/Cargo.toml (3)
20-32: Inconsistency between AI summary and actual dependencies.The AI summary mentions dependencies
keyring,tokio,uuid, andreqwestthat are not present in the actual Cargo.toml. This suggests either:
- These dependencies were removed in a later commit
- The AI summary was generated from a different version
- The summary is incorrect
34-36: LGTM! macOS-specific dependencies are appropriate.The platform-specific dependencies for macOS native functionality are properly configured with appropriate feature flags for WebView integration.
38-43: LGTM! Release profile optimizations are appropriate.The release profile is well-configured for a desktop application with aggressive size optimizations. The trade-off of longer compile times is acceptable for release builds, and
panic = "abort"withstrip = truewill produce smaller binaries suitable for distribution.packages/desktop/src-tauri/capabilities/default.json (1)
1-30: Capabilities configuration looks well-scoped.The permissions are appropriately restricted:
- Remote URLs limited to localhost (dev) and production domain
shell:allow-openrestricted to http/https schemesopener:allow-open-pathscoped to$DOWNLOAD/**onlypackages/desktop/src-tauri/src/lib.rs (3)
16-17: Back navigation is a no-op on Windows/Linux.The
navigate_backfunction does nothing on non-macOS platforms. Windows and Linux users clicking "Go Back" (Ctrl+[) will see no effect.Consider implementing platform-specific back navigation for Windows/Linux using JavaScript injection or Tauri commands, or disable/hide the menu item on unsupported platforms.
21-32: Origin validation is well-implemented.Proper URL parsing with scheme validation (https required, http only for localhost) provides solid protection against origin manipulation attacks.
84-135: LGTM on app initialization and plugin setup.The Tauri builder correctly initializes all required plugins and properly wires up deep-link handling with appropriate error logging and fallbacks.
app/hooks/useTauri.ts (2)
30-47: LGTM onopenInBrowserimplementation.Good defensive coding with server-side guard, try/catch, and fallback to
window.open. The return value semantics (false on fallback) are clear from the function signature.
49-67: LGTM onopenDownloadsFolderimplementation.Dynamic imports correctly defer loading Tauri plugins to runtime, with appropriate error handling and logging.
app/components/FilePartRenderer.tsx (1)
140-149: LGTM on Tauri-specific download toast.The success toast with "Show in folder" action provides good UX for desktop users. The implementation correctly gates this to Tauri environments only.
Consider whether browser users should also receive a success toast (without the folder action) for consistency.
app/components/AllFilesDialog.tsx (2)
61-74: Good Tauri download UX with error handling.Success toast with folder action for Tauri, plus error toast on failure. The implementation is consistent with
FilePartRenderer.
347-364: Batch download provides complete feedback for all environments.Unlike single-file downloads, batch ZIP downloads show a success toast for both Tauri and browser environments, which is good UX consistency.
app/desktop-login/route.ts (1)
7-35: OAuth flow implementation looks correct.The handler properly:
- Creates CSRF state via Redis-backed
createOAuthState- Validates required environment variables
- Constructs the authorization URL with state parameter
- Has appropriate error handling with distinct error codes
app/api/auth/desktop-callback/route.ts (3)
8-15: Good XSS prevention withescapeHtml.The escape function covers all necessary HTML special characters. Combined with
JSON.stringifyfor JavaScript context inrenderSuccessPage, this provides robust XSS protection.
38-45: LGTM on OAuth state verification.The
verifyAndConsumeOAuthStatecall properly validates the state parameter and consumes it in a single atomic operation, preventing CSRF attacks and replay attacks.
89-136: Success page rendering is secure.The dual-escaping strategy (
escapeHtmlfor the href attribute,JSON.stringifyfor the JavaScript redirect) correctly handles different injection contexts. The auto-redirect with fallback button provides good UX.lib/desktop-auth.ts (4)
36-63: Solid write path + TTL for transfer tokens.Good use of a short-lived token, namespaced key, and TTL-bound Redis
set. Consider optionally rejecting emptysealedSessionto fail fast.
117-135: OAuth state creation w/ TTL is clean.Creating a random state, namespacing, and TTL storage is straightforward and matches the “server-stored state” CSRF pattern.
137-158: OAuth state consumption is correctly single-use.Using
DELand checkingdeleted === 1gives you atomic consume semantics here—nice.
28-34: Code is correct; Web Crypto API works in Next.js 15 Node.js and Edge runtimes.
crypto.getRandomValues(Web Crypto) is available in Node.js v15.0.0+ and exposed in Next.js Edge runtime. This approach is compatible with both runtimes. One caveat: ensure your bundler does not polyfillnode:cryptointo edge builds—use the platform-native Web Crypto API instead. If using a custom bundler or edge provider, verify that the CSPRNG implementation is not a shim or broken polyfill.
- Add custom user agent (HackerAI-Desktop/1.0) to identify desktop app - Update middleware to detect desktop app and redirect to custom error page - Add error pages in desktop-callback route with Home and Try Again buttons - Make login redirects Tauri-aware in page.tsx and PricingDialog.tsx - Open desktop-login in external browser instead of redirecting to WorkOS
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In @app/desktop-callback/route.ts:
- Around line 15-75: The renderErrorPage function interpolates the title,
message, and retryUrl parameters directly into HTML and JavaScript without
escaping special characters, creating an XSS vulnerability if these parameters
ever contain user-controlled input. Create a helper function that escapes HTML
special characters (such as &, <, >, ", and ') by replacing them with their HTML
entity equivalents. Apply this escaping helper to title and message when they
are interpolated into the HTML content, and to retryUrl when it is used in both
the HTML onclick attribute and the JavaScript code to prevent any injected
scripts from executing.
🧹 Nitpick comments (2)
app/desktop-callback/route.ts (1)
4-13: Consider validating that the parsed value is positive.
parseIntcan return negative numbers, which would pass theNumber.isFinitecheck but result in unexpected cookie behavior (immediate deletion). Consider adding a check forparsed > 0.Suggested fix
function getCookieMaxAge(): number { const envMaxAge = process.env.WORKOS_COOKIE_MAX_AGE; if (envMaxAge) { const parsed = parseInt(envMaxAge, 10); - if (Number.isFinite(parsed)) { + if (Number.isFinite(parsed) && parsed > 0) { return parsed; } } return 60 * 60 * 24 * 400; // 400 days default (WorkOS default) }app/page.tsx (1)
22-33: LGTM! Consider extracting the TAURI login redirect pattern.The async handling and fallback logic are correct. The
openInBrowserhelper handles errors internally and returnsfalseon failure, ensuring the fallback to/loginworks properly.This pattern is duplicated in
PricingDialog.tsx(lines 180-191, 193-204). Consider extracting to a shared helper likehandleTauriLogin()to reduce duplication.
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (5)
app/components/PricingDialog.tsxapp/desktop-callback/route.tsapp/page.tsxmiddleware.tspackages/desktop/src-tauri/tauri.conf.json
🚧 Files skipped from review as they are similar to previous changes (1)
- packages/desktop/src-tauri/tauri.conf.json
🧰 Additional context used
🧬 Code graph analysis (3)
app/page.tsx (1)
app/hooks/useTauri.ts (2)
useTauri(25-28)openInBrowser(30-47)
app/components/PricingDialog.tsx (1)
app/hooks/useTauri.ts (2)
useTauri(25-28)openInBrowser(30-47)
app/desktop-callback/route.ts (3)
app/api/auth/desktop-callback/route.ts (1)
GET(17-87)app/desktop-login/route.ts (1)
GET(7-35)lib/desktop-auth.ts (1)
exchangeDesktopTransferToken(65-115)
🔇 Additional comments (6)
app/desktop-callback/route.ts (1)
77-131: Overall flow looks correct.The GET handler properly validates inputs, exchanges the token, and sets the session cookie with appropriate security attributes (
httpOnly,securein production,sameSite: "lax"). The error handling is comprehensive.One minor consideration: returning 400 for all token exchange failures may mask server-side issues (e.g., Redis unavailable). You might consider returning 500 when
exchangeDesktopTransferTokenfails due to infrastructure issues, similar to howapp/api/auth/desktop-callback/route.tsuses 500 for session transfer failures.middleware.ts (3)
11-14: User-Agent detection is acceptable for UX but not security.The
isDesktopAppcheck relies on User-Agent which can be easily spoofed. This is fine for UX decisions (redirecting to a friendlier error page) but ensure no security-critical logic depends solely on this check.
49-55: Path matching logic handles wildcards correctly.The wildcard pattern matching for
:path*suffix is implemented correctly, converting Next.js route patterns to prefix checks. However, consider extracting this to a reusable utility if similar matching is needed elsewhere.
45-61: No action required — cookie name is correct.The middleware correctly uses
wos-session, which is the default session cookie name for@workos-inc/authkit-nextjsv2.12.0 (the version specified in package.json). The codebase uses this cookie name consistently across all authentication endpoints and test fixtures, with no custom override of theWORKOS_COOKIE_NAMEenvironment variable.app/components/PricingDialog.tsx (2)
156-156: Hook placement is correct.The
useTauri()hook is called at the top level of the component, following React's rules of hooks.
180-204: Both sign-in and sign-up use the same desktop-login flow for Tauri users.For Tauri users, both
handleSignInandhandleSignUpredirect to/desktop-login, unifying the desktop auth flow. This is by design—unlike the web routes which use separate WorkOS functions (getSignInUrl()vsgetSignUpUrl()), the desktop flow uses a single generic authkit provider that doesn't differentiate between signin and signup intents.If you need to preserve the signup vs signin intent distinction in the desktop flow, you would need to modify the
/desktop-loginroute to support intent parameters and use the appropriate WorkOS functions based on that intent. However, this represents a more significant refactoring than passing a query parameter.
- Use Tauri v2 Opener plugin instead of non-existent shell.open API - Update rust-version to 1.78 to meet Tauri 2 MSRV requirement - Make transfer token exchange atomic with redis.getdel() - Add XSS escaping to desktop-callback error pages - Add try-catch to all openInBrowser calls for graceful fallback - Log navigation errors in lib.rs instead of discarding - URL-encode token in Rust callback URL construction - Add explicit env validation before using WORKOS_* variables
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 5
🤖 Fix all issues with AI agents
In @app/api/auth/desktop-callback/route.ts:
- Around line 76-90: The deep link currently inserts transferToken raw into
deepLinkUrl which can break the query string if the token contains reserved
characters; update the deepLinkUrl construction in route.ts to URL-encode the
token (use encodeURIComponent on the value returned from
createDesktopTransferToken) before interpolating it into the
`hackerai://auth?token=...&origin=...` string so renderSuccessPage receives a
safe, properly encoded deep link.
In @app/components/ChatHeader.tsx:
- Around line 83-109: Significant duplication: handleSignIn and handleSignUp in
ChatHeader.tsx replicate logic in Header.tsx; extract the shared behavior into a
single utility (e.g., handleDesktopAuth) in a common module
(app/hooks/useTauri.ts or app/lib/desktop-auth-helpers.ts) that accepts the auth
type ('signin' | 'signup') and isTauri, performs the openInBrowser try/catch and
falls back to setting window.location.href, and then replace the local
handleSignIn and handleSignUp with simple delegations like () =>
handleDesktopAuth('signin', isTauri) and () => handleDesktopAuth('signup',
isTauri) to remove duplication and centralize error handling.
In @packages/desktop/src-tauri/src/lib.rs:
- Around line 36-85: handle_auth_deep_link uses the token query param without
validating its format which risks misuse; add a compiled Regex (e.g., static
TOKEN_FORMAT with OnceLock) and an accessor get_token_format() returning
&'static Regex, then in handle_auth_deep_link (before URL-encoding or
navigating) call get_token_format().is_match(&token) and if it fails log an
error and return; update Cargo.toml to include regex = "1" so the crate builds.
- Around line 21-34: The ALLOWED_HOSTS constant is hardcoded causing deployment
inflexibility; replace it with a lazily-initialized runtime/build-time
configuration: introduce a static OnceLock<Vec<String>> (e.g., ALLOWED_HOSTS or
via get_allowed_hosts()) and implement get_allowed_hosts() to read ALLOWED_HOSTS
from an environment variable (fallback to "hackerai.co,localhost"), parsing on
commas into trimmed strings; then update validate_origin to call
get_allowed_hosts().iter().any(...) instead of using the fixed ALLOWED_HOSTS and
keep the same scheme check logic.
🧹 Nitpick comments (6)
app/components/Header.tsx (1)
17-29: LGTM! Consider adding error logging.The async TAURI-aware sign-in flow correctly attempts desktop browser opening before falling back to web navigation. The early return on success prevents double navigation.
Optional: Log errors for debugging
While the empty catch block is intentional (fall through to web navigation), consider logging errors to aid debugging:
if (isTauri) { try { const opened = await openInBrowser( `${window.location.origin}/desktop-login`, ); if (opened) return; - } catch { + } catch (err) { + console.warn("[Header] Failed to open desktop login:", err); // Fall through to web navigation } }packages/desktop/src-tauri/Cargo.toml (1)
7-7: Document the Rust 1.78 MSRV requirement.The minimum supported Rust version (1.78) should be documented in the project README or development setup guide to ensure contributors have the correct toolchain.
packages/desktop/src-tauri/src/lib.rs (2)
23-34: Origin validation allows potential subdomain bypass.Line 28 checks if
host == *allowed, which only matches exact hosts. However, this doesn't prevent subdomain attacks if an attacker controls a subdomain. For example, ifhackerai.cois allowed,evil.hackerai.cowould not match, which is correct. But consider documenting this behavior and ensuring wildcard domains aren't added to ALLOWED_HOSTS.Add a comment documenting the exact-match behavior:
fn validate_origin(origin: &str) -> bool { match url::Url::parse(origin) { Ok(parsed) => { let host = parsed.host_str().unwrap_or(""); let scheme = parsed.scheme(); + // Exact match only - subdomains must be explicitly allowed let is_allowed_host = ALLOWED_HOSTS.iter().any(|allowed| host == *allowed); let is_valid_scheme = scheme == "https" || (host == "localhost" && scheme == "http"); is_allowed_host && is_valid_scheme } Err(_) => false, } }
59-73: Improve error user experience with more specific error messages.When navigation fails (lines 61-67), the error page only shows
error=navigation_failed. Consider providing more context about the specific failure to help users troubleshoot.Ok(parsed_url) => { if let Err(e) = window.navigate(parsed_url) { - log::error!("Failed to navigate to callback URL: {}", e); + let error_msg = format!("navigation_failed: {}", e); + log::error!("Failed to navigate to callback URL: {}", e); // Try to navigate to error page - let error_url = format!("{}/login?error=navigation_failed", origin); + let error_url = format!("{}/login?error={}", origin, urlencoding::encode(&error_msg)); if let Ok(error_parsed) = error_url.parse() { let _ = window.navigate(error_parsed); } } }
app/desktop-callback/route.ts (1)
103-112: Consider providing more context for unauthenticated error.The
error=unauthenticatedcase (lines 103-112) displays a generic message. If possible, provide more specific guidance about why authentication failed (e.g., expired session, cancelled auth flow).if (error === "unauthenticated") { + const errorDetail = url.searchParams.get("error_description") || "You need to sign in to access this page."; return new Response( renderErrorPage( "Sign In Required", - "You need to sign in to access this page.", + errorDetail, retryUrl ), { status: 401, headers: { "Content-Type": "text/html" } } ); }
app/api/auth/desktop-callback/route.ts (1)
100-147: Harden the inline<script>assignment against</script>termination.
JSON.stringify(...)does not prevent a literal</script>from terminating the script tag in HTML parsing. It’s unlikely here, but cheap to harden.Proposed change
function renderSuccessPage(deepLinkUrl: string): string { const safeUrlForHtml = escapeHtml(deepLinkUrl); - const safeUrlForJs = JSON.stringify(deepLinkUrl); + const safeUrlForJs = JSON.stringify(deepLinkUrl).replace(/</g, "\\u003c"); return ` @@ <script> window.location.href = ${safeUrlForJs}; </script> </body> </html>`; }
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (10)
app/api/auth/desktop-callback/route.tsapp/components/ChatHeader.tsxapp/components/Header.tsxapp/components/PricingDialog.tsxapp/desktop-callback/route.tsapp/hooks/useTauri.tsapp/page.tsxlib/desktop-auth.tspackages/desktop/src-tauri/Cargo.tomlpackages/desktop/src-tauri/src/lib.rs
🚧 Files skipped from review as they are similar to previous changes (4)
- app/hooks/useTauri.ts
- lib/desktop-auth.ts
- app/components/PricingDialog.tsx
- app/page.tsx
🧰 Additional context used
🧬 Code graph analysis (3)
app/components/ChatHeader.tsx (1)
app/hooks/useTauri.ts (2)
useTauri(20-23)openInBrowser(25-38)
app/desktop-callback/route.ts (2)
app/api/auth/desktop-callback/route.ts (1)
GET(17-98)lib/desktop-auth.ts (1)
exchangeDesktopTransferToken(65-114)
app/components/Header.tsx (2)
__mocks__/workos.ts (1)
useAuth(1-7)app/hooks/useTauri.ts (2)
useTauri(20-23)openInBrowser(25-38)
🔇 Additional comments (9)
packages/desktop/src-tauri/Cargo.toml (2)
9-11: Consider if all three crate types are necessary.The library is configured with
lib,cdylib, andstaticlibcrate types. For a standard Tauri desktop application, typically onlylibis needed (the binary target handles the executable). Thecdylibandstaticlibtypes are used for FFI and static linking scenarios.Verify if these additional crate types are required by your build or deployment process. If not used, simplify to:
[lib] name = "hackerai_desktop_lib" -crate-type = ["lib", "cdylib", "staticlib"] +crate-type = ["lib"]
20-32: Ensure Tauri v2 plugins are properly integrated and configured.The dependencies use compatible v2 plugins (all official Tauri v2 plugins). However, test the following integrations before production:
- Updater plugin: Requires configuration in
tauri.conf(pubkey,createUpdaterArtifacts, endpoints). Verify the migration from the built-in v1 updater is complete.- Deep-link plugin: Test on the installed app (not dev mode). Configure schemes properly and verify single-instance integration to avoid duplicated listener callbacks on macOS.
- Listener management: Guard against listener remounting issues when using both deep-link and other event-based plugins together.
app/desktop-callback/route.ts (4)
4-13: LGTM! Robust environment variable parsing.The
getCookieMaxAge()function correctly handles invalid values and provides a sensible default (400 days matching WorkOS convention).
15-22: HTML escaping implementation is correct.The
escapeHtml()function covers the essential HTML special characters for XSS prevention in the error page context.
77-91: Secure TAURI integration in error page retry handler.The dynamic import of
@tauri-apps/plugin-openerwithin the error page is correctly implemented:
- Checks for TAURI environment before importing
- Falls back to standard navigation on failure
- Uses JSON.stringify for safe URL injection (line 79)
142-148: Cookie security settings are appropriate.The session cookie configuration correctly uses:
httpOnly: trueto prevent XSS accesssecurebased on environment (production only)sameSite: "lax"for reasonable CSRF protection- Appropriate
maxAgefrom environmentNote: Consider
sameSite: "strict"if the authentication flow doesn't require cross-site navigation, though"lax"is a reasonable default for most OAuth flows.app/components/Header.tsx (1)
31-43: Both sign-in and sign-up use the same/desktop-loginendpoint in the Tauri flow — confirm this is intentional.Both
handleSignInandhandleSignUpnavigate to/desktop-loginwhen running under Tauri, while the web fallbacks differ (/loginvs/signup). The desktop callback handler does not differentiate between the two flows. If the desktop login endpoint should treat them differently, pass a query parameter to preserve sign-in/sign-up intent.app/api/auth/desktop-callback/route.ts (2)
8-15: HTML escaping looks correct for the current usage.
escapeHtml()is applied to the HTML attribute and message body, which avoids obvious injection in the rendered pages.Also applies to: 149-192
1-6: ValidateWORKOS_COOKIE_PASSWORDlength before callingsealData(avoid silent failures).The
cookiePasswordenvironment variable is checked for presence at line 50 but not for length. Sinceiron-session@8requires thesealDatapassword to be at least 32 characters, a shorter password will causesealDatato throw at line 72, falling into the generic catch block and returning a confusing "Authentication failed" error instead of a clear configuration error. Add an explicit length check:if (!clientId || !cookiePassword) { console.error("[Desktop Auth] Missing required environment variables"); return new Response(renderErrorPage("Server configuration error. Please try again later."), { status: 500, headers: { "Content-Type": "text/html" }, }); } + + if (cookiePassword.length < 32) { + console.error("[Desktop Auth] WORKOS_COOKIE_PASSWORD must be at least 32 characters"); + return new Response(renderErrorPage("Server configuration error. Please try again later."), { + status: 500, + headers: { "Content-Type": "text/html" }, + }); + }Likely an incorrect or invalid review comment.
# Conflicts: # middleware.ts # pnpm-lock.yaml
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
🧹 Nitpick comments (3)
.cursor/rules/convex_rules.mdc (1)
483-484: Consider whether these formatting changes belong in this PR.These JSDoc indentation adjustments appear unrelated to the Tauri desktop app implementation. They may have been inadvertently included through auto-formatting.
If these were accidental, consider reverting them to keep the PR focused on desktop app changes. Otherwise, a brief note in the commit message explaining why guideline documentation was updated would help reviewers understand the connection.
Also applies to: 496-497, 509-510, 535-536
app/components/PricingDialog.tsx (2)
165-178: Consider removing the redundant try-catch.
openInBrowseralready catches exceptions internally and returnsfalseon failure (seeuseTauri.ts), so the outer try-catch here will never execute its catch block. Theif (opened) returncheck alone handles the fallback.Suggested simplification
const handleSignIn = async () => { if (isTauri) { - try { - const opened = await openInBrowser( - `${window.location.origin}/desktop-login`, - ); - if (opened) return; - } catch { - // Fall through to web navigation - } + const opened = await openInBrowser( + `${window.location.origin}/desktop-login`, + ); + if (opened) return; } // eslint-disable-next-line react-hooks/immutability window.location.href = "/login"; };
165-193: Consider extracting duplicate Tauri navigation logic.
handleSignInandhandleSignUpshare identical Tauri handling patterns. For maintainability, this could be extracted into a helper.Example helper extraction
const navigateWithTauriFallback = async (desktopPath: string, webPath: string) => { if (isTauri) { const opened = await openInBrowser(`${window.location.origin}${desktopPath}`); if (opened) return; } window.location.href = webPath; }; const handleSignIn = () => navigateWithTauriFallback("/desktop-login", "/login"); const handleSignUp = () => navigateWithTauriFallback("/desktop-login", "/signup");
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (6)
.cursor/rules/convex_rules.mdcapp/components/AllFilesDialog.tsxapp/components/FilePartRenderer.tsxapp/components/PricingDialog.tsxmiddleware.tspackage.json
🚧 Files skipped from review as they are similar to previous changes (3)
- app/components/FilePartRenderer.tsx
- package.json
- app/components/AllFilesDialog.tsx
🧰 Additional context used
🧠 Learnings (8)
📚 Learning: 2025-12-24T18:09:08.574Z
Learnt from: CR
Repo: hackerai-tech/hackerai PR: 0
File: .cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-24T18:09:08.574Z
Learning: Applies to convex/**/*.{ts,js} : Use `query`, `mutation`, and `action` for public functions that are exposed to the public API
Applied to files:
.cursor/rules/convex_rules.mdc
📚 Learning: 2025-12-24T18:09:08.574Z
Learnt from: CR
Repo: hackerai-tech/hackerai PR: 0
File: .cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-24T18:09:08.574Z
Learning: Applies to convex/**/*.{ts,js} : Use `internalQuery`, `internalMutation`, and `internalAction` for private functions that should not be part of the public API
Applied to files:
.cursor/rules/convex_rules.mdc
📚 Learning: 2025-12-24T18:09:08.574Z
Learnt from: CR
Repo: hackerai-tech/hackerai PR: 0
File: .cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-24T18:09:08.574Z
Learning: Applies to convex/**/*.{ts,js} : Thoughtfully organize files with public query, mutation, or action functions within the `convex/` directory for file-based routing
Applied to files:
.cursor/rules/convex_rules.mdc
📚 Learning: 2025-12-24T18:09:08.574Z
Learnt from: CR
Repo: hackerai-tech/hackerai PR: 0
File: .cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-24T18:09:08.574Z
Learning: Applies to convex/**/*.{ts,js} : ALWAYS use the new function syntax for Convex functions with `query`, `mutation`, `action` decorators that include `args`, `returns`, and `handler` properties
Applied to files:
.cursor/rules/convex_rules.mdc
📚 Learning: 2025-12-24T18:09:08.574Z
Learnt from: CR
Repo: hackerai-tech/hackerai PR: 0
File: .cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-24T18:09:08.574Z
Learning: Applies to convex/**/*.{ts,js} : ALWAYS include argument and return validators for all Convex functions including `query`, `internalQuery`, `mutation`, `internalMutation`, `action`, and `internalAction`
Applied to files:
.cursor/rules/convex_rules.mdc
📚 Learning: 2025-12-24T18:09:08.574Z
Learnt from: CR
Repo: hackerai-tech/hackerai PR: 0
File: .cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-24T18:09:08.574Z
Learning: Applies to convex/**/*.{ts,js} : When using `ctx.runQuery`, `ctx.runMutation`, or `ctx.runAction` to call a function in the same file, add a type annotation on the return value to work around TypeScript circularity limitations
Applied to files:
.cursor/rules/convex_rules.mdc
📚 Learning: 2025-12-24T18:09:08.574Z
Learnt from: CR
Repo: hackerai-tech/hackerai PR: 0
File: .cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-24T18:09:08.574Z
Learning: Applies to convex/**/*.{ts,js} : Function references for public functions use the `api` object from `convex/_generated/api.ts` with file-based routing (e.g., `api.example.f` for function `f` in `convex/example.ts`)
Applied to files:
.cursor/rules/convex_rules.mdc
📚 Learning: 2025-12-24T18:09:08.574Z
Learnt from: CR
Repo: hackerai-tech/hackerai PR: 0
File: .cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-24T18:09:08.574Z
Learning: Applies to convex/**/*.{ts,js} : Use as few calls from actions to queries and mutations as possible to avoid race conditions from split transaction logic
Applied to files:
.cursor/rules/convex_rules.mdc
🧬 Code graph analysis (1)
app/components/PricingDialog.tsx (1)
app/hooks/useTauri.ts (2)
useTauri(20-23)openInBrowser(25-38)
🔇 Additional comments (4)
app/components/PricingDialog.tsx (1)
11-11: LGTM!The Tauri imports and hook usage are correctly integrated, matching the expected API from
useTauri.ts.Also applies to: 141-141
middleware.ts (3)
10-13: LGTM!The desktop-related paths are correctly added to allow unauthenticated access for the OAuth flow.
27-30: LGTM!User-Agent based detection is appropriate here since it only influences UX routing, not authentication security. The actual session validation still goes through WorkOS authkit.
52-61: Verify behavior for desktop users with expired sessions.The check uses
cookies.has("wos-session")(existence) rather than session validity. If a desktop user has an expired/invalid cookie:
hasSessionistrue(cookie exists)- authkit runs and sets
session.user = null- The browser-request check (line 78) redirects to
authorizationUrl(web OAuth)This means desktop users with stale sessions experience the web OAuth flow rather than being routed to
/desktop-callback. Depending on your intended UX, you might want to handle this by checking after authkit whether a desktop app with an invalid session should also redirect todesktop-callback.
- URL-encode transferToken in deep link for defensive encoding - Add token format validation in Rust (64-char hex check) - Add Cache-Control: no-store to all auth responses - Extract navigateToAuth() utility to reduce code duplication - Make ALLOWED_HOSTS configurable via HACKERAI_ALLOWED_HOSTS env var
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In @app/api/auth/desktop-callback/route.ts:
- Line 6: Replace the local instantiation const workos = new
WorkOS(process.env.WORKOS_API_KEY); with the shared WorkOS instance exported
from the existing module (import the exported workos/workosClient symbol from
the WorkOS module instead), removing the duplicated constructor call and any
direct usage of process.env.WORKOS_API_KEY here so the route uses the centrally
configured instance (which includes clientId) for consistent configuration.
🧹 Nitpick comments (4)
app/page.tsx (1)
20-23: Consider awaiting the async call for consistency.The function is marked
asyncbut doesn'tawaitthenavigateToAuthcall. While this works functionally sincenavigateToAuthhandles errors internally, awaiting would be more consistent.Suggested fix
const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); - navigateToAuth("/login"); + await navigateToAuth("/login"); };packages/desktop/src-tauri/src/lib.rs (1)
46-100: Consider logging/handling when the main window is not found.Lines 59-89 silently skip processing if
app.get_webview_window("main")returnsNone. While this might be intentional, adding a warning log would help diagnose issues where deep links are received but the window isn't available.Suggested improvement
if let Some(window) = app.get_webview_window("main") { // existing code... + } else { + log::warn!("Main window not found, cannot handle auth deep link"); }app/desktop-callback/route.ts (1)
15-22: Minor inconsistency:'vs'for single quote escaping.This file uses
'whileapp/api/auth/desktop-callback/route.tsuses'. Both are valid HTML entity encodings for single quotes, but consistency across files would be preferable.app/api/auth/desktop-callback/route.ts (1)
154-197: Consider extracting shared HTML rendering utilities.The
renderErrorPagefunction here and inapp/desktop-callback/route.tsare similar but not identical (this one has a single message, the other has title + message + retry URL). If more routes need similar error pages, consider extracting a shared utility.
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (8)
app/api/auth/desktop-callback/route.tsapp/components/ChatHeader.tsxapp/components/Header.tsxapp/components/PricingDialog.tsxapp/desktop-callback/route.tsapp/hooks/useTauri.tsapp/page.tsxpackages/desktop/src-tauri/src/lib.rs
🚧 Files skipped from review as they are similar to previous changes (1)
- app/hooks/useTauri.ts
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: fkesheh
Repo: hackerai-tech/hackerai PR: 140
File: packages/desktop/src-tauri/src/lib.rs:0-0
Timestamp: 2026-01-12T11:23:52.231Z
Learning: In the Tauri desktop app at packages/desktop/src-tauri/src/lib.rs, allowed hosts for OAuth origin validation are configured via the `HACKERAI_ALLOWED_HOSTS` environment variable (comma-separated list), with defaults of `hackerai.co,localhost`.
📚 Learning: 2026-01-12T11:23:52.231Z
Learnt from: fkesheh
Repo: hackerai-tech/hackerai PR: 140
File: packages/desktop/src-tauri/src/lib.rs:0-0
Timestamp: 2026-01-12T11:23:52.231Z
Learning: In the Tauri desktop app at packages/desktop/src-tauri/src/lib.rs, allowed hosts for OAuth origin validation are configured via the `HACKERAI_ALLOWED_HOSTS` environment variable (comma-separated list), with defaults of `hackerai.co,localhost`.
Applied to files:
packages/desktop/src-tauri/src/lib.rs
🧬 Code graph analysis (5)
app/page.tsx (1)
app/hooks/useTauri.ts (1)
navigateToAuth(40-52)
app/api/auth/desktop-callback/route.ts (5)
app/api/workos.ts (1)
workos(7-7)__mocks__/workos-node.ts (1)
WorkOS(21-21)app/desktop-callback/route.ts (1)
GET(97-156)app/desktop-login/route.ts (1)
GET(7-35)lib/desktop-auth.ts (2)
verifyAndConsumeOAuthState(136-157)createDesktopTransferToken(36-63)
app/components/PricingDialog.tsx (1)
app/hooks/useTauri.ts (1)
navigateToAuth(40-52)
app/components/ChatHeader.tsx (1)
app/hooks/useTauri.ts (1)
navigateToAuth(40-52)
app/components/Header.tsx (1)
app/hooks/useTauri.ts (1)
navigateToAuth(40-52)
🔇 Additional comments (24)
app/page.tsx (1)
16-16: LGTM!Import aligns with the centralized Tauri navigation pattern used across the codebase.
app/components/PricingDialog.tsx (5)
11-11: LGTM!Import is correctly added for the Tauri navigation helper.
193-197: LGTM!Correct usage of
navigateToAuthfor unauthenticated users in the team plan flow.
218-225: LGTM!Appropriate use of
/signuppath for the Free plan's "Get Started" action for unauthenticated users.
259-267: LGTM!Correct use of
/loginfor the Pro plan button when the user is not authenticated.
293-301: LGTM!Consistent with Pro plan handling—redirects unauthenticated users to login before upgrade.
app/components/Header.tsx (3)
7-7: LGTM!Import path correctly uses the
@/alias consistent with the codebase conventions.
37-54: LGTM!Desktop header buttons correctly use
/loginfor sign-in and/signupfor sign-up, maintaining semantic consistency.
70-87: LGTM!Mobile header buttons mirror the desktop behavior, ensuring consistent authentication navigation across breakpoints.
app/components/ChatHeader.tsx (3)
25-25: LGTM!Import correctly added for Tauri navigation support.
162-178: LGTM!Desktop empty-state header buttons correctly implement Tauri-aware navigation with appropriate fallback paths.
245-261: LGTM!Mobile empty-state header buttons are consistent with the desktop variant and the overall Tauri navigation pattern.
packages/desktop/src-tauri/src/lib.rs (6)
4-16: LGTM! macOS-specific back navigation using WKWebView.The implementation correctly uses conditional compilation and handles errors gracefully with logging.
18-19: LGTM! No-op fallback for non-macOS platforms.Clean pattern for platform-specific functionality.
21-26: LGTM! Allowed hosts configuration with sensible defaults.Based on learnings, the
HACKERAI_ALLOWED_HOSTSenvironment variable provides configurable origin validation with reasonable defaults ofhackerai.coandlocalhost.
28-30: LGTM! Token format validation.Validates tokens as exactly 64 hexadecimal characters, matching the expected format for transfer tokens.
32-44: LGTM! Origin validation with scheme and host checks.Properly validates that origins use HTTPS (or HTTP for localhost only) and match the allowed hosts list.
102-153: LGTM! Clean Tauri app initialization with plugins and menu setup.The
run()function properly initializes all required plugins, sets up deep link handling for desktop builds, and configures the navigation menu with a Go Back item.app/desktop-callback/route.ts (3)
4-13: LGTM! Cookie max age with environment variable override.Properly parses the environment variable with validation and falls back to the WorkOS default of 400 days.
24-95: LGTM! Well-structured error page rendering with XSS protection.The
renderErrorPagefunction properly escapes user-controlled values and usesJSON.stringifyfor the JavaScript context. The TAURI-aware retry logic with fallback to regular navigation is a good pattern.
97-156: LGTM! Secure token exchange and session cookie handling.The GET handler properly validates input, exchanges the transfer token atomically, and sets appropriate cookie security attributes. The error handling covers the expected failure modes with informative messages.
app/api/auth/desktop-callback/route.ts (3)
8-15: LGTM! HTML escaping function for XSS protection.Properly escapes all necessary HTML special characters.
17-102: LGTM! Comprehensive OAuth callback handling with proper validation.The GET handler correctly:
- Validates the OAuth state parameter to prevent CSRF
- Verifies required environment variables before use
- Exchanges the authorization code for tokens
- Seals the session data securely
- Creates a transfer token for the desktop app
- Returns appropriate error responses for each failure mode
105-152: LGTM! Success page with secure deep link handling.The
renderSuccessPagefunction properly escapes the deep link URL for both HTML attribute context (escapeHtml) and JavaScript context (JSON.stringify), preventing XSS attacks.
- @tauri-apps/api: ^2.1.1 → ^2.9.1 - @tauri-apps/cli: ^2.1.0 → ^2.9.6 - @tauri-apps/plugin-deep-link: ^2.2.0 → ^2.4.6 - @tauri-apps/plugin-os: ^2.2.0 → ^2.3.2 - @tauri-apps/plugin-process: ^2.2.0 → ^2.3.1 - @tauri-apps/plugin-shell: ^2.2.0 → ^2.3.4 - @tauri-apps/plugin-updater: ^2.3.0 → ^2.9.0 - @tauri-apps/plugin-opener: ^2.2.0 → ^2.5.3
Replace duplicate WorkOS instances in desktop-login and desktop-callback routes with the shared instance from app/api/workos.ts. This ensures consistent configuration (includes clientId) and eliminates code duplication.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
🧹 Nitpick comments (2)
app/api/auth/desktop-callback/route.ts (1)
152-195: Consider adding a "Try Again" link that redirects to the desktop login flow.The error page currently only has a link to
hackerai://auth?error=auth_failedwhich returns to the desktop app with an error. Consider also providing a link to retry the login flow directly from the browser (e.g.,/desktop-login), similar to howapp/desktop-callback/route.tsprovides aretryUrl.Suggested improvement
<h1>Authentication Error</h1> <p>${safeMessage}</p> <a href="hackerai://auth?error=auth_failed">Return to App</a> + <a href="/desktop-login" style="margin-left: 1rem; background: #22c55e;">Try Again</a> </div>packages/desktop/package.json (1)
6-12: Consider adding acleanscript for build artifacts.For a desktop application with multiple build configurations, having a clean script can help prevent stale artifacts from causing issues during development.
🧹 Optional: Add clean script
"scripts": { "dev": "tauri dev -c src-tauri/tauri.dev.conf.json", "dev:prod": "tauri dev", "build": "node scripts/build.js", "build:dev": "APP_URL=http://localhost:3000 node scripts/build.js --debug", "build:prod": "APP_URL=https://hackerai.co node scripts/build.js", - "tauri": "tauri" + "tauri": "tauri", + "clean": "rm -rf src-tauri/target dist" },
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (3)
app/api/auth/desktop-callback/route.tsapp/desktop-login/route.tspackages/desktop/package.json
🧰 Additional context used
🧠 Learnings (1)
📓 Common learnings
Learnt from: fkesheh
Repo: hackerai-tech/hackerai PR: 140
File: packages/desktop/src-tauri/src/lib.rs:0-0
Timestamp: 2026-01-12T11:23:52.231Z
Learning: In the Tauri desktop app at packages/desktop/src-tauri/src/lib.rs, allowed hosts for OAuth origin validation are configured via the `HACKERAI_ALLOWED_HOSTS` environment variable (comma-separated list), with defaults of `hackerai.co,localhost`.
🧬 Code graph analysis (2)
app/api/auth/desktop-callback/route.ts (3)
app/desktop-callback/route.ts (1)
GET(97-156)lib/desktop-auth.ts (2)
verifyAndConsumeOAuthState(136-157)createDesktopTransferToken(36-63)app/api/workos.ts (1)
workos(7-7)
app/desktop-login/route.ts (2)
app/desktop-callback/route.ts (1)
GET(97-156)lib/desktop-auth.ts (1)
createOAuthState(116-134)
🔇 Additional comments (10)
app/desktop-login/route.ts (2)
1-4: LGTM!Imports are clean and appropriately reference the shared
workosinstance andcreateOAuthStatehelper for consistent configuration across the codebase.
5-33: OAuth initiation flow looks correct.The implementation properly:
- Validates required environment variables before use
- Creates Redis-backed OAuth state for CSRF protection
- Uses the shared WorkOS instance for consistent configuration
- Has appropriate error handling with user-friendly redirects
One minor observation: the callback URL is constructed using
url.origin, which is correct for the server-side redirect flow.app/api/auth/desktop-callback/route.ts (6)
1-4: LGTM!Imports are appropriate for the OAuth callback handling, using iron-session for secure session sealing and the shared WorkOS instance.
6-13: HTML escaping utility is adequate for the current use cases.The
escapeHtmlfunction covers the essential characters for HTML context (&,<,>,",'). This is sufficient for the current usage where you're escaping URLs for HTML attributes and error messages.
15-48: OAuth state validation is well-implemented.Good security practices:
- Single-use state verification via
verifyAndConsumeOAuthStateprevents replay attacks- Missing state is rejected with a clear error
- Invalid/expired state returns 400 with appropriate messaging
The state is consumed atomically (Redis DEL returns 1 only on first delete), which prevents CSRF attacks effectively.
50-59: Environment variable validation is correct.Checking both
WORKOS_CLIENT_IDandWORKOS_COOKIE_PASSWORDbefore proceeding prevents cryptic runtime errors and returns a proper 500 status for server misconfiguration.
61-100: Authentication and token creation flow looks solid.The implementation correctly:
- Exchanges the OAuth code for session data
- Seals sensitive session data with iron-session before storing
- Creates a short-lived transfer token via Redis
- Properly encodes the deep link URL parameters
One consideration: the catch block logs the error but returns a generic message, which is appropriate for security (avoiding information leakage).
103-150: Success page rendering handles escaping correctly.Good approach using:
escapeHtml()for the HTML attribute context in the anchor hrefJSON.stringify()for the JavaScript context, which properly escapes special charactersThe auto-redirect via JavaScript with a fallback button provides good UX.
packages/desktop/package.json (2)
1-28: LGTM! Well-structured Tauri desktop package configuration.The package.json is properly configured for a Tauri v2 desktop application with appropriate scripts for development and production builds. The separation of
build:devandbuild:prodwith differentAPP_URLvalues is a clean approach for environment-specific builds.
15-21: All Tauri plugin versions are already current.Verification confirms all pinned versions match the latest stable releases on npm. No updates are needed.
Add the minisign public key to tauri.conf.json to enable update signature verification. This ensures users only receive updates signed with the corresponding private key.
Change updater endpoint from non-existent releases.hackerai.co to GitHub Releases. Tauri auto-generates latest.json during build which contains the update manifest with download URLs and signatures.
- Generate latest.json with platform-specific download URLs and signatures - Include .sig signature files in release artifacts - Use environment variables for safe GitHub context interpolation - Release is created as draft for manual review before publishing
- Fix Upstash Redis serialization by letting it handle JSON natively - Increase transfer token TTL from 60s to 300s - Add retry mechanism to index.html for network failures - Update error message to remove misleading "60 seconds" reference
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 6
🤖 Fix all issues with AI agents
In @.github/workflows/desktop-build.yml:
- Around line 233-236: The create-release job currently doesn't wait for
build-macos-universal, so the universal DMG may not be attached; update the
workflow so either (a) add build-macos-universal as a dependency of
create-release (make create-release needs include build-macos-universal in
addition to build) so the universal artifact is produced before release
creation, or (b) implement logic in build-macos-universal to wait for/create the
release and upload the universal DMG to it after create-release runs; reference
the job names build-macos-universal and create-release to locate where to add
the dependency or upload step.
- Around line 180-191: The fallback assignment MACOS_X64_FILE="$MACOS_FILE" can
cause arm64 artifacts to be advertised as darwin-x86_64; remove that fallback
and only set MACOS_X64_FILE when an actual x86_64 artifact is found (keep the
initial ls lookup and the subsequent -f existence check), and only add the
darwin-x86_64 entry to PLATFORMS when MACOS_X64_FILE exists and its signature is
present (i.e., preserve MACOS_X64_FILE, MACOS_X64_BASENAME, SIG and the jq
addition but do not substitute MACOS_FILE when no x64 file exists).
- Around line 154-162: The heredoc writing to release-files/latest.json includes
leading indentation which produces invalid JSON; update the heredoc to use an
unindented form (remove the leading spaces before the JSON) or switch to a
tab-stripping heredoc operator (use <<-EOF) so the leading tabs are stripped, or
alternatively generate the JSON atomically using jq to avoid indentation issues;
ensure the produced file content exactly matches valid JSON for "version",
"notes", "pub_date", and "platforms".
In @lib/desktop-auth.ts:
- Around line 36-45: The function createDesktopTransferToken accepts
sealedSession without validation, so empty strings can be stored and later fail
during exchange; update createDesktopTransferToken to validate that
sealedSession is a non-empty string (e.g., typeof sealedSession === 'string' and
trimmed length > 0) before proceeding, and if validation fails log a clear error
via console.error/processLogger and return null (or throw a descriptive Error)
to fail fast and avoid storing invalid values in Redis.
In @packages/desktop/src/index.html:
- Around line 120-136: The current retry() uses "isOnline || navigator.onLine"
which lets navigator.onLine trump checkConnectivity(); instead, first
short-circuit on navigator.onLine (if false immediately call showError and
return), otherwise await checkConnectivity() and only redirect to APP_URL when
isOnline is true; remove the OR fallback so navigator.onLine cannot bypass
checkConnectivity(), keep the existing timeout/visibility logic and use the same
helpers (retry(), checkConnectivity(), showLoader(), showError(), APP_URL).
🧹 Nitpick comments (3)
lib/desktop-auth.ts (1)
9-12: Consider removing unusedcreatedAtfield.The
createdAtfield is stored but never used during token exchange. Since Redis TTL handles expiration, this field is redundant. Remove it to simplify the code, or document its purpose if retained for auditing.♻️ Suggested simplification
type TransferTokenData = { sealedSession: string; - createdAt: number; };And update line 50-53:
- const data: TransferTokenData = { - sealedSession, - createdAt: Date.now(), - }; + const data: TransferTokenData = { + sealedSession, + };.github/workflows/desktop-build.yml (2)
15-16: Unused environment variable.
APP_URLis defined but not referenced anywhere in this workflow. If it's not needed, consider removing it to avoid confusion.
105-105: Boolean input comparison may behave unexpectedly.The
releaseinput is defined astype: boolean, but in GitHub Actions expressions, workflow_dispatch inputs are always strings. The comparisongithub.event.inputs.release == 'true'works, but for clarity and safety, consider usinginputs.release == truewhich is the recommended way to access typed inputs in reusable workflows and newer GitHub Actions syntax.Suggested fix
- if: startsWith(github.ref, 'refs/tags/desktop-v') || github.event.inputs.release == 'true' + if: startsWith(github.ref, 'refs/tags/desktop-v') || inputs.release == true
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (5)
.github/workflows/desktop-build.ymlapp/desktop-callback/route.tslib/desktop-auth.tspackages/desktop/src-tauri/tauri.conf.jsonpackages/desktop/src/index.html
🚧 Files skipped from review as they are similar to previous changes (2)
- app/desktop-callback/route.ts
- packages/desktop/src-tauri/tauri.conf.json
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-12-24T18:09:08.574Z
Learnt from: CR
Repo: hackerai-tech/hackerai PR: 0
File: .cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-24T18:09:08.574Z
Learning: Applies to convex/**/*.{ts,js} : Always add `'use node';` to the top of files containing actions that use Node.js built-in modules
Applied to files:
lib/desktop-auth.ts
📚 Learning: 2025-12-24T18:09:08.574Z
Learnt from: CR
Repo: hackerai-tech/hackerai PR: 0
File: .cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-12-24T18:09:08.574Z
Learning: Applies to convex/**/crons.ts : If a cron calls an internal function, always import the `internal` object from `_generated/api` even if the internal function is defined in the same `crons.ts` file
Applied to files:
lib/desktop-auth.ts
🔇 Additional comments (10)
packages/desktop/src/index.html (1)
104-118: Connectivity check looks reasonable.Using
mode: "no-cors"with a HEAD request is appropriate for a desktop app to avoid CORS issues. It will correctly detect network-level failures (DNS, timeout, connection refused) while avoiding cross-origin restrictions.lib/desktop-auth.ts (6)
1-7: LGTM!Constants are well-defined with appropriate TTLs and key prefixes. The token format regex correctly validates 64-character hex strings (32 bytes).
14-26: LGTM - Consider singleton pattern for cleaner code.While Upstash Redis uses HTTP/REST (so no connection pooling concern), a module-level singleton would avoid repeated environment variable reads and be more idiomatic.
28-34: LGTM!Secure token generation using
crypto.getRandomValueswith 256 bits of entropy.
65-116: LGTM - Good use of atomicgetdeloperation.The atomic get-and-delete prevents race conditions where a token could be reused. Token format validation before Redis access is a good practice.
The type handling (lines 97-108) is defensive for Upstash's JSON auto-deserialization behavior, though the double cast on line 100 could be avoided by narrowing the type declaration on line 83.
118-136: LGTM!Clean implementation that reuses the secure token generator. Storing a placeholder value is appropriate since only existence matters for state validation.
138-159: LGTM!Using
del's return value for atomic verification is an effective pattern - it ensures the state can only be consumed once, preventing replay attacks..github/workflows/desktop-build.yml (3)
18-41: LGTM!The matrix configuration is well-structured with
fail-fast: falseto ensure all platform builds complete independently, and the target architecture mappings are correct.
270-272: The hardcoded binary namehackerai-desktopis correct. The Cargo.toml package and explicit [[bin]] configuration both define the binary name ashackerai-desktop, which matches the workflow paths. TheproductNamefield in tauri.conf.json ("HackerAI") is separate and controls the application display name, not the binary name.
82-91: Thetauri-apps/tauri-action@v0version is appropriate for this Tauri v2 project.The project uses Tauri v2 (
@tauri-apps/cli ^2.9.6,tauri = 2), andtauri-action@v0(currently v0.6.1) fully supports Tauri v2. Major version pinning to@v0follows GitHub Actions best practices and will automatically apply security patches and minor updates.
| # Initialize latest.json | ||
| cat > release-files/latest.json << EOF | ||
| { | ||
| "version": "$VERSION", | ||
| "notes": "See release notes on GitHub", | ||
| "pub_date": "$DATE", | ||
| "platforms": {} | ||
| } | ||
| EOF |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Heredoc indentation produces invalid JSON.
The heredoc content has leading whitespace that will be included in the output, resulting in malformed JSON. Use <<-EOF (with tab indentation) or remove the indentation.
Proposed fix
- # Initialize latest.json
- cat > release-files/latest.json << EOF
- {
- "version": "$VERSION",
- "notes": "See release notes on GitHub",
- "pub_date": "$DATE",
- "platforms": {}
- }
- EOF
+ # Initialize latest.json
+ cat > release-files/latest.json << 'EOF'
+{
+ "version": "__VERSION__",
+ "notes": "See release notes on GitHub",
+ "pub_date": "__DATE__",
+ "platforms": {}
+}
+EOF
+ # Replace placeholders
+ sed -i "s/__VERSION__/$VERSION/g" release-files/latest.json
+ sed -i "s/__DATE__/$DATE/g" release-files/latest.jsonAlternatively, generate with jq directly:
- # Initialize latest.json
- cat > release-files/latest.json << EOF
- {
- "version": "$VERSION",
- "notes": "See release notes on GitHub",
- "pub_date": "$DATE",
- "platforms": {}
- }
- EOF
+ # Initialize latest.json
+ jq -n --arg v "$VERSION" --arg d "$DATE" '{version: $v, notes: "See release notes on GitHub", pub_date: $d, platforms: {}}' > release-files/latest.json📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| # Initialize latest.json | |
| cat > release-files/latest.json << EOF | |
| { | |
| "version": "$VERSION", | |
| "notes": "See release notes on GitHub", | |
| "pub_date": "$DATE", | |
| "platforms": {} | |
| } | |
| EOF | |
| # Initialize latest.json | |
| cat > release-files/latest.json << 'EOF' | |
| { | |
| "version": "__VERSION__", | |
| "notes": "See release notes on GitHub", | |
| "pub_date": "__DATE__", | |
| "platforms": {} | |
| } | |
| EOF | |
| # Replace placeholders | |
| sed -i "s/__VERSION__/$VERSION/g" release-files/latest.json | |
| sed -i "s/__DATE__/$DATE/g" release-files/latest.json |
| # Initialize latest.json | |
| cat > release-files/latest.json << EOF | |
| { | |
| "version": "$VERSION", | |
| "notes": "See release notes on GitHub", | |
| "pub_date": "$DATE", | |
| "platforms": {} | |
| } | |
| EOF | |
| # Initialize latest.json | |
| jq -n --arg v "$VERSION" --arg d "$DATE" '{version: $v, notes: "See release notes on GitHub", pub_date: $d, platforms: {}}' > release-files/latest.json |
🤖 Prompt for AI Agents
In @.github/workflows/desktop-build.yml around lines 154 - 162, The heredoc
writing to release-files/latest.json includes leading indentation which produces
invalid JSON; update the heredoc to use an unindented form (remove the leading
spaces before the JSON) or switch to a tab-stripping heredoc operator (use
<<-EOF) so the leading tabs are stripped, or alternatively generate the JSON
atomically using jq to avoid indentation issues; ensure the produced file
content exactly matches valid JSON for "version", "notes", "pub_date", and
"platforms".
| # macOS x64 | ||
| MACOS_X64_FILE=$(ls release-files/*x86_64*.app.tar.gz 2>/dev/null | head -1 || echo "") | ||
| if [ -z "$MACOS_X64_FILE" ]; then | ||
| MACOS_X64_FILE="$MACOS_FILE" | ||
| fi | ||
| if [ -n "$MACOS_X64_FILE" ] && [ -f "$MACOS_X64_FILE" ]; then | ||
| MACOS_X64_BASENAME=$(basename "$MACOS_X64_FILE") | ||
| SIG=$(cat "${MACOS_X64_FILE}.sig" 2>/dev/null || echo "") | ||
| if [ -n "$SIG" ]; then | ||
| PLATFORMS=$(echo "$PLATFORMS" | jq --arg url "https://github.com/${GH_REPO}/releases/download/${GH_REF_NAME}/${MACOS_X64_BASENAME}" --arg sig "$SIG" '. + {"darwin-x86_64": {"url": $url, "signature": $sig}}') | ||
| fi | ||
| fi |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fallback may serve wrong architecture binary.
If no x64-specific file is found, MACOS_X64_FILE falls back to MACOS_FILE which is the arm64 binary. This could cause x64 users to receive an incompatible arm64 binary via the auto-updater.
Proposed fix
# macOS x64
MACOS_X64_FILE=$(ls release-files/*x86_64*.app.tar.gz 2>/dev/null | head -1 || echo "")
- if [ -z "$MACOS_X64_FILE" ]; then
- MACOS_X64_FILE="$MACOS_FILE"
- fi
if [ -n "$MACOS_X64_FILE" ] && [ -f "$MACOS_X64_FILE" ]; then
MACOS_X64_BASENAME=$(basename "$MACOS_X64_FILE")
SIG=$(cat "${MACOS_X64_FILE}.sig" 2>/dev/null || echo "")
if [ -n "$SIG" ]; then
PLATFORMS=$(echo "$PLATFORMS" | jq --arg url "https://github.com/${GH_REPO}/releases/download/${GH_REF_NAME}/${MACOS_X64_BASENAME}" --arg sig "$SIG" '. + {"darwin-x86_64": {"url": $url, "signature": $sig}}')
fi
+ else
+ echo "Warning: No macOS x64 artifact found, darwin-x86_64 platform will not be included in latest.json"
fi🤖 Prompt for AI Agents
In @.github/workflows/desktop-build.yml around lines 180 - 191, The fallback
assignment MACOS_X64_FILE="$MACOS_FILE" can cause arm64 artifacts to be
advertised as darwin-x86_64; remove that fallback and only set MACOS_X64_FILE
when an actual x86_64 artifact is found (keep the initial ls lookup and the
subsequent -f existence check), and only add the darwin-x86_64 entry to
PLATFORMS when MACOS_X64_FILE exists and its signature is present (i.e.,
preserve MACOS_X64_FILE, MACOS_X64_BASENAME, SIG and the jq addition but do not
substitute MACOS_FILE when no x64 file exists).
| build-macos-universal: | ||
| needs: build | ||
| runs-on: macos-latest | ||
| if: startsWith(github.ref, 'refs/tags/desktop-v') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Universal binary not included in release.
The build-macos-universal job runs in parallel with create-release (both depend only on build). The universal DMG will not be included in the GitHub release. If you want the universal binary in the release, create-release should depend on build-macos-universal or handle it separately.
Suggested approaches
Option 1: Make create-release depend on build-macos-universal:
create-release:
- needs: build
+ needs: [build, build-macos-universal]
runs-on: ubuntu-latestThen download and include the universal artifact in the release files.
Option 2: Have build-macos-universal upload directly to the release after create-release creates it.
🤖 Prompt for AI Agents
In @.github/workflows/desktop-build.yml around lines 233 - 236, The
create-release job currently doesn't wait for build-macos-universal, so the
universal DMG may not be attached; update the workflow so either (a) add
build-macos-universal as a dependency of create-release (make create-release
needs include build-macos-universal in addition to build) so the universal
artifact is produced before release creation, or (b) implement logic in
build-macos-universal to wait for/create the release and upload the universal
DMG to it after create-release runs; reference the job names
build-macos-universal and create-release to locate where to add the dependency
or upload step.
| - name: Create universal binary | ||
| run: | | ||
| ARM64_APP=$(find arm64 -name "*.app" -type d | head -1) | ||
| X64_APP=$(find x64 -name "*.app" -type d | head -1) | ||
|
|
||
| if [ -z "$ARM64_APP" ] || [ -z "$X64_APP" ]; then | ||
| echo "Could not find app bundles" | ||
| exit 1 | ||
| fi | ||
|
|
||
| mkdir -p universal | ||
| cp -R "$ARM64_APP" universal/ | ||
|
|
||
| UNIVERSAL_APP="universal/$(basename "$ARM64_APP")" | ||
| ARM64_BIN="$ARM64_APP/Contents/MacOS/hackerai-desktop" | ||
| X64_BIN="$X64_APP/Contents/MacOS/hackerai-desktop" | ||
| UNIVERSAL_BIN="$UNIVERSAL_APP/Contents/MacOS/hackerai-desktop" | ||
|
|
||
| lipo -create -output "$UNIVERSAL_BIN" "$ARM64_BIN" "$X64_BIN" | ||
|
|
||
| hdiutil create -volname "HackerAI" -srcfolder universal -ov -format UDZO HackerAI-universal.dmg |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
cd .github/workflows && cat -n desktop-build.yml | head -100Repository: hackerai-tech/hackerai
Length of output: 3477
🏁 Script executed:
cd .github/workflows && wc -l desktop-build.ymlRepository: hackerai-tech/hackerai
Length of output: 87
🏁 Script executed:
cd .github/workflows && sed -n '200,300p' desktop-build.ymlRepository: hackerai-tech/hackerai
Length of output: 2968
🌐 Web query:
Tauri macOS code signing GitHub Actions best practices
💡 Result:
Below are concise best-practices for signing & notarizing Tauri macOS builds in GitHub Actions, with sources.
Setup & tools
- Use the official Tauri docs + tauri-action to perform signing/notarization in CI (tauri build reads env vars and will sign/notarize automatically). [1][2]
- Prefer notarytool (Apple) and staple the notarization ticket after success (use a notarize action that supports notarytool and stapling). [3][4]
Secrets & credentials
- Store signing certificate and password as GitHub Secrets (APPLE_CERTIFICATE, APPLE_CERTIFICATE_PASSWORD) and never check them into repo. Import into a temporary keychain in the workflow. [1][2]
- For notarization prefer App Store Connect API keys (APPLE_API_ISSUER, APPLE_API_KEY and the private key file) over Apple ID + app-specific password; keep the private key file in GitHub Secrets or the repo’s private storage and write it to a safe path at runtime. [1][6]
Runner, Xcode & architecture
- Build on macos-latest runners and run separate matrix jobs for x86_64 and aarch64 to produce universal artifacts (or build both and create universal if desired). Ensure Xcode version meets Tauri requirements. [8][2]
Security & keychain handling
- Create an ephemeral keychain in the workflow, import the certificate there, unlock it only for the step(s) that need it, then delete the keychain. Avoid leaving certs in the default keychain. [2]
- Limit job permissions (contents: write only for release steps) and use least-privilege tokens. Keep App Store Connect keys scoped to necessary roles. [2]
Workflow specifics (practical)
- Set the Tauri-required env vars from secrets before tauri-action runs: APPLE_CERTIFICATE, APPLE_CERTIFICATE_PASSWORD, APPLE_SIGNING_IDENTITY, APPLE_API_ISSUER, APPLE_API_KEY (or APPLE_ID / APPLE_PASSWORD if using Apple ID). Tauri will sign/notarize during tauri build. [1][2]
- Ensure CI=true when building artifacts in CI if required by your packaging tools. [1][2]
- After successful notarization, staple the ticket to the .app/.dmg and attach the stapled artifact to your release. Use an action that polls notarization status until success. [3][4]
Reliability & debugging
- Increase notarization action timeouts/retries to handle Apple service delays; log notarization UUIDs so you can query status manually if needed. [4]
- Test signing and notarization on a disposable Apple developer account or dedicated team project first to validate workflow. [1]
Maintenance & compliance
- Rotate App Store Connect keys and certificates periodically. Keep private key backups in secure vaults (not plaintext secrets). Monitor Apple Developer account membership/roles so CI keys don’t lose permissions. [1][6]
References
- Tauri macOS code signing & notarization docs. [1]
- tauri-action (example GitHub Actions workflow & env vars). [2]
- Notarize / Xcode notarization GitHub Actions (notarize/staple examples). [3][4]
- Tauri GitHub issues & guidance on ad-hoc signing and CI notes. [5]
If you want, I can: (a) generate a ready-to-use GitHub Actions workflow file (with ephemeral keychain steps and matrix builds) tailored to your project, or (b) show exact secret names and example action steps. Which would you prefer?
🌐 Web query:
macOS Gatekeeper unsigned app bundle behavior requirements
💡 Result:
Summary — how Gatekeeper treats unsigned (or improperly bundled) macOS app bundles
-
Gatekeeper expects a well-formed app bundle (Info.plist with CFBundlePackageType = "APPL", executables under Contents/MacOS, resources in appropriate subfolders). If the bundle shape or Info.plist is missing/incorrect, the system may not treat it as an app and Gatekeeper checks/alerts can be bypassed or behave unexpectedly. [1][2]
-
Unsigned or Developer‑ID but un‑notarized apps: macOS will block or show an “unidentified developer” / “can’t be opened” warning by default; Catalina+ also enforces notarization for apps distributed outside the App Store unless the user explicitly overrides Gatekeeper. Users can open such apps by using the “Open Anyway” / right‑click → Open flow in System Settings / Finder. [1]
-
Gatekeeper’s checks are performed only on quarantined downloads the first time they’re opened; to trigger Gatekeeper for testing you must deliver the app so it gets the quarantine attribute (download, AirDrop, Mail, etc.). Use spctl and codesign to pre‑check conformance, but Gatekeeper may perform additional runtime/bundle structure checks not covered by those tools. [3]
-
Common reasons Gatekeeper rejects a bundle (summary of requirements):
- Missing/incorrect signature or notarization (Catalina+ often requires notarization for non‑App Store apps). [1]
- Signatures that don’t cover all code or old/unsupported signature formats (must be signed on macOS 10.9+ to produce modern signatures). [2]
- Embedded code/resources not signed or placed in incorrect directories (all code in signed locations must be signed). [2][3]
- Symlinks that point outside the bundle (except to /System or /Library) or dangling symlinks. [2]
- Linking to libraries outside the bundle or nonstandard @rpath targets (Gatekeeper rejects bundles that load libraries from outside expected locations). [3]
-
Testing guidance: sign on macOS 10.9+ (use Developer ID), notarize for Catalina and later, staple the ticket, then validate with spctl -a -t exec -vv .app and test by distributing a quarantined copy to ensure Gatekeeper behavior matches expectations. [2][3]
Sources:
[1] Apple Support — “Safely open apps on your Mac.”
[2] Apple Technical Note TN2206: macOS Code Signing In Depth.
[3] Apple Code Signing Guide / Procedures (Gatekeeper testing, spctl notes).
Sign the universal binary after creating it with lipo.
The universal binary created via lipo loses the individual signatures from the arm64 and x64 builds and is not signed. Add a code signing step after lipo creates the binary and before the DMG is built, using the Apple signing certificate and identity. Without it, macOS Gatekeeper will trigger "unidentified developer" warnings on macOS Catalina and later. Consider notarizing the final DMG as well if this is for public distribution.
| export async function createDesktopTransferToken( | ||
| sealedSession: string, | ||
| ): Promise<string | null> { | ||
| const redis = getRedis(); | ||
| if (!redis) { | ||
| console.error( | ||
| "[Desktop Auth] Redis not configured, cannot create transfer token", | ||
| ); | ||
| return null; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add input validation for sealedSession.
The function should validate that sealedSession is a non-empty string before storing. Currently, an empty string would be stored successfully but fail during exchange (line 110). Fail fast to provide clearer error messages.
🔧 Suggested fix
export async function createDesktopTransferToken(
sealedSession: string,
): Promise<string | null> {
+ if (!sealedSession || typeof sealedSession !== "string") {
+ console.error("[Desktop Auth] Invalid sealedSession provided");
+ return null;
+ }
+
const redis = getRedis();
if (!redis) {📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| export async function createDesktopTransferToken( | |
| sealedSession: string, | |
| ): Promise<string | null> { | |
| const redis = getRedis(); | |
| if (!redis) { | |
| console.error( | |
| "[Desktop Auth] Redis not configured, cannot create transfer token", | |
| ); | |
| return null; | |
| } | |
| export async function createDesktopTransferToken( | |
| sealedSession: string, | |
| ): Promise<string | null> { | |
| if (!sealedSession || typeof sealedSession !== "string") { | |
| console.error("[Desktop Auth] Invalid sealedSession provided"); | |
| return null; | |
| } | |
| const redis = getRedis(); | |
| if (!redis) { | |
| console.error( | |
| "[Desktop Auth] Redis not configured, cannot create transfer token", | |
| ); | |
| return null; | |
| } |
🤖 Prompt for AI Agents
In @lib/desktop-auth.ts around lines 36 - 45, The function
createDesktopTransferToken accepts sealedSession without validation, so empty
strings can be stored and later fail during exchange; update
createDesktopTransferToken to validate that sealedSession is a non-empty string
(e.g., typeof sealedSession === 'string' and trimmed length > 0) before
proceeding, and if validation fails log a clear error via
console.error/processLogger and return null (or throw a descriptive Error) to
fail fast and avoid storing invalid values in Redis.
| async function retry() { | ||
| showLoader(); | ||
| document.getElementById("status-text").textContent = "Connecting to HackerAI..."; | ||
|
|
||
| const isOnline = await checkConnectivity(); | ||
|
|
||
| if (isOnline || navigator.onLine) { | ||
| window.location.href = APP_URL; | ||
| setTimeout(() => { | ||
| if (document.visibilityState !== "hidden") { | ||
| showError("Unable to connect. Please check your internet connection."); | ||
| } | ||
| }, 3000); | ||
| } else { | ||
| showError("Unable to connect. Please check your internet connection."); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Logic flaw: navigator.onLine fallback defeats connectivity check.
The condition isOnline || navigator.onLine at line 126 will redirect even when checkConnectivity() returns false, as long as navigator.onLine is true. Since navigator.onLine only indicates whether a network interface exists (not actual internet connectivity), this bypasses your server reachability check.
If the server is unreachable but the user has a network connection, they'll be redirected to a broken page and rely solely on the 3-second timeout fallback.
Proposed fix
async function retry() {
showLoader();
document.getElementById("status-text").textContent = "Connecting to HackerAI...";
const isOnline = await checkConnectivity();
- if (isOnline || navigator.onLine) {
+ if (isOnline) {
window.location.href = APP_URL;
setTimeout(() => {
if (document.visibilityState !== "hidden") {
showError("Unable to connect. Please check your internet connection.");
}
}, 3000);
} else {
showError("Unable to connect. Please check your internet connection.");
}
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| async function retry() { | |
| showLoader(); | |
| document.getElementById("status-text").textContent = "Connecting to HackerAI..."; | |
| const isOnline = await checkConnectivity(); | |
| if (isOnline || navigator.onLine) { | |
| window.location.href = APP_URL; | |
| setTimeout(() => { | |
| if (document.visibilityState !== "hidden") { | |
| showError("Unable to connect. Please check your internet connection."); | |
| } | |
| }, 3000); | |
| } else { | |
| showError("Unable to connect. Please check your internet connection."); | |
| } | |
| } | |
| async function retry() { | |
| showLoader(); | |
| document.getElementById("status-text").textContent = "Connecting to HackerAI..."; | |
| const isOnline = await checkConnectivity(); | |
| if (isOnline) { | |
| window.location.href = APP_URL; | |
| setTimeout(() => { | |
| if (document.visibilityState !== "hidden") { | |
| showError("Unable to connect. Please check your internet connection."); | |
| } | |
| }, 3000); | |
| } else { | |
| showError("Unable to connect. Please check your internet connection."); | |
| } | |
| } |
🤖 Prompt for AI Agents
In @packages/desktop/src/index.html around lines 120 - 136, The current retry()
uses "isOnline || navigator.onLine" which lets navigator.onLine trump
checkConnectivity(); instead, first short-circuit on navigator.onLine (if false
immediately call showError and return), otherwise await checkConnectivity() and
only redirect to APP_URL when isOnline is true; remove the OR fallback so
navigator.onLine cannot bypass checkConnectivity(), keep the existing
timeout/visibility logic and use the same helpers (retry(), checkConnectivity(),
showLoader(), showError(), APP_URL).
Summary
hackerai://protocol)Test plan
pnpm devin web package andpnpm devin packages/desktop to test localhost developmentpnpm buildin packages/desktop🤖 Generated with Claude Code
Summary by CodeRabbit
Release Notes
New Features
Chores
✏️ Tip: You can customize this high-level summary in your review settings.