Skip to content

Conversation

@kfirstri
Copy link
Contributor

@kfirstri kfirstri commented Jan 7, 2026

Summary

Implement actual login flow with http calls --

This is waiting for https://github.com/base44-dev/apper/pull/2449

@kfirstri kfirstri self-assigned this Jan 7, 2026
@github-project-automation github-project-automation bot moved this to Backlog in Dev Jan 7, 2026
@github-actions
Copy link
Contributor

github-actions bot commented Jan 7, 2026

🚀 Package Preview Available!


Install this PR's preview build with npm:

npm i @base44-preview/cli@0.0.1-pr.10.1d6afbb

Prefer not to change any import paths? Install using npm alias so your code still imports base44:

npm i "base44@npm:@base44-preview/cli@0.0.1-pr.10.1d6afbb"

Or add it to your package.json dependencies:

{
  "dependencies": {
    "base44": "npm:@base44-preview/cli@0.0.1-pr.10.1d6afbb"
  }
}

Preview published to npm registry — try new features instantly!

@claude
Copy link

claude bot commented Jan 7, 2026

Pull Request Review: Implement Login Flow with HTTP

Summary

This PR successfully transitions from mock authentication to a real OAuth Device Code flow implementation. The changes are well-structured and follow OAuth 2.0 standards properly.

✅ Strengths

1. Solid OAuth Implementation

  • Correctly implements OAuth 2.0 Device Authorization Grant (RFC 8628)
  • Proper handling of polling states (authorization_pending, slow_down)
  • Well-structured error handling with appropriate error types
  • Good separation between authClient (OAuth endpoints) and httpClient (authenticated API calls)

2. Schema Validation

  • Excellent use of Zod for runtime validation of API responses
  • Schema transformations properly convert snake_case API responses to camelCase
  • Type safety throughout the authentication flow

3. Token Refresh Logic

  • Smart automatic token refresh in httpClient.ts:9-28 via afterResponse hook
  • Graceful degradation when refresh fails (deletes auth and lets 401 propagate)
  • Prevents the retry loop issue by checking if token refresh succeeded

🐛 Potential Issues

1. Critical: Infinite Retry Loop Risk (src/core/utils/httpClient.ts:25-27)

// Retry the request with new token
request.headers.set("Authorization", `Bearer ${newAccessToken}`);
return ky(request);

Issue: If the server returns 401 even after refresh (e.g., token was revoked), this could cause an infinite loop because the new request will trigger the same afterResponse hook again.

Fix: Add a flag to prevent retrying more than once:

async function handleUnauthorized(
  request: KyRequest,
  _options: NormalizedOptions,
  response: KyResponse
): Promise<Response | void> {
  if (response.status !== 401) {
    return;
  }

  // Prevent infinite retry loops
  if (request.headers.get("X-Token-Refreshed") === "true") {
    return;
  }

  const newAccessToken = await refreshAndSaveTokens();

  if (!newAccessToken) {
    return;
  }

  request.headers.set("Authorization", `Bearer ${newAccessToken}`);
  request.headers.set("X-Token-Refreshed", "true");
  return ky(request);
}

2. Race Condition in Token Refresh (src/core/auth/config.ts:75-92)

Multiple concurrent requests could trigger parallel token refreshes, causing one refresh to overwrite another's tokens.

Recommendation: Add a simple locking mechanism:

let refreshPromise: Promise<string | null> | null = null;

export async function refreshAndSaveTokens(): Promise<string | null> {
  if (refreshPromise) {
    return refreshPromise;
  }

  refreshPromise = (async () => {
    try {
      const auth = await readAuth();
      const tokenResponse = await renewAccessToken(auth.refreshToken);
      
      await writeAuth({
        ...auth,
        accessToken: tokenResponse.accessToken,
        refreshToken: tokenResponse.refreshToken,
      });
      
      return tokenResponse.accessToken;
    } catch {
      await deleteAuth();
      return null;
    } finally {
      refreshPromise = null;
    }
  })();

  return refreshPromise;
}

3. Security: Hardcoded Placeholder Data (src/cli/commands/auth/login.ts:76-84)

await writeAuth({
  accessToken: response.accessToken,
  refreshToken: response.refreshToken,
  email: "user@base44.com",  // ⚠️ Placeholder
  name: "Base44 User",        // ⚠️ Placeholder
});

Issue: All users will have the same email/name, which could cause issues if other commands depend on this data.

Recommendations:

  • Add a GitHub issue to track the /userinfo endpoint implementation
  • Consider making email/name optional in AuthDataSchema until the endpoint is ready
  • Update whoami command to indicate these are placeholder values

4. Missing Error Context (src/core/auth/api.ts)

The AuthApiError constructor accepts a cause parameter but it's never used. Consider passing the underlying error for better debugging:

throw new AuthApiError(
  `Failed to generate device code: ${response.status} ${response.statusText}`,
  new Error(await response.text()) // Include response body
);

5. OAuth Error Schema Incomplete (src/core/auth/schema.ts:62-71)

The OAuthErrorSchema only includes 5 error types. The OAuth 2.0 spec defines additional errors like invalid_client, invalid_request, unsupported_grant_type.

Fix: Either add all OAuth errors or use a more permissive schema:

export const OAuthErrorSchema = z.object({
  error: z.string(),
  error_description: z.string().optional(),
  error_uri: z.string().optional(),
});

⚡ Performance Considerations

  1. Polling Interval: The code uses a hardcoded 2000ms interval (login.ts:52) but the API returns an interval field in the device code response. Consider using the server-provided interval:
interval: deviceCodeResponse.interval * 1000,
  1. Token Expiration: The TokenResponse includes expiresIn but it's not stored or used. Consider proactive token refresh before expiration rather than waiting for 401s.

🔒 Security Considerations

  1. ✅ Good: Using application/x-www-form-urlencoded for token endpoints (OAuth 2.0 standard)
  2. ✅ Good: Tokens are stored locally in auth file (appropriate for CLI)
  3. ✅ Good: Refresh token is properly used and rotated
  4. ⚠️ Consider: Add file permissions check/set for auth file (should be 600/rw-------)
  5. ⚠️ Consider: The auth file path should be documented for users who need to clear credentials manually

📝 Test Coverage

Missing Tests: There are no tests for the authentication flow. Consider adding:

  1. Unit tests for:

    • generateDeviceCode() - mock API responses
    • getTokenFromDeviceCode() - test all OAuth error states
    • renewAccessToken() - test success and failure cases
    • Schema validation edge cases
  2. Integration tests for:

    • Complete login flow with mock server
    • Token refresh flow
    • Error handling (expired device code, denied authorization, etc.)

Example test structure:

// tests/core/auth/api.test.ts
describe('generateDeviceCode', () => {
  it('should successfully generate device code', async () => {
    // Mock ky response
    // Assert correct transformation
  });
  
  it('should throw AuthApiError on HTTP error', async () => {
    // Mock 500 response
    // Assert error thrown
  });
});

🎨 Code Quality

  1. ✅ Good: Consistent error handling patterns
  2. ✅ Good: TypeScript types are well-defined
  3. ✅ Good: Clear separation of concerns (api.ts, config.ts, schema.ts)
  4. ✅ Good: JSDoc comments where needed
  5. ⚠️ Minor: Trailing newline in authClient.ts:16 should be removed
  6. ⚠️ Minor: Unused import in runCommand.ts - AuthApiError and AuthValidationError are imported but not used after the changes

📋 Recommendations Summary

Must Fix (High Priority)

  1. Add retry loop prevention in handleUnauthorized
  2. Implement token refresh locking mechanism

Should Fix (Medium Priority)

  1. 📝 Create issue for /userinfo endpoint and track placeholder data
  2. 🔒 Set proper file permissions on auth file (600)
  3. 🧪 Add comprehensive test coverage for auth flows
  4. 📊 Use server-provided polling interval
  5. 🔧 Make OAuth error schema more flexible

Nice to Have (Low Priority)

  1. 🧹 Remove unused imports in runCommand.ts
  2. 🧹 Remove trailing newline in authClient.ts
  3. 📖 Add JSDoc to httpClient.ts explaining the retry mechanism
  4. ⏰ Store and use token expiration for proactive refresh

Conclusion

This is a solid implementation of OAuth Device Code flow that replaces the mock authentication correctly. The main concerns are the potential infinite retry loop and race condition in token refresh, both of which should be addressed before merging. Once those are fixed, this will be production-ready.

Great work on following OAuth 2.0 standards and maintaining good code structure! 🎉

Recommendation: Request changes for issues #1 and #2, then approve after fixes.

@claude
Copy link

claude bot commented Jan 8, 2026

Pull Request Review: Login Flow Implementation with HTTP

Thank you for implementing the actual OAuth 2.0 Device Authorization Grant flow! This is a solid implementation overall. Here's my detailed feedback:


Strengths

  1. Proper OAuth 2.0 Implementation: Correctly implements the Device Authorization Grant flow (RFC 8628)
  2. Good Error Handling: Distinguishes between polling states (authorization_pending, slow_down) and actual errors
  3. Token Refresh Logic: Implements proactive token refresh with proper locking mechanism to prevent concurrent refreshes
  4. Smart HTTP Client Design: Separates authClient (for OAuth endpoints) from httpClient (for authenticated API calls)
  5. Schema Validation: Good use of Zod schemas with proper snake_case to camelCase transformation

🐛 Critical Issues

1. Race Condition in Token Refresh (httpClient.ts:56-62)

The beforeRequest hook has a potential race condition:

// Proactively refresh if token is expired or about to expire
if (isTokenExpired(auth)) {
  const newAccessToken = await refreshAndSaveTokens();
  if (newAccessToken) {
    request.headers.set("Authorization", `Bearer ${newAccessToken}`);
    return;
  }
}
// This line could use the old expired token if refresh returns null
request.headers.set("Authorization", `Bearer ${auth.accessToken}`);

Issue: If refreshAndSaveTokens() returns null (refresh failed), the code falls through and still sets the old expired token.

Fix: Should throw an error or skip setting the header entirely when refresh fails:

if (isTokenExpired(auth)) {
  const newAccessToken = await refreshAndSaveTokens();
  if (!newAccessToken) {
    throw new Error('Authentication expired. Please login again.');
  }
  request.headers.set("Authorization", `Bearer ${newAccessToken}`);
} else {
  request.headers.set("Authorization", `Bearer ${auth.accessToken}`);
}

2. Memory Leak in retriedRequests WeakSet (httpClient.ts:11, 39)

WeakSet<KyRequest> might not work as expected because:

  • KyRequest objects might not be properly garbage collected during the retry flow
  • The WeakSet may not persist the "retried" status across the retry call at line 41

Recommendation: Consider using a WeakMap with timestamps for cleanup, or a simple Set with manual cleanup after a timeout.

3. Silent Error Swallowing (config.ts:107-110)

} catch {
  // Refresh failed - delete auth, user needs to login again
  await deleteAuth();
  return null;
}

Issue: This silently deletes the auth file without logging why. Users will be confused when they're suddenly logged out.

Fix: Log the error before deleting:

} catch (error) {
  console.error('Token refresh failed:', error);
  await deleteAuth();
  return null;
}

⚠️ Security Concerns

4. Hardcoded Placeholder Credentials (login.ts:77-87)

email: "user@base44.com",
name: "Base44 User",

Issue: While there's a TODO comment, storing hardcoded placeholder data could lead to:

  • Users not realizing they need to implement the actual endpoint
  • Potential confusion in production if the TODO is forgotten
  • Schema validation passes with fake data

Recommendation:

  • Either make email/name optional in the schema until userinfo is available
  • Or fetch userinfo immediately (even if basic) before saving auth data
  • Add a loud warning log that placeholder data is being used

5. Missing Client Secret Consideration

The OAuth flow uses client_id only, which is correct for a public client (CLI). However, ensure the server is configured to:

  • Not require client secrets for this client
  • Properly validate the redirect/callback restrictions
  • Rate limit device code generation to prevent abuse

🔧 Code Quality Issues

6. Trailing Newlines (authClient.ts:16-17, utils/index.ts:2)

Extra blank lines at end of files - minor inconsistency with codebase style.

7. Error Context Loss (runCommand.ts:17-20)

The change from specific error handling to generic Error handling loses important context:

-    if (e instanceof AuthValidationError) {
-      const issues = e.issues.map((i) => i.message).join(", ");
-      log.error(`Invalid response from server: ${issues}`);
-    } else if (e instanceof AuthApiError || e instanceof Error) {

Issue: Users no longer see the specific validation issues, just a generic stack trace.

Recommendation: Keep the specific error type handling for better UX.

8. Unused Schema Field (schema.ts:20)

verification_uri_complete is parsed but never used. Consider:

  • Using it to provide a better UX (pre-filled device code URL)
  • Or removing it from the schema if not needed

9. Magic Numbers (config.ts:8)

const TOKEN_REFRESH_BUFFER_MS = 60 * 1000;

Consider making this configurable or documenting why 60 seconds was chosen.


📊 Performance Considerations

10. Unnecessary Auth Read in Every Request (httpClient.ts:53)

Every HTTP request calls readAuth(), which reads and parses a JSON file from disk.

Impact: Could cause performance issues with many concurrent requests.

Fix: Implement an in-memory cache with invalidation:

let authCache: { auth: AuthData; timestamp: number } | null = null;
const CACHE_TTL = 5000; // 5 seconds

async function getCachedAuth(): Promise<AuthData> {
  if (authCache && Date.now() - authCache.timestamp < CACHE_TTL) {
    return authCache.auth;
  }
  const auth = await readAuth();
  authCache = { auth, timestamp: Date.now() };
  return auth;
}

🧪 Missing Test Coverage

Critical Gap: No tests found for the entire auth flow.

Recommended Tests:

  1. auth/api.test.ts: Mock HTTP responses for device code and token flows
  2. auth/config.test.ts: Test token refresh logic, expiration checking
  3. utils/httpClient.test.ts: Test retry logic and token refresh in requests
  4. Integration test for the full login flow

Example structure:

describe('getTokenFromDeviceCode', () => {
  it('returns null for authorization_pending', async () => {
    // Mock authClient to return authorization_pending
    // Assert returns null
  });
  
  it('throws error for expired_token', async () => {
    // Mock authClient to return expired_token error
    // Assert throws AuthApiError
  });
});

📝 Documentation & Polish

11. Missing JSDoc Comments

Key functions lack documentation:

  • generateDeviceCode() - What does it return? When does it throw?
  • refreshAndSaveTokens() - What triggers a refresh? What happens on failure?
  • isTokenExpired() - Why the 60-second buffer?

12. TODO Comments

The TODO in login.ts:78 should have a tracking issue number for accountability.


🎯 Overall Assessment

Code Quality: 7/10
Security: 7/10 (public OAuth client is appropriate for CLI)
Performance: 6/10 (file I/O on every request)
Test Coverage: 2/10 (no tests)

🚀 Action Items (Priority Order)

  1. High: Fix the race condition in httpClient.ts beforeRequest hook
  2. High: Add tests for auth flow (at least unit tests for api.ts)
  3. Medium: Implement in-memory auth cache to avoid repeated file reads
  4. Medium: Restore specific error handling in runCommand.ts
  5. Medium: Log errors before deleting auth in refresh failure
  6. Low: Clean up trailing newlines
  7. Low: Add JSDoc comments
  8. Low: Consider using verification_uri_complete for better UX

Nice Touches

  • The retry lock mechanism is elegant
  • Error schema validation is thorough
  • Separation of auth and API clients is clean architecture
  • Proper OAuth 2.0 error code handling

Great work on moving from mock to real implementation! The foundation is solid, and the above issues are mostly polish and edge cases. Would love to see tests added before merging. 🎉

@claude
Copy link

claude bot commented Jan 8, 2026

Pull Request Review: Login Flow Implementation

Summary

This PR implements an OAuth 2.0 device code flow for authentication with HTTP calls, replacing mock implementations. The implementation includes token refresh mechanisms and proper error handling. Overall, the code is well-structured and follows OAuth standards.


Code Quality & Best Practices

✅ Strengths

  • Good separation of concerns: authClient for OAuth endpoints and httpClient for authenticated API calls
  • Proper OAuth 2.0 implementation: Correctly follows the device code flow standard
  • Token refresh mechanism: Well-designed with proactive refresh and request retry logic
  • Schema validation: Good use of Zod for runtime validation with proper transformations
  • Concurrency control: Lock mechanism prevents race conditions in token refresh

⚠️ Issues & Suggestions

1. Security: Client ID in Source Code (High Priority)

Location: src/core/consts.ts:24

export const AUTH_CLIENT_ID = "base44_cli";

Issue: Client ID is hardcoded. While this is acceptable for public clients (CLIs), ensure this matches your OAuth provider's configuration.

Recommendation: Document that this is intentional for a public OAuth client. Consider adding a comment explaining why this isn't in environment variables.


2. Error Handling: Silent Failure in httpClient (Medium Priority)

Location: src/core/utils/httpClient.ts:64-67

try {
  const auth = await readAuth();
  // ...
} catch {
  // No auth available, continue without header
}

Issue: All exceptions are silently caught. If readAuth() fails due to corrupted data or file system errors, the user won't be notified.

Recommendation: Only catch expected errors (e.g., file not found). Rethrow unexpected errors:

} catch (error) {
  if (error instanceof Error && error.message.includes('not found')) {
    // No auth available, continue without header
    return;
  }
  throw error;
}

3. Race Condition: WeakSet in Multiple Retries (Medium Priority)

Location: src/core/utils/httpClient.ts:11

const retriedRequests = new WeakSet<KyRequest>();

Issue: The WeakSet prevents infinite loops, but there's a potential issue:

  1. Request fails with 401
  2. Token refresh succeeds
  3. Retry happens with new token
  4. If retry also gets 401, it won't retry again (expected)
  5. However, if a new request is made with the same Request object, it might be blocked

Analysis: This is likely fine since each request should be a new object, but worth considering.

Recommendation: Consider adding a comment explaining the lifecycle and limitations of this approach.


4. Logic Issue: Double Token Refresh (Medium Priority)

Location: src/core/utils/httpClient.ts:50-71

beforeRequest: [
  async (request) => {
    const auth = await readAuth();
    if (isTokenExpired(auth)) {
      const newAccessToken = await refreshAndSaveTokens();
      // ...
    }
  }
],
afterResponse: [handleUnauthorized],

Issue: If token expires between proactive check and server validation, you'll attempt refresh twice:

  1. beforeRequest detects expiry → refreshes
  2. Server might still return 401 → afterResponse refreshes again

Analysis: The lock in refreshAndSaveTokens() prevents concurrent refreshes, so this is safe but inefficient.

Recommendation: Consider adding a timestamp check in handleUnauthorized to skip refresh if it was just done:

let lastRefreshTime = 0;
const REFRESH_COOLDOWN = 5000; // 5 seconds

async function handleUnauthorized(...) {
  if (Date.now() - lastRefreshTime < REFRESH_COOLDOWN) {
    return; // Recently refreshed, don't retry
  }
  // ... rest of logic
  lastRefreshTime = Date.now();
}

5. Missing Error Details in getUserInfo (Low Priority)

Location: src/core/auth/api.ts:136

export async function getUserInfo(): Promise<UserInfoResponse> {
  const response = await httpClient.get("oauth/userinfo");
  const result = UserInfoSchema.safeParse(await response.json());
  // ...
}

Issue: No HTTP error handling before parsing response.

Recommendation: Add error checking:

if (!response.ok) {
  throw new AuthApiError(`Failed to fetch user info: ${response.status}`);
}

6. Hardcoded User Data in Login (Medium Priority)

Location: src/cli/commands/auth/login.ts:78-83

await writeAuth({
  accessToken: response.accessToken,
  refreshToken: response.refreshToken,
  expiresAt,
  email: "user@base44.com",
  name: "Base44 User",
});

Issue: Placeholder values for email/name with a TODO comment.

Recommendation:

  • Option A: Call getUserInfo() after authentication to populate real data
  • Option B: If user info isn't critical immediately, make these fields optional in the schema and fetch them lazily
  • Current approach: Acceptable for MVP but should be tracked

Security Concerns

✅ Good Security Practices

  1. No token logging: Tokens are never logged or exposed in error messages
  2. Secure token storage: Stored in a dedicated auth file (ensure file permissions are restrictive)
  3. Token expiration handling: Proper expiry checking with buffer time
  4. No token in URL: Uses POST body for token requests (not query params)

⚠️ Security Recommendations

1. Token Storage Permissions (High Priority)

Action Required: Verify that the auth file created by writeJsonFile() has restrictive permissions (0600 on Unix systems).

Check: src/core/utils/fs.ts - ensure file creation sets proper permissions:

// Should set permissions to 0600 (owner read/write only)
await writeFile(path, data, { mode: 0o600 });

2. HTTPS Enforcement (Medium Priority)

Location: src/core/consts.ts:26-30

const DEFAULT_API_URL = "https://app.base44.com";

export function getBase44ApiUrl(): string {
  return process.env.BASE44_API_URL || DEFAULT_API_URL;
}

Issue: BASE44_API_URL env var could be set to HTTP, leaking tokens.

Recommendation: Add validation:

export function getBase44ApiUrl(): string {
  const url = process.env.BASE44_API_URL || DEFAULT_API_URL;
  if (!url.startsWith('https://')) {
    throw new Error('API URL must use HTTPS');
  }
  return url;
}

Performance Considerations

✅ Good Performance Practices

  1. Proactive token refresh: Prevents failed requests due to expiry
  2. Refresh lock: Prevents duplicate concurrent refresh requests
  3. Efficient HTTP client: ky is lightweight and modern

💡 Suggestions

1. Token Refresh Buffer Configuration

Location: src/core/auth/config.ts:7-8

const TOKEN_REFRESH_BUFFER_MS = 60 * 1000;

Suggestion: 60 seconds might be too aggressive for long-lived tokens. Consider:

  • Making it configurable
  • Using a percentage of token lifetime (e.g., refresh when 90% expired)

2. Unnecessary Token Parsing in Refresh Path

Minor: When refreshing tokens, the full TokenResponse is parsed even though we only need access token and refresh token. This is fine for now but could be optimized with a partial schema.


Test Coverage

❌ Critical Gap: No Tests for New Code

Files without tests:

  • src/core/auth/api.ts - Core OAuth flow (146 lines)
  • src/core/auth/config.ts - Token refresh logic (118 lines)
  • src/core/utils/httpClient.ts - HTTP client with retry (75 lines)
  • src/cli/commands/auth/login.ts - Login command flow

Required Test Coverage:

  1. Unit Tests for src/core/auth/api.ts:

    • ✓ Device code generation success/failure
    • ✓ Token polling (pending, success, error states)
    • ✓ Token refresh success/failure
    • ✓ OAuth error handling
    • ✓ Schema validation failures
  2. Unit Tests for src/core/auth/config.ts:

    • ✓ Token expiry detection
    • ✓ Refresh lock mechanism (concurrent calls)
    • ✓ Refresh failure handling (should delete auth)
    • ✓ Auth data validation
  3. Integration Tests for src/core/utils/httpClient.ts:

    • ✓ Automatic token refresh on 401
    • ✓ Proactive token refresh before expiry
    • ✓ Retry prevention (single retry per request)
    • ✓ Request without auth (fresh install)
  4. E2E Tests for Login Flow:

    • ✓ Complete device flow (mock server)
    • ✓ User info retrieval
    • ✓ Timeout handling

Recommendation: Follow the existing test pattern from tests/core/project.test.ts. Consider using vitest with mocked HTTP responses.


Additional Issues

1. Inconsistent Error Handling Style

Location: src/cli/utils/runCommand.ts:17-25

} catch (e) {
  if (e instanceof Error) {
    log.error(e.stack ?? e.message);
  } else {
    log.error(String(e));
  }
}

Issue: Changed from specific error type handling to generic. This loses the benefit of custom error types.

Previous behavior (deleted):

if (e instanceof AuthValidationError) {
  const issues = e.issues.map((i) => i.message).join(", ");
  log.error(`Invalid response from server: ${issues}`);
}

Recommendation: Keep custom error handling or explain why it was removed.


2. Export Statement Formatting

Location: src/core/utils/index.ts:2

export * from "./httpClient.js";

Note: Missing semicolon (present in line 1). Inconsistent with project style.


3. Unused getUserInfo Function

Location: src/core/auth/api.ts:135-146

export async function getUserInfo(): Promise<UserInfoResponse> {
  // ...
}

Issue: This function is exported but never used. The TODO in login.ts suggests it should be called.

Recommendation: Either use it or remove it. If keeping for future use, add a comment explaining.


Summary

Must Fix Before Merge

  1. Add comprehensive test coverage for all new authentication code
  2. Verify token storage file permissions are restrictive (0600)
  3. Implement HTTPS validation for BASE44_API_URL
  4. Fix error handling in httpClient beforeRequest hook

Should Fix

  1. Use getUserInfo() or remove placeholder user data
  2. Add comment explaining public client ID usage
  3. Improve error handling granularity in runCommand
  4. Fix semicolon inconsistency

Nice to Have

  1. Add cooldown mechanism for token refresh retries
  2. Make token refresh buffer configurable
  3. Add WeakSet lifecycle documentation

Overall Assessment

Quality: Good ⭐⭐⭐⭐☆
Security: Good (with recommendations) 🔒
Testing: Critical Gap ❌

The implementation is solid and follows OAuth 2.0 standards well. The token refresh mechanism is well-designed. However, test coverage is essential before merging to ensure the authentication flow works correctly across all scenarios.

Great work on the architecture and separation of concerns! 🚀

@claude
Copy link

claude bot commented Jan 8, 2026

Pull Request Review: Login Flow with HTTP Implementation

Summary

This PR implements a real OAuth 2.0 device code flow to replace the mock authentication. Overall, the implementation is solid and follows OAuth 2.0 best practices. Below are my findings across code quality, security, bugs, performance, and testing.


✅ Strengths

Good Architectural Decisions

  • Separation of concerns: authClient for OAuth endpoints (no auth headers) vs httpClient for authenticated API calls is clean
  • Token refresh strategy: Proactive refresh with 60-second buffer (src/core/auth/config.ts:8) prevents expiration during requests
  • Concurrent refresh protection: Lock mechanism (src/core/auth/config.ts:11) prevents race conditions
  • Schema validation: Using Zod with snake_case → camelCase transformation is good for API boundary validation

OAuth 2.0 Compliance

  • Proper device code flow implementation
  • Correct handling of polling states (authorization_pending, slow_down)
  • Following RFC 8628 standards

🔴 Critical Issues

1. Race Condition in httpClient Token Refresh (src/core/utils/httpClient.ts:56-62)

if (isTokenExpired(auth)) {
  const newAccessToken = await refreshAndSaveTokens();
  if (newAccessToken) {
    request.headers.set("Authorization", `Bearer ${newAccessToken}`);
    return;
  }
}
request.headers.set("Authorization", `Bearer ${auth.accessToken}`);

Problem: If token refresh succeeds but returns early, line 64 still executes with the old auth.accessToken.

Fix: Add else or early return to prevent fallthrough:

if (isTokenExpired(auth)) {
  const newAccessToken = await refreshAndSaveTokens();
  if (newAccessToken) {
    request.headers.set("Authorization", `Bearer ${newAccessToken}`);
    return;
  }
  // Token refresh failed, continue without header
  return;
}
request.headers.set("Authorization", `Bearer ${auth.accessToken}`);

2. Circular Dependency Risk (src/core/utils/httpClient.ts:136 + src/core/auth/api.ts:15)

  • httpClient imports from auth/config → which imports auth/api → which imports httpClient
  • getUserInfo() uses httpClient, called during login before tokens are fully saved

Recommendation: Break the cycle by:

  • Making getUserInfo() use authClient with explicit auth header, OR
  • Moving getUserInfo() to use a different client pattern

3. Silent Token Refresh Failure (src/core/auth/config.ts:108-110)

} catch {
  await deleteAuth();
  return null;
}

Problem: Swallowing all errors makes debugging impossible. Users won't know why they're logged out.

Fix: Log the error or differentiate between network failures vs invalid tokens:

} catch (error) {
  console.error("Token refresh failed:", error);
  await deleteAuth();
  return null;
}

⚠️ Security Concerns

4. Token Storage Security

Auth tokens are stored in a local JSON file (getAuthFilePath()). Ensure:

  • File has restrictive permissions (0600 on Unix)
  • Path is in user-specific directory (not shared)
  • Consider OS keychain/credential manager for production

5. Missing PKCE for Device Flow

While not required by RFC 8628, PKCE (code_challenge/code_verifier) adds extra security against interception attacks. Consider adding if the auth server supports it.

6. Error Information Leakage (src/core/auth/api.ts:117)

throw new AuthApiError(`Token refresh failed: ${response.statusText}`);

Be cautious about exposing detailed server errors to end users. Consider sanitizing error messages.


🐛 Potential Bugs

7. getUserInfo Called Before Token is Saved (src/cli/commands/auth/login.ts:76)

async function saveAuthData(response: TokenResponse): Promise<void> {
  const userInfo = await getUserInfo();  // Uses httpClient which needs auth
  // ...
}

Problem: getUserInfo() calls httpClient.get(), which tries to read auth from disk in beforeRequest hook. But auth isn't saved yet, so this may fail or use stale tokens.

Fix: Either:

  • Pass access token explicitly to getUserInfo(), or
  • Call getUserInfo() after saving tokens with a partial auth object

8. WeakSet Memory Leak Potential (src/core/utils/httpClient.ts:11)

const retriedRequests = new WeakSet<KyRequest>();

KyRequest objects may not be garbage collected promptly in long-running processes. This is likely fine for a CLI, but be aware.

9. Outdated Comment (src/cli/commands/auth/login.ts:78)

// For now, we store placeholder values until a /userinfo endpoint is available

The endpoint IS implemented (oauth/userinfo), so this comment is misleading.


🚀 Performance Considerations

10. Unnecessary Double Auth Check

httpClient checks token expiration in beforeRequest, then afterResponse handles 401s. This means:

  • If server clock differs or revokes tokens, you make an extra request
  • Consider: server-driven expiration is more reliable than client-side checks

Suggestion: You could remove proactive refresh and rely solely on 401 handling, or add a clock skew tolerance.

11. Blocking Refresh Lock

The refresh lock (src/core/auth/config.ts:90) is good for preventing concurrent refreshes, but all requests wait for a single refresh. This is acceptable for a CLI but could be improved with a queue pattern for high-concurrency scenarios.


📋 Test Coverage

12. Missing Tests for New Code

  • ❌ No tests for src/core/auth/api.ts (OAuth flows)
  • ❌ No tests for src/core/utils/httpClient.ts (token refresh, retry logic)
  • ❌ No tests for src/core/auth/config.ts (refresh lock, expiration check)
  • ✅ Existing test file: tests/core/project.test.ts (unrelated to this PR)

Recommendation: Add unit tests covering:

  • Device code flow success/failure paths
  • Token refresh logic and lock behavior
  • 401 retry mechanism (once, not infinite)
  • Schema validation edge cases
  • Expiration calculation

📝 Code Quality Suggestions

13. Inconsistent Error Handling (src/cli/utils/runCommand.ts:19-20)

if (e instanceof Error) {
  log.error(e.stack ?? e.message);

Changed from specific error types to generic Error. While simpler, you lose context. Consider keeping structured error handling or at least differentiate auth errors.

14. Magic Numbers

  • TOKEN_REFRESH_BUFFER_MS = 60 * 1000 - Good, well-named constant
  • Could extract 2000 from polling interval calculation for clarity

15. Type Safety in Error Handling

Multiple functions parse JSON and use .safeParse() correctly, but error messages could include the actual validation errors from Zod for better debugging.

16. Duplicate Code in API Functions

The three OAuth functions (generateDeviceCode, getTokenFromDeviceCode, renewAccessToken) share similar patterns:

  1. Make request
  2. Check response.ok
  3. Parse with Zod
  4. Throw validation error

Consider extracting a helper function:

async function fetchAndValidate<T>(
  response: Response,
  schema: z.Schema<T>,
  errorPrefix: string
): Promise<T> { ... }

🎯 Recommendations Summary

Must Fix Before Merge

  1. ✅ Fix race condition in httpClient token fallthrough (Issue Add Claude Code GitHub Workflow #1)
  2. ✅ Fix getUserInfo() being called before tokens are saved (Issue feat(parse config): parse config.jsonc file and sub-dirs configs like entities and functions #7)
  3. ✅ Remove outdated comment about userinfo endpoint (Issue remove project structure from readme #9)

Should Fix

  1. Add error logging in token refresh catch block (Issue more everything to single repo #3)
  2. Add basic unit tests for auth flows and httpClient (Issue base44 ai docs command #12)
  3. Verify auth file permissions/storage location (Issue Add repository field to package.json for npm provenance #4)

Consider for Future

  1. Resolve circular dependency (Issue chore(npm): change npm to and fix workflows #2)
  2. Evaluate PKCE support (Issue feat(login) - mock implementation #5)
  3. Refactor duplicate OAuth code (Issue feat(create with template): add an option to choose a template #16)

📊 Overall Assessment

Code Quality: 7/10 - Well-structured, good separation of concerns, but needs polish
Security: 7/10 - Follows OAuth best practices, but token storage and error handling need review
Reliability: 6/10 - Critical race condition and missing tests are concerns
Performance: 8/10 - Good proactive refresh strategy, minimal issues

Recommendation: Request changes to fix critical issues (#1, #7) before merging. The architecture is solid but needs debugging before production use.

Great work on implementing real OAuth! The foundation is strong. 👍

@claude
Copy link

claude bot commented Jan 11, 2026

Pull Request Review: Login Flow Implementation

Summary

This PR implements a real OAuth2 device code flow for authentication, replacing the mock implementation. The changes introduce proper HTTP calls, token management with refresh capabilities, and a dedicated HTTP client with automatic token refresh.


🟢 Strengths

Code Quality

  • Well-structured OAuth2 implementation: Follows the device code flow spec correctly
  • Good separation of concerns: Distinct clients for auth endpoints (authClient) vs authenticated API calls (httpClient)
  • Proper error handling: Distinguishes between polling states and actual errors
  • Type safety: Strong TypeScript types with Zod validation throughout
  • Defensive token refresh: Proactive refresh with 60s buffer and proper locking mechanism

Architecture

  • The distinction between authClient and httpClient is smart - auth endpoints don't need Authorization headers
  • Token refresh lock prevents race conditions from concurrent requests
  • WeakSet for tracking retried requests is a good pattern

🔴 Critical Issues

1. Race Condition in getUserInfo() - CRITICAL BUG 🐛

Location: src/cli/commands/auth/login.ts:104

const token = await waitForAuthentication(...);
const userInfo = await getUserInfo();  // ← Uses httpClient which needs auth!
await saveAuthData(token, userInfo);

Problem: getUserInfo() is called BEFORE saveAuthData(), but httpClient depends on readAuth() to get the access token. The auth file doesn't exist yet, so this will fail.

Fix: Either:

  • Save minimal auth first, then fetch userInfo
  • Use authClient with manual Authorization header for getUserInfo
  • Pass the token directly to getUserInfo instead of relying on httpClient's beforeRequest hook

Example fix:

// Option 1: Save minimal auth first
await writeAuth({
  accessToken: token.accessToken,
  refreshToken: token.refreshToken,
  expiresAt: Date.now() + token.expiresIn * 1000,
  email: '',
  name: '',
});
const userInfo = await getUserInfo();
await writeAuth({
  accessToken: token.accessToken,
  refreshToken: token.refreshToken,
  expiresAt: Date.now() + token.expiresIn * 1000,
  email: userInfo.email,
  name: userInfo.name,
});

2. Unused Import

Location: src/cli/commands/auth/login.ts:16

import { UserInfo } from "node:os";  // ← Never used

This should be removed.


3. Security: Missing Token Validation

Location: src/core/auth/config.ts:108-110

} catch {
  // Refresh failed - delete auth, user needs to login again
  await deleteAuth();
  return null;
}

Issue: Silent failure could mask security issues. If token refresh fails, you delete the auth file without logging why.

Recommendation:

} catch (error) {
  // Log the error for debugging but don't expose it to user
  console.error('Token refresh failed:', error);
  await deleteAuth();
  return null;
}

4. Error Handling: httpClient Silently Swallows Auth Errors

Location: src/core/utils/httpClient.ts:65-67

} catch {
  // No auth available, continue without header
}

Issue: If readAuth() throws for reasons other than "file not found" (e.g., corrupted file), this silently continues without authentication. This makes debugging difficult.

Better approach:

} catch (error) {
  // Only swallow "not logged in" errors
  if (error instanceof Error && error.message.includes('not found')) {
    return;
  }
  throw error; // Re-throw other errors like corrupted auth file
}

5. Comment is Outdated

Location: src/cli/commands/auth/login.ts:84

// For now, we store placeholder values until a /userinfo endpoint is available

This comment is misleading since you're now calling getUserInfo(). Remove it.


🟡 Performance & Best Practices

6. Missing Response Type Validation in httpClient

Location: src/core/utils/httpClient.ts

The httpClient doesn't validate response bodies. Every consumer must call .json() and validate. Consider adding response transformation hooks or typed methods.


7. Potential Memory Leak in WeakSet

Location: src/core/utils/httpClient.ts:11

const retriedRequests = new WeakSet<KyRequest>();

Note: WeakSet is appropriate here, but be aware that if requests are held in closures/callbacks, they won't be GC'd. This is likely fine for a CLI tool but worth monitoring.


8. OAuth Scope Hardcoded

Location: src/core/auth/api.ts:21

scope: "apps:read apps:write",

Consider making this configurable or at least document why these specific scopes are needed.


🔵 Testing & Documentation

9. No Tests for Core Auth Logic

This PR adds significant authentication logic but no tests. Critical flows that need testing:

  • Device code generation and polling
  • Token refresh with concurrent requests
  • httpClient retry logic with 401 responses
  • Race condition handling in refreshAndSaveTokens

Recommendation: Add integration tests that mock the OAuth server responses.


10. Missing Error Message Consistency

Different error types use different formats:

  • src/core/auth/api.ts:28: "Failed to generate device code: 400 Bad Request"
  • src/core/auth/api.ts:139: "Failed to fetch user info: 400"

Consider standardizing error messages across all API calls.


🟣 Security Review

11. Token Storage Security ⚠️

Tokens are stored in plain text JSON files. While acceptable for a CLI tool, consider:

  • Document that users should protect the ~/.base44/ directory
  • Consider using OS keychain/credential manager for sensitive environments
  • Add a note in README about token security

12. No Rate Limiting on Token Refresh

If refreshAndSaveTokens() fails repeatedly, there's no backoff or rate limiting. An attacker could potentially trigger many refresh attempts.


📋 Minor Issues

  1. Magic Number: TOKEN_REFRESH_BUFFER_MS = 60 * 1000 - consider making this configurable or at least document why 60 seconds
  2. Typo in success message: "Successfully logged as" should be "Successfully logged in as" (login.ts:108)
  3. Inconsistent error handling: runCommand now catches all errors and logs stack trace, which is good, but loses error type discrimination

✅ Recommendations Summary

Must Fix Before Merge:

  1. Fix the race condition in login flow (getUserInfo before auth is saved)
  2. Remove unused import

Should Fix:
3. Add error logging to token refresh failure
4. Fix outdated comment
5. Improve error handling in httpClient beforeRequest hook

Nice to Have:
6. Add tests for auth flows
7. Standardize error messages
8. Fix typo in success message
9. Document token security considerations


Overall Assessment

Code Quality: ⭐⭐⭐⭐ (4/5)
Security: ⭐⭐⭐ (3/5) - Good patterns, but needs better error visibility
Test Coverage: ⭐ (1/5) - No tests added
Documentation: ⭐⭐⭐ (3/5) - Code is well-commented but missing security notes

Verdict: Strong implementation with solid OAuth2 patterns, but has a critical race condition that must be fixed before merge. The code is well-structured and mostly follows best practices. Adding tests would significantly improve confidence in the auth flow.


🤖 Review generated by Claude Code

@claude
Copy link

claude bot commented Jan 11, 2026

Pull Request Review: feat(login): implement login flow with http

Summary

This PR implements a complete OAuth device code flow for authentication, replacing the mock implementation with real HTTP calls. The changes introduce proper token management with refresh capabilities and a well-structured HTTP client architecture.

✅ Strengths

Code Quality

  • Excellent separation of concerns: The authClient and httpClient are properly separated, with authClient handling OAuth flows and httpClient handling authenticated API requests
  • Strong typing: Good use of Zod schemas for runtime validation with proper TypeScript types derived from them
  • Clean schema transformations: Snake_case to camelCase conversion in Zod schemas is elegant and maintains API contract separation
  • Good error handling patterns: Proper distinction between AuthApiError and AuthValidationError

Security

  • Proper OAuth 2.0 device flow implementation: Correctly implements the device code grant type per RFC 8628
  • Token refresh mechanism: Proactive token refresh with 60-second buffer prevents failed requests
  • Refresh lock pattern: The refreshPromise lock prevents race conditions during concurrent token refreshes (line 11, config.ts:11)
  • Retry protection: WeakSet<KyRequest> prevents infinite retry loops (httpClient.ts:11)
  • Environment configuration: Supports BASE44_API_URL override for testing/staging environments

🐛 Issues & Concerns

Critical Issues

1. Race Condition in httpClient Token Refresh (httpClient.ts:50-71)

The beforeRequest hook checks expiration and refreshes the token, but then the afterResponse hook on line 70 can also trigger a refresh on 401. This creates a race condition:

// Scenario: Token expires during a request burst
// Request A: beforeRequest triggers refresh
// Request B: beforeRequest sees refresh in progress, waits
// Request A: Uses old token (set before refresh completed)
// Request A: Gets 401, afterResponse triggers another refresh

Recommendation: Set the token on the request in beforeRequest after checking isTokenExpired, not before. Currently line 64 sets the token but line 56-62 may have just refreshed it.

2. Timing Issue in Token Setting (httpClient.ts:51-64)

try {
  const auth = await readAuth();
  
  if (isTokenExpired(auth)) {
    const newAccessToken = await refreshAndSaveTokens();
    if (newAccessToken) {
      request.headers.set("Authorization", `Bearer ${newAccessToken}`);
      return;  // ✅ Good - uses new token
    }
  }
  
  request.headers.set("Authorization", `Bearer ${auth.accessToken}`);  // ⚠️ Uses stale auth.accessToken
} catch {
  // No auth available, continue without header
}

The auth object is read at line 53, but if the token is refreshed at line 57, the auth.accessToken at line 64 is stale. Should re-read auth after refresh or use the returned token.

Fix:

try {
  const auth = await readAuth();
  
  if (isTokenExpired(auth)) {
    const newAccessToken = await refreshAndSaveTokens();
    request.headers.set("Authorization", `Bearer ${newAccessToken ?? auth.accessToken}`);
    return;
  }
  
  request.headers.set("Authorization", `Bearer ${auth.accessToken}`);
} catch {
  // No auth available, continue without header
}

3. Missing Error Status Details (api.ts:138-139)

if (!response.ok) {
  throw new AuthApiError(`Failed to fetch user info: ${response.status}`);
}

Should include response.statusText like in other error handlers for better debugging. Also consider including response body if available.

Medium Priority Issues

4. Incomplete Comment (login.ts:87)

// For now, we store placeholder values until a /userinfo endpoint is available

This comment is outdated - the code actually calls getUserInfo() on line 107, which calls the /oauth/userinfo endpoint. The comment should be removed.

5. Silent Token Refresh Failure (config.ts:107-109)

} catch {
  // Refresh failed - delete auth, user needs to login again
  await deleteAuth();
  return null;
}

Silently swallowing all errors makes debugging difficult. Consider:

  • Logging the error (if a logging mechanism exists)
  • Only deleting auth for specific error types (not network errors)
  • Different handling for network errors vs. invalid refresh tokens

Better approach:

} catch (error) {
  if (error instanceof AuthApiError && error.message.includes('invalid_grant')) {
    // Invalid refresh token - delete auth
    await deleteAuth();
  }
  // For other errors (network, etc.), log but don't delete auth
  console.error('Token refresh failed:', error);
  return null;
}

6. Missing Polling Error Handling (api.ts:76-77)

The code handles authorization_pending and slow_down, but according to OAuth 2.0 device flow spec, slow_down should increase the polling interval. Currently, it's treated the same as authorization_pending.

Recommendation:

if (error === "authorization_pending") {
  return null;
} else if (error === "slow_down") {
  // Should signal to increase polling interval
  throw new Error("SLOW_DOWN"); // Let caller handle interval increase
}

7. Token Type Not Validated (schema.ts:42)

The token_type is parsed but never validated. OAuth tokens should verify token_type === "Bearer" to ensure correct token usage.

token_type: z.string().refine(val => val.toLowerCase() === 'bearer', {
  message: "Only Bearer tokens are supported"
}),

Minor Issues

8. Inconsistent Error Messages (api.ts:68-69, 116-117)

Some errors use error.message, others use response.statusText. Standardize error message formatting.

9. Outdated Dependency Pattern

Using default export for httpClient and authClient is fine, but consider named exports for better tree-shaking and import consistency with the rest of the codebase.

10. Missing JSDoc for Public APIs

Functions like generateDeviceCode, getTokenFromDeviceCode, renewAccessToken should have JSDoc comments explaining parameters, return values, and thrown errors.

🧪 Test Coverage

Major Gap: No tests are included for the new authentication functionality.

Recommended test coverage:

  • Unit tests for generateDeviceCode() with mocked HTTP responses
  • Unit tests for getTokenFromDeviceCode() handling pending/slow_down/success/error states
  • Unit tests for renewAccessToken()
  • Unit tests for getUserInfo()
  • Integration tests for the token refresh mechanism
  • Tests for isTokenExpired() edge cases (exactly at boundary, etc.)
  • Tests for the retry lock mechanism in httpClient
  • Tests for concurrent refresh requests

🎯 Performance Considerations

Positive

  • ✅ Proactive token refresh prevents request failures
  • ✅ Refresh lock prevents unnecessary concurrent refresh requests
  • WeakSet for retry tracking has no memory leak issues

Potential Improvements

  • The beforeRequest hook runs for every request and reads the auth file from disk each time. Consider:
    • Caching auth data in memory with TTL
    • Only checking expiration, not reading file every time
    • Using a singleton pattern for auth state

📋 Additional Recommendations

  1. Add request timeout configuration - The ky instances don't specify timeouts. OAuth requests should have reasonable timeouts.

  2. Add retry logic for network errors - Currently only retries on 401. Consider retry for 5xx errors or network failures.

  3. Validate scope in token response - The received scope should be validated against requested scope.

  4. Export path mapping - The @core alias in imports suggests a path mapping configuration. Ensure it's properly configured in tsconfig.json and bundler config.

  5. Consider using ky.extend() instead of ky.create() - For the httpClient retry mechanism, extend() might be more appropriate.

  6. Package.json concern - The new dependency ky@1.14.2 is good, but ensure it's compatible with the Node.js version specified in package.json engines field.

🔐 Security Best Practices - Additional Notes

  • ✅ Tokens stored in ~/.base44/auth/auth.json - ensure this has restrictive permissions (0600)
  • ✅ No tokens in environment variables or logs
  • ✅ Client ID is hardcoded (not secret) - correct for public client
  • ⚠️ Consider adding token encryption at rest for additional security
  • ⚠️ Validate that auth.json is never committed to git (should be in .gitignore)

🎬 Verdict

Approval: Conditional - The implementation is solid overall with good architecture, but has critical issues with token refresh timing that should be fixed before merge.

Priority fixes before merge:

  1. Fix the token setting race condition in httpClient.ts (Issue chore(npm): change npm to and fix workflows #2)
  2. Remove outdated comment in login.ts (Issue Add repository field to package.json for npm provenance #4)
  3. Include statusText in getUserInfo error (Issue more everything to single repo #3)
  4. Add basic unit tests for critical auth flows

Post-merge improvements:

  • Add comprehensive test coverage
  • Improve error handling in token refresh
  • Add memory caching for auth data to reduce file I/O

Great work overall! The OAuth implementation is architecturally sound and follows security best practices. Once the token refresh timing issues are resolved, this will be a solid foundation for the CLI authentication system. 🚀

@claude
Copy link

claude bot commented Jan 11, 2026

Pull Request Review: feat(login): implement login flow with http

Summary

This PR implements the actual OAuth device code flow with HTTP calls, replacing the mock implementation. Overall, the implementation is solid and follows OAuth 2.0 device flow standards. However, there are several areas that need attention.


🔴 Critical Issues

1. Race Condition in Token Refresh (httpClient.ts:56-62)

Location: src/core/utils/httpClient.ts:56-62

The beforeRequest hook can create a race condition when multiple requests happen concurrently:

if (isTokenExpired(auth)) {
  const newAccessToken = await refreshAndSaveTokens();
  if (newAccessToken) {
    request.headers.set("Authorization", `Bearer ${newAccessToken}`);
    return;
  }
}

Problem: If two requests detect an expired token simultaneously, both will call refreshAndSaveTokens(). While the function has a lock, the first request's header will be set, but the second request might still use the old token from line 64.

Impact: Potential 401 errors on concurrent requests.

Recommendation: After proactive refresh completes, re-read auth to ensure consistency:

if (isTokenExpired(auth)) {
  await refreshAndSaveTokens();
  const refreshedAuth = await readAuth();
  request.headers.set("Authorization", `Bearer ${refreshedAuth.accessToken}`);
  return;
}

2. Silent Error Swallowing (httpClient.ts:65-67)

Location: src/core/utils/httpClient.ts:65-67

} catch {
  // No auth available, continue without header
}

Problem: This swallows ALL errors, not just "auth file not found". If there's a parsing error, disk I/O error, or validation error, they'll be silently ignored.

Impact: Debugging issues will be extremely difficult. Users won't know why requests are failing.

Recommendation:

} catch (error) {
  if (error instanceof Error && error.message.includes("Authentication file not found")) {
    // No auth available, continue without header
    return;
  }
  // Log or rethrow other errors
  throw error;
}

3. Error Handling in auth/api.ts (multiple locations)

Location: src/core/auth/api.ts:138-140

The getUserInfo function doesn't use throwHttpErrors: false like other functions:

const response = await authClient.get("oauth/userinfo", {
  headers: { Authorization: `Bearer ${accessToken}` },
});

Problem: If ky throws by default, this will throw a ky HTTPError instead of the AuthApiError you're checking for elsewhere. Inconsistent with the pattern used in other functions.

Recommendation: Add throwHttpErrors: false for consistency.


🟡 Code Quality Issues

4. Unused Import (auth/api.ts:15)

Location: src/core/auth/api.ts:15

import httpClient from "../utils/httpClient.js";

Issue: httpClient is imported but never used in this file. Only authClient is used.

Recommendation: Remove the unused import.

5. Outdated Comment (auth/login.ts:87)

Location: src/cli/commands/auth/login.ts:87

// For now, we store placeholder values until a /userinfo endpoint is available

Issue: The comment is outdated - the /userinfo endpoint IS now available and being used.

Recommendation: Remove the outdated comment or update it.

6. Typo in Success Message (auth/login.ts:111)

Location: src/cli/commands/auth/login.ts:111

log.success(`Successfully logged as ${chalk.bold(userInfo.email)}`);

Issue: Grammar error - should be "logged in as" not "logged as"

Recommendation: Fix to: Successfully logged in as ${chalk.bold(userInfo.email)}

7. Error Handling Changed Without Update (cli/utils/runCommand.ts:20-24)

Location: src/cli/utils/runCommand.ts:20-24

The error handling was simplified but now prints full stack traces:

if (e instanceof Error) {
  log.error(e.stack ?? e.message);
}

Issue: Stack traces can be overwhelming for users. The previous version had specialized handling for AuthValidationError which provided cleaner error messages.

Recommendation: Keep specialized error handling for known error types, only show stack traces for unexpected errors:

if (e instanceof AuthValidationError || e instanceof AuthApiError) {
  log.error(e.message);
} else if (e instanceof Error) {
  log.error(e.stack ?? e.message);
}

🟢 Security Considerations

8. Token Storage

Tokens are stored in a local file. Ensure the auth file has appropriate permissions (600) to prevent other users from reading it.

Recommendation: Verify in writeJsonFile that file permissions are set correctly.

9. No Token in Logs

Good job not logging tokens or sensitive data.

10. HTTPS Enforcement ⚠️

The code uses the OAuth URL from the server response directly without validating it's HTTPS.

Recommendation: Validate that verificationUriComplete uses HTTPS before displaying to user.


📊 Performance Considerations

11. Proactive Token Refresh

The 60-second buffer for token refresh is good practice.

12. Polling Interval

Using the server-provided interval value is correct per OAuth 2.0 device flow spec.


🧪 Test Coverage

13. Missing Tests

No tests were added for the new functionality.

Missing coverage:

  • Device code generation flow
  • Token polling logic
  • Token refresh logic
  • Error handling for various OAuth error codes
  • Race condition scenarios in httpClient
  • getUserInfo flow

Recommendation: Add integration tests for:

  • Complete login flow (can use mock server)
  • Token refresh scenarios
  • Concurrent request handling
  • Error states (expired device code, denied authorization, etc.)

Example test structure:

describe("OAuth Device Flow", () => {
  it("should handle authorization_pending correctly", async () => {
    // Mock server returns authorization_pending
    // Verify it returns null and retries
  });
  
  it("should refresh token when expired", async () => {
    // Test httpClient with expired token
  });
});

🏗️ Architecture & Best Practices

14. Separation of Concerns

Good separation between:

  • authClient (OAuth endpoints)
  • httpClient (authenticated API calls)

15. Schema Validation

Using Zod for runtime validation is excellent.

16. Lock Mechanism

The refreshPromise lock in config.ts prevents concurrent refresh requests - good implementation.


📝 Minor Improvements

17. Type Safety

The WeakSet<KyRequest> in httpClient is clever but could use a comment explaining why WeakSet (allows garbage collection of completed requests).

18. Magic Numbers

Consider extracting TOKEN_REFRESH_BUFFER_MS to a shared constants file if it needs to be configurable.


Summary

Approve with changes requested.

The OAuth implementation is well-structured and follows standards, but the race condition in token refresh and the error swallowing need to be addressed before merging. The missing tests are also a concern for such critical authentication functionality.

Priority fixes:

  1. Fix race condition in httpClient.ts beforeRequest hook
  2. Fix error swallowing - only catch expected errors
  3. Add throwHttpErrors: false to getUserInfo
  4. Add tests for the authentication flows

Nice-to-haves:

  • Fix typo in success message
  • Remove outdated comment
  • Improve error handling in runCommand
  • Validate HTTPS in OAuth URLs

@claude
Copy link

claude bot commented Jan 11, 2026

Pull Request Review: feat(login): implement login flow with http

Overview

This PR implements a real OAuth device code flow replacing the mock implementation. The changes add proper HTTP API integration using ky, token refresh mechanisms, and user info fetching. Overall, this is a solid implementation with good structure, but there are several important issues to address.


🔴 Critical Issues

1. Race Condition in Token Refresh (src/core/auth/config.ts:88-112)

The refreshAndSaveTokens() function has a subtle race condition vulnerability:

if (refreshPromise) {
  return refreshPromise;
}

refreshPromise = (async () => {
  // ... refresh logic
})();

Problem: There's a small window between checking refreshPromise and assigning it where multiple concurrent calls could create multiple refresh requests. The check and assignment are not atomic.

Solution: Consider using a proper mutex/lock pattern or ensure the assignment happens before any await points:

if (refreshPromise) {
  return refreshPromise;
}

const promise = (async () => {
  // ... logic
})();

refreshPromise = promise;
return promise;

2. Unsafe Request Mutation (src/core/utils/httpClient.ts:39-41)

retriedRequests.add(request);
request.headers.set("Authorization", `Bearer ${newAccessToken}`);
return ky(request);

Problem: Mutating the original KyRequest object could lead to unexpected behavior. The request object might be shared or reused elsewhere. According to ky documentation, you should create a new request or use ky's API properly.

Solution: Clone the request or use ky's options:

const options = { headers: { Authorization: `Bearer ${newAccessToken}` } };
return ky(request, options);

3. Missing Error Context in Login Flow (src/cli/commands/auth/login.ts:37-79)

The waitForAuthentication function silently swallows all errors:

} catch (error) {
  log.error("Authentication timeout or error");
  throw error;
}

Problem: Users won't know why authentication failed (network error, server error, timeout, etc.). The generic error message provides no actionable information.

Solution: Provide specific error messages based on error type and include relevant details.


⚠️ Important Issues

4. Potential Memory Leak (src/core/utils/httpClient.ts:11)

const retriedRequests = new WeakSet<KyRequest>();

Problem: While WeakSet helps, if requests are long-lived or referenced elsewhere, this could grow. Also, there's no cleanup mechanism if a request is aborted or fails.

Consideration: WeakSet is actually appropriate here since it allows garbage collection. However, document why this approach was chosen and consider edge cases where requests might be retained longer than expected.

5. Silent Auth Failures (src/core/utils/httpClient.ts:65-67)

} catch {
  // No auth available, continue without header
}

Problem: The beforeRequest hook silently catches ALL errors, including file system errors, corrupted auth data, or unexpected exceptions. This makes debugging difficult.

Solution: At minimum, log unexpected errors:

} catch (error) {
  // Expected: auth file doesn't exist (user not logged in)
  // Unexpected: file system errors, JSON parse errors, etc.
  if (error instanceof AuthNotFoundError) {
    // Expected, continue without header
  } else {
    console.error('Unexpected error reading auth:', error);
  }
}

6. Token Refresh During Initial Token Fetch (src/core/utils/httpClient.ts:56)

The beforeRequest hook calls refreshAndSaveTokens() even during login:

Problem: During the initial login flow, if getUserInfo() call happens while the token file is being written, you might trigger an unnecessary refresh or race condition.

Consideration: This might be okay depending on timing, but worth testing edge cases. Consider adding a flag to skip refresh during initial authentication.

7. Missing Validation in getUserInfo (src/core/auth/api.ts:137-154)

The getUserInfo function doesn't validate the access token before making the request.

Consideration: While the server will reject invalid tokens, client-side validation (checking if token is empty, expired format, etc.) could provide better error messages.


🟡 Code Quality & Best Practices

8. Inconsistent Error Handling (Multiple Files)

  • src/core/auth/api.ts: Good error handling with specific error types
  • src/cli/utils/runCommand.ts:20-26: Removed specific error type handling, now only catches generic Error
  • src/core/utils/httpClient.ts: Silent error catching

Recommendation: Establish consistent error handling patterns across the codebase. Consider keeping AuthValidationError and AuthApiError handling in runCommand.ts.

9. Removed Error Details (src/core/errors.ts:8-13)

// Before:
public readonly issues: Array<{ message: string; path: string[] }>

// After: (removed)

Problem: The AuthValidationError no longer includes detailed validation issues. This makes debugging schema validation failures much harder.

Recommendation: Keep the issues array or at least include the Zod error details in the error message for better debugging.

10. Magic Numbers (src/core/auth/config.ts:7)

const TOKEN_REFRESH_BUFFER_MS = 60 * 1000;

Good: This is well-documented. However, consider making it configurable via environment variable for testing purposes.

11. Package Version Simplification (src/cli/index.ts:7)

// Before: getPackageVersion()
// After: packageJson.version

Good: Nice simplification! The previous approach was over-engineered for a bundled app.

12. Trailing Newlines Inconsistency

  • Some files have trailing newlines removed (e.g., src/core/resources/entity/config.ts:36)
  • Some keep them

Recommendation: Configure and enforce this in .editorconfig or ESLint to maintain consistency.


🔒 Security Concerns

13. Token Storage Security

The auth tokens are stored in ~/.base44/auth.json (based on getAuthFilePath()).

Current State: No information about file permissions in this PR.

Recommendation: Ensure the auth file has restricted permissions (0600) to prevent other users on the system from reading tokens. Add this check in writeAuth():

await fs.chmod(authFilePath, 0o600);

14. Environment Variable for API URL (src/core/consts.ts:26-30)

export function getBase44ApiUrl(): string {
  return process.env.BASE44_API_URL || DEFAULT_API_URL;
}

Security Consideration: While useful for testing, this allows users to redirect all API calls to arbitrary servers. Consider:

  • Documenting this is for development only
  • Adding validation that the URL uses HTTPS in production
  • Warning users if a custom API URL is being used

15. No Rate Limiting on Token Refresh

The refreshAndSaveTokens() function can be called repeatedly if the lock fails or in edge cases.

Recommendation: Add rate limiting or exponential backoff to prevent abuse or accidental DOS of the auth server.


📊 Performance Considerations

16. Proactive Token Refresh (src/core/utils/httpClient.ts:56-62)

if (isTokenExpired(auth)) {
  const newAccessToken = await refreshAndSaveTokens();
  // ...
}

Good: Proactive refresh prevents requests from failing due to expired tokens.

Consideration: This blocks every single HTTP request to check token expiration. For performance:

  • Cache the expiresAt value in memory
  • Only check file when approaching expiration
  • Or use a timer-based refresh instead of checking on every request

17. Polling Interval (src/cli/commands/auth/login.ts:59)

interval: interval * 1000,

Good: Now respects the server's requested polling interval instead of hardcoding 2 seconds. This reduces unnecessary API calls and respects server rate limits.


🧪 Test Coverage

No Tests Added

This PR adds significant new functionality but includes zero tests:

Missing Test Coverage:

  1. generateDeviceCode() - API call, schema validation
  2. getTokenFromDeviceCode() - OAuth error handling, polling states
  3. renewAccessToken() - Token refresh flow
  4. getUserInfo() - User info fetching
  5. refreshAndSaveTokens() - Race condition prevention, error handling
  6. Token expiration checking (isTokenExpired())
  7. HTTP client hooks (beforeRequest, afterResponse)
  8. Schema transformations (snake_case → camelCase)

Recommendation: Add comprehensive tests covering:

  • Happy path scenarios
  • Error scenarios (network errors, invalid responses, 401s)
  • Race conditions in token refresh
  • Schema validation failures
  • OAuth error codes (authorization_pending, slow_down, etc.)

Example test structure:

describe('Token Refresh', () => {
  it('prevents concurrent refresh requests', async () => {
    // Test the race condition fix
  });
  
  it('handles refresh failure by deleting auth', async () => {
    // Test cleanup on failure
  });
});

✅ What's Good

  1. Clean OAuth Implementation: Proper device code flow with all standard OAuth parameters
  2. Token Refresh Logic: Good attempt at preventing concurrent refreshes with the promise lock
  3. Schema Validation: Using Zod with proper transformations (snake_case to camelCase)
  4. Separation of Concerns: authClient for OAuth endpoints vs httpClient for authenticated APIs
  5. User Experience: Better UX with verificationUriComplete and clearer user code display
  6. Proactive Token Refresh: 60-second buffer prevents most 401 errors
  7. Code Cleanup: Removed unnecessary packageVersion.ts utility

📋 Recommendations Summary

Before Merging:

  1. ✅ Fix the race condition in refreshAndSaveTokens()
  2. ✅ Fix unsafe request mutation in handleUnauthorized()
  3. ✅ Improve error messages in login flow
  4. ✅ Add proper error handling/logging for auth failures
  5. ✅ Set file permissions on auth file
  6. ✅ Add test coverage (at least basic happy path tests)

Nice to Have:
7. Restore detailed error context in AuthValidationError
8. Add rate limiting to token refresh
9. Optimize token expiration checks to avoid blocking every request
10. Add HTTPS validation for custom API URLs
11. Document the WeakSet usage rationale
12. Add environment variable for TOKEN_REFRESH_BUFFER_MS


Conclusion

This PR represents a significant improvement, moving from mocks to real OAuth implementation. The architecture is sound with good separation of concerns. However, the critical issues around race conditions and request mutation must be fixed before merging, and the complete lack of tests is concerning for authentication code. Once these are addressed, this will be a solid foundation for the CLI's authentication system.

Status: ⚠️ Needs Changes - Address critical issues and add test coverage before merging.

@claude
Copy link

claude bot commented Jan 11, 2026

Pull Request Review: Login Flow Implementation

Overview

This PR implements the actual OAuth 2.0 device flow for authentication, replacing the previous mock implementation. Overall, the implementation is well-structured and follows OAuth 2.0 standards. Below are my findings organized by category.


✅ Code Quality & Best Practices

Strengths

  • Clean separation of concerns: The code is well-organized with separate modules for API calls (api.ts), configuration (config.ts), HTTP client (httpClient.ts), and schemas (schema.ts)
  • Proper error handling: Custom error types (AuthApiError, AuthValidationError) are used consistently
  • Good use of Zod schemas: All API responses are validated with proper error messages
  • Token refresh with concurrency control: The refreshAndSaveTokens() function uses a promise lock to prevent race conditions (lines 88-117 in config.ts)
  • Proactive token refresh: The HTTP client checks token expiration before requests and refreshes proactively with a 60-second buffer

Issues & Suggestions

1. Circular dependency risk ⚠️
src/core/utils/httpClient.ts imports from src/core/auth/config.ts, which imports from src/core/auth/api.ts, which imports authClient.ts. While not currently circular, this creates tight coupling. Consider:

  • Making httpClient and authClient separate concerns
  • Using dependency injection for token refresh logic

2. Missing error context in api.ts 🔧
The getUserInfo() function at line 137-142 doesn't use throwHttpErrors: false like other functions. If ky throws an HTTP error, it won't be caught properly. Should match the pattern used in other API functions:

const response = await authClient.get("oauth/userinfo", {
  headers: { Authorization: `Bearer ${accessToken}` },
  throwHttpErrors: false,  // Add this
});

3. Silent error swallowing in httpClient 🔧
src/core/utils/httpClient.ts:66 - The beforeRequest hook silently catches all errors. If readAuth() fails for reasons other than "not logged in", the error is hidden. Consider logging or differentiating between expected and unexpected errors.

4. Unused variable 🧹
src/core/utils/httpClient.ts:19 - The _options parameter is prefixed with underscore but this isn't using TypeScript's noUnusedLocals. This is minor but worth noting.

5. Error handling simplification removed 📝
src/cli/utils/runCommand.ts - The error handling was simplified, but now shows full stack traces (e.stack ?? e.message). This might be too verbose for end users. Consider keeping user-friendly error messages for known error types.


🐛 Potential Bugs

1. WeakSet memory leak potential ⚠️
src/core/utils/httpClient.ts:11 - Using WeakSet<KyRequest> to track retried requests. However, if the same request object is reused (which shouldn't happen but isn't guaranteed), this could cause issues. Consider adding a timeout-based cleanup or using a different mechanism.

2. Token expiration check edge case 🔧
src/core/auth/config.ts:79-80 - The isTokenExpired() function uses >= which means a token that expires in exactly 60 seconds is considered expired. This is fine, but the comment says "about to expire" which might be misleading.

3. Double refresh possibility ⚠️
In httpClient.ts, both beforeRequest and afterResponse can trigger token refresh:

  • beforeRequest (lines 56-62) refreshes if token is expired
  • afterResponse (lines 17-42) refreshes on 401

If a token expires between the beforeRequest check and the actual request, you could get two refresh attempts. The lock in refreshAndSaveTokens() prevents corruption, but one refresh will fail unnecessarily.

4. Auth file deletion on refresh failure ⚠️
src/core/auth/config.ts:108-109 - When token refresh fails, the auth file is deleted immediately. This means if there's a temporary network issue, the user is logged out. Consider:

  • Retrying the refresh
  • Only deleting auth on specific error types (e.g., invalid_grant)
  • Logging the error before deletion

⚡ Performance Considerations

1. No request timeout configuration 📝
The ky clients don't specify timeouts. For auth flows, consider adding reasonable timeouts (e.g., 30 seconds) to prevent hanging requests.

2. Polling interval optimization
Good: The implementation correctly uses the interval from the device code response (line 59 in login.ts). This respects the server's rate limiting.

3. Unnecessary promise overhead 🔧
src/cli/commands/auth/login.ts:22 - Using async () => { return await generateDeviceCode(); } is redundant. Can simplify to generateDeviceCode:

const deviceCodeResponse = await runTask(
  "Generating device code...",
  generateDeviceCode,  // Direct reference
  {
    successMessage: "Device code generated",
    errorMessage: "Failed to generate device code",
  }
);

🔒 Security Concerns

1. Client ID in source code ⚠️
src/core/consts.ts:24 - The AUTH_CLIENT_ID is hardcoded as "base44_cli". For public clients this is acceptable per OAuth 2.0 spec, but ensure:

  • The backend properly validates this is a public client (no client secret required)
  • The client ID has appropriate scope restrictions on the server side

2. Token storage
Tokens are stored in ~/.base44/auth/auth.json. This is reasonable for a CLI tool. Ensure:

  • The directory/file has appropriate permissions (only readable by the user)
  • Consider documenting that users should not share this file

3. No token type validation 📝
The code doesn't validate that token_type is "Bearer" (line 43 in schema.ts). While you store it, you always assume Bearer tokens. Consider validating:

token_type: z.literal("Bearer").or(z.literal("bearer")),

4. Environment variable for API URL ⚠️
src/core/consts.ts:29 - Using BASE44_API_URL env variable is good for testing, but document this clearly and warn about security implications of pointing to untrusted endpoints.


🧪 Test Coverage

Critical Issue: No tests for authentication flow

The PR adds significant authentication logic but includes no tests. I found only ./tests/core/project.test.ts in the codebase. This is a significant gap.

Recommended test coverage:

  1. Unit tests for api.ts:

    • Mock HTTP responses and test schema validation
    • Test error handling for various OAuth error codes
    • Test polling behavior (authorization_pending, slow_down)
  2. Unit tests for config.ts:

    • Test token expiration logic
    • Test concurrent refresh prevention (the promise lock)
    • Test auth file read/write/delete
  3. Integration tests for httpClient.ts:

    • Test automatic token refresh on 401
    • Test retry prevention
    • Test header injection
  4. Unit tests for login.ts:

    • Test the full login flow with mocked API calls
    • Test timeout handling
    • Test error scenarios

📚 Additional Recommendations

1. Add JSDoc comments 📝
Functions like generateDeviceCode(), getTokenFromDeviceCode(), etc., would benefit from JSDoc describing their parameters, return values, and error conditions.

2. Consider rate limiting 💡
The device code polling could benefit from implementing the slow_down response (OAuth 2.0 device flow extension). Currently, slow_down is treated the same as authorization_pending (line 75 in api.ts). The spec says you should increase the polling interval.

3. Add logging 📝
For debugging purposes, consider adding optional debug logging (controlled by env var) for:

  • Token refresh events
  • API calls
  • Auth state changes

4. Package version retrieval
Good improvement replacing the runtime readFileSync with import of package.json at line 7 in cli/index.ts. This is cleaner and works better with bundlers.

5. Consider refresh token rotation 📝
Check if the backend supports refresh token rotation (returning a new refresh token with each refresh). If so, ensure the current implementation (line 102 in config.ts) handles this correctly - it does appear to update the refresh token.

6. Export missing types 🔧
src/core/utils/index.ts is missing the export for httpClient:

export { default as httpClient } from "./httpClient.js";

Summary

Category Status
Code Quality ✅ Good with minor issues
Potential Bugs ⚠️ 4 issues requiring attention
Performance ✅ Generally good
Security ⚠️ 2 concerns to address
Test Coverage ❌ Critical gap - no tests

Priority Actions:

  1. Add tests - This is critical for authentication code
  2. Fix the getUserInfo error handling - Add throwHttpErrors: false
  3. Review auth deletion on refresh failure - Too aggressive for temporary network issues
  4. Document security considerations - File permissions, environment variables

The implementation demonstrates good understanding of OAuth 2.0 device flow and includes thoughtful features like token refresh with race condition prevention. With tests and the above fixes, this will be solid production code.


Great work overall! The architecture is clean and extensible. 🎉

@kfirstri kfirstri merged commit a72dc72 into main Jan 11, 2026
5 checks passed
@kfirstri kfirstri deleted the login-flow branch January 11, 2026 14:02
@github-project-automation github-project-automation bot moved this from Backlog to Done in Dev Jan 11, 2026
kfirstri added a commit that referenced this pull request Jan 11, 2026
* fix issue with error throwing, add refresh mechanism

* added refresh logic fixes

* fix package version issue and add a userInfo method

* actual call the getUserInfo service

* added userInfo call and log

* small ui changes

* get user info with access token

* tiny changes

* fix issue with correct way to use ky
kfirstri added a commit that referenced this pull request Jan 11, 2026
* fix issue with error throwing, add refresh mechanism

* added refresh logic fixes

* fix package version issue and add a userInfo method

* actual call the getUserInfo service

* added userInfo call and log

* small ui changes

* get user info with access token

* tiny changes

* fix issue with correct way to use ky
kfirstri added a commit that referenced this pull request Jan 11, 2026
* feat(login): implement login flow with http (#10)

* fix issue with error throwing, add refresh mechanism

* added refresh logic fixes

* fix package version issue and add a userInfo method

* actual call the getUserInfo service

* added userInfo call and log

* small ui changes

* get user info with access token

* tiny changes

* fix issue with correct way to use ky

* move the client init into the function

* dont lazy load the appclient

* performance fix for entity api
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

2 participants