Skip to content

Conversation

@webdevcody
Copy link
Collaborator

@webdevcody webdevcody commented Dec 29, 2025

…cess, no by passing

Summary by CodeRabbit

  • New Features

    • Web login screen, session-based web auth with short-lived WebSocket tokens, protected detailed health endpoint, centralized frontend API helpers, and optional Electron API key IPC.
  • Security

    • Multi-path auth (API key, session token, cookie), single-use WS tokens, timing-safe API key checks, login rate limiting, dynamic CORS origins, and JSON Content-Type enforcement.
  • Platform

    • PTY-based Claude usage support extended to Linux.
  • Tests

    • E2E/auth stabilization: test auth helpers, global setup, and adjusted Playwright config.
  • Other

    • New dev/test scripts, Docker/startup logging, and updated default CORS in compose.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Dec 29, 2025

Caution

Review failed

The pull request is closed.

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

📝 Walkthrough

Walkthrough

Adds multi-path authentication (API key, session token, session cookie, single-use short-lived wsTokens), login/logout/token routes, WS upgrade authentication, JSON-content middleware, dynamic CORS and cookie parsing, frontend auth/api clients, web login UI, and e2e test auth helpers.

Changes

Cohort / File(s) Change Summary
Server: deps & scripts
apps/server/package.json
Add cookie-parser, @types/cookie, @types/cookie-parser; add dev:test script.
Server: auth core & middleware
apps/server/src/lib/auth.ts, apps/server/src/middleware/require-json-content-type.ts, apps/server/src/routes/auth/index.ts
Implement multi-source auth (X-API-Key header/query, X-Session-Token header, automaker_session cookie), session lifecycle (create/validate/invalidate), single‑use short-lived wsTokens, timing-safe key compare, cookie helpers, auth middleware, require-JSON middleware, and new /api/auth routes (status, login, token, logout) with per-IP rate limiting.
Server: init, CORS, WS upgrade, health
apps/server/src/index.ts, apps/server/src/routes/health/index.ts
Wire cookie-parser, dynamic CORS origin logic, mount auth and health/detailed routes, enforce JSON Content-Type for write requests, integrate WS upgrade authentication and reject unauthorized upgrades; re-export createDetailedHandler.
Server: tests & services
apps/server/tests/unit/lib/auth.test.ts, apps/server/src/services/claude-usage-service.ts
Expand auth unit tests to cover sessions/wsTokens/API-key paths; unify PTY path for Windows/Linux, rename PTY helper and adjust platform shells/working dir.
UI: HTTP helpers & auth client
apps/ui/src/lib/api-fetch.ts, apps/ui/src/lib/http-api-client.ts
Add centralized authenticated fetch helpers (apiFetch/apiGet/apiPost/...), getAuthHeaders; implement dual-mode client auth utilities (get/init API key, session token management, login/logout/verify/fetchSessionToken).
UI: terminal & websocket
apps/ui/src/components/views/terminal-view.tsx, apps/ui/src/components/views/terminal-view/terminal-panel.tsx
Replace raw fetches with API helpers, include API/session token on cleanup, fetch short-lived wsToken for web-mode WS connections, make connect async to await wsToken.
UI: auth flow, routes & components
apps/ui/src/routes/__root.tsx, apps/ui/src/routes/login.tsx, apps/ui/src/components/views/login-view.tsx, apps/ui/src/components/claude-usage-popover.tsx, apps/ui/src/components/views/board-view/board-header.tsx, apps/ui/src/components/views/settings-view.tsx
Initialize auth on startup (Electron vs Web), gate rendering on authChecked/isAuthenticated, add /login route and LoginView, gate Claude usage UI on CLI verification and API-key presence.
UI: file IO and replacements
apps/ui/src/components/dialogs/file-browser-dialog.tsx, multiple UI files
Replace manual fetch calls with centralized apiPost/apiGet wrappers across UI; adjust imports and dependency arrays accordingly.
Electron main/preload/types
apps/ui/src/main.ts, apps/ui/src/preload.ts, apps/ui/src/lib/electron.ts, apps/ui/src/types/electron.d.ts
Ensure/generate persistent API key, expose auth:getApiKey IPC and optional electronAPI.getApiKey() in preload/types, pass AUTOMAKER_API_KEY to server env.
Store persistence
apps/ui/src/store/setup-store.ts
Persist claudeAuthStatus in persisted store partialize config.
E2E test infra & helpers
apps/ui/playwright.config.ts, apps/ui/tests/*, apps/ui/tests/utils/*, apps/ui/tests/global-setup.ts, apps/ui/tests/utils/api/client.ts, apps/ui/tests/utils/core/interactions.ts
Add test auth helpers (authenticateWithApiKey, authenticateForTests, handleLoginScreenIfPresent, gotoWithAuth), globalSetup, force mock agent in Playwright, update tests to authenticate before navigation, and add pretest server-kill script usage.
Test scripts & infra
apps/ui/scripts/kill-test-servers.mjs, apps/ui/package.json, package.json
Add kill-test-servers script and prepend to pretest, add dev:docker script to repo root.
Docker / compose / init
docker-compose.yml, docker-compose.override.yml.example, Dockerfile, init.mjs
Change default CORS origin, add NODE_ENV=development hint in override example, remove NODE_ENV prod set in Dockerfile production stage, mirror server logs to console+file in init.mjs.
Docs: design
docs/plans/2025-12-29-api-security-hardening-design.md
New design doc outlining wsToken flow, timing-safe compare, login rate limiting, banner suppression, single-use tokens, rollout and tests.

Sequence Diagram(s)

sequenceDiagram
    participant User as User
    participant Client as Browser/Electron
    participant Server as Backend
    participant Disk as Disk

    rect rgba(220,240,255,0.9)
    Note over User,Server: Web login → session creation and cookie
    User->>Client: Submit API key via login form
    Client->>Server: POST /api/auth/login { apiKey }
    Server->>Server: validateApiKey(key) (timing-safe)
    alt valid
        Server->>Server: createSession() -> sessionToken
        Server->>Disk: persist sessions file
        Server->>Client: 200 { token } + Set-Cookie automaker_session (HttpOnly)
    else invalid or rate-limited
        Server->>Client: 401/429
    end
    end
Loading
sequenceDiagram
    participant Client as Browser/Electron
    participant Server as Backend
    participant Auth as Auth module
    participant WS as WebSocket

    rect rgba(240,255,220,0.9)
    Note over Client,Server: WebSocket upgrade authentication
    Client->>Server: HTTP Upgrade (with X-API-Key or X-Session-Token or automaker_session cookie or wsToken query)
    Server->>Auth: checkRawAuthentication(headers, query, cookies)
    alt authenticated
        Server->>Client: 101 Switching Protocols
        Client->>WS: Open persistent WS
    else unauthorized
        Server->>Client: 403 Forbidden (upgrade rejected)
    end
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

Testers-Requested, Enhancement, Tests

Poem

🐇 I nibbled keys and hid them well,
I planted cookies in a shell,
Tokens hop and vanish soon,
WS doors close by the moon,
I twitch my nose — secure and swell!

Pre-merge checks and finishing touches

❌ Failed checks (1 inconclusive)
Check name Status Explanation Resolution
Title check ❓ Inconclusive The title is truncated and vague, using ellipsis without conveying the specific security improvement being implemented. Complete the title and be more specific about what security enhancement is being added, e.g., 'Add API key authentication to all protected endpoints' or 'Require API token for all API access'.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed Docstring coverage is 83.05% which is sufficient. The required threshold is 80.00%.

📜 Recent review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 57b7f92 and 46caae0.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (50)
  • Dockerfile
  • apps/server/package.json
  • apps/server/src/index.ts
  • apps/server/src/lib/auth.ts
  • apps/server/src/middleware/require-json-content-type.ts
  • apps/server/src/routes/auth/index.ts
  • apps/server/src/routes/health/index.ts
  • apps/server/src/services/claude-usage-service.ts
  • apps/server/tests/unit/lib/auth.test.ts
  • apps/server/tests/unit/services/agent-service.test.ts
  • apps/ui/package.json
  • apps/ui/playwright.config.ts
  • apps/ui/scripts/kill-test-servers.mjs
  • apps/ui/src/components/claude-usage-popover.tsx
  • apps/ui/src/components/dialogs/file-browser-dialog.tsx
  • apps/ui/src/components/views/board-view/board-header.tsx
  • apps/ui/src/components/views/login-view.tsx
  • apps/ui/src/components/views/settings-view.tsx
  • apps/ui/src/components/views/terminal-view.tsx
  • apps/ui/src/components/views/terminal-view/terminal-panel.tsx
  • apps/ui/src/lib/api-fetch.ts
  • apps/ui/src/lib/electron.ts
  • apps/ui/src/lib/http-api-client.ts
  • apps/ui/src/main.ts
  • apps/ui/src/preload.ts
  • apps/ui/src/routes/__root.tsx
  • apps/ui/src/routes/login.tsx
  • apps/ui/src/store/setup-store.ts
  • apps/ui/src/types/electron.d.ts
  • apps/ui/tests/agent/start-new-chat-session.spec.ts
  • apps/ui/tests/context/add-context-image.spec.ts
  • apps/ui/tests/context/context-file-management.spec.ts
  • apps/ui/tests/context/delete-context-file.spec.ts
  • apps/ui/tests/features/add-feature-to-backlog.spec.ts
  • apps/ui/tests/features/edit-feature.spec.ts
  • apps/ui/tests/features/feature-manual-review-flow.spec.ts
  • apps/ui/tests/features/feature-skip-tests-toggle.spec.ts
  • apps/ui/tests/git/worktree-integration.spec.ts
  • apps/ui/tests/global-setup.ts
  • apps/ui/tests/profiles/profiles-crud.spec.ts
  • apps/ui/tests/projects/new-project-creation.spec.ts
  • apps/ui/tests/projects/open-existing-project.spec.ts
  • apps/ui/tests/utils/api/client.ts
  • apps/ui/tests/utils/core/interactions.ts
  • apps/ui/tests/utils/navigation/views.ts
  • docker-compose.override.yml.example
  • docker-compose.yml
  • docs/plans/2025-12-29-api-security-hardening-design.md
  • init.mjs
  • package.json

Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @webdevcody, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the security of the API endpoints by introducing a dual-mode authentication system. For Electron environments, an API key is used, while web-based access now leverages secure HTTP-only session cookies, alongside new dedicated authentication routes. This change also refines CORS handling, secures WebSocket connections, and updates the UI to seamlessly integrate with these new authentication mechanisms, including a new login flow for web users. Additionally, the Claude usage service has been extended to support Linux.

Highlights

  • Enhanced API Security: Implemented a robust authentication system for API endpoints, supporting both API key-based authentication for Electron applications and secure HTTP-only cookie-based session authentication for web clients.
  • Cookie-based Session Management: Introduced cookie-parser and developed a comprehensive session management system, including session creation, validation, invalidation, and persistence to a .sessions file to maintain state across server restarts.
  • Dynamic CORS Configuration: Updated the Cross-Origin Resource Sharing (CORS) policy to dynamically allow origins based on the CORS_ORIGIN environment variable or localhost for development, while supporting credentials for secure communication.
  • Dedicated Authentication Routes: Added new /api/auth endpoints for managing authentication flows, including login, logout, and status checks, which are accessible without prior authentication.
  • Secure WebSocket Connections: All WebSocket connections now require authentication, verifying either an API key provided in headers or query parameters, or a valid session cookie.
  • Automated API Key Management: The server now automatically ensures an API key exists by loading it from an environment variable or a .api-key file, or generating a new one on first run. This key is printed to the console for web mode users.
  • UI Integration for Authentication: The user interface has been updated to integrate with the new authentication system, featuring a new login view for web mode and utilizing a new api-fetch utility for all authenticated API requests.
  • Expanded Claude Usage Service Support: The Claude usage service has been enhanced to support Linux environments for PTY execution, aligning its functionality with Windows' node-pty implementation.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This is a substantial and well-executed pull request that significantly enhances the application's security by mandating authentication for all API endpoints. The new system adeptly handles both API key-based authentication for Electron and session/cookie-based authentication for the web. The introduction of dedicated authentication routes and a centralized API fetching utility are excellent architectural improvements. My feedback focuses on a few key areas to further improve robustness, performance, and maintainability, including fixing a potential issue in cookie parsing, making file I/O asynchronous on the server, and ensuring consistency in the client-side authentication logic.

Comment on lines 257 to 264
const cookies = cookieHeader.split(';').reduce(
(acc, cookie) => {
const [key, value] = cookie.trim().split('=');
acc[key] = value;
return acc;
},
{} as Record<string, string>
);
Copy link
Contributor

Choose a reason for hiding this comment

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

high

This manual cookie parsing logic is brittle and may fail with complex cookie values (e.g., values containing =). It's better to use a dedicated library for this. The cookie package is a standard for this and is already available as an indirect dependency via cookie-parser.

You can use it by adding import cookie from 'cookie'; at the top of the file and then simplifying this block.

    const cookies = cookie.parse(cookieHeader);

Comment on lines 947 to 952
// Add API key for Electron mode auth
const apiKey = getApiKey();
if (apiKey) {
url += `&apiKey=${encodeURIComponent(apiKey)}`;
}
// In web mode, cookies are sent automatically with same-origin WebSocket
Copy link
Contributor

Choose a reason for hiding this comment

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

high

The WebSocket connection logic for the terminal is missing the sessionToken query parameter for web mode. While cookies might work for same-origin connections, this can be fragile. The server-side implementation supports sessionToken in the query, and the main event WebSocket client (http-api-client.ts) already sends it for robustness.

For consistency and to ensure authentication works reliably across different environments (e.g., different ports in development), you should also add the session token to the URL here. You'll need to import getSessionToken from @/lib/http-api-client.

Suggested change
// Add API key for Electron mode auth
const apiKey = getApiKey();
if (apiKey) {
url += `&apiKey=${encodeURIComponent(apiKey)}`;
}
// In web mode, cookies are sent automatically with same-origin WebSocket
// Add API key for Electron mode auth
const apiKey = getApiKey();
if (apiKey) {
url += `&apiKey=${encodeURIComponent(apiKey)}`;
} else {
// In web mode, add session token as query param for robustness
const sessionToken = getSessionToken();
if (sessionToken) {
url += `&sessionToken=${encodeURIComponent(sessionToken)}`;
}
}
// In web mode, cookies are sent automatically with same-origin WebSocket

Comment on lines 61 to 69
function saveSessions(): void {
try {
fs.mkdirSync(path.dirname(SESSIONS_FILE), { recursive: true });
const sessions = Array.from(validSessions.entries());
fs.writeFileSync(SESSIONS_FILE, JSON.stringify(sessions), { encoding: 'utf-8', mode: 0o600 });
} catch (error) {
console.error('[Auth] Failed to save sessions:', error);
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

Using fs.writeFileSync is a synchronous, blocking operation. In a server environment, this can block the event loop and degrade performance, especially if session creation/invalidation is frequent. It's better to use the asynchronous version, fs.promises.writeFile.

Note that you will also need to update the functions that call saveSessions() (i.e., createSession, validateSession, invalidateSession) to await this asynchronous operation.

Suggested change
function saveSessions(): void {
try {
fs.mkdirSync(path.dirname(SESSIONS_FILE), { recursive: true });
const sessions = Array.from(validSessions.entries());
fs.writeFileSync(SESSIONS_FILE, JSON.stringify(sessions), { encoding: 'utf-8', mode: 0o600 });
} catch (error) {
console.error('[Auth] Failed to save sessions:', error);
}
}
async function saveSessions(): Promise<void> {
try {
await fs.promises.mkdir(path.dirname(SESSIONS_FILE), { recursive: true });
const sessions = Array.from(validSessions.entries());
await fs.promises.writeFile(SESSIONS_FILE, JSON.stringify(sessions), { encoding: 'utf-8', mode: 0o600 });
} catch (error) {
console.error('[Auth] Failed to save sessions:', error);
}
}

Comment on lines 291 to 317
export function isRequestAuthenticated(req: Request): boolean {
// Check API key header
const headerKey = req.headers['x-api-key'] as string | undefined;
if (headerKey && headerKey === API_KEY) {
return true;
}

// Check session token header
const sessionTokenHeader = req.headers['x-session-token'] as string | undefined;
if (sessionTokenHeader && validateSession(sessionTokenHeader)) {
return true;
}

// Check query parameter
const queryKey = req.query.apiKey as string | undefined;
if (queryKey && queryKey === API_KEY) {
return true;
}

// Check cookie
const sessionToken = req.cookies?.[SESSION_COOKIE_NAME] as string | undefined;
if (sessionToken && validateSession(sessionToken)) {
return true;
}

return false;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

The logic in this function is very similar to the logic inside authMiddleware. This duplication can lead to maintenance issues, where a change in authentication logic might be applied in one place but not the other. Consider refactoring to reduce this duplication. For example, authMiddleware could potentially use isRequestAuthenticated for its checks, or both could delegate to a common private function to handle the core validation logic.

Comment on lines 66 to 67
const hasApiKey = !!apiKeys.anthropic || !!claudeAuthStatus?.hasEnvApiKey;
const showUsageTracking = !hasApiKey && !isWindows;
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

The logic to determine showUsageTracking here is inconsistent with the logic in board-header.tsx. The board-header.tsx component also checks isCliVerified, which seems more correct as the usage tracking section is only relevant if the user is authenticated via the Claude CLI. For consistency and correctness, I recommend using the same logic here.

Suggested change
const hasApiKey = !!apiKeys.anthropic || !!claudeAuthStatus?.hasEnvApiKey;
const showUsageTracking = !hasApiKey && !isWindows;
const hasApiKey = !!apiKeys.anthropic || !!claudeAuthStatus?.hasEnvApiKey;
const isCliVerified =
claudeAuthStatus?.authenticated && claudeAuthStatus?.method === 'cli_authenticated';
const showUsageTracking = !hasApiKey && !isWindows && isCliVerified;

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (5)
apps/ui/src/components/views/login-view.tsx (1)

21-39: Consider clearing the API key from state after successful login.

The login flow is well-implemented with proper loading states and error handling. As a security hygiene measure, consider clearing the apiKey from component state after successful authentication to minimize the window where sensitive data resides in memory.

🔎 Optional: Clear API key on success
      const result = await login(apiKey.trim());
      if (result.success) {
+       setApiKey(''); // Clear sensitive data from state
        // Redirect to home/board on success
        navigate({ to: '/' });
      } else {
apps/ui/src/routes/__root.tsx (1)

81-124: Consider removing location.pathname from dependency array to prevent auth re-initialization on every navigation.

The auth initialization effect runs on every route change due to location.pathname in the dependency array. This causes unnecessary fetchSessionToken() and checkAuthStatus() calls when navigating between authenticated routes. The redirect logic at lines 113-116 can be moved to a separate effect that depends on isAuthenticated.

🔎 Proposed fix
   useEffect(() => {
     const initAuth = async () => {
       try {
         // Initialize API key for Electron mode
         await initApiKey();

         // In Electron mode, we're always authenticated via header
         if (isElectronMode()) {
           setIsAuthenticated(true);
           setAuthChecked(true);
           return;
         }

         // In web mode, try to fetch session token (works if cookie is valid)
         const tokenFetched = await fetchSessionToken();

         if (tokenFetched) {
           setIsAuthenticated(true);
           setAuthChecked(true);
           return;
         }

         // Fallback: check auth status via cookie
         const status = await checkAuthStatus();
         setIsAuthenticated(status.authenticated);
         setAuthChecked(true);
-
-        // Redirect to login if not authenticated and not already on login page
-        if (!status.authenticated && location.pathname !== '/login') {
-          navigate({ to: '/login' });
-        }
       } catch (error) {
         console.error('Failed to initialize auth:', error);
         setAuthChecked(true);
       }
     };

     initAuth();
-  }, [location.pathname, navigate]);
+  }, []);
+
+  // Redirect to login if not authenticated (web mode only)
+  useEffect(() => {
+    if (!isElectronMode() && authChecked && !isAuthenticated && location.pathname !== '/login') {
+      navigate({ to: '/login' });
+    }
+  }, [authChecked, isAuthenticated, location.pathname, navigate]);
apps/server/src/index.ts (1)

222-272: Consider extracting cookie parsing to a shared utility.

The manual cookie parsing (lines 257-264) duplicates logic that cookie-parser handles for HTTP requests. Since the request object in WebSocket upgrades doesn't go through Express middleware, this is necessary, but consider extracting the parsing logic to a shared utility function in lib/auth.ts for consistency.

apps/ui/src/lib/api-fetch.ts (1)

14-21: Consider extracting getServerUrl to avoid duplication.

This function is identical to the one in http-api-client.ts (lines 36-42). Consider extracting it to a shared constants/config module to maintain DRY principle.

apps/server/src/lib/auth.ts (1)

291-317: isRequestAuthenticated duplicates logic from authMiddleware.

This function replicates the authentication checking logic from authMiddleware (lines 214-269). Consider refactoring to share the validation logic to reduce duplication and ensure consistency.

🔎 Suggested refactor to reduce duplication
+/**
+ * Internal helper to check all authentication methods
+ */
+function checkAuthentication(req: Request): { authenticated: boolean; method?: string } {
+  const headerKey = req.headers['x-api-key'] as string | undefined;
+  if (headerKey && headerKey === API_KEY) {
+    return { authenticated: true, method: 'api_key_header' };
+  }
+
+  const sessionTokenHeader = req.headers['x-session-token'] as string | undefined;
+  if (sessionTokenHeader && validateSession(sessionTokenHeader)) {
+    return { authenticated: true, method: 'session_token_header' };
+  }
+
+  const queryKey = req.query.apiKey as string | undefined;
+  if (queryKey && queryKey === API_KEY) {
+    return { authenticated: true, method: 'api_key_query' };
+  }
+
+  const sessionToken = req.cookies?.[SESSION_COOKIE_NAME] as string | undefined;
+  if (sessionToken && validateSession(sessionToken)) {
+    return { authenticated: true, method: 'session_cookie' };
+  }
+
+  return { authenticated: false };
+}
+
 export function authMiddleware(req: Request, res: Response, next: NextFunction): void {
-  // ... existing implementation
+  const { authenticated } = checkAuthentication(req);
+  if (authenticated) {
+    next();
+    return;
+  }
+  // Return appropriate error...
 }
 
 export function isRequestAuthenticated(req: Request): boolean {
-  // ... existing implementation
+  return checkAuthentication(req).authenticated;
 }
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 57b7f92 and d68de99.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (25)
  • apps/server/package.json
  • apps/server/src/index.ts
  • apps/server/src/lib/auth.ts
  • apps/server/src/routes/auth/index.ts
  • apps/server/src/routes/health/index.ts
  • apps/server/src/services/claude-usage-service.ts
  • apps/ui/src/components/claude-usage-popover.tsx
  • apps/ui/src/components/dialogs/file-browser-dialog.tsx
  • apps/ui/src/components/views/board-view/board-header.tsx
  • apps/ui/src/components/views/login-view.tsx
  • apps/ui/src/components/views/settings-view.tsx
  • apps/ui/src/components/views/terminal-view.tsx
  • apps/ui/src/components/views/terminal-view/terminal-panel.tsx
  • apps/ui/src/lib/api-fetch.ts
  • apps/ui/src/lib/electron.ts
  • apps/ui/src/lib/http-api-client.ts
  • apps/ui/src/main.ts
  • apps/ui/src/preload.ts
  • apps/ui/src/routes/__root.tsx
  • apps/ui/src/routes/login.tsx
  • apps/ui/src/store/setup-store.ts
  • apps/ui/src/types/electron.d.ts
  • docker-compose.yml
  • init.mjs
  • package.json
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Always import from shared packages (@automaker/*), never from old relative paths

Files:

  • apps/ui/src/types/electron.d.ts
  • apps/ui/src/preload.ts
  • apps/ui/src/components/views/login-view.tsx
  • apps/server/src/routes/auth/index.ts
  • apps/ui/src/components/dialogs/file-browser-dialog.tsx
  • apps/ui/src/components/claude-usage-popover.tsx
  • apps/ui/src/components/views/settings-view.tsx
  • apps/ui/src/routes/__root.tsx
  • apps/ui/src/components/views/terminal-view/terminal-panel.tsx
  • apps/server/src/routes/health/index.ts
  • apps/ui/src/lib/electron.ts
  • apps/ui/src/components/views/board-view/board-header.tsx
  • apps/ui/src/lib/http-api-client.ts
  • apps/ui/src/components/views/terminal-view.tsx
  • apps/server/src/lib/auth.ts
  • apps/ui/src/lib/api-fetch.ts
  • apps/server/src/services/claude-usage-service.ts
  • apps/ui/src/routes/login.tsx
  • apps/ui/src/store/setup-store.ts
  • apps/server/src/index.ts
  • apps/ui/src/main.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Use resolveModelString() from @automaker/model-resolver to convert model aliases (haiku, sonnet, opus) to full model names

Files:

  • apps/ui/src/types/electron.d.ts
  • apps/ui/src/preload.ts
  • apps/ui/src/components/views/login-view.tsx
  • apps/server/src/routes/auth/index.ts
  • apps/ui/src/components/dialogs/file-browser-dialog.tsx
  • apps/ui/src/components/claude-usage-popover.tsx
  • apps/ui/src/components/views/settings-view.tsx
  • apps/ui/src/routes/__root.tsx
  • apps/ui/src/components/views/terminal-view/terminal-panel.tsx
  • apps/server/src/routes/health/index.ts
  • apps/ui/src/lib/electron.ts
  • apps/ui/src/components/views/board-view/board-header.tsx
  • apps/ui/src/lib/http-api-client.ts
  • apps/ui/src/components/views/terminal-view.tsx
  • apps/server/src/lib/auth.ts
  • apps/ui/src/lib/api-fetch.ts
  • apps/server/src/services/claude-usage-service.ts
  • apps/ui/src/routes/login.tsx
  • apps/ui/src/store/setup-store.ts
  • apps/server/src/index.ts
  • apps/ui/src/main.ts
apps/server/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Use createEventEmitter() from lib/events.ts for all server operations to emit events that stream to frontend via WebSocket

Files:

  • apps/server/src/routes/auth/index.ts
  • apps/server/src/routes/health/index.ts
  • apps/server/src/lib/auth.ts
  • apps/server/src/services/claude-usage-service.ts
  • apps/server/src/index.ts
🧠 Learnings (2)
📚 Learning: 2025-12-28T05:07:48.138Z
Learnt from: CR
Repo: AutoMaker-Org/automaker PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T05:07:48.138Z
Learning: Applies to apps/server/src/**/*.{ts,tsx} : Use `createEventEmitter()` from `lib/events.ts` for all server operations to emit events that stream to frontend via WebSocket

Applied to files:

  • init.mjs
  • apps/server/src/index.ts
📚 Learning: 2025-12-28T05:07:48.138Z
Learnt from: CR
Repo: AutoMaker-Org/automaker PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T05:07:48.138Z
Learning: Frontend UI must use TanStack Router for file-based routing, organize components in components/views/, implement stores with Zustand, and use custom hooks in hooks/ directory

Applied to files:

  • apps/ui/src/components/views/settings-view.tsx
  • apps/ui/src/routes/login.tsx
🧬 Code graph analysis (10)
apps/ui/src/components/views/login-view.tsx (1)
apps/ui/src/lib/http-api-client.ts (1)
  • login (128-151)
apps/server/src/routes/auth/index.ts (1)
apps/server/src/lib/auth.ts (6)
  • isRequestAuthenticated (291-317)
  • validateApiKey (175-177)
  • createSession (137-146)
  • getSessionCookieOptions (182-196)
  • getSessionCookieName (201-203)
  • invalidateSession (167-170)
apps/ui/src/components/dialogs/file-browser-dialog.tsx (1)
apps/ui/src/lib/api-fetch.ts (1)
  • apiPost (124-131)
apps/ui/src/components/claude-usage-popover.tsx (1)
apps/ui/src/store/setup-store.ts (1)
  • useSetupStore (125-183)
apps/ui/src/routes/__root.tsx (1)
apps/ui/src/lib/http-api-client.ts (4)
  • initApiKey (81-100)
  • isElectronMode (71-73)
  • fetchSessionToken (157-180)
  • checkAuthStatus (105-123)
apps/ui/src/components/views/terminal-view/terminal-panel.tsx (1)
apps/ui/src/lib/http-api-client.ts (1)
  • getApiKey (53-53)
apps/ui/src/components/views/terminal-view.tsx (2)
apps/ui/src/lib/api-fetch.ts (4)
  • apiDeleteRaw (156-161)
  • apiGet (113-119)
  • apiPost (124-131)
  • apiFetch (84-108)
apps/ui/src/lib/http-api-client.ts (1)
  • getApiKey (53-53)
apps/server/src/lib/auth.ts (1)
init.mjs (1)
  • req (173-175)
apps/ui/src/lib/api-fetch.ts (1)
apps/ui/src/lib/http-api-client.ts (2)
  • getApiKey (53-53)
  • getSessionToken (56-56)
apps/ui/src/routes/login.tsx (2)
apps/ui/src/routes/__root.tsx (1)
  • Route (267-269)
apps/ui/src/components/views/login-view.tsx (1)
  • LoginView (15-104)
🔇 Additional comments (45)
apps/server/src/services/claude-usage-service.ts (3)

21-21: LGTM! Linux platform detection added.

The isLinux flag follows the existing pattern and correctly detects the Linux platform using os.platform().


52-56: LGTM! Platform routing updated to include Linux.

The condition correctly routes both Windows and Linux to the PTY-based implementation, while macOS continues using the expect-based approach. The method rename to executeClaudeUsageCommandPty appropriately reflects its generalized usage.


159-166: LGTM! Platform-specific PTY configuration is correct.

The implementation correctly handles platform differences:

  • Working directory: Appropriate environment variables and fallbacks for each platform (USERPROFILE for Windows, HOME for Linux).
  • Shell/args construction:
    • Windows: cmd.exe /c claude /usage with separate arguments is correct for cmd.exe's behavior.
    • Linux: /bin/sh -c "claude /usage" with the command string as a single argument is correct for POSIX shell's -c option.

Both approaches correctly execute claude /usage on their respective platforms.

package.json (1)

25-25: LGTM! Clean Docker development workflow addition.

The new dev:docker script provides a convenient way to spin up the entire stack using Docker Compose, complementing the existing development scripts.

apps/ui/src/lib/electron.ts (1)

434-434: LGTM! Clean API surface extension.

The optional getApiKey method integrates cleanly into the Electron API surface, supporting the dual-mode (Electron/web) authentication model introduced in this PR.

apps/ui/src/store/setup-store.ts (1)

179-179: LGTM! Proper persistence of authentication state.

Persisting claudeAuthStatus ensures that authentication state (including environment-based API keys) is retained across reloads, which improves the user experience by avoiding re-checking on every startup.

apps/ui/src/components/views/terminal-view/terminal-panel.tsx (1)

944-957: WebSocket authentication approach is acceptable, with security caveat.

The implementation correctly adds the API key as a query parameter for Electron mode, while web mode relies on cookie-based authentication. The logic is sound and properly encoded.

Security note: Query parameters can be logged by proxies, load balancers, and server access logs. However, this is a standard and acceptable approach for WebSocket authentication when cookies aren't available (Electron mode). The same-origin cookie approach for web mode is more secure and appropriate for that context.

apps/ui/src/types/electron.d.ts (1)

467-467: LGTM! Type definition is consistent.

The type signature matches the implementation interface in electron.ts and properly models the optional API key retrieval method.

docker-compose.yml (1)

52-52: LGTM! Security improvement for Docker environment.

Changing the default CORS origin from wildcard (*) to a specific localhost origin (http://localhost:3007) is a security improvement that prevents unintended cross-origin access while still supporting the local Docker development workflow.

init.mjs (1)

355-369: LGTM! Improved logging visibility.

The change to pipe server output to both the console and log file is a UX improvement, making it easier to see important startup information (like the generated API key) in real-time while still preserving logs for troubleshooting.

apps/ui/src/components/views/settings-view.tsx (1)

59-67: LGTM! Comprehensive API key detection.

The composite check for hasApiKey now correctly accounts for both user-provided API keys and environment-based keys (ANTHROPIC_API_KEY), ensuring that usage tracking is hidden appropriately regardless of how authentication is configured. The optional chaining on claudeAuthStatus is properly defensive.

apps/ui/src/routes/login.tsx (1)

1-6: LGTM!

Clean route definition following TanStack Router's file-based routing pattern. The import path and route configuration align with the existing patterns in __root.tsx. Based on learnings, this correctly uses TanStack Router for file-based routing.

apps/ui/src/preload.ts (1)

22-24: LGTM!

The new getApiKey IPC channel follows the established pattern for exposing secure IPC methods via the context bridge. The return type Promise<string | null> appropriately handles the case where no API key is configured.

apps/server/src/routes/health/index.ts (1)

1-25: LGTM! Good security separation.

Keeping the basic health check unauthenticated is the correct approach for load balancers and container orchestration (e.g., Kubernetes liveness/readiness probes), while moving the detailed health endpoint to require authentication prevents information disclosure. The re-export pattern allows flexible mounting with auth middleware at a higher level.

apps/ui/src/components/views/board-view/board-header.tsx (1)

38-49: LGTM! Clear authentication gating logic.

The decomposition into hasApiKey, isCliVerified, and showUsageTracking improves readability. The logic correctly gates the usage popover to only CLI-authenticated users who don't have an API key configured, which aligns with the component in claude-usage-popover.tsx (Lines 34-36) that uses the same verification pattern.

apps/ui/src/components/claude-usage-popover.tsx (2)

34-37: LGTM!

The isCliVerified derivation is consistent with board-header.tsx, ensuring unified authentication gating across the codebase.


77-106: LGTM! Proper conditional data fetching.

The guards prevent unnecessary API calls when CLI authentication isn't verified. The dependency arrays correctly include isCliVerified to ensure effects re-run when authentication state changes.

apps/ui/src/components/dialogs/file-browser-dialog.tsx (1)

17-17: LGTM! Good refactor to centralized API utilities.

Replacing the raw fetch with apiPost ensures consistent authentication headers and base URL handling across the application. The error handling is preserved, and the typed response apiPost<BrowseResult> maintains type safety.

Also applies to: 96-118

apps/ui/src/components/views/login-view.tsx (1)

61-70: Good security practice using password input type.

Using type="password" for the API key input prevents shoulder-surfing and screen recording exposure. The autoFocus and font-mono styling choices are appropriate for this authentication flow.

apps/server/package.json (1)

32-32: Dependencies look appropriate for cookie-based session handling.

The cookie-parser middleware is a well-established Express choice for cookie parsing. Version 1.4.7 is current and compatible with Express 5.2.1. Type definitions are correctly placed in devDependencies.

apps/ui/src/routes/__root.tsx (2)

12-17: LGTM!

The auth-related imports are correctly sourced from @/lib/http-api-client and properly organized.


203-228: LGTM!

The conditional rendering for different auth states is well-structured - login route bypasses sidebar, loading state during auth check, and null return for redirect handling. The guards are properly ordered.

apps/server/src/routes/auth/index.ts (3)

78-84: Consider if returning the session token in the response body is necessary.

The session token is already set as an HTTP-only cookie (line 78). Returning it in the response body (line 83) allows JavaScript to access and store it, which partially defeats the XSS protection that HTTP-only cookies provide. If this is intentional for cross-origin scenarios, consider documenting this trade-off.

Is the explicit token return required for cross-origin cookie issues, or can clients rely solely on the HTTP-only cookie for authentication?


30-46: LGTM!

The status endpoint correctly uses isRequestAuthenticated to check auth state without requiring authentication, allowing the UI to determine if login is needed.


127-147: LGTM!

The logout endpoint properly invalidates the server-side session and clears the cookie with appropriate security options.

apps/ui/src/main.ts (3)

63-101: LGTM!

The API key management is well-implemented:

  • Secure file permissions (0o600)
  • Persistent storage in userData directory
  • Loads existing key or generates new one
  • Called synchronously before server startup ensuring key availability

375-376: LGTM!

The API key is correctly passed to the server process via environment variable, ensuring both processes share the same key.


715-718: LGTM!

The IPC handler correctly exposes the API key to the renderer process for authenticated HTTP requests.

apps/ui/src/lib/http-api-client.ts (4)

240-255: LGTM!

WebSocket authentication correctly uses query parameters since browser WebSocket API doesn't support custom headers. The fallback chain (API key → session token) handles both Electron and web modes appropriately.


320-339: LGTM!

Header construction properly prioritizes API key (Electron mode) and falls back to session token (web mode). The early return after setting API key header prevents accidentally including both.


341-376: LGTM!

All HTTP methods correctly include credentials: 'include' for cookie-based session auth fallback.


63-66: Remove unused parameter from clearSessionToken function.

The function signature includes a parameter token: string | null that is not used in the function body. This appears to be a copy-paste error from setSessionToken.

🔎 Proposed fix
 // Clear session token (called on logout)
-export const clearSessionToken = (): void => {
+export const clearSessionToken = (): void => {
   cachedSessionToken = null;
 };

Note: The current code is actually correct as shown - the function has no parameters. This comment is invalid if the annotated code accurately reflects the file. Please verify the actual implementation.

apps/ui/src/components/views/terminal-view.tsx (3)

489-509: LGTM!

The synchronous XMLHttpRequest during page unload correctly includes both authentication headers:

  • X-API-Key for API authentication
  • X-Terminal-Token for terminal-specific authentication
  • withCredentials = true for cookie fallback

Using sync XHR here is appropriate since async operations don't complete during beforeunload.


436-453: LGTM!

Migration to apiGet with proper typing and error handling. The dependency array correctly removed serverUrl since it's now handled internally by the api helpers.


830-866: LGTM!

The terminal creation correctly uses apiFetch for authenticated requests while preserving terminal-specific X-Terminal-Token header. Error handling for session limits (429) is comprehensive.

apps/server/src/index.ts (3)

115-149: LGTM!

Dynamic CORS configuration correctly:

  • Allows no-origin requests (Electron, curl)
  • Supports configurable CORS_ORIGIN allowlist
  • Permits localhost for development
  • Rejects unknown origins for security
  • Enables credentials for cookie auth

274-297: LGTM!

WebSocket upgrade handler correctly authenticates connections before upgrading. Unauthorized attempts receive proper 401 response and socket destruction.


180-188: LGTM!

Route ordering is correct:

  • /api/health and /api/auth mounted before authMiddleware (public endpoints)
  • authMiddleware applied to all /api/* routes
  • Protected detailed health endpoint mounted after middleware
apps/ui/src/lib/api-fetch.ts (3)

34-57: LGTM!

Header construction correctly prioritizes API key over session token and properly merges additional headers. The early return pattern prevents accidentally including both auth headers.


84-108: LGTM!

The core fetch wrapper properly:

  • Handles auth header injection with skipAuth escape hatch
  • Stringifies non-string bodies
  • Supports absolute URLs
  • Always includes credentials

110-161: LGTM!

Convenience wrappers provide good DX with proper TypeScript generics. The apiDeleteRaw variant is useful for checking response status before parsing.

apps/server/src/lib/auth.ts (4)

28-56: LGTM!

Session loading correctly filters expired sessions during startup, preventing stale session restoration. Good logging of loaded vs expired counts for debugging.


61-69: LGTM!

Session persistence with secure file permissions (0o600) ensures sessions survive server restarts while protecting the file from other users.


136-162: LGTM!

Session management functions correctly handle creation, validation with expiration checking, and cleanup on expiration.


214-269: LGTM!

The middleware correctly implements a priority chain for authentication methods:

  1. API key header (Electron)
  2. Session token header (Web explicit)
  3. API key query param (fallback)
  4. Session cookie (Web implicit)

Returns appropriate 401/403 status codes for missing vs invalid credentials.

Comment on lines 113 to 125
// Print API key to console for web mode users
console.log(`
╔═══════════════════════════════════════════════════════════════════════╗
║ 🔐 API Key for Web Mode Authentication ║
╠═══════════════════════════════════════════════════════════════════════╣
║ ║
║ When accessing via browser, you'll be prompted to enter this key: ║
║ ║
${API_KEY}
║ ║
║ In Electron mode, authentication is handled automatically. ║
╚═══════════════════════════════════════════════════════════════════════╝
`);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Console banner box is misaligned and API key may be logged to files.

  1. The banner box doesn't account for the API key length (36 chars for UUID), causing visual misalignment.
  2. The API key printed to console may be captured in log files if stdout is redirected, which could be a security concern in production deployments.

Consider:

  • Adding padding to align the box
  • Only printing the key in development mode or when explicitly requested
🔎 Proposed fix for alignment and conditional logging
-// Print API key to console for web mode users
-console.log(`
+// Print API key to console for web mode users (development only or when requested)
+const shouldPrintApiKey = process.env.NODE_ENV !== 'production' || process.env.PRINT_API_KEY === 'true';
+if (shouldPrintApiKey) {
+  console.log(`
 ╔═══════════════════════════════════════════════════════════════════════╗
 ║  🔐 API Key for Web Mode Authentication                               ║
 ╠═══════════════════════════════════════════════════════════════════════╣
 ║                                                                       ║
 ║  When accessing via browser, you'll be prompted to enter this key:    ║
 ║                                                                       ║
-║    ${API_KEY}
+║    ${API_KEY.padEnd(67)}║
 ║                                                                       ║
-║  In Electron mode, authentication is handled automatically.          ║
+║  In Electron mode, authentication is handled automatically.           ║
 ╚═══════════════════════════════════════════════════════════════════════╝
 `);
+}
📝 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.

Suggested change
// Print API key to console for web mode users
console.log(`
╔═══════════════════════════════════════════════════════════════════════╗
🔐 API Key for Web Mode Authentication
╠═══════════════════════════════════════════════════════════════════════╣
When accessing via browser, you'll be prompted to enter this key: ║
${API_KEY}
In Electron mode, authentication is handled automatically.
╚═══════════════════════════════════════════════════════════════════════╝
`);
// Print API key to console for web mode users (development only or when requested)
const shouldPrintApiKey = process.env.NODE_ENV !== 'production' || process.env.PRINT_API_KEY === 'true';
if (shouldPrintApiKey) {
console.log(`
╔═══════════════════════════════════════════════════════════════════════╗
🔐 API Key for Web Mode Authentication
╠═══════════════════════════════════════════════════════════════════════╣
When accessing via browser, you'll be prompted to enter this key: ║
${API_KEY.padEnd(67)}
In Electron mode, authentication is handled automatically.
╚═══════════════════════════════════════════════════════════════════════╝
`);
}
🤖 Prompt for AI Agents
In apps/server/src/lib/auth.ts around lines 113 to 125, the console banner
currently assumes a fixed width and prints the raw API key unconditionally;
update it to compute the banner inner width dynamically based on the API key
length (ensure at least the current minimum width), pad the API key line so the
box borders remain aligned, and rebuild the surrounding banner lines to match
that width; additionally, stop unguarded logging of the secret by only printing
the banner when running in development or when an explicit opt-in
flag/environment variable (e.g. SHOW_API_KEY or NODE_ENV === 'development') is
set, otherwise omit the API key output entirely.

Test User and others added 3 commits December 29, 2025 17:17
Security improvements identified for the protect-api-with-api-key branch:
- Use short-lived wsToken for WebSocket auth (not session tokens in URLs)
- Add AUTOMAKER_HIDE_API_KEY env var to suppress console logging
- Add rate limiting to login endpoint (5 attempts/min/IP)
- Use timing-safe comparison for API key validation
- Make WebSocket tokens single-use

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Use crypto.timingSafeEqual() for API key validation (prevents timing attacks)
- Make WebSocket tokens single-use (invalidated after first validation)
- Add AUTOMAKER_HIDE_API_KEY env var to suppress API key banner in logs
- Add rate limiting to login endpoint (5 attempts/minute/IP)
- Update client to fetch short-lived wsToken for WebSocket auth
  (session tokens no longer exposed in URLs)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Added NODE_ENV variable for development in docker-compose.override.yml.example.
- Changed default NODE_ENV to development in Dockerfile.
- Implemented fetchWsToken function to retrieve short-lived WebSocket tokens for secure authentication in TerminalPanel.
- Updated connect function to use wsToken for WebSocket connections when API key is not available.
- Introduced verifySession function to validate session status after login and on app load, ensuring session integrity.
- Modified RootLayoutContent to verify session cookie validity and redirect to login if the session is invalid or expired.

These changes improve the security and reliability of the authentication process.
@webdevcody
Copy link
Collaborator Author

@CodeRabbit re-review

@coderabbitai
Copy link

coderabbitai bot commented Dec 30, 2025

@webdevcody I'll re-review the changes in this PR for you.

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

- Changed the resolved URL for the @electron/node-gyp module from SSH to HTTPS for improved accessibility and compatibility.
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (2)
apps/server/src/lib/auth.ts (2)

131-146: API key banner alignment issue remains.

The banner box is misaligned because line 139 doesn't account for the API key length (36 characters for UUID) and is missing the closing . This was noted in a previous review.

🔎 Proposed fix for alignment
-║    ${API_KEY}
+║    ${API_KEY.padEnd(67)}║

391-398: Duplication with checkAuthentication noted in previous review.

This function is a thin wrapper that calls checkAuthentication and returns only the boolean result. While this creates some duplication with the middleware, it provides a cleaner API for callers that only need a boolean. The current approach is acceptable.

🧹 Nitpick comments (4)
apps/ui/src/lib/http-api-client.ts (1)

227-270: Consider making the verification endpoint more explicit.

The verifySession() function uses /api/settings/status for verification, which creates a tight coupling to a specific endpoint. While functional, this could be fragile if that endpoint changes or is unavailable.

Consider one of these approaches:

  1. Use a dedicated /api/auth/verify endpoint for session verification
  2. Document the dependency on /api/settings/status clearly in comments
  3. Add fallback verification logic if the primary endpoint fails

This is a minor maintainability concern and can be addressed in a future refactor.

apps/ui/src/routes/__root.tsx (1)

214-220: LGTM! Proper loading state prevents unauthorized content flash.

The loading state prevents users from briefly seeing protected content before authentication completes. The check for !isElectronMode() correctly limits this behavior to web mode only.

Minor UX enhancement: Consider making the loading message more specific:

-        <div className="text-muted-foreground">Loading...</div>
+        <div className="text-muted-foreground">Verifying authentication...</div>

This is purely cosmetic and can be deferred.

apps/server/src/routes/auth/index.ts (1)

27-45: Document the single-instance limitation of in-memory rate limiting.

The in-memory rate limiting approach works well for single-instance deployments (local development, single-container Docker), but won't work correctly in multi-instance scenarios where each instance maintains its own loginAttempts map.

Consider adding a comment to document this limitation:

 // Rate limiting configuration
 const RATE_LIMIT_WINDOW_MS = 60 * 1000; // 1 minute window
 const RATE_LIMIT_MAX_ATTEMPTS = 5; // Max 5 attempts per window

-// In-memory rate limit tracking (resets on server restart)
+// In-memory rate limit tracking (resets on server restart)
+// NOTE: This is per-instance. For multi-instance deployments, use a shared store (Redis, etc.)
 const loginAttempts = new Map<string, { count: number; windowStart: number }>();

For now, this is acceptable given the deployment scenarios, but should be addressed before horizontal scaling.

apps/server/src/lib/auth.ts (1)

309-316: API key in query parameter lacks timing-safe comparison for length check.

When checking queryKey, the flow goes to validateApiKey which is timing-safe. However, the existence check if (queryKey) could theoretically leak timing information about whether the parameter was provided.

In practice, this is a minor concern since the mere presence of a query parameter is observable in logs/network. The actual value validation is timing-safe.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d68de99 and e498f39.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (12)
  • Dockerfile
  • apps/server/package.json
  • apps/server/src/index.ts
  • apps/server/src/lib/auth.ts
  • apps/server/src/routes/auth/index.ts
  • apps/server/tests/unit/lib/auth.test.ts
  • apps/ui/src/components/views/settings-view.tsx
  • apps/ui/src/components/views/terminal-view/terminal-panel.tsx
  • apps/ui/src/lib/http-api-client.ts
  • apps/ui/src/routes/__root.tsx
  • docker-compose.override.yml.example
  • docs/plans/2025-12-29-api-security-hardening-design.md
💤 Files with no reviewable changes (1)
  • Dockerfile
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Always import from shared packages (@automaker/*), never from old relative paths

Files:

  • apps/ui/src/components/views/settings-view.tsx
  • apps/ui/src/components/views/terminal-view/terminal-panel.tsx
  • apps/ui/src/routes/__root.tsx
  • apps/server/src/index.ts
  • apps/server/tests/unit/lib/auth.test.ts
  • apps/server/src/lib/auth.ts
  • apps/ui/src/lib/http-api-client.ts
  • apps/server/src/routes/auth/index.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Use resolveModelString() from @automaker/model-resolver to convert model aliases (haiku, sonnet, opus) to full model names

Files:

  • apps/ui/src/components/views/settings-view.tsx
  • apps/ui/src/components/views/terminal-view/terminal-panel.tsx
  • apps/ui/src/routes/__root.tsx
  • apps/server/src/index.ts
  • apps/server/tests/unit/lib/auth.test.ts
  • apps/server/src/lib/auth.ts
  • apps/ui/src/lib/http-api-client.ts
  • apps/server/src/routes/auth/index.ts
apps/server/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Use createEventEmitter() from lib/events.ts for all server operations to emit events that stream to frontend via WebSocket

Files:

  • apps/server/src/index.ts
  • apps/server/src/lib/auth.ts
  • apps/server/src/routes/auth/index.ts
🧠 Learnings (2)
📚 Learning: 2025-12-28T05:07:48.138Z
Learnt from: CR
Repo: AutoMaker-Org/automaker PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T05:07:48.138Z
Learning: Frontend UI must use TanStack Router for file-based routing, organize components in components/views/, implement stores with Zustand, and use custom hooks in hooks/ directory

Applied to files:

  • apps/ui/src/components/views/settings-view.tsx
📚 Learning: 2025-12-28T05:07:48.138Z
Learnt from: CR
Repo: AutoMaker-Org/automaker PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T05:07:48.138Z
Learning: Applies to apps/server/src/**/*.{ts,tsx} : Use `createEventEmitter()` from `lib/events.ts` for all server operations to emit events that stream to frontend via WebSocket

Applied to files:

  • apps/ui/src/components/views/terminal-view/terminal-panel.tsx
  • apps/server/src/index.ts
🧬 Code graph analysis (5)
apps/ui/src/components/views/settings-view.tsx (2)
apps/ui/src/store/setup-store.ts (1)
  • useSetupStore (125-183)
init.mjs (1)
  • isWindows (33-33)
apps/ui/src/components/views/terminal-view/terminal-panel.tsx (1)
apps/ui/src/lib/http-api-client.ts (3)
  • fetchWsToken (306-338)
  • getSessionToken (56-56)
  • getApiKey (53-53)
apps/ui/src/routes/__root.tsx (1)
apps/ui/src/lib/http-api-client.ts (3)
  • initApiKey (81-100)
  • isElectronMode (71-73)
  • verifySession (227-270)
apps/server/tests/unit/lib/auth.test.ts (1)
apps/server/src/lib/auth.ts (2)
  • isAuthEnabled (374-376)
  • getAuthStatus (381-386)
apps/server/src/routes/auth/index.ts (1)
apps/server/src/lib/auth.ts (7)
  • isRequestAuthenticated (391-398)
  • validateApiKey (233-247)
  • createSession (158-167)
  • getSessionCookieOptions (252-266)
  • getSessionCookieName (271-273)
  • createWsConnectionToken (199-207)
  • invalidateSession (190-193)
🪛 LanguageTool
docs/plans/2025-12-29-api-security-hardening-design.md

[grammar] ~53-~53: Use a hyphen to join words.
Context: ...ver/src/routes/auth/index.ts` - Add rate limiting logic to login handler --- ##...

(QB_NEW_EN_HYPHEN)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: e2e
🔇 Additional comments (36)
apps/ui/src/components/views/settings-view.tsx (3)

3-3: LGTM!

The import is necessary to access the Claude authentication status from the setup store.


59-59: LGTM!

Correctly retrieves the Claude authentication status using the Zustand selector pattern.


62-70: Previous review concern resolved – logic looks correct.

The implementation now includes the isCliVerified check as suggested in the previous review, ensuring consistency with board-header.tsx. The logic correctly:

  • Checks for API keys from both user input and environment variables
  • Verifies CLI authentication status and method
  • Combines all conditions appropriately with Windows platform exclusion
docker-compose.override.yml.example (1)

11-11: LGTM! Appropriate development configuration.

Adding NODE_ENV=development aligns with the server-side changes that conditionally enable/disable features based on environment (e.g., secure cookies, CORS origins). This is a good practice for development setups.

apps/ui/src/lib/http-api-client.ts (3)

44-73: LGTM! Clean dual-mode authentication state management.

The module-level caching approach is appropriate for a singleton client, and the exported getters/setters provide controlled access to authentication state for both Electron (API key) and Web (session token) modes.


81-100: LGTM! Proper initialization with guard pattern.

The initialization guard and error handling ensure robust startup behavior for both Electron and Web modes. The logging is helpful for debugging authentication issues.


306-371: LGTM! Well-structured WebSocket authentication flow.

The separation of fetchWsToken() and establishWebSocket() provides good error handling and supports both Electron (API key) and Web (wsToken) modes. The fallback warning at line 363 is appropriate for debugging auth issues.

apps/ui/src/components/views/terminal-view/terminal-panel.tsx (1)

977-1207: LGTM! WebSocket authentication properly implements short-lived wsToken flow.

The connection logic correctly implements the security-hardened approach:

  • Electron mode: Uses API key directly (line 982-984)
  • Web mode: Fetches short-lived wsToken before connecting (line 987-990)
  • Falls back to cookie-based auth if wsToken is unavailable (line 991-992)

This addresses the security concern from previous reviews about exposing session tokens in URLs. The wsToken approach is more secure and aligns with the design document.

Note: The code duplication issue raised in the previous comment should still be addressed to improve maintainability.

apps/ui/src/routes/__root.tsx (1)

79-121: Verify the redirect logic handles navigation race conditions.

The authentication initialization effect depends on location.pathname and navigate, which means it will re-run whenever the user navigates. While the guards at lines 107 and 114 prevent infinite loops, there could be edge cases:

  1. User navigates to another route while auth is being verified
  2. Multiple simultaneous navigation attempts during auth initialization
  3. Browser back/forward button during auth check

Test the following scenarios to ensure robust behavior:

  1. User navigates to a protected route before auth completes
  2. User clicks browser back button during auth initialization
  3. Rapid navigation between routes while verifySession() is in flight

Consider adding a ref to track if initialization is already in progress:

const authInitInProgressRef = useRef(false);

useEffect(() => {
  if (authInitInProgressRef.current) return; // Prevent concurrent init
  authInitInProgressRef.current = true;
  
  const initAuth = async () => {
    try {
      // ... existing logic ...
    } finally {
      authInitInProgressRef.current = false;
    }
  };
  
  initAuth();
}, [location.pathname, navigate]);

This would prevent race conditions from concurrent auth checks.

apps/server/package.json (2)

32-32: LGTM! Standard dependency addition for cookie handling.

Adding cookie-parser is appropriate for the new cookie-based session authentication. The version (1.4.7) is current and stable.


41-42: LGTM! Type definitions for TypeScript support.

Adding type definitions for cookie and cookie-parser provides proper TypeScript support for the new cookie handling functionality.

apps/server/tests/unit/lib/auth.test.ts (3)

13-26: LGTM! Test correctly validates authentication requirement.

The test suite correctly reflects the always-on authentication model. The updated error message "Authentication required." is clearer than the previous version.


59-64: LGTM! Test aligns with always-on authentication.

The test correctly expects isAuthEnabled() to always return true, matching the new authentication model where API key protection is always active.


67-76: LGTM! Test reflects multi-path authentication.

The test correctly expects the api_key_or_session method, which accurately describes the new authentication system that supports both API key headers and session cookies.

docs/plans/2025-12-29-api-security-hardening-design.md (1)

1-94: LGTM! Comprehensive security hardening design.

The design document clearly outlines five security improvements with specific implementation targets. The phased implementation order is logical, starting with simple isolated changes and progressing to more complex coordinated updates.

Based on my review of the other files in this PR:

  • ✅ Fix #1 (wsToken): Implemented in http-api-client.ts and terminal-panel.tsx
  • ✅ Fix #3 (rate limiting): Implemented in apps/server/src/routes/auth/index.ts (next file to review)
  • ⏳ Fix #2, #4, #5: Should be in apps/server/src/lib/auth.ts (not provided in this review)

The implementation appears to follow the design document correctly.

apps/server/src/routes/auth/index.ts (5)

51-59: LGTM! Standard IP detection with appropriate fallbacks.

The IP detection correctly handles reverse proxy scenarios with X-Forwarded-For and provides sensible fallbacks.

Note on security: X-Forwarded-For can be spoofed by clients if not behind a trusted proxy. However, this rate limiting is a defense-in-depth measure—the primary security is the timing-safe API key validation. The current implementation is acceptable for the intended deployment scenarios.


65-86: LGTM! Correct rate limit checking with proper cleanup.

The rate limit logic correctly handles window expiration and provides accurate retryAfter values for clients to implement exponential backoff. The window-based approach is more user-friendly than a simple counter.


135-182: LGTM! Secure login implementation with proper rate limiting.

The login endpoint correctly implements:

  1. ✅ Rate limiting before processing (prevents timing attacks)
  2. ✅ Records all attempts (including malformed requests)
  3. ✅ Timing-safe API key validation via validateApiKey()
  4. ✅ HTTP-only cookie for primary auth
  5. ✅ Returns session token for header-based auth (documented use case)

Security note: The session token is returned in the response body (line 180) to support explicit header-based authentication (X-Session-Token). While this reduces some benefits of HTTP-only cookies, it's necessary for cross-origin scenarios and is documented in the comments. The primary session cookie remains HTTP-only.


191-209: LGTM! Secure wsToken generation for WebSocket authentication.

The /token endpoint correctly implements the security-hardened approach described in the design document:

  • ✅ Validates existing session before issuing wsToken
  • ✅ Generates a NEW short-lived token (not exposing the session cookie)
  • ✅ Returns 401 for unauthenticated requests
  • ✅ Documents the 5-minute expiry

This addresses the previous security concern about exposing session values. The wsToken is a separate, short-lived credential suitable for WebSocket URLs.


216-236: LGTM! Proper logout implementation with session cleanup.

The logout endpoint correctly:

  • ✅ Invalidates the server-side session
  • ✅ Clears the HTTP-only cookie with matching options
  • ✅ Handles missing session gracefully

The cookie clearing options (lines 225-230) match the creation options, ensuring proper cleanup.

apps/server/src/index.ts (7)

12-13: Good use of the cookie library for parsing.

The import of cookie addresses the previous review feedback about brittle manual cookie parsing. This is the correct approach.


20-21: Imports align with the new authentication architecture.

The new imports (validateWsConnectionToken, checkRawAuthentication, createAuthRoutes, createDetailedHandler) support the multi-path authentication system being introduced.

Also applies to: 23-23


116-142: CORS configuration correctly handles credentials with dynamic origins.

The logic properly:

  • Allows no-origin requests (Electron, curl, mobile apps)
  • Respects CORS_ORIGIN env var allowlist
  • Permits localhost for development
  • Rejects other origins by default

One consideration: the allowedOrigins[0] !== '*' check on line 125 allows * to bypass validation, which would then fall through to localhost-only. This is likely intentional but worth documenting.


176-178: Auth routes are correctly placed before the auth middleware.

The /api/health and /api/auth routes are mounted before authMiddleware is applied, allowing unauthenticated access to health checks and login endpoints.


183-184: Detailed health endpoint is protected.

The /api/health/detailed endpoint is mounted after authMiddleware is applied on line 181, ensuring it requires authentication.


218-253: WebSocket authentication function is well-structured.

The function correctly:

  • Parses URL and query parameters
  • Uses the cookie library for parsing (addressing past review)
  • Delegates to shared checkRawAuthentication for consistency
  • Additionally validates short-lived wsToken for WebSocket-specific auth

The layered approach (shared auth logic + WebSocket-specific tokens) is appropriate for the security model.


259-265: WebSocket upgrade now requires authentication.

All WebSocket connections are now authenticated before upgrade, with proper 401 response and socket destruction on failure. This closes the security gap mentioned in the PR objectives.

apps/server/src/lib/auth.ts (9)

29-37: Periodic cleanup prevents unbounded memory growth.

The 60-second interval cleanup for expired WebSocket tokens is appropriate given their 5-minute lifetime. This prevents memory leaks from tokens that are created but never used.


42-70: Session loading handles expiration correctly.

The function properly filters out expired sessions during load and logs helpful diagnostics. Using synchronous read at startup is acceptable since this runs once during initialization.


75-86: Session persistence is now async.

This addresses the previous review feedback about blocking I/O. The file permissions (0o600) correctly restrict access to the owner only.


115-124: Synchronous file write during key generation.

The fs.writeFileSync at line 119 is acceptable here since it only runs once during startup when generating a new API key. The 0o600 permissions correctly restrict access.


173-185: Fire-and-forget pattern for session cleanup is appropriate.

The synchronous return with async persistence (saveSessions().catch(...)) ensures fast validation while still persisting changes. The error handling prevents unhandled rejections.


214-227: Single-use WebSocket tokens are correctly implemented.

The token is deleted immediately upon lookup (line 219), before checking expiration. This ensures tokens can only be used once, even if validation fails due to expiration.


233-247: Timing-safe API key comparison is correctly implemented.

The implementation:

  1. Validates input type and existence
  2. Uses crypto.timingSafeEqual for constant-time comparison
  3. Handles length mismatch by performing a dummy comparison to maintain constant time

This prevents timing attacks that could leak information about the key.


259-266: Session cookie options are secure.

The configuration correctly:

  • Sets httpOnly: true to prevent XSS access
  • Enables secure only in production (allows HTTP in development)
  • Uses sameSite: 'strict' for CSRF protection
  • Sets appropriate max age matching session lifetime

404-409: checkRawAuthentication provides clean interface for WebSocket auth.

This wrapper appropriately exposes the authentication check for non-Express contexts like WebSocket upgrade handlers, returning only the boolean result needed.

Comment on lines +318 to +322
// Check for session cookie (web mode)
const sessionToken = cookies[SESSION_COOKIE_NAME];
if (sessionToken && validateSession(sessionToken)) {
return { authenticated: true };
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Cookie session validation inconsistency.

Unlike other auth methods (lines 293-297, 302-307, 311-316), the cookie-based session check on line 320 doesn't return invalid_session error type when the token exists but is invalid. It falls through to no_auth.

This means an expired cookie session returns "Authentication required" (401) instead of "Invalid or expired session token" (403), which may confuse users.

🔎 Proposed fix for consistent error handling
   // Check for session cookie (web mode)
   const sessionToken = cookies[SESSION_COOKIE_NAME];
-  if (sessionToken && validateSession(sessionToken)) {
-    return { authenticated: true };
+  if (sessionToken) {
+    if (validateSession(sessionToken)) {
+      return { authenticated: true };
+    }
+    return { authenticated: false, errorType: 'invalid_session' };
   }

   return { authenticated: false, errorType: 'no_auth' };
📝 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.

Suggested change
// Check for session cookie (web mode)
const sessionToken = cookies[SESSION_COOKIE_NAME];
if (sessionToken && validateSession(sessionToken)) {
return { authenticated: true };
}
// Check for session cookie (web mode)
const sessionToken = cookies[SESSION_COOKIE_NAME];
if (sessionToken) {
if (validateSession(sessionToken)) {
return { authenticated: true };
}
return { authenticated: false, errorType: 'invalid_session' };
}
return { authenticated: false, errorType: 'no_auth' };
🤖 Prompt for AI Agents
In apps/server/src/lib/auth.ts around lines 318 to 322, the cookie session check
currently returns authenticated on a valid token but falls through to "no_auth"
when a session cookie exists but validateSession(token) returns false; change
the logic so that when a session cookie is present but
validateSession(sessionToken) fails you immediately return the same
invalid_session response shape used elsewhere (e.g., { authenticated: false,
errorType: 'invalid_session', message: 'Invalid or expired session token' }) so
cookie-based failures map to 403 like the other auth methods.

- Introduced `requireJsonContentType` middleware to ensure that all POST, PUT, and PATCH requests have the Content-Type set to application/json.
- This enhancement improves security by preventing CSRF and content-type confusion attacks, ensuring only properly formatted requests are processed.
Copy link

@coderabbitai coderabbitai bot left a 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 (1)
apps/server/src/index.ts (1)

135-139: Consider allowing HTTPS localhost origins for local development.

The current logic only allows http://localhost: and http://127.0.0.1: origins. Developers using HTTPS for local development (e.g., with mkcert or self-signed certificates) would be blocked.

🔎 Proposed fix to support HTTPS localhost
-      // For local development, allow localhost origins
-      if (origin.startsWith('http://localhost:') || origin.startsWith('http://127.0.0.1:')) {
+      // For local development, allow localhost origins (HTTP and HTTPS)
+      if (
+        origin.startsWith('http://localhost:') ||
+        origin.startsWith('http://127.0.0.1:') ||
+        origin.startsWith('https://localhost:') ||
+        origin.startsWith('https://127.0.0.1:')
+      ) {
         callback(null, origin);
         return;
       }
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e498f39 and adfc353.

📒 Files selected for processing (2)
  • apps/server/src/index.ts
  • apps/server/src/middleware/require-json-content-type.ts
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Always import from shared packages (@automaker/*), never from old relative paths

Files:

  • apps/server/src/middleware/require-json-content-type.ts
  • apps/server/src/index.ts
apps/server/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Use createEventEmitter() from lib/events.ts for all server operations to emit events that stream to frontend via WebSocket

Files:

  • apps/server/src/middleware/require-json-content-type.ts
  • apps/server/src/index.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Use resolveModelString() from @automaker/model-resolver to convert model aliases (haiku, sonnet, opus) to full model names

Files:

  • apps/server/src/middleware/require-json-content-type.ts
  • apps/server/src/index.ts
🧠 Learnings (1)
📚 Learning: 2025-12-28T05:07:48.138Z
Learnt from: CR
Repo: AutoMaker-Org/automaker PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T05:07:48.138Z
Learning: Applies to apps/server/src/**/*.{ts,tsx} : Use `createEventEmitter()` from `lib/events.ts` for all server operations to emit events that stream to frontend via WebSocket

Applied to files:

  • apps/server/src/index.ts
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: e2e
🔇 Additional comments (4)
apps/server/src/index.ts (3)

177-179: LGTM - Correct middleware positioning for API security.

The requireJsonContentType middleware is correctly mounted before all API routes (including auth and health), ensuring consistent JSON content-type enforcement across the entire API surface for CSRF protection.


227-258: LGTM - Proper cookie parsing and multi-source authentication.

The WebSocket authentication correctly uses cookie.parse() from the cookie package (line 238) for robust cookie parsing, avoiding the brittleness of manual string manipulation. The function properly checks multiple authentication sources (API key, session token, session cookie, and wsToken) for flexible client support.


264-270: LGTM - WebSocket authentication properly enforced.

The authentication check correctly protects all WebSocket upgrade requests, rejecting unauthorized connections with a 401 response before the upgrade completes. This aligns with the PR's security objectives.

apps/server/src/middleware/require-json-content-type.ts (1)

29-50: The middleware behavior is documented as intentional. The code comment in apps/server/src/index.ts (lines 177-178) explicitly states the requirement is to "prevent CSRF and content-type confusion attacks." This security rationale is sound and the strict enforcement is working as designed. No issues found in the codebase.

- Added comprehensive unit tests for authentication middleware, including session token validation, API key authentication, and cookie-based authentication.
- Implemented tests for session management functions such as creating, updating, archiving, and deleting sessions.
- Improved test coverage for queue management in session handling, ensuring robust error handling and validation.
- Introduced checks for session metadata and working directory validation to ensure proper session creation.
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (3)
apps/server/tests/unit/services/agent-service.test.ts (1)

93-112: Consider testing behavior rather than implementation details.

The assertion on line 111 checks for an exact count of fs.readFile calls, which tightly couples the test to the current implementation. If the service changes how it stores or retrieves session data (e.g., combining files, adding caching), this test will fail even if the public behavior remains correct.

Consider refactoring to verify the observable behavior (e.g., that the second call returns cached data quickly, or that the session state is consistent) rather than counting internal I/O operations.

apps/server/tests/unit/lib/auth.test.ts (2)

3-4: Remove unused imports.

The fs and path imports are not used anywhere in this test file.

🔎 Proposed fix
 import { describe, it, expect, beforeEach, vi } from 'vitest';
 import { createMockExpressContext } from '../../utils/mocks.js';
-import fs from 'fs';
-import path from 'path';
 
 /**

150-160: Add timer cleanup to prevent test pollution.

If a test fails before reaching vi.useRealTimers(), fake timers remain active and could affect subsequent tests. Move timer restoration to an afterEach hook within the relevant describe blocks or use try-finally.

🔎 Example approach using afterEach

For the validateSession suite:

 describe('validateSession', () => {
+  afterEach(() => {
+    vi.useRealTimers();
+  });
+
   it('should validate a valid session token', async () => {
     // ... existing test
   });

   it('should reject expired session token', async () => {
     vi.useFakeTimers();
     const { createSession, validateSession } = await import('@/lib/auth.js');
     const token = await createSession();

     // Advance time past session expiration (30 days)
     vi.advanceTimersByTime(31 * 24 * 60 * 60 * 1000);

     expect(validateSession(token)).toBe(false);
-    vi.useRealTimers();
   });
 });

Apply similar changes to the validateWsConnectionToken suite.

Also applies to: 207-217

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between adfc353 and 4c65855.

📒 Files selected for processing (2)
  • apps/server/tests/unit/lib/auth.test.ts
  • apps/server/tests/unit/services/agent-service.test.ts
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Always import from shared packages (@automaker/*), never from old relative paths

Files:

  • apps/server/tests/unit/services/agent-service.test.ts
  • apps/server/tests/unit/lib/auth.test.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Use resolveModelString() from @automaker/model-resolver to convert model aliases (haiku, sonnet, opus) to full model names

Files:

  • apps/server/tests/unit/services/agent-service.test.ts
  • apps/server/tests/unit/lib/auth.test.ts
🧬 Code graph analysis (1)
apps/server/tests/unit/lib/auth.test.ts (1)
apps/server/src/lib/auth.ts (13)
  • authMiddleware (336-369)
  • createSession (158-167)
  • getSessionCookieName (271-273)
  • validateSession (173-185)
  • invalidateSession (190-193)
  • createWsConnectionToken (199-207)
  • validateWsConnectionToken (214-227)
  • validateApiKey (233-247)
  • getSessionCookieOptions (252-266)
  • isRequestAuthenticated (391-398)
  • checkRawAuthentication (404-410)
  • isAuthEnabled (374-376)
  • getAuthStatus (381-386)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: e2e
🔇 Additional comments (12)
apps/server/tests/unit/services/agent-service.test.ts (1)

239-264: Verify model string usage in tests.

The test uses the hardcoded model string 'claude-sonnet-4-20250514'. According to the coding guidelines, resolveModelString() from @automaker/model-resolver should be used to convert model aliases to full model names.

While the guideline specifically mentions aliases like "haiku", "sonnet", and "opus", it's unclear whether tests should also use resolveModelString() for consistency with production code, or if hardcoded strings are acceptable for test stability.

Please verify whether test files should use resolveModelString() or if hardcoded model strings are appropriate for testing purposes.

Similar usage appears on:

  • Line 420: service.setSessionModel('session-1', 'claude-sonnet-4-20250514')
  • Line 426: service.setSessionModel('nonexistent', 'claude-sonnet-4-20250514')
  • Line 609: model: 'claude-sonnet-4-20250514'

As per coding guidelines, please clarify the expected approach for model strings in tests.

apps/server/tests/unit/lib/auth.test.ts (11)

11-16: LGTM!

The beforeEach hook correctly resets modules and clears environment variables to ensure test isolation, which is essential when testing module-level initialization behavior.


18-115: LGTM!

The authMiddleware test suite provides comprehensive coverage of all authentication paths: API key (header and query), session tokens (header and cookie), and proper error responses (401 for missing auth, 403 for invalid credentials).


117-134: LGTM!

Session creation tests properly verify that tokens are generated as non-empty strings and that each call produces unique tokens, which is critical for security.


136-161: LGTM!

Session validation tests comprehensively cover valid tokens, invalid tokens, and expiration scenarios using fake timers.


163-172: LGTM!

The session invalidation test properly verifies the complete lifecycle, ensuring that a valid session becomes invalid after invalidation.


174-191: LGTM!

WebSocket token creation tests correctly verify token generation and uniqueness, matching the pattern established for session token creation.


193-227: LGTM!

WebSocket token validation tests are comprehensive, including the critical single-use behavior where a token is consumed after first validation. The expiration test correctly verifies the 5-minute timeout.


229-272: LGTM!

API key validation tests are thorough, covering correct keys, incorrect keys, empty strings, null/undefined inputs, and importantly, timing-safe comparison behavior. The use of as any in lines 259-260 is appropriate for testing invalid input types that TypeScript would normally reject.


274-302: LGTM!

Cookie options tests properly verify all security-relevant properties including httpOnly, sameSite, and environment-dependent secure flag behavior.


304-311: LGTM!

The cookie name test correctly verifies the expected constant value.


313-398: LGTM!

The remaining test suites (isRequestAuthenticated, checkRawAuthentication, isAuthEnabled, getAuthStatus) provide comprehensive coverage of authentication helper functions. Tests correctly verify all authentication methods (API key, session token, cookies) and properly assert that authentication is always enabled with the combined api_key_or_session method.

Comment on lines +117 to +172
describe('createSession', () => {
it('should create a new session and return token', async () => {
const { createSession } = await import('@/lib/auth.js');
const token = await createSession();

const { isAuthEnabled } = await import('@/lib/auth.js');
expect(isAuthEnabled()).toBe(false);
expect(token).toBeDefined();
expect(typeof token).toBe('string');
expect(token.length).toBeGreaterThan(0);
});

it('should return true when API key is set', async () => {
process.env.AUTOMAKER_API_KEY = 'test-key';
it('should create unique tokens for each session', async () => {
const { createSession } = await import('@/lib/auth.js');
const token1 = await createSession();
const token2 = await createSession();

const { isAuthEnabled } = await import('@/lib/auth.js');
expect(isAuthEnabled()).toBe(true);
expect(token1).not.toBe(token2);
});
});

describe('getAuthStatus', () => {
it('should return disabled status when no API key', async () => {
delete process.env.AUTOMAKER_API_KEY;
describe('validateSession', () => {
it('should validate a valid session token', async () => {
const { createSession, validateSession } = await import('@/lib/auth.js');
const token = await createSession();

const { getAuthStatus } = await import('@/lib/auth.js');
const status = getAuthStatus();
expect(validateSession(token)).toBe(true);
});

expect(status).toEqual({
enabled: false,
method: 'none',
});
it('should reject invalid session token', async () => {
const { validateSession } = await import('@/lib/auth.js');

expect(validateSession('invalid-token')).toBe(false);
});

it('should reject expired session token', async () => {
vi.useFakeTimers();
const { createSession, validateSession } = await import('@/lib/auth.js');
const token = await createSession();

// Advance time past session expiration (30 days)
vi.advanceTimersByTime(31 * 24 * 60 * 60 * 1000);

expect(validateSession(token)).toBe(false);
vi.useRealTimers();
});
});

describe('invalidateSession', () => {
it('should invalidate a session token', async () => {
const { createSession, validateSession, invalidateSession } = await import('@/lib/auth.js');
const token = await createSession();

expect(validateSession(token)).toBe(true);
await invalidateSession(token);
expect(validateSession(token)).toBe(false);
});
});
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, check the structure and setup of the test file
head -120 apps/server/tests/unit/lib/auth.test.ts | tail -30

Repository: AutoMaker-Org/automaker

Length of output: 1147


🏁 Script executed:

# Check for any test setup, mocking, or teardown
rg -n "beforeEach|afterEach|vi\.mock|mock|setup|teardown" apps/server/tests/unit/lib/auth.test.ts -B 2 -A 2

Repository: AutoMaker-Org/automaker

Length of output: 383


🏁 Script executed:

# Check the auth.ts implementation for saveSessions and loadSessions
rg -n "saveSessions|loadSessions|fs\." apps/server/src/lib/auth.ts -B 2 -A 5

Repository: AutoMaker-Org/automaker

Length of output: 2569


🏁 Script executed:

# Check test utilities and mocks configuration
find apps/server/tests -name "*.ts" -type f | xargs rg -l "mock|Mock" | head -5

Repository: AutoMaker-Org/automaker

Length of output: 322


🏁 Script executed:

# Check if there's a vitest config or setup file that might handle mocking
find apps/server -name "vitest.config.*" -o -name "setup*.ts" -o -name "*.setup.ts" | head -10

Repository: AutoMaker-Org/automaker

Length of output: 122


🏁 Script executed:

cat -n apps/server/tests/setup.ts

Repository: AutoMaker-Org/automaker

Length of output: 468


🏁 Script executed:

cat -n apps/server/vitest.config.ts

Repository: AutoMaker-Org/automaker

Length of output: 2555


🏁 Script executed:

cat -n apps/server/tests/utils/mocks.ts

Repository: AutoMaker-Org/automaker

Length of output: 3237


🏁 Script executed:

# Check if there's a .gitigore or similar that indicates session files are gitignored
find apps/server -name ".gitignore" -o -name "*.env*" | xargs grep -l "session\|\.auth\|\.key" 2>/dev/null

Repository: AutoMaker-Org/automaker

Length of output: 91


Mock the file system or implement cleanup in session tests.

Tests create and invalidate sessions, triggering real file writes via saveSessions() to /tmp/test-data. No mocks or cleanup are applied, leaving orphaned session files. A createMockFs() utility already exists in tests/utils/mocks.ts but isn't used here. Either mock fs using vi.mock() or apply createMockFs() with proper module isolation, or implement afterEach() cleanup to delete session files.

Comment on lines +374 to +402
it('should validate working directory', async () => {
// Set ALLOWED_ROOT_DIRECTORY to restrict paths
const originalAllowedRoot = process.env.ALLOWED_ROOT_DIRECTORY;
process.env.ALLOWED_ROOT_DIRECTORY = '/allowed/projects';

// Re-import platform to initialize with new env var
vi.resetModules();
const { initAllowedPaths } = await import('@automaker/platform');
initAllowedPaths();

const { AgentService } = await import('@/services/agent-service.js');
const testService = new AgentService('/test/data', mockEvents as any);
vi.mocked(fs.readFile).mockResolvedValue('{}');
vi.mocked(fs.writeFile).mockResolvedValue(undefined);

await expect(
testService.createSession('Test Session', undefined, '/invalid/path')
).rejects.toThrow();

// Restore original value
if (originalAllowedRoot) {
process.env.ALLOWED_ROOT_DIRECTORY = originalAllowedRoot;
} else {
delete process.env.ALLOWED_ROOT_DIRECTORY;
}
vi.resetModules();
const { initAllowedPaths: reinit } = await import('@automaker/platform');
reinit();
});
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Isolate environment manipulation to prevent test interference.

This test modifies global state (process.env.ALLOWED_ROOT_DIRECTORY) and clears the module cache (vi.resetModules()), which can cause race conditions and flaky failures when tests run concurrently.

Vitest executes tests in parallel by default, so manipulating shared global state without proper isolation can affect other tests. Even though the test attempts cleanup, there's a timing window where concurrent tests may observe the modified environment.

🔎 Recommended approaches to fix

Option 1: Mark this test to run sequentially

-    it('should validate working directory', async () => {
+    it.sequential('should validate working directory', async () => {

Option 2: Use a separate test suite with sequential execution

-  describe('createSession', () => {
+  describe('createSession', () => {
     // ... other tests ...
+  });
+
+  describe.sequential('createSession - environment validation', () => {
     it('should validate working directory', async () => {
       // ... test body ...
     });
   });

Option 3: Mock the validation function directly instead of manipulating the environment, if the architecture allows it.

📝 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.

Suggested change
it('should validate working directory', async () => {
// Set ALLOWED_ROOT_DIRECTORY to restrict paths
const originalAllowedRoot = process.env.ALLOWED_ROOT_DIRECTORY;
process.env.ALLOWED_ROOT_DIRECTORY = '/allowed/projects';
// Re-import platform to initialize with new env var
vi.resetModules();
const { initAllowedPaths } = await import('@automaker/platform');
initAllowedPaths();
const { AgentService } = await import('@/services/agent-service.js');
const testService = new AgentService('/test/data', mockEvents as any);
vi.mocked(fs.readFile).mockResolvedValue('{}');
vi.mocked(fs.writeFile).mockResolvedValue(undefined);
await expect(
testService.createSession('Test Session', undefined, '/invalid/path')
).rejects.toThrow();
// Restore original value
if (originalAllowedRoot) {
process.env.ALLOWED_ROOT_DIRECTORY = originalAllowedRoot;
} else {
delete process.env.ALLOWED_ROOT_DIRECTORY;
}
vi.resetModules();
const { initAllowedPaths: reinit } = await import('@automaker/platform');
reinit();
});
it.sequential('should validate working directory', async () => {
// Set ALLOWED_ROOT_DIRECTORY to restrict paths
const originalAllowedRoot = process.env.ALLOWED_ROOT_DIRECTORY;
process.env.ALLOWED_ROOT_DIRECTORY = '/allowed/projects';
// Re-import platform to initialize with new env var
vi.resetModules();
const { initAllowedPaths } = await import('@automaker/platform');
initAllowedPaths();
const { AgentService } = await import('@/services/agent-service.js');
const testService = new AgentService('/test/data', mockEvents as any);
vi.mocked(fs.readFile).mockResolvedValue('{}');
vi.mocked(fs.writeFile).mockResolvedValue(undefined);
await expect(
testService.createSession('Test Session', undefined, '/invalid/path')
).rejects.toThrow();
// Restore original value
if (originalAllowedRoot) {
process.env.ALLOWED_ROOT_DIRECTORY = originalAllowedRoot;
} else {
delete process.env.ALLOWED_ROOT_DIRECTORY;
}
vi.resetModules();
const { initAllowedPaths: reinit } = await import('@automaker/platform');
reinit();
});
🤖 Prompt for AI Agents
apps/server/tests/unit/services/agent-service.test.ts lines 374-402: the test
mutates process.env.ALLOWED_ROOT_DIRECTORY and calls vi.resetModules(), which
can leak state and cause flakiness when Vitest runs tests in parallel; instead,
avoid changing global env and module cache by mocking the platform validation:
mock the function in '@automaker/platform' that checks/initializes allowed paths
(using vi.mock or vi.spyOn to return the desired allowed-roots behavior for this
test), remove process.env assignment and vi.resetModules calls, and assert
createSession rejects for the mocked invalid path; if mocking the platform
function is not feasible, mark this test (or its describe block) as serial
(describe.serial or test.serial) to ensure it runs isolated and keep cleanup
minimal.

- Added `authenticateForTests` utility to streamline API key authentication in tests, using a fallback for local testing.
- Updated context image test to include authentication step before navigation, ensuring proper session handling.
- Increased timeout for context view visibility to accommodate slower server responses.
- Introduced a test API key in the Playwright configuration for consistent testing environments.
Copy link

@coderabbitai coderabbitai bot left a 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 (1)
apps/ui/tests/context/add-context-image.spec.ts (1)

122-137: Refactor duplicate authentication logic for better maintainability.

The test authenticates twice: first via API (line 123), then manually through the UI (lines 128-137). This redundancy makes the code harder to maintain and duplicates the hardcoded API key ('test-api-key-for-e2e-tests') that already exists in client.ts line 300.

Consider refactoring to check the result of authenticateForTests and only fall back to manual login if it returns false:

🔎 Proposed refactor to eliminate duplication
+    // Authenticate with the server before navigating
+    const isAuthenticated = await authenticateForTests(page);
+
     await page.goto('/');
     await waitForNetworkIdle(page);
 
-    // Check if we're on the login screen and authenticate if needed
-    const loginInput = page.locator('input[type="password"][placeholder*="API key"]');
-    const isLoginScreen = await loginInput.isVisible({ timeout: 2000 }).catch(() => false);
-    if (isLoginScreen) {
-      const apiKey = process.env.AUTOMAKER_API_KEY || 'test-api-key-for-e2e-tests';
-      await loginInput.fill(apiKey);
-      await page.locator('button:has-text("Login")').click();
-      await page.waitForURL('**/', { timeout: 5000 });
-      await waitForNetworkIdle(page);
-    }
+    // If API authentication failed, try manual login as fallback
+    if (!isAuthenticated) {
+      const loginInput = page.locator('input[type="password"][placeholder*="API key"]');
+      const isLoginScreen = await loginInput.isVisible({ timeout: 2000 }).catch(() => false);
+      if (isLoginScreen) {
+        const apiKey = process.env.AUTOMAKER_API_KEY || 'test-api-key-for-e2e-tests';
+        await loginInput.fill(apiKey);
+        await page.locator('button:has-text("Login")').click();
+        await page.waitForURL('**/', { timeout: 5000 });
+        await waitForNetworkIdle(page);
+      }
+    }
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4c65855 and 59a6a23.

📒 Files selected for processing (4)
  • apps/ui/playwright.config.ts
  • apps/ui/tests/context/add-context-image.spec.ts
  • apps/ui/tests/utils/api/client.ts
  • apps/ui/tests/utils/navigation/views.ts
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Always import from shared packages (@automaker/*), never from old relative paths

Files:

  • apps/ui/playwright.config.ts
  • apps/ui/tests/utils/api/client.ts
  • apps/ui/tests/context/add-context-image.spec.ts
  • apps/ui/tests/utils/navigation/views.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Use resolveModelString() from @automaker/model-resolver to convert model aliases (haiku, sonnet, opus) to full model names

Files:

  • apps/ui/playwright.config.ts
  • apps/ui/tests/utils/api/client.ts
  • apps/ui/tests/context/add-context-image.spec.ts
  • apps/ui/tests/utils/navigation/views.ts
🧬 Code graph analysis (2)
apps/ui/tests/context/add-context-image.spec.ts (3)
apps/ui/tests/utils/api/client.ts (1)
  • authenticateForTests (298-302)
apps/ui/tests/utils/core/waiting.ts (1)
  • waitForNetworkIdle (9-11)
apps/ui/tests/utils/navigation/views.ts (1)
  • navigateToContext (22-42)
apps/ui/tests/utils/navigation/views.ts (1)
apps/ui/tests/utils/core/waiting.ts (1)
  • waitForElement (16-27)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: e2e
🔇 Additional comments (4)
apps/ui/playwright.config.ts (1)

42-43: API key configuration properly integrated with test utilities.

The AUTOMAKER_API_KEY environment variable is correctly configured in playwright.config.ts with an appropriate test-only default. The authenticateForTests utility in apps/ui/tests/utils/api/client.ts uses the same fallback pattern, and test files properly call this utility for authentication. Implementation is consistent and well-integrated.

apps/ui/tests/utils/navigation/views.ts (1)

40-41: LGTM! Timeout increase is appropriate.

The increased timeout (10s → 15s) appropriately accommodates the additional authentication overhead introduced by the API token requirements. The inline comment clearly documents the rationale.

apps/ui/tests/context/add-context-image.spec.ts (1)

141-143: LGTM! Good defensive programming to prevent race conditions.

Adding the toBeAttached check ensures the file input element exists in the DOM before attempting to set files, preventing potential race condition failures.

apps/ui/tests/utils/api/client.ts (1)

274-302: No action required—the authentication setup is correct.

The backend does not have a hardcoded test API key. Instead, it reads the AUTOMAKER_API_KEY environment variable, which the frontend test configuration (playwright.config.ts) properly sets to the same fallback value ('test-api-key-for-e2e-tests') across all frontend test utilities. The authentication mechanism is environment-based and consistent.

- Added `dev:test` script to package.json for streamlined testing without file watching.
- Introduced `kill-test-servers` script to ensure no existing servers are running on test ports before executing tests.
- Enhanced Playwright configuration to use mock agent for tests, ensuring consistent API responses and disabling rate limiting.
- Updated various test files to include authentication steps and handle login screens, improving reliability and reducing flakiness in tests.
- Added `global-setup` for e2e tests to ensure proper initialization before test execution.
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 7

🧹 Nitpick comments (11)
apps/ui/scripts/kill-test-servers.mjs (1)

37-44: Consider explicit exit code for test script clarity.

The current error handling with .catch(console.error) doesn't exit with a non-zero code on failure. While acceptable for a cleanup script (where it's reasonable to proceed even if cleanup fails), explicitly handling success/failure could improve clarity in the test workflow.

🔎 Optional: Add explicit exit handling
-main().catch(console.error);
+main()
+  .then(() => process.exit(0))
+  .catch((error) => {
+    console.error(error);
+    process.exit(1);
+  });

Note: The current implementation is acceptable since killProcessOnPort catches all errors internally, so main() won't reject. This suggestion is purely for explicitness.

apps/ui/tests/global-setup.ts (1)

6-10: Consider removing or implementing actual setup logic.

The globalSetup function is currently a no-op that only logs a message. If no setup is required, this file and the global setup configuration can be removed to reduce unnecessary code. If setup will be added later, consider adding a TODO comment explaining what's planned.

Suggested approach

If no setup is needed:

-/**
- * Global setup for all e2e tests
- * This runs once before all tests start
- */
-
-async function globalSetup() {
-  // Note: Server killing is handled by the pretest script in package.json
-  // GlobalSetup runs AFTER webServer starts, so we can't kill the server here
-  console.log('[GlobalSetup] Setup complete');
-}
-
-export default globalSetup;

Or if setup is planned, add a TODO:

 async function globalSetup() {
+  // TODO: Add any global test setup here (e.g., database seeding, global state initialization)
   // Note: Server killing is handled by the pretest script in package.json
   // GlobalSetup runs AFTER webServer starts, so we can't kill the server here
   console.log('[GlobalSetup] Setup complete');
 }
apps/ui/tests/utils/core/interactions.ts (3)

51-59: Consider using static import and adding error handling.

The dynamic import of authenticateForTests is unnecessary here since it's a test utility that's always available. A static import would be clearer and more maintainable. Additionally, there's no error handling if authentication fails.

Proposed improvement

Move to static import at the top of the file:

 import { Page, expect } from '@playwright/test';
+import { authenticateForTests } from '../api/client';
 import { getByTestId, getButtonByText } from './elements';

Then simplify the function:

 export async function gotoWithAuth(page: Page, url: string): Promise<void> {
-  const { authenticateForTests } = await import('../api/client');
   await authenticateForTests(page);
   await page.goto(url);
 }

92-93: Avoid hardcoded timeout for UI state synchronization.

The hardcoded 100ms timeout is fragile and may not be sufficient on slower systems. Consider waiting for the button's enabled state instead, which is already done on line 99.

Proposed fix
     await loginInput.fill(apiKey);
 
-    // Wait a moment for the button to become enabled
-    await page.waitForTimeout(100);
-
     // Wait for button to be enabled (it's disabled when input is empty)
     const loginButton = page
       .locator('[data-testid="login-submit-button"], button:has-text("Login")')
       .first();
     await expect(loginButton).toBeEnabled({ timeout: 5000 });

The toBeEnabled assertion already waits up to 5 seconds, making the arbitrary 100ms delay unnecessary.


88-90: Consolidate API key retrieval logic.

The API key retrieval logic on line 89 duplicates the same logic in authenticateForTests (apps/ui/tests/utils/api/client.ts:358). Consider extracting this to a shared constant or function.

Example consolidation

In a shared location (e.g., apps/ui/tests/utils/constants.ts):

export function getTestApiKey(): string {
  return process.env.AUTOMAKER_API_KEY || 'test-api-key-for-e2e-tests';
}

Then use it in both places:

+import { getTestApiKey } from '../constants';
+
 export async function handleLoginScreenIfPresent(page: Page): Promise<boolean> {
   // ... selector setup ...
   
   if (loginVisible) {
-    const apiKey = process.env.AUTOMAKER_API_KEY || 'test-api-key-for-e2e-tests';
+    const apiKey = getTestApiKey();
     await loginInput.fill(apiKey);
apps/ui/tests/profiles/profiles-crud.spec.ts (1)

24-28: Consider consolidating authentication approach.

Similar to the worktree test, this file uses both authenticateForTests and handleLoginScreenIfPresent. If this dual approach is intentional (e.g., to handle edge cases where programmatic auth doesn't fully set up the session), consider:

  1. Documenting why both are needed
  2. Extracting this pattern into a helper like authenticateAndNavigate(page, url) to reduce duplication

This would centralize the logic and make the intent clearer.

Example helper function

In apps/ui/tests/utils/core/interactions.ts:

/**
 * Authenticate and navigate with login screen handling
 * Use this when tests need robust authentication that handles both programmatic and UI login
 */
export async function authenticateAndNavigateRobust(page: Page, url: string): Promise<void> {
  await authenticateForTests(page);
  await page.goto(url);
  await page.waitForLoadState('load');
  await handleLoginScreenIfPresent(page);
}

Then use it:

-    await authenticateForTests(page);
-    await page.goto('/');
-    await page.waitForLoadState('load');
-    await handleLoginScreenIfPresent(page);
+    await authenticateAndNavigateRobust(page, '/');
apps/ui/tests/features/add-feature-to-backlog.spec.ts (2)

66-71: Authentication flow is correct, but consider extracting the pattern.

This test correctly implements the dual authentication approach (programmatic + UI fallback) that's also used in the worktree and profiles tests. Since this pattern appears in multiple tests, consider extracting it into a shared helper function to reduce duplication and make the intent explicit.

See the earlier comment on apps/ui/tests/profiles/profiles-crud.spec.ts (lines 24-28) for an example helper function implementation.


66-66: Remove obvious comment.

The comment "// Authenticate before navigating" states what the code clearly shows on the next line. Consider removing it to reduce noise.

-    // Authenticate before navigating
     await authenticateForTests(page);
apps/ui/tests/context/add-context-image.spec.ts (2)

17-17: Refactor: Use imported authenticateForTests utility.

The authenticateForTests function is imported but never used. This test should follow the same pattern as other test files by calling authenticateForTests(page) before navigation.

🔎 Suggested refactor
   await setupProjectWithFixture(page, getFixturePath());
+  await authenticateForTests(page);

125-134: Refactor: Replace inline login handling with handleLoginScreenIfPresent.

This inline login handling duplicates logic already available in the handleLoginScreenIfPresent utility. For consistency with other test files and to reduce duplication, use the shared utility instead.

🔎 Suggested refactor

Import the utility at the top of the file:

 import {
   resetContextDirectory,
   setupProjectWithFixture,
   getFixturePath,
   navigateToContext,
   waitForContextFile,
   waitForNetworkIdle,
   authenticateForTests,
+  handleLoginScreenIfPresent,
 } from '../utils';

Then replace the inline login handling:

   await page.goto('/');
   await waitForNetworkIdle(page);
-
-  // Check if we're on the login screen and authenticate if needed
-  const loginInput = page.locator('input[type="password"][placeholder*="API key"]');
-  const isLoginScreen = await loginInput.isVisible({ timeout: 2000 }).catch(() => false);
-  if (isLoginScreen) {
-    const apiKey = process.env.AUTOMAKER_API_KEY || 'test-api-key-for-e2e-tests';
-    await loginInput.fill(apiKey);
-    await page.locator('button:has-text("Login")').click();
-    await page.waitForURL('**/', { timeout: 5000 });
-    await waitForNetworkIdle(page);
-  }
+  await handleLoginScreenIfPresent(page);
apps/server/src/routes/auth/index.ts (1)

219-244: Logout implementation is correct and secure.

The endpoint properly invalidates the session and clears the cookie with appropriate security options.

💡 Optional: Use getSessionCookieOptions() for consistency

For consistency with the login endpoint, consider using the helper function:

     // Clear the cookie
-    res.clearCookie(cookieName, {
-      httpOnly: true,
-      secure: process.env.NODE_ENV === 'production',
-      sameSite: 'strict',
-      path: '/',
-    });
+    const cookieOptions = getSessionCookieOptions();
+    res.clearCookie(cookieName, cookieOptions);

Note: clearCookie ignores maxAge, so this works fine.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 59a6a23 and 46caae0.

📒 Files selected for processing (22)
  • apps/server/package.json
  • apps/server/src/routes/auth/index.ts
  • apps/ui/package.json
  • apps/ui/playwright.config.ts
  • apps/ui/scripts/kill-test-servers.mjs
  • apps/ui/src/components/views/login-view.tsx
  • apps/ui/tests/agent/start-new-chat-session.spec.ts
  • apps/ui/tests/context/add-context-image.spec.ts
  • apps/ui/tests/context/context-file-management.spec.ts
  • apps/ui/tests/context/delete-context-file.spec.ts
  • apps/ui/tests/features/add-feature-to-backlog.spec.ts
  • apps/ui/tests/features/edit-feature.spec.ts
  • apps/ui/tests/features/feature-manual-review-flow.spec.ts
  • apps/ui/tests/features/feature-skip-tests-toggle.spec.ts
  • apps/ui/tests/git/worktree-integration.spec.ts
  • apps/ui/tests/global-setup.ts
  • apps/ui/tests/profiles/profiles-crud.spec.ts
  • apps/ui/tests/projects/new-project-creation.spec.ts
  • apps/ui/tests/projects/open-existing-project.spec.ts
  • apps/ui/tests/utils/api/client.ts
  • apps/ui/tests/utils/core/interactions.ts
  • apps/ui/tests/utils/navigation/views.ts
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Always import from shared packages (@automaker/*), never from old relative paths

Files:

  • apps/ui/tests/features/feature-manual-review-flow.spec.ts
  • apps/ui/tests/global-setup.ts
  • apps/ui/tests/agent/start-new-chat-session.spec.ts
  • apps/ui/tests/utils/api/client.ts
  • apps/ui/tests/features/add-feature-to-backlog.spec.ts
  • apps/ui/tests/profiles/profiles-crud.spec.ts
  • apps/ui/tests/utils/core/interactions.ts
  • apps/ui/tests/utils/navigation/views.ts
  • apps/ui/tests/projects/new-project-creation.spec.ts
  • apps/server/src/routes/auth/index.ts
  • apps/ui/tests/features/feature-skip-tests-toggle.spec.ts
  • apps/ui/tests/context/delete-context-file.spec.ts
  • apps/ui/tests/projects/open-existing-project.spec.ts
  • apps/ui/tests/git/worktree-integration.spec.ts
  • apps/ui/src/components/views/login-view.tsx
  • apps/ui/playwright.config.ts
  • apps/ui/tests/context/context-file-management.spec.ts
  • apps/ui/tests/features/edit-feature.spec.ts
  • apps/ui/tests/context/add-context-image.spec.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Use resolveModelString() from @automaker/model-resolver to convert model aliases (haiku, sonnet, opus) to full model names

Files:

  • apps/ui/tests/features/feature-manual-review-flow.spec.ts
  • apps/ui/tests/global-setup.ts
  • apps/ui/tests/agent/start-new-chat-session.spec.ts
  • apps/ui/tests/utils/api/client.ts
  • apps/ui/tests/features/add-feature-to-backlog.spec.ts
  • apps/ui/tests/profiles/profiles-crud.spec.ts
  • apps/ui/tests/utils/core/interactions.ts
  • apps/ui/tests/utils/navigation/views.ts
  • apps/ui/tests/projects/new-project-creation.spec.ts
  • apps/server/src/routes/auth/index.ts
  • apps/ui/tests/features/feature-skip-tests-toggle.spec.ts
  • apps/ui/tests/context/delete-context-file.spec.ts
  • apps/ui/tests/projects/open-existing-project.spec.ts
  • apps/ui/tests/git/worktree-integration.spec.ts
  • apps/ui/src/components/views/login-view.tsx
  • apps/ui/playwright.config.ts
  • apps/ui/tests/context/context-file-management.spec.ts
  • apps/ui/tests/features/edit-feature.spec.ts
  • apps/ui/tests/context/add-context-image.spec.ts
apps/server/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Use createEventEmitter() from lib/events.ts for all server operations to emit events that stream to frontend via WebSocket

Files:

  • apps/server/src/routes/auth/index.ts
🧠 Learnings (1)
📚 Learning: 2025-12-30T01:02:07.114Z
Learnt from: illia1f
Repo: AutoMaker-Org/automaker PR: 324
File: apps/ui/src/components/views/board-view/components/kanban-card/kanban-card.tsx:122-131
Timestamp: 2025-12-30T01:02:07.114Z
Learning: Tailwind CSS v4 uses postfix syntax for the important modifier: append ! at the end of the utility class (e.g., backdrop-blur-[0px]! or hover:bg-red-500!). The older v3 style used a prefix (!) at the start (e.g., !backdrop-blur-[0px]); prefer the postfix form for consistency across TSX files.

Applied to files:

  • apps/ui/src/components/views/login-view.tsx
🧬 Code graph analysis (16)
apps/ui/tests/features/feature-manual-review-flow.spec.ts (2)
apps/ui/tests/utils/api/client.ts (1)
  • authenticateForTests (357-361)
apps/ui/tests/utils/core/interactions.ts (1)
  • handleLoginScreenIfPresent (65-115)
apps/ui/scripts/kill-test-servers.mjs (2)
apps/server/src/services/dev-server-service.ts (1)
  • killProcessOnPort (55-98)
init.mjs (1)
  • pid (88-88)
apps/ui/tests/agent/start-new-chat-session.spec.ts (1)
apps/ui/tests/utils/api/client.ts (1)
  • authenticateForTests (357-361)
apps/ui/tests/features/add-feature-to-backlog.spec.ts (2)
apps/ui/tests/utils/api/client.ts (1)
  • authenticateForTests (357-361)
apps/ui/tests/utils/core/interactions.ts (1)
  • handleLoginScreenIfPresent (65-115)
apps/ui/tests/profiles/profiles-crud.spec.ts (3)
apps/ui/tests/utils/project/setup.ts (1)
  • setupMockProjectWithProfiles (777-882)
apps/ui/tests/utils/api/client.ts (1)
  • authenticateForTests (357-361)
apps/ui/tests/utils/core/interactions.ts (1)
  • handleLoginScreenIfPresent (65-115)
apps/ui/tests/utils/core/interactions.ts (1)
apps/ui/tests/utils/api/client.ts (1)
  • authenticateForTests (357-361)
apps/ui/tests/utils/navigation/views.ts (2)
apps/ui/tests/utils/api/client.ts (1)
  • authenticateForTests (357-361)
apps/ui/tests/utils/core/waiting.ts (1)
  • waitForElement (16-27)
apps/server/src/routes/auth/index.ts (1)
apps/server/src/lib/auth.ts (7)
  • isRequestAuthenticated (391-398)
  • validateApiKey (233-247)
  • createSession (158-167)
  • getSessionCookieOptions (252-266)
  • getSessionCookieName (271-273)
  • createWsConnectionToken (199-207)
  • invalidateSession (190-193)
apps/ui/tests/features/feature-skip-tests-toggle.spec.ts (2)
apps/ui/tests/utils/api/client.ts (1)
  • authenticateForTests (357-361)
apps/ui/tests/utils/core/interactions.ts (1)
  • handleLoginScreenIfPresent (65-115)
apps/ui/tests/context/delete-context-file.spec.ts (1)
apps/ui/tests/utils/api/client.ts (1)
  • authenticateForTests (357-361)
apps/ui/tests/projects/open-existing-project.spec.ts (2)
apps/ui/tests/utils/api/client.ts (1)
  • authenticateForTests (357-361)
apps/ui/tests/utils/core/interactions.ts (1)
  • handleLoginScreenIfPresent (65-115)
apps/ui/tests/git/worktree-integration.spec.ts (2)
apps/ui/tests/utils/api/client.ts (1)
  • authenticateForTests (357-361)
apps/ui/tests/utils/core/interactions.ts (1)
  • handleLoginScreenIfPresent (65-115)
apps/ui/src/components/views/login-view.tsx (1)
apps/ui/src/lib/http-api-client.ts (1)
  • login (130-164)
apps/ui/playwright.config.ts (1)
init.mjs (1)
  • require (17-17)
apps/ui/tests/context/context-file-management.spec.ts (1)
apps/ui/tests/utils/api/client.ts (1)
  • authenticateForTests (357-361)
apps/ui/tests/features/edit-feature.spec.ts (2)
apps/ui/tests/utils/api/client.ts (1)
  • authenticateForTests (357-361)
apps/ui/tests/utils/core/interactions.ts (1)
  • handleLoginScreenIfPresent (65-115)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: e2e
🔇 Additional comments (27)
apps/ui/src/components/views/login-view.tsx (1)

15-110: Excellent implementation of the login component!

The component demonstrates solid practices:

  • Comprehensive error handling (server errors, network failures, loading states)
  • Good UX with clear messaging, loading indicators, and disabled states during submission
  • Proper accessibility with labels, autoFocus, and test IDs
  • Security-conscious use of type="password" for the API key input
  • Clean integration with the login function from http-api-client and navigation on success

The help text section is particularly helpful for guiding users to locate the API key.

apps/ui/package.json (1)

31-31: LGTM!

Adding the kill-test-servers step before fixture setup ensures a clean test environment and prevents port conflicts from stale processes.

apps/ui/tests/agent/start-new-chat-session.spec.ts (1)

65-67: Authentication flow looks correct.

The authentication step before navigation follows the intended pattern for the new security requirements. The sequence of authenticate → navigate → wait is appropriate.

apps/ui/tests/context/context-file-management.spec.ts (1)

35-37: Authentication integration is correct.

The authentication call is properly placed after project setup and before navigation, maintaining the consistent pattern across the test suite.

apps/ui/tests/context/delete-context-file.spec.ts (1)

37-39: Authentication implementation is consistent.

The authentication flow matches the pattern used across other test files in the suite.

apps/ui/tests/git/worktree-integration.spec.ts (1)

52-56: No action needed. This follows the standard pattern established throughout the test suite where both authenticateForTests and handleLoginScreenIfPresent are used together across all test files (new-project-creation, open-existing-project, profiles-crud, feature tests, etc.). This is an intentional, defensive approach, not redundancy.

apps/ui/tests/projects/open-existing-project.spec.ts (1)

14-20: LGTM! Authentication flow properly integrated.

The test correctly authenticates before navigation and handles potential login screens after page load, following the consistent pattern established across the test suite.

Also applies to: 83-86

apps/ui/tests/projects/new-project-creation.spec.ts (1)

10-16: LGTM! Consistent authentication pattern.

The authentication flow is properly implemented and matches the pattern used across other test files.

Also applies to: 35-38

apps/ui/tests/features/edit-feature.spec.ts (1)

19-20: LGTM! Proper authentication sequence.

The authentication and login handling are correctly integrated into the test flow.

Also applies to: 68-72

apps/ui/tests/features/feature-manual-review-flow.spec.ts (1)

22-23: LGTM! Authentication properly integrated.

The test follows the established authentication pattern correctly.

Also applies to: 88-92

apps/ui/tests/features/feature-skip-tests-toggle.spec.ts (1)

22-23: LGTM! Authentication flow integrated correctly.

The changes follow the standard pattern used across the test suite.

Also applies to: 70-74

apps/ui/tests/context/add-context-image.spec.ts (1)

138-140: Good practice: Explicit file input attachment check.

Adding an explicit wait for the file input to be attached to the DOM before interacting with it is a good defensive pattern that prevents flakiness.

apps/ui/tests/utils/api/client.ts (1)

357-361: LGTM! Clean authentication wrapper.

The authenticateForTests function provides a clean interface for tests to authenticate using environment configuration with a sensible fallback.

apps/ui/tests/utils/navigation/views.ts (1)

11-12: LGTM! Pre-navigation authentication properly added.

All navigation functions now correctly authenticate before navigating, ensuring tests have proper access to protected endpoints.

Also applies to: 44-45, 90-91, 123-124, 156-157, 184-185

apps/server/src/routes/auth/index.ts (5)

1-26: Clear documentation and appropriate imports.

The file header provides excellent context about the security model and the authentication flow for both web and Electron modes.


64-89: Rate limit checking logic is correct.

The function properly handles window expiration, calculates retry-after time, and cleans up expired entries.


91-105: Login attempt tracking logic is sound.

The function correctly handles both new windows and incremental counting within the current window.


112-128: Status endpoint implementation is straightforward and correct.

The endpoint properly delegates to isRequestAuthenticated and returns the expected payload.


192-217: WebSocket token endpoint correctly generates a separate short-lived token.

The implementation properly creates a new, short-lived token (via createWsConnectionToken) rather than exposing the session cookie value. This addresses the security concern from the previous review.

apps/ui/playwright.config.ts (5)

6-7: LGTM! Hardcoding mock agent for tests is appropriate.

This ensures consistent test behavior and prevents real API calls during testing, which aligns with the PR's security and testing goals.


13-14: Sequential execution is appropriate for auth-based tests.

Setting workers: 1 prevents race conditions with the shared authenticated server state. The retries: 0 setting provides faster feedback but may require tests to be more robust against transient issues.


37-50: Well-configured test server environment.

The changes ensure test isolation and proper authentication:

  • dev:test avoids file watching that could cause port conflicts
  • reuseExistingServer: false ensures the test API key is used
  • Test API key with a clear default value supports reproducible test runs
  • Hiding the API key banner reduces log noise

63-64: Appropriate to skip Electron in test environment.

Forcing VITE_SKIP_ELECTRON: 'true' prevents duplicate server spawning and simplifies the test environment since Electron functionality isn't needed for Playwright web tests.


22-23: This usage is correct. Using require.resolve() in Playwright TypeScript configs is the standard and documented approach for resolving the globalSetup path. The file's TypeScript configuration enables CommonJS interoperability (esModuleInterop: true, allowSyntheticDefaultImports: true), making require available in this context.

apps/server/package.json (3)

12-12: Good addition of test-specific script.

The dev:test script appropriately removes file watching (tsx without watch) to prevent server restarts during test execution, which could cause port conflicts or test flakiness.


42-43: Good practice to include TypeScript type definitions.

Adding @types/cookie and @types/cookie-parser provides proper type safety for the new cookie handling functionality.


33-33: No security vulnerabilities found in cookie-parser@1.4.7. Version 1.4.7 has no recorded CVEs and passes security database checks (Snyk). Dependency is safe to use.

Comment on lines +37 to +48
// Clean up old rate limit entries periodically (every 5 minutes)
setInterval(
() => {
const now = Date.now();
loginAttempts.forEach((data, ip) => {
if (now - data.windowStart > RATE_LIMIT_WINDOW_MS * 2) {
loginAttempts.delete(ip);
}
});
},
5 * 60 * 1000
);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

The top-level setInterval is never cleared, causing a potential memory leak.

If this module is ever reloaded (e.g., during hot module replacement in development), the interval will continue running. This creates a memory leak as old intervals accumulate.

Consider one of these approaches:

  • Store the interval ID and export a cleanup function
  • Move the interval setup into a server lifecycle hook that can be properly torn down
  • Use a WeakMap or other automatic cleanup mechanism
🔎 Recommended fix: store and export cleanup function
+let cleanupInterval: NodeJS.Timeout | null = null;
+
 // Clean up old rate limit entries periodically (every 5 minutes)
-setInterval(
+cleanupInterval = setInterval(
   () => {
     const now = Date.now();
     loginAttempts.forEach((data, ip) => {
       if (now - data.windowStart > RATE_LIMIT_WINDOW_MS * 2) {
         loginAttempts.delete(ip);
       }
     });
   },
   5 * 60 * 1000
 );
+
+export function cleanup(): void {
+  if (cleanupInterval) {
+    clearInterval(cleanupInterval);
+    cleanupInterval = null;
+  }
+  loginAttempts.clear();
+}
📝 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.

Suggested change
// Clean up old rate limit entries periodically (every 5 minutes)
setInterval(
() => {
const now = Date.now();
loginAttempts.forEach((data, ip) => {
if (now - data.windowStart > RATE_LIMIT_WINDOW_MS * 2) {
loginAttempts.delete(ip);
}
});
},
5 * 60 * 1000
);
let cleanupInterval: NodeJS.Timeout | null = null;
// Clean up old rate limit entries periodically (every 5 minutes)
cleanupInterval = setInterval(
() => {
const now = Date.now();
loginAttempts.forEach((data, ip) => {
if (now - data.windowStart > RATE_LIMIT_WINDOW_MS * 2) {
loginAttempts.delete(ip);
}
});
},
5 * 60 * 1000
);
export function cleanup(): void {
if (cleanupInterval) {
clearInterval(cleanupInterval);
cleanupInterval = null;
}
loginAttempts.clear();
}
🤖 Prompt for AI Agents
In apps/server/src/routes/auth/index.ts around lines 37 to 48, the top-level
setInterval used to clean up old rate-limit entries is never cleared which can
leak timers if the module is reloaded; capture the returned interval ID, export
(or otherwise expose) a cleanup function that calls clearInterval on that ID and
removes any other in-memory state as needed, and update any server lifecycle or
HMR code to call that cleanup during teardown; alternatively move the interval
registration into a lifecycle hook so it can be torn down cleanly.

Comment on lines +50 to +62
/**
* Get client IP address from request
* Handles X-Forwarded-For header for reverse proxy setups
*/
function getClientIp(req: Request): string {
const forwarded = req.headers['x-forwarded-for'];
if (forwarded) {
// X-Forwarded-For can be a comma-separated list; take the first (original client)
const forwardedIp = Array.isArray(forwarded) ? forwarded[0] : forwarded.split(',')[0];
return forwardedIp.trim();
}
return req.ip || req.socket.remoteAddress || 'unknown';
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

X-Forwarded-For can be spoofed if not behind a trusted proxy.

The current implementation unconditionally trusts the X-Forwarded-For header, which can be set by any client. If the server is directly exposed to the internet (not behind a trusted reverse proxy), attackers can bypass rate limiting by spoofing this header with different IP addresses.

Consider:

  • Only trust X-Forwarded-For when the request comes from a known proxy IP
  • Use Express's trust proxy setting and rely on req.ip instead (which respects the trust proxy configuration)
  • Document the requirement that the server must be deployed behind a trusted reverse proxy
🔎 Recommended fix: use Express trust proxy and req.ip

In the main server setup (apps/server/src/index.ts), configure trust proxy:

// Trust proxy if behind reverse proxy (nginx, cloudflare, etc.)
app.set('trust proxy', true); // Or specify trusted proxy IPs

Then simplify this function:

 function getClientIp(req: Request): string {
-  const forwarded = req.headers['x-forwarded-for'];
-  if (forwarded) {
-    // X-Forwarded-For can be a comma-separated list; take the first (original client)
-    const forwardedIp = Array.isArray(forwarded) ? forwarded[0] : forwarded.split(',')[0];
-    return forwardedIp.trim();
-  }
-  return req.ip || req.socket.remoteAddress || 'unknown';
+  // req.ip respects Express's trust proxy setting
+  return req.ip || req.socket.remoteAddress || 'unknown';
 }
📝 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.

Suggested change
/**
* Get client IP address from request
* Handles X-Forwarded-For header for reverse proxy setups
*/
function getClientIp(req: Request): string {
const forwarded = req.headers['x-forwarded-for'];
if (forwarded) {
// X-Forwarded-For can be a comma-separated list; take the first (original client)
const forwardedIp = Array.isArray(forwarded) ? forwarded[0] : forwarded.split(',')[0];
return forwardedIp.trim();
}
return req.ip || req.socket.remoteAddress || 'unknown';
}
/**
* Get client IP address from request
* Handles X-Forwarded-For header for reverse proxy setups
*/
function getClientIp(req: Request): string {
// req.ip respects Express's trust proxy setting
return req.ip || req.socket.remoteAddress || 'unknown';
}
🤖 Prompt for AI Agents
In apps/server/src/routes/auth/index.ts around lines 50 to 62, the getClientIp
function unconditionally trusts the X-Forwarded-For header which can be spoofed;
update the app to use Express's trust proxy and change this function to rely on
req.ip (which respects trust proxy) or only parse X-Forwarded-For after
verifying the request came from a trusted proxy IP; specifically set
app.set('trust proxy', true) or a list of trusted proxy IPs in
apps/server/src/index.ts, replace the manual forwarded-header parsing with
returning req.ip || req.socket.remoteAddress || 'unknown', and add a short
comment documenting the requirement that the server must run behind a trusted
reverse proxy when relying on X-Forwarded-For.

Comment on lines +178 to +189
// Create session and set cookie
const sessionToken = await createSession();
const cookieOptions = getSessionCookieOptions();
const cookieName = getSessionCookieName();

res.cookie(cookieName, sessionToken, cookieOptions);
res.json({
success: true,
message: 'Logged in successfully.',
// Return token for explicit header-based auth (works around cross-origin cookie issues)
token: sessionToken,
});
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Returning the session token in the response body defeats HTTP-only cookie security.

Line 188 returns the session token in the response body with a comment about "cross-origin cookie issues." This completely undermines the HTTP-only cookie protection described in the file header (lines 8-10). An XSS attack can simply read this response and exfiltrate the session token, even though it cannot read the HTTP-only cookie directly.

The session token is meant to be opaque to the client when using cookies. If cross-origin scenarios require header-based authentication, the proper solution is:

  • Use a separate authentication mechanism for cross-origin scenarios (e.g., a different login endpoint that returns a token meant for headers)
  • Or accept that cookies don't work cross-origin and require users to use the API key directly in those scenarios

Returning both cookie and token from the same endpoint is the worst of both worlds - it has the complexity of cookies but none of the XSS protection.

🔎 Recommended fix: remove token from response
     res.cookie(cookieName, sessionToken, cookieOptions);
     res.json({
       success: true,
       message: 'Logged in successfully.',
-      // Return token for explicit header-based auth (works around cross-origin cookie issues)
-      token: sessionToken,
     });

If cross-origin scenarios truly need header-based auth, create a separate endpoint or login mode rather than compromising the security of cookie-based auth.

📝 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.

Suggested change
// Create session and set cookie
const sessionToken = await createSession();
const cookieOptions = getSessionCookieOptions();
const cookieName = getSessionCookieName();
res.cookie(cookieName, sessionToken, cookieOptions);
res.json({
success: true,
message: 'Logged in successfully.',
// Return token for explicit header-based auth (works around cross-origin cookie issues)
token: sessionToken,
});
// Create session and set cookie
const sessionToken = await createSession();
const cookieOptions = getSessionCookieOptions();
const cookieName = getSessionCookieName();
res.cookie(cookieName, sessionToken, cookieOptions);
res.json({
success: true,
message: 'Logged in successfully.',
});
🤖 Prompt for AI Agents
In apps/server/src/routes/auth/index.ts around lines 178 to 189, the handler
currently returns the session token in the JSON body which defeats HTTP-only
cookie protection; remove the token from the response body (do not include
token: sessionToken), only set the cookie and return a success message/status,
ensure you do not log or expose the token anywhere, and if header-based auth for
cross-origin use is required implement a separate login endpoint or explicit
"token" mode rather than returning the cookie token here.

Comment on lines +14 to +35
async function killProcessOnPort(port) {
try {
const { stdout } = await execAsync(`lsof -ti:${port}`);
const pids = stdout.trim().split('\n').filter(Boolean);

if (pids.length > 0) {
console.log(`[KillTestServers] Found process(es) on port ${port}: ${pids.join(', ')}`);
for (const pid of pids) {
try {
await execAsync(`kill -9 ${pid}`);
console.log(`[KillTestServers] Killed process ${pid}`);
} catch (error) {
// Process might have already exited
}
}
// Wait a moment for the port to be released
await new Promise((resolve) => setTimeout(resolve, 500));
}
} catch (error) {
// No process on port, which is fine
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Add Windows support for cross-platform compatibility.

The script uses Unix-only commands (lsof, kill) and will fail on Windows. Developers on Windows won't be able to run the test suite.

🔎 Proposed cross-platform implementation

Based on the pattern in apps/server/src/services/dev-server-service.ts (lines 54-97), here's a cross-platform solution:

 async function killProcessOnPort(port) {
   try {
-    const { stdout } = await execAsync(`lsof -ti:${port}`);
-    const pids = stdout.trim().split('\n').filter(Boolean);
-
-    if (pids.length > 0) {
-      console.log(`[KillTestServers] Found process(es) on port ${port}: ${pids.join(', ')}`);
-      for (const pid of pids) {
-        try {
-          await execAsync(`kill -9 ${pid}`);
-          console.log(`[KillTestServers] Killed process ${pid}`);
-        } catch (error) {
-          // Process might have already exited
+    if (process.platform === 'win32') {
+      // Windows: find and kill process on port
+      const { stdout } = await execAsync(`netstat -ano | findstr :${port}`);
+      const lines = stdout.trim().split('\n');
+      const pids = new Set();
+      for (const line of lines) {
+        const parts = line.trim().split(/\s+/);
+        const pid = parts[parts.length - 1];
+        if (pid && pid !== '0') {
+          pids.add(pid);
+        }
+      }
+      if (pids.size > 0) {
+        console.log(`[KillTestServers] Found process(es) on port ${port}: ${[...pids].join(', ')}`);
+        for (const pid of pids) {
+          try {
+            await execAsync(`taskkill /F /PID ${pid}`);
+            console.log(`[KillTestServers] Killed process ${pid}`);
+          } catch (error) {
+            // Process might have already exited
+          }
         }
+        await new Promise((resolve) => setTimeout(resolve, 500));
+      }
+    } else {
+      // macOS/Linux: use lsof
+      const { stdout } = await execAsync(`lsof -ti:${port}`);
+      const pids = stdout.trim().split('\n').filter(Boolean);
+
+      if (pids.length > 0) {
+        console.log(`[KillTestServers] Found process(es) on port ${port}: ${pids.join(', ')}`);
+        for (const pid of pids) {
+          try {
+            await execAsync(`kill -9 ${pid}`);
+            console.log(`[KillTestServers] Killed process ${pid}`);
+          } catch (error) {
+            // Process might have already exited
+          }
+        }
+        await new Promise((resolve) => setTimeout(resolve, 500));
       }
-      // Wait a moment for the port to be released
-      await new Promise((resolve) => setTimeout(resolve, 500));
     }
   } catch (error) {
     // No process on port, which is fine
   }
 }
📝 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.

Suggested change
async function killProcessOnPort(port) {
try {
const { stdout } = await execAsync(`lsof -ti:${port}`);
const pids = stdout.trim().split('\n').filter(Boolean);
if (pids.length > 0) {
console.log(`[KillTestServers] Found process(es) on port ${port}: ${pids.join(', ')}`);
for (const pid of pids) {
try {
await execAsync(`kill -9 ${pid}`);
console.log(`[KillTestServers] Killed process ${pid}`);
} catch (error) {
// Process might have already exited
}
}
// Wait a moment for the port to be released
await new Promise((resolve) => setTimeout(resolve, 500));
}
} catch (error) {
// No process on port, which is fine
}
}
async function killProcessOnPort(port) {
try {
if (process.platform === 'win32') {
// Windows: find and kill process on port
const { stdout } = await execAsync(`netstat -ano | findstr :${port}`);
const lines = stdout.trim().split('\n');
const pids = new Set();
for (const line of lines) {
const parts = line.trim().split(/\s+/);
const pid = parts[parts.length - 1];
if (pid && pid !== '0') {
pids.add(pid);
}
}
if (pids.size > 0) {
console.log(`[KillTestServers] Found process(es) on port ${port}: ${[...pids].join(', ')}`);
for (const pid of pids) {
try {
await execAsync(`taskkill /F /PID ${pid}`);
console.log(`[KillTestServers] Killed process ${pid}`);
} catch (error) {
// Process might have already exited
}
}
await new Promise((resolve) => setTimeout(resolve, 500));
}
} else {
// macOS/Linux: use lsof
const { stdout } = await execAsync(`lsof -ti:${port}`);
const pids = stdout.trim().split('\n').filter(Boolean);
if (pids.length > 0) {
console.log(`[KillTestServers] Found process(es) on port ${port}: ${pids.join(', ')}`);
for (const pid of pids) {
try {
await execAsync(`kill -9 ${pid}`);
console.log(`[KillTestServers] Killed process ${pid}`);
} catch (error) {
// Process might have already exited
}
}
await new Promise((resolve) => setTimeout(resolve, 500));
}
}
} catch (error) {
// No process on port, which is fine
}
}
🤖 Prompt for AI Agents
In apps/ui/scripts/kill-test-servers.mjs around lines 14 to 35, the current
implementation uses Unix-only commands (lsof, kill) and will fail on Windows;
replace it with a cross-platform implementation: detect process.platform, for
POSIX keep lsof/kilL flow, for Windows use a netstat/FindStr or PowerShell
command to find the PID(s) listening on the port (or use `netstat -ano | findstr
:PORT`), parse the PID list, then kill using `taskkill /PID <pid> /F` (or
Stop-Process in PowerShell) and handle errors similarly; ensure you trim and
filter PIDs, log found PIDs, attempt to terminate each PID with appropriate
platform-specific command, catch and ignore already-exited processes, and keep
the short post-kill delay to allow the port to be released.

Comment on lines +283 to +351
export async function authenticateWithApiKey(page: Page, apiKey: string): Promise<boolean> {
try {
// Ensure we're on a page (needed for cookies to work)
const currentUrl = page.url();
if (!currentUrl || currentUrl === 'about:blank') {
await page.goto('http://localhost:3007', { waitUntil: 'domcontentloaded' });
}

// Use browser context fetch to ensure cookies are set in the browser
const response = await page.evaluate(
async ({ url, apiKey }) => {
const res = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ apiKey }),
});
const data = await res.json();
return { success: data.success, token: data.token };
},
{ url: `${API_BASE_URL}/api/auth/login`, apiKey }
);

if (response.success && response.token) {
// Manually set the cookie in the browser context
// The server sets a cookie named 'automaker_session' (see SESSION_COOKIE_NAME in auth.ts)
await page.context().addCookies([
{
name: 'automaker_session',
value: response.token,
domain: 'localhost',
path: '/',
httpOnly: true,
sameSite: 'Lax',
},
]);

// Verify the session is working by polling auth status
// This replaces arbitrary timeout with actual condition check
let attempts = 0;
const maxAttempts = 10;
while (attempts < maxAttempts) {
const statusResponse = await page.evaluate(
async ({ url }) => {
const res = await fetch(url, {
credentials: 'include',
});
return res.json();
},
{ url: `${API_BASE_URL}/api/auth/status` }
);

if (statusResponse.authenticated === true) {
return true;
}
attempts++;
// Use a very short wait between polling attempts (this is acceptable for polling)
await page.waitForFunction(() => true, { timeout: 50 });
}

return false;
}

return false;
} catch (error) {
console.error('Authentication error:', error);
return false;
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Several improvements needed in authentication implementation.

The authentication flow is well-structured overall, but there are a few issues to address:

  1. Line 288: Hardcoded URL should use API_BASE_URL
  2. Line 340: Anti-pattern timeout hack
  3. Lines 322-341: Missing error logging when polling fails
🔎 Proposed fixes
   const currentUrl = page.url();
   if (!currentUrl || currentUrl === 'about:blank') {
-    await page.goto('http://localhost:3007', { waitUntil: 'domcontentloaded' });
+    await page.goto(API_BASE_URL, { waitUntil: 'domcontentloaded' });
   }
       if (statusResponse.authenticated === true) {
         return true;
       }
       attempts++;
-      // Use a very short wait between polling attempts (this is acceptable for polling)
-      await page.waitForFunction(() => true, { timeout: 50 });
+      // Short wait between polling attempts
+      await page.waitForTimeout(50);
     }
-
+    
+    console.error('Authentication polling failed: max attempts reached');
     return false;

Comment on lines +103 to +106
await Promise.race([
page.waitForURL((url) => !url.pathname.includes('/login'), { timeout: 10000 }),
appContent.first().waitFor({ state: 'visible', timeout: 10000 }),
]).catch(() => {});
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Handle navigation errors explicitly.

The empty catch block silently suppresses navigation errors, which could hide real issues during test execution. Consider logging or handling specific error cases.

Proposed improvement
     // Wait for navigation away from login - either to content or URL change
     await Promise.race([
       page.waitForURL((url) => !url.pathname.includes('/login'), { timeout: 10000 }),
       appContent.first().waitFor({ state: 'visible', timeout: 10000 }),
-    ]).catch(() => {});
+    ]).catch((err) => {
+      console.warn('[handleLoginScreenIfPresent] Navigation after login timed out:', err.message);
+      // Continue anyway - the subsequent load state wait will catch critical issues
+    });
📝 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.

Suggested change
await Promise.race([
page.waitForURL((url) => !url.pathname.includes('/login'), { timeout: 10000 }),
appContent.first().waitFor({ state: 'visible', timeout: 10000 }),
]).catch(() => {});
await Promise.race([
page.waitForURL((url) => !url.pathname.includes('/login'), { timeout: 10000 }),
appContent.first().waitFor({ state: 'visible', timeout: 10000 }),
]).catch((err) => {
console.warn('[handleLoginScreenIfPresent] Navigation after login timed out:', err.message);
// Continue anyway - the subsequent load state wait will catch critical issues
});
🤖 Prompt for AI Agents
In apps/ui/tests/utils/core/interactions.ts around lines 103 to 106, the
Promise.race call ends with an empty catch that silently swallows navigation
errors; replace the empty catch with explicit handling: catch the error, if it's
a known/expected timeout (e.g. error.name === 'TimeoutError' or message includes
'Timeout') log a concise warning including the error details (console.warn or
the test logger) and allow the test to continue, otherwise rethrow the error so
real failures surface; ensure the logged message includes context (which wait
failed) and the original error object.

Comment on lines +18 to +33
// Check if we're on the login screen and handle it
const loginInput = page
.locator('[data-testid="login-api-key-input"], input[type="password"][placeholder*="API key"]')
.first();
const isLoginScreen = await loginInput.isVisible({ timeout: 2000 }).catch(() => false);
if (isLoginScreen) {
const apiKey = process.env.AUTOMAKER_API_KEY || 'test-api-key-for-e2e-tests';
await loginInput.fill(apiKey);
await page.waitForTimeout(100);
await page
.locator('[data-testid="login-submit-button"], button:has-text("Login")')
.first()
.click();
await page.waitForURL('**/board', { timeout: 5000 });
await page.waitForLoadState('load');
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Refactor: Eliminate duplicated login handling across navigation functions.

The login screen handling logic is duplicated across navigateToBoard, navigateToContext, navigateToAgent, and navigateToWelcome. This same logic already exists in the handleLoginScreenIfPresent utility function (in core/interactions.ts). This duplication creates a maintenance burden where login logic changes must be replicated in multiple places.

🔎 Recommended refactor

Import the utility at the top:

 import { Page } from '@playwright/test';
-import { clickElement } from '../core/interactions';
+import { clickElement, handleLoginScreenIfPresent } from '../core/interactions';
 import { waitForElement } from '../core/waiting';
 import { authenticateForTests } from '../api/client';

Then replace all inline login handling blocks with a single call. For example, in navigateToBoard:

   await page.goto('/board');
   await page.waitForLoadState('load');
-
-  // Check if we're on the login screen and handle it
-  const loginInput = page
-    .locator('[data-testid="login-api-key-input"], input[type="password"][placeholder*="API key"]')
-    .first();
-  const isLoginScreen = await loginInput.isVisible({ timeout: 2000 }).catch(() => false);
-  if (isLoginScreen) {
-    const apiKey = process.env.AUTOMAKER_API_KEY || 'test-api-key-for-e2e-tests';
-    await loginInput.fill(apiKey);
-    await page.waitForTimeout(100);
-    await page
-      .locator('[data-testid="login-submit-button"], button:has-text("Login")')
-      .first()
-      .click();
-    await page.waitForURL('**/board', { timeout: 5000 });
-    await page.waitForLoadState('load');
-  }
+  await handleLoginScreenIfPresent(page);

Apply the same pattern to navigateToContext (lines 51-66), navigateToAgent (lines 130-145), and navigateToWelcome (lines 190-207).

Also applies to: 51-66, 130-145, 190-207

🤖 Prompt for AI Agents
In apps/ui/tests/utils/navigation/views.ts around lines 18-33 (and also replace
duplicated blocks at 51-66, 130-145, 190-207), the login-screen handling block
is duplicated; import the existing handleLoginScreenIfPresent utility from
core/interactions at the top of the file and replace each inline
login-detection-and-login sequence with a single await
handleLoginScreenIfPresent(page) call (keeping the same async/await flow and
timeouts where needed) so all navigation functions reuse the centralized login
logic.

@webdevcody
Copy link
Collaborator Author

@CodeRabbit re review this pr

@coderabbitai
Copy link

coderabbitai bot commented Dec 30, 2025

@webdevcody I'll perform a full re-review of all the changes in this PR for you.

✅ Actions performed

Full review triggered.

@webdevcody webdevcody merged commit 1fcaa52 into main Dec 30, 2025
14 checks passed
@webdevcody webdevcody deleted the protect-api-with-api-key branch December 30, 2025 05:42
RayFernando1337 added a commit to RayFernando1337/automaker that referenced this pull request Jan 1, 2026
API requests were being made before initApiKey() completed, causing
401 Unauthorized errors on app startup in Electron mode.

Changes:
- Add waitForApiKeyInit() to track and await API key initialization
- Make HTTP methods (get/post/put/delete) wait for auth before requests
- Defer WebSocket connection until API key is ready
- Add explicit auth wait in useSettingsMigration hook

Fixes race condition introduced in PR AutoMaker-Org#321
RayFernando1337 added a commit to RayFernando1337/automaker that referenced this pull request Jan 1, 2026
API requests were being made before initApiKey() completed, causing
401 Unauthorized errors on app startup in Electron mode.

Changes:
- Add waitForApiKeyInit() to track and await API key initialization
- Make HTTP methods (get/post/put/delete) wait for auth before requests
- Defer WebSocket connection until API key is ready
- Add explicit auth wait in useSettingsMigration hook

Fixes race condition introduced in PR AutoMaker-Org#321
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants