Skip to content

Add Clerk authentication with Angular login component and Electron IPC token storage#635

Draft
Copilot wants to merge 13 commits intomasterfrom
copilot/create-angular-login-component
Draft

Add Clerk authentication with Angular login component and Electron IPC token storage#635
Copilot wants to merge 13 commits intomasterfrom
copilot/create-angular-login-component

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Mar 31, 2026

  • Add SET_AUTH_TOKEN event constant to gui-js/libs/shared/src/lib/constants/constants.ts
  • Create gui-js/libs/ui-components/src/lib/login/login.component.ts
  • Create gui-js/libs/ui-components/src/lib/login/login.component.html
  • Create gui-js/libs/ui-components/src/lib/login/login.component.scss
  • Create gui-js/libs/core/src/lib/services/clerk/clerk.service.ts
  • Update gui-js/apps/minsky-electron/src/app/events/electron.events.ts to handle JWT token and export openLoginWindow(): Promise<string | null>
  • Export login component and clerk service from index files
  • Add /headless/login route (consistent with other popup windows), Clerk config to environment, @clerk/clerk-js dependency
  • Fix login window not rendering: use createPopupWindowWithRouting (which prepends rendererAppURL) instead of bare loadURL
  • Clean up dead-commented code in clerk.service.ts
  • Export openLoginWindow(): Promise<string | null> from electron.events.ts — opens the #/headless/login popup and resolves with the JWT token once SET_AUTH_TOKEN fires, or null if the window is closed before authentication completes
Original prompt

Create Angular Login Component for Clerk with Electron Integration

Context

The minsky application is an Angular + Electron application located in the gui-js directory. It uses:

  • Angular for the frontend (located in gui-js/apps/minsky-web and gui-js/apps/minsky-electron)
  • Electron for the desktop backend
  • IPC (Inter-Process Communication) for frontend-backend communication via ipcRenderer and ipcMain
  • Standalone Angular components (using the new Angular standalone API)

Objectives

Create a complete Clerk authentication integration with the following components:

1. Angular Login Component

Create a new standalone Angular component at gui-js/libs/ui-components/src/lib/login/login.component.ts that:

  • Implements Clerk authentication using @clerk/clerk-js
  • Provides a login UI with email/password or OAuth options
  • Handles the authentication flow
  • Extracts the JWT token from Clerk after successful authentication
  • Sends the JWT token to the Electron backend via IPC
  • Shows loading states and error messages
  • Follows the existing component pattern in the codebase (standalone components with Material UI)

2. Clerk Service

Create an Angular service at gui-js/libs/core/src/lib/services/clerk/clerk.service.ts that:

  • Initializes Clerk with publishable key from environment configuration
  • Manages authentication state
  • Provides methods to get the JWT token
  • Handles token refresh
  • Provides observable streams for auth state changes
  • Integrates with the existing ElectronService to send tokens via IPC

3. Electron Backend Integration

Update the Electron backend to:

  • Add a new IPC event handler in gui-js/apps/minsky-electron/src/app/events/electron.events.ts to receive JWT tokens
  • Store the JWT token securely (using the existing StoreManager)
  • Add a new event constant in the shared events file
  • Provide methods to retrieve and validate the stored token

4. Environment Configuration

Update environment files to include Clerk configuration:

  • Add Clerk publishable key to gui-js/apps/minsky-web/src/environments/environment.web.ts
  • Add a production environment file if needed

5. Package Dependencies

Update gui-js/package.json to include:

  • @clerk/clerk-js for Clerk SDK
  • Any necessary peer dependencies

6. Export and Integration

  • Export the new login component from gui-js/libs/ui-components/src/index.ts
  • Export the new Clerk service from gui-js/libs/core/src/index.ts
  • Add routing configuration example (can be added to gui-js/apps/minsky-web/src/app/app-routing.module.ts)

Technical Requirements

IPC Communication Pattern

Follow the existing IPC pattern:

// Frontend (Angular)
const token = await this.clerkService.getToken();
await this.electronService.invoke(events.SET_AUTH_TOKEN, token);

// Backend (Electron)
ipcMain.handle(events.SET_AUTH_TOKEN, async (event, token: string) => {
  StoreManager.store.set('authToken', token);
  return { success: true };
});

Component Structure

Follow the existing standalone component pattern:

@Component({
  selector: 'minsky-login',
  templateUrl: './login.component.html',
  styleUrls: ['./login.component.scss'],
  standalone: true,
  imports: [
    CommonModule,
    ReactiveFormsModule,
    MatButtonModule,
    MatInputModule,
    MatProgressSpinnerModule,
    // ... other Material imports
  ],
})
export class LoginComponent implements OnInit { }

Service Pattern

Follow the existing service pattern with dependency injection:

@Injectable({
  providedIn: 'root',
})
export class ClerkService {
  constructor(private electronService: ElectronService) { }
}

Files to Create

  1. gui-js/libs/ui-components/src/lib/login/login.component.ts
  2. gui-js/libs/ui-components/src/lib/login/login.component.html
  3. gui-js/libs/ui-components/src/lib/login/login.component.scss
  4. gui-js/libs/core/src/lib/services/clerk/clerk.service.ts
  5. gui-js/libs/shared/src/lib/events.ts (update to add new event constant)

Files to Modify

  1. gui-js/apps/minsky-electron/src/app/events/electron.events.ts - Add JWT token handler
  2. gui-js/libs/ui-components/src/index.ts - Export login component
  3. gui-js/libs/core/src/index.ts - Export Clerk service
  4. gui-js/apps/minsky-web/src/environments/environment.web.ts - Add Clerk config
  5. gui-js/package.json - Add Clerk dependency
  6. gui-js/apps/minsky-web/src/app/app-routing.module.ts - Add login route (optional, for demonstration)

Implementation Notes

  • Use Angular Material components for UI consistency
  • Handle both Electron and web environments (check electronService.isElectron)
  • Implement proper error handling and user feedback
  • Follow TypeScript best practices with proper typing
  • Use async/await for asynchronous operations
  • Ensure the token is refreshed before expiration
  • Store the token securely in the Electron store (never in localStorage in the renderer)

Security Considera...

This pull request was created from Copilot chat.


This change is Reviewable

Summary by CodeRabbit

Release Notes

  • New Features
    • Added Clerk-based authentication system with email/password sign-in
    • Added login interface with sign-out capability
    • Added "Upgrade via Clerk" menu option to access authenticated upgrades
    • Added "Logout Clerk" menu option for session management
    • Integrated authentication token storage for seamless upgrade workflows

… Electron IPC support

Agent-Logs-Url: https://github.com/highperformancecoder/minsky/sessions/dcea3c3b-13de-494d-88b9-bdc278391926

Co-authored-by: highperformancecoder <3075825+highperformancecoder@users.noreply.github.com>
Copilot AI changed the title [WIP] Add Angular login component for Clerk authentication Add Clerk authentication with Angular login component and Electron IPC token storage Mar 31, 2026
@highperformancecoder
Copy link
Copy Markdown
Owner

@copilot I have added a menu item to call the /login route, but the window doesn't render, and the developer tools window doesn't attach, so I can't debug why it is stalling.

Agent-Logs-Url: https://github.com/highperformancecoder/minsky/sessions/ac00893a-9811-4d4f-8ef1-9b889eb6d5cc

Co-authored-by: highperformancecoder <3075825+highperformancecoder@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Mar 31, 2026

@copilot I have added a menu item to call the /login route, but the window doesn't render, and the developer tools window doesn't attach, so I can't debug why it is stalling.

The window wasn't rendering because the menu item was calling window.loadURL('#/login') directly, which just passes the bare fragment #/login as a URL — the Angular renderer app was never loaded into that window.

Fixed in commit 9d1d261 by replacing the createWindow + loadURL pair with WindowManager.createPopupWindowWithRouting, which correctly prepends the renderer app URL (e.g. http://localhost:4200 in dev, or the packaged file:// path in production) before navigating to the #/login route.

@highperformancecoder
Copy link
Copy Markdown
Owner

@copilot The login window is including and

which it shouldn't. I can't see these things in the login.component.html file, so where are they coming from?

Copy link
Copy Markdown
Contributor Author

Copilot AI commented Mar 31, 2026

@copilot The login window is including and

which it shouldn't. I can't see these things in the login.component.html file, so where are they coming from?

Your comment appears to have been cut off — it ends with "The login window is including". Could you complete the description of what you're seeing so I can investigate?

@highperformancecoder
Copy link
Copy Markdown
Owner

@copilot The login window is including the minsky-header tag and the div with class example-button-row, that its shouldn't. I can't see this in login.component.html, so have no idea where they are coming from.

…oute from main layout

Agent-Logs-Url: https://github.com/highperformancecoder/minsky/sessions/ce140fa8-df43-43d0-832d-90cac7a292b8

Co-authored-by: highperformancecoder <3075825+highperformancecoder@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Mar 31, 2026

@copilot The login window is including the minsky-header tag and the div with class example-button-row, that its shouldn't. I can't see this in login.component.html, so have no idea where they are co...

The minsky-header and example-button-row were appearing because app.component.html only bypasses the app shell for routes containing 'headless' — the /login route fell into the else branch and got wrapped in the full chrome.

Fixed in commit 62a9ead by adding || router.url.includes('login') to that condition, so the login window renders only its own content (just like the headless popup windows).

@highperformancecoder
Copy link
Copy Markdown
Owner

@copilot - why not just use /headless like the other windows? Seems simpler

…indows

Agent-Logs-Url: https://github.com/highperformancecoder/minsky/sessions/0bde9861-8dca-48e7-965d-c608f6258171

Co-authored-by: highperformancecoder <3075825+highperformancecoder@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Mar 31, 2026

@copilot - why not just use /headless like the other windows? Seems simpler

Done in commit f9b417d. Moved the route to headless/login, updated the menu item URL to #/headless/login, and reverted the || router.url.includes('login') special-case from app.component.html — the existing headless check handles it now.

@highperformancecoder
Copy link
Copy Markdown
Owner

@copilot Thanks - looks good. What I need now is to add a promise that I can wait on - so call /headless/login and have a promise I can await on that is resolved when the token is set in the StoreManager.

Agent-Logs-Url: https://github.com/highperformancecoder/minsky/sessions/f860580e-1854-46ba-96ed-117e3d0cfdce

Co-authored-by: highperformancecoder <3075825+highperformancecoder@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Mar 31, 2026

@copilot Thanks - looks good. What I need now is to add a promise that I can wait on - so call /headless/login and have a promise I can await on that is resolved when the token is set in the StoreMana...

Done in commit 6f7259d. Added openLoginWindow(): Promise<string | null> exported from electron.events.ts. It opens the #/headless/login popup and resolves with the JWT token once SET_AUTH_TOKEN fires from the renderer, or with null if the window is closed before authentication completes. Usage:

import { openLoginWindow } from '../events/electron.events';

const token = await openLoginWindow();
if (token) { /* proceed with authenticated request */ }

The ApplicationMenuManager menu item now awaits openLoginWindow() as well.

Comment on lines +66 to +72
const response = await fetch(initialUrl, {
method: 'GET',
headers: {
'Authorization': `Bearer ${token}`
},
redirect: 'manual' // This tells fetch NOT to follow the link automatically
});

Check failure

Code scanning / CodeQL

Server-side request forgery Critical

The
URL
of this request depends on a
user-provided value
.

Copilot Autofix

AI 1 day ago

In general, to fix this problem you should ensure that any URL used in fetch or downloadURL is restricted to a safe subset, typically by validating it against an allow‑list of hostnames and schemes and by rejecting or normalizing any unexpected value. Instead of blindly trusting the download_url field from the backend, parse it with URL, check that it uses https: and points to an expected host (or set of hosts), and only then pass it to fetch/downloadURL. If validation fails, abort the operation or show an error.

The best targeted fix here is to add a small helper, e.g. sanitizeAssetUrl, that takes the tainted URL (like minskyAsset or ravelAsset), parses it, enforces https: and a specific host or host suffix, and returns either a safe normalized string or null. We then: (1) call this helper on minskyAsset and ravelAsset immediately after retrieving them in upgradeUsingClerk, and (2) modify getFinalUrl so it receives only already-sanitized URLs, or at least re-validates initialUrl before using it in fetch. Concretely:

  • In CommandsManager.ts, define const allowedDownloadHost = '...'; (or a small allow‑list) and a sanitizeAssetUrl(assetUrl: string | undefined): string | null function near the other helpers (e.g., after backendAPI or before getFinalUrl).
  • In upgradeUsingClerk, after getRelease(...), pass minskyAsset and ravelAsset through sanitizeAssetUrl. If both sanitize to null, show the “Everything’s up to date” message as before. If one sanitizes to null but not the other, proceed only with the safe one, or show an error dialog explaining that the download URL is invalid.
  • Update calls to getFinalUrl to pass the sanitized URLs and the token correctly. Also fix the inconsistent call getFinalUrl(ravelAsset) (missing token).
  • Within getFinalUrl, perform a quick re-check of initialUrl (again parse with URL and ensure allowed protocol/hostname) before calling fetch, to ensure it is never used with an unsafe target even if another caller reuses it in the future.

This preserves existing functionality (still uses backend-provided URLs) but adds a strict guardrail so that only URLs to the intended domain and scheme are ever used for SSRF‑capable operations.

Suggested changeset 1
gui-js/apps/minsky-electron/src/app/managers/CommandsManager.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/gui-js/apps/minsky-electron/src/app/managers/CommandsManager.ts b/gui-js/apps/minsky-electron/src/app/managers/CommandsManager.ts
--- a/gui-js/apps/minsky-electron/src/app/managers/CommandsManager.ts
+++ b/gui-js/apps/minsky-electron/src/app/managers/CommandsManager.ts
@@ -43,6 +43,22 @@
 }
 
 const backendAPI='https://minskybe-x7dj1.sevalla.app/api';
+const allowedDownloadHost='minskybe-x7dj1.sevalla.app';
+
+// Ensure that asset download URLs are restricted to expected schemes/hosts.
+function sanitizeAssetUrl(assetUrl: string | undefined | null): string | null {
+  if (!assetUrl) return null;
+  try {
+    const parsed = new URL(assetUrl);
+    // Only allow HTTPS downloads from the expected host (or subdomains, if desired).
+    if (parsed.protocol !== 'https:') return null;
+    if (parsed.hostname !== allowedDownloadHost) return null;
+    return parsed.toString();
+  } catch {
+    return null;
+  }
+}
+
 // perform a call on the backend API, returning the JSON encoded result
 // options is passed to the constructor of a ClienRequest object https://www.electronjs.org/docs/latest/api/client-request#requestendchunk-encoding-callback
 async function callBackendAPI(options: string|Object, token: string) {
@@ -61,9 +77,14 @@
 }
 
 // to handle redirects
-async function getFinalUrl(initialUrl, token) {
-try {
-    const response = await fetch(initialUrl, {
+async function getFinalUrl(initialUrl: string, token: string) {
+  // Re-validate the URL before using it in a network request.
+  const safeUrl = sanitizeAssetUrl(initialUrl);
+  if (!safeUrl) {
+    throw new Error('Invalid or unsafe download URL');
+  }
+  try {
+    const response = await fetch(safeUrl, {
       method: 'GET',
       headers: {
         'Authorization': `Bearer ${token}`
@@ -74,10 +95,16 @@
     // In 'manual' mode, a redirect returns an 'opaqueredirect' type or status 302
     if (response.status >= 300 && response.status < 400) {
       const redirectUrl = response.headers.get('location');
-      if (redirectUrl) return redirectUrl;
+      if (redirectUrl) {
+        const safeRedirect = sanitizeAssetUrl(redirectUrl);
+        if (!safeRedirect) {
+          throw new Error('Invalid or unsafe redirect URL');
+        }
+        return safeRedirect;
+      }
     }
 
-    if (response.ok) return initialUrl;
+    if (response.ok) return safeUrl;
     
     throw new Error(`Server responded with ${response.status}`);
   } catch (error) {
@@ -1494,21 +1519,24 @@
     let token=StoreManager.store.get('authToken');
 
     const window=WindowManager.getMainWindow();
-    let minskyAsset;
+    let minskyAssetRaw;
     if (installCase===InstallCase.theLot)
-      minskyAsset=await CommandsManager.getRelease('minsky', false, token);
-    let ravelAsset=await CommandsManager.getRelease('ravel', installCase===InstallCase.previousRavel, token);
+      minskyAssetRaw=await CommandsManager.getRelease('minsky', false, token);
+    let ravelAssetRaw=await CommandsManager.getRelease('ravel', installCase===InstallCase.previousRavel, token);
 
+    const minskyAsset = sanitizeAssetUrl(minskyAssetRaw);
+    const ravelAsset = sanitizeAssetUrl(ravelAssetRaw);
+
     if (minskyAsset) {
       if (ravelAsset) { // stash ravel upgrade to be installed on next startup
-        StoreManager.store.set('ravelPlugin',await getFinalUrl(ravelAsset));
+        StoreManager.store.set('ravelPlugin', await getFinalUrl(ravelAsset, token));
       }
       window.webContents.session.on('will-download',this.downloadMinsky);
-      window.webContents.downloadURL(await getFinalUrl(minskyAsset,token));
+      window.webContents.downloadURL(await getFinalUrl(minskyAsset, token));
       return;
     } else if (ravelAsset) {
       window.webContents.session.on('will-download',this.downloadRavel);
-      window.webContents.downloadURL(await getFinalUrl(ravelAsset,token));
+      window.webContents.downloadURL(await getFinalUrl(ravelAsset, token));
       return;
     }
     dialog.showMessageBoxSync(WindowManager.getMainWindow(),{
EOF
Copilot is powered by AI and may make mistakes. Always verify output.
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

OK - the security issue is less severe than made out, as the backend server is trusted. But we can sanitise on delivered names - need to figure out what they are - and prefer to do this in the upgrade* methods, rather than the generic finalURL method.

return release?.release?.download_url;
}
catch (error) {
console.error(error);

Check warning

Code scanning / CodeQL

Log injection Medium

Log entry depends on a
user-provided value
.
Log entry depends on a
user-provided value
.

Copilot Autofix

AI 1 day ago

In general, to fix log injection issues, any value that can contain untrusted content (especially from network or user-controlled sources) should be sanitized before being written to logs. For plain-text logs, this typically means removing or replacing newline (\n) and carriage return (\r) characters so that a single log statement cannot be split into multiple log lines. It also helps to add a clear prefix or context indicating where the untrusted data is.

For this specific case, the best fix without changing existing functionality is to sanitize the error value before passing it to console.error in CommandsManager.getRelease. We should convert error to a string in a controlled way, strip \r and \n, and then log a message that includes that sanitized string. This ensures that regardless of whether the caught value is an Error, a string containing newlines, or something else, the logged line will not be able to inject extra lines. Since we already use standard JavaScript/TypeScript features, we can implement sanitization inline with String.prototype.replace, without introducing new dependencies.

Concretely, in gui-js/apps/minsky-electron/src/app/managers/CommandsManager.ts, around lines 1485–1487, replace console.error(error); with code that builds a safe string: e.g., const msg = String(error).replace(/[\r\n]/g, ' '); console.error('getRelease error:', msg);. No new imports or helper functions are strictly necessary.

Suggested changeset 1
gui-js/apps/minsky-electron/src/app/managers/CommandsManager.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/gui-js/apps/minsky-electron/src/app/managers/CommandsManager.ts b/gui-js/apps/minsky-electron/src/app/managers/CommandsManager.ts
--- a/gui-js/apps/minsky-electron/src/app/managers/CommandsManager.ts
+++ b/gui-js/apps/minsky-electron/src/app/managers/CommandsManager.ts
@@ -1483,7 +1483,8 @@
       return release?.release?.download_url;
     }
     catch (error) {
-      console.error(error);
+      const safeErrorMessage = String(error).replace(/[\r\n]/g, ' ');
+      console.error('getRelease error:', safeErrorMessage);
       return "";
     }
   }
EOF
@@ -1483,7 +1483,8 @@
return release?.release?.download_url;
}
catch (error) {
console.error(error);
const safeErrorMessage = String(error).replace(/[\r\n]/g, ' ');
console.error('getRelease error:', safeErrorMessage);
return "";
}
}
Copilot is powered by AI and may make mistakes. Always verify output.
@highperformancecoder
Copy link
Copy Markdown
Owner

@CodeRabbit review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 2, 2026

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

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 2, 2026

📝 Walkthrough

Walkthrough

The changes introduce a Clerk-based authentication system integrated with Electron, featuring a login component, IPC handlers for auth token persistence, backend API communication for upgrades, and menu options for user authentication and upgrade workflows.

Changes

Cohort / File(s) Summary
Authentication Infrastructure
gui-js/libs/core/src/lib/services/clerk/clerk.service.ts, gui-js/libs/core/src/index.ts, gui-js/libs/shared/src/lib/constants/constants.ts, gui-js/apps/minsky-web/src/environments/environment*
Added ClerkService with initialization, sign-in, sign-out, and token management; exported new Clerk service; added SET_AUTH_TOKEN event constant; configured Clerk publishable key in environment files.
Login UI Component
gui-js/libs/ui-components/src/lib/login/login.component*, gui-js/libs/ui-components/src/index.ts
Created new standalone LoginComponent with reactive form, authentication state management, error handling, and responsive UI template with Material spinners; exported from module index.
Electron Integration
gui-js/apps/minsky-electron/src/app/events/electron.events.ts, gui-js/apps/minsky-electron/src/app/managers/WindowManager.ts, gui-js/apps/minsky-electron/src/app/managers/StoreManager.ts
Added IPC handler for SET_AUTH_TOKEN to persist auth tokens; introduced openLoginWindow() method with Promise-based auth token resolution; added optional authToken field to MinskyStore.
Application Features
gui-js/apps/minsky-electron/src/app/managers/ApplicationMenuManager.ts, gui-js/apps/minsky-electron/src/app/managers/CommandsManager.ts
Added menu items for "Upgrade via Clerk" and "Logout Clerk"; refactored upgrade() to use new buildState() method; introduced getRelease() for backend-driven asset resolution and upgradeUsingClerk() with semver-based release selection and redirect handling.
Routing & Template Updates
gui-js/apps/minsky-web/src/app/app-routing.module.ts, gui-js/apps/minsky-web/src/app/app.component.html
Added route for headless/login mapping to LoginComponent; fixed template conditional syntax for headless URL detection.
Dependencies
gui-js/package.json
Added @clerk/clerk-js (^6.3.3) dependency.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant LoginUI as Login<br/>Component
    participant ClerkSvc as ClerkService
    participant Clerk as Clerk<br/>Client
    participant Electron as Electron<br/>IPC
    participant Store as Store<br/>Manager

    User->>LoginUI: Navigate to login
    LoginUI->>ClerkSvc: initialize()
    ClerkSvc->>Clerk: Create & load Clerk instance
    Clerk-->>ClerkSvc: Initialized
    ClerkSvc-->>LoginUI: Ready
    
    User->>LoginUI: Enter email & password
    LoginUI->>LoginUI: Validate form
    LoginUI->>ClerkSvc: signInWithEmailPassword(email, password)
    ClerkSvc->>Clerk: signIn.create(email, password)
    Clerk-->>ClerkSvc: SignIn result
    
    alt Sign-in successful
        ClerkSvc->>Clerk: Activate session
        ClerkSvc->>ClerkSvc: getToken()
        Clerk-->>ClerkSvc: JWT token
        ClerkSvc->>Electron: sendTokenToElectron()
        Electron->>Electron: IPC: SET_AUTH_TOKEN
        Electron->>Store: Update authToken
        Store-->>Electron: Success
        Electron-->>ClerkSvc: Response
        ClerkSvc-->>LoginUI: Success
        LoginUI->>Electron: closeWindow()
    else Sign-in failed
        ClerkSvc-->>LoginUI: Error
        LoginUI->>User: Display error message
    end
Loading
sequenceDiagram
    participant User
    participant App as Electron<br/>App
    participant Menu as Application<br/>Menu
    participant CmdMgr as CommandsManager
    participant WinMgr as WindowManager
    participant Backend as Backend<br/>API
    participant Clerk as Clerk<br/>Auth
    participant Store as Store

    User->>Menu: Click "Upgrade via Clerk"
    Menu->>CmdMgr: upgradeUsingClerk()
    CmdMgr->>WinMgr: openLoginWindow()
    WinMgr-->>CmdMgr: Promise<token>
    CmdMgr->>CmdMgr: Wait for auth token
    
    Note over App: User authenticates in login window
    App->>Store: SET_AUTH_TOKEN event
    Store->>WinMgr: _resolveAuthToken(token)
    WinMgr-->>CmdMgr: Token received
    
    CmdMgr->>CmdMgr: buildState(previous)
    CmdMgr->>Backend: getRelease(product, previous, token)
    Backend-->>CmdMgr: Download URL
    CmdMgr->>CmdMgr: getFinalUrl() with redirect handling
    CmdMgr->>CmdMgr: Download & install assets
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 A token hops through Electron's door,
Clerk authenticates, then downloads more,
Login forms and upgrades aligned,
Auth flows smooth, a feature refined!
🔐✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and concisely summarizes the main changes: adding Clerk authentication integration with an Angular login component and Electron IPC token storage, which aligns with the overall objectives and file-level changes.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch copilot/create-angular-login-component

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Copy link
Copy Markdown
Contributor

@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: 12

🧹 Nitpick comments (4)
gui-js/apps/minsky-web/src/environments/environment.web.ts (1)

9-9: Use a non-empty dev value or explicit startup guard for Clerk key.

Line 9 is ''; if Clerk init is switched to config-driven loading, this will fail at runtime with little context. Prefer a real dev key or fail fast with a clear error when unset.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@gui-js/apps/minsky-web/src/environments/environment.web.ts` at line 9, The
environment value clerkPublishableKey is an empty string which will cause silent
runtime failures when Clerk is initialized; either set a non-empty development
publishable key in environment.web.ts (clerkPublishableKey) or add a startup
guard where Clerk is initialized that checks environment.clerkPublishableKey and
throws/logs a clear error and exits if unset. Update the environment.web.ts
constant and/or the Clerk init path to validate clerkPublishableKey before
calling the Clerk SDK (e.g., initClerk or wherever Clerk is created) so failures
are fast and informative.
gui-js/apps/minsky-electron/src/app/managers/ApplicationMenuManager.ts (1)

135-137: Logout Clerk label does not match the action.

Line 136 opens the login window; it does not perform an immediate logout. Either rename this item (e.g., “Clerk Login / Manage Session”) or call a direct logout/clear-token flow.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@gui-js/apps/minsky-electron/src/app/managers/ApplicationMenuManager.ts`
around lines 135 - 137, The menu item labeled "Logout Clerk" currently calls
WindowManager.openLoginWindow() which only opens the login UI and does not
perform a logout; update the menu to either rename the label to reflect the
action (e.g., "Clerk Login / Manage Session") or change the click handler to
invoke the actual logout/clear-token flow (for example call your logout routine
such as AuthService.logout() or SessionManager.clearSession() before or instead
of WindowManager.openLoginWindow()); ensure the label and the click handler are
consistent (update the label 'Logout Clerk' or replace
WindowManager.openLoginWindow() with the real logout function).
gui-js/libs/ui-components/src/lib/login/login.component.html (1)

27-27: Announce auth failures to assistive tech.

The general errorMessage is only visually rendered right now, so screen-reader users may miss failed sign-ins.

♿ Suggested tweak
-      <p *ngIf="errorMessage" class="error-message">{{ errorMessage }}</p>
+      <p *ngIf="errorMessage" class="error-message" role="alert">{{ errorMessage }}</p>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@gui-js/libs/ui-components/src/lib/login/login.component.html` at line 27, The
error message paragraph (the element that displays errorMessage in
login.component.html) is only visually rendered and not announced to assistive
tech; update that element to be an accessible live region by adding attributes
such as aria-live="assertive" and role="alert" (and optionally
aria-atomic="true") so screen readers will announce sign-in failures when
errorMessage is set; locate the <p> that binds to errorMessage and add these
attributes to it.
gui-js/libs/core/src/lib/services/clerk/clerk.service.ts (1)

35-60: Consider sending the Electron token as part of sign-in.

A successful signInWithEmailPassword() still requires callers to remember a second sendTokenToElectron() call. Missing that step would leave Electron waiting on SET_AUTH_TOKEN even though Clerk already created a session.

♻️ Suggested refactor
     if (result.status === 'complete') {
       await this.clerk.setActive({ session: result.createdSessionId });
+      await this.sendTokenToElectron();
     } else {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@gui-js/libs/core/src/lib/services/clerk/clerk.service.ts` around lines 35 -
60, signInWithEmailPassword currently completes Clerk sign-in but doesn't notify
Electron, leaving the renderer waiting for SET_AUTH_TOKEN; after successfully
setting the active session in signInWithEmailPassword, call sendTokenToElectron
(or inline the logic: await this.getToken() then
this.electronService.invoke(events.SET_AUTH_TOKEN, token) when
this.electronService.isElectron) so the token is sent automatically; update
signInWithEmailPassword to await sendTokenToElectron() only on success and keep
existing signOut behavior unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@gui-js/apps/minsky-electron/src/app/events/electron.events.ts`:
- Around line 292-297: The handler for ipcMain.handle(events.SET_AUTH_TOKEN) is
persisting the raw bearer token to disk via StoreManager.store.set/delete
(StoreManager.store), which is insecure; update the handler to stop writing the
token to persistent store and instead keep it in memory (e.g., use or create an
in-memory AuthTokenManager singleton or a module-scoped variable accessed by the
renderer via IPC) and remove calls to StoreManager.store.set/delete; if
persistent storage is absolutely required, replace direct disk persistence with
an OS-backed secret store integration (e.g., keytar or platform-specific secure
storage) and ensure any persisted value is encrypted and has a strict
expiry/rotation policy rather than storing the raw bearer token.

In `@gui-js/apps/minsky-electron/src/app/managers/ApplicationMenuManager.ts`:
- Around line 131-133: The "Upgrade via Clerk" menu action currently spins in a
non-cancelable loop; modify CommandsManager.upgradeUsingClerk to stop reopening
the login when WindowManager.openLoginWindow returns null (user
closed/cancelled) by handling a null/undefined authToken result and aborting the
upgrade flow (or returning an explicit cancellation) instead of looping forever;
optionally replace the infinite while (!authToken) with a finite retry policy or
a user confirmation prompt before retrying so the operation can be cancelled by
the user.

In `@gui-js/apps/minsky-electron/src/app/managers/CommandsManager.ts`:
- Around line 1469-1488: getRelease currently swallows backend/auth failures by
returning a falsy URL (via optional chaining and a catch that returns ""), so
upgradeUsingClerk sees "no update" instead of surfacing errors; update
getRelease (the static method) to stop collapsing errors: remove or change the
catch to rethrow the caught error (or throw a new Error with context) so
auth/backend failures propagate, and explicitly validate the parsed response
(check that the parsed object from callBackendAPI and
release.release.download_url exist) before returning the URL — only return an
empty string when you deterministically know there is no release (e.g., no
prevRelease found when previous==true), but do not suppress exceptions from
callBackendAPI or malformed responses.
- Around line 1491-1494: The loop in CommandsManager.upgradeUsingClerk
continuously calls WindowManager.openLoginWindow() until
StoreManager.store.get('authToken') exists, but openLoginWindow() can resolve
null when the user closes the popup; update upgradeUsingClerk to capture the
result of WindowManager.openLoginWindow(), check for null, and
break/return/throw to allow cancelation instead of immediately reopening the
dialog—use the returned value from openLoginWindow() (or a boolean) before
re-checking StoreManager.store.get('authToken') so a closed popup stops the loop
and cancels the upgrade flow.
- Around line 1503-1505: The stored Ravel URL is resolved without the auth
token, causing future installs to use "Bearer undefined"; update the stash logic
so StoreManager.store.set('ravelPlugin', await getFinalUrl(ravelAsset, token))
passes the same auth token that other getFinalUrl calls in this file use (e.g.
the authToken/AuthManager.getToken() value present in CommandsManager). Ensure
you retrieve the current token the same way as other code in this module and
pass it as the second argument to getFinalUrl when ravelAsset is truthy.
- Around line 1399-1405: The default (unsupported-platform) branch in
CommandsManager.ts calls an out-of-scope window.close(), which throws; remove
that call and ensure the flow exits cleanly after
dialog.showMessageBoxSync(WindowManager.getMainWindow(), ...) — either delete
the window.close() line or replace it with a valid call on the WindowManager
(e.g., WindowManager.getMainWindow().close() or app.quit()) so the dialog is
shown and the process exits without referencing an undefined window identifier.
- Around line 1385-1398: The `case 'linux'` block in CommandsManager.ts declares
lets (aexec, osRelease, distroInfo, extractor, state assignment) that leak
across the switch and trigger the noSwitchDeclarations lint rule; fix it by
wrapping the entire case 'linux' body in { ... } so those let/const bindings are
block-scoped and local to that case (ensure you move the existing code for case
'linux' including state={...}, promisify(exec) assignment, os-release selection,
grep calls and extractor usage inside the new braces and keep the final break).

In `@gui-js/apps/minsky-electron/src/app/managers/StoreManager.ts`:
- Line 22: The StoreManager currently persists a sensitive authToken property in
electron-store; instead remove authToken from the persisted store schema and
move storage to the OS credential vault (e.g., use keytar). Update StoreManager
to stop writing/reading authToken from electron-store: add async methods like
getAuthToken/setAuthToken/deleteAuthToken that call keytar (or another OS
credential API), migrate any existing token on startup by reading the old
electron-store value, saving it to keytar, and deleting it from electron-store,
and keep only non-sensitive session state in electron-store.

In `@gui-js/apps/minsky-electron/src/app/managers/WindowManager.ts`:
- Around line 31-32: openLoginWindow currently overwrites the shared resolver
_resolveAuthToken when called a second time, orphaning the first caller; fix by
detecting an in-flight auth promise/resolver and returning that existing promise
instead of creating a new one, and only create/assign a new _resolveAuthToken
and _authTokenPromise when none exists; ensure you also reuse any existing
_loginWindow instead of creating a second popup (see references to
openLoginWindow, _resolveAuthToken, _authTokenPromise and _loginWindow and the
similar logic around lines 387-398) so multiple callers share the same
promise/window rather than replacing the resolver.

In `@gui-js/apps/minsky-web/src/environments/environment.ts`:
- Around line 6-8: You added a standalone environment object with
clerkPublishableKey instead of adding it to the existing AppConfig, so the value
can be unused; modify the code to move clerkPublishableKey into the existing
AppConfig interface/object (extend the AppConfig declaration to include
clerkPublishableKey and set its value there) and remove the separate environment
export, then update ClerkService to read the key from the injected AppConfig
(use the AppConfig property rather than a hardcoded string) so the publishable
key is consistently sourced from the app config.

In `@gui-js/libs/core/src/lib/services/clerk/clerk.service.ts`:
- Around line 18-20: The publishable key is hard-coded as publishableKey and
passed to new Clerk(...) in ClerkService; instead accept the publishable key
from the app-level configuration (e.g. inject a ConfigService or add a
constructor parameter) and use that value when instantiating Clerk (this.clerk =
new Clerk(config.get('CLERK_PUBLISHABLE_KEY'))), including a clear error or
fallback if the config value is missing; update the ClerkService
constructor/initialization to read the injected config and remove the hard-coded
literal.

In `@gui-js/libs/ui-components/src/lib/login/login.component.ts`:
- Around line 54-73: The window is closed unconditionally after onSubmit which
prevents retries on failure and can crash outside Electron; change logic so
electronService.closeWindow() is only called for successful flows and only when
running in Electron: move the closeWindow call into the successful branch after
setting isAuthenticated (inside the try after sendTokenToElectron) and call it
guarded by the Electron runtime check (e.g., electronService.isElectron() or
electronService?.isElectron) or by checking that electronService.closeWindow
exists; apply the same pattern to the signOut flow (the signOut handler) so
signOut only calls electronService.closeWindow() when running in Electron and
after a successful sign-out.

---

Nitpick comments:
In `@gui-js/apps/minsky-electron/src/app/managers/ApplicationMenuManager.ts`:
- Around line 135-137: The menu item labeled "Logout Clerk" currently calls
WindowManager.openLoginWindow() which only opens the login UI and does not
perform a logout; update the menu to either rename the label to reflect the
action (e.g., "Clerk Login / Manage Session") or change the click handler to
invoke the actual logout/clear-token flow (for example call your logout routine
such as AuthService.logout() or SessionManager.clearSession() before or instead
of WindowManager.openLoginWindow()); ensure the label and the click handler are
consistent (update the label 'Logout Clerk' or replace
WindowManager.openLoginWindow() with the real logout function).

In `@gui-js/apps/minsky-web/src/environments/environment.web.ts`:
- Line 9: The environment value clerkPublishableKey is an empty string which
will cause silent runtime failures when Clerk is initialized; either set a
non-empty development publishable key in environment.web.ts
(clerkPublishableKey) or add a startup guard where Clerk is initialized that
checks environment.clerkPublishableKey and throws/logs a clear error and exits
if unset. Update the environment.web.ts constant and/or the Clerk init path to
validate clerkPublishableKey before calling the Clerk SDK (e.g., initClerk or
wherever Clerk is created) so failures are fast and informative.

In `@gui-js/libs/core/src/lib/services/clerk/clerk.service.ts`:
- Around line 35-60: signInWithEmailPassword currently completes Clerk sign-in
but doesn't notify Electron, leaving the renderer waiting for SET_AUTH_TOKEN;
after successfully setting the active session in signInWithEmailPassword, call
sendTokenToElectron (or inline the logic: await this.getToken() then
this.electronService.invoke(events.SET_AUTH_TOKEN, token) when
this.electronService.isElectron) so the token is sent automatically; update
signInWithEmailPassword to await sendTokenToElectron() only on success and keep
existing signOut behavior unchanged.

In `@gui-js/libs/ui-components/src/lib/login/login.component.html`:
- Line 27: The error message paragraph (the element that displays errorMessage
in login.component.html) is only visually rendered and not announced to
assistive tech; update that element to be an accessible live region by adding
attributes such as aria-live="assertive" and role="alert" (and optionally
aria-atomic="true") so screen readers will announce sign-in failures when
errorMessage is set; locate the <p> that binds to errorMessage and add these
attributes to it.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 33112e71-d975-4bcc-90af-1cf487513a1c

📥 Commits

Reviewing files that changed from the base of the PR and between 7ba2cff and 8663d10.

⛔ Files ignored due to path filters (1)
  • gui-js/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (17)
  • gui-js/apps/minsky-electron/src/app/events/electron.events.ts
  • gui-js/apps/minsky-electron/src/app/managers/ApplicationMenuManager.ts
  • gui-js/apps/minsky-electron/src/app/managers/CommandsManager.ts
  • gui-js/apps/minsky-electron/src/app/managers/StoreManager.ts
  • gui-js/apps/minsky-electron/src/app/managers/WindowManager.ts
  • gui-js/apps/minsky-web/src/app/app-routing.module.ts
  • gui-js/apps/minsky-web/src/app/app.component.html
  • gui-js/apps/minsky-web/src/environments/environment.ts
  • gui-js/apps/minsky-web/src/environments/environment.web.ts
  • gui-js/libs/core/src/index.ts
  • gui-js/libs/core/src/lib/services/clerk/clerk.service.ts
  • gui-js/libs/shared/src/lib/constants/constants.ts
  • gui-js/libs/ui-components/src/index.ts
  • gui-js/libs/ui-components/src/lib/login/login.component.html
  • gui-js/libs/ui-components/src/lib/login/login.component.scss
  • gui-js/libs/ui-components/src/lib/login/login.component.ts
  • gui-js/package.json

Comment on lines +292 to +297
ipcMain.handle(events.SET_AUTH_TOKEN, async (event, token: string | null) => {
if (token) {
StoreManager.store.set('authToken', token);
} else {
StoreManager.store.delete('authToken');
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Avoid persisting the raw bearer token in the app store.

This writes a reusable auth credential to disk via StoreManager.store. Any user or process that can read the app-data directory can replay it until expiry. Please keep the token in memory only, or move persistence to OS-backed secret storage.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@gui-js/apps/minsky-electron/src/app/events/electron.events.ts` around lines
292 - 297, The handler for ipcMain.handle(events.SET_AUTH_TOKEN) is persisting
the raw bearer token to disk via StoreManager.store.set/delete
(StoreManager.store), which is insecure; update the handler to stop writing the
token to persistent store and instead keep it in memory (e.g., use or create an
in-memory AuthTokenManager singleton or a module-scoped variable accessed by the
renderer via IPC) and remove calls to StoreManager.store.set/delete; if
persistent storage is absolutely required, replace direct disk persistence with
an OS-backed secret store integration (e.g., keytar or platform-specific secure
storage) and ensure any persisted value is encrypted and has a strict
expiry/rotation policy rather than storing the raw bearer token.

Comment on lines +131 to +133
label: 'Upgrade via Clerk',
click() {CommandsManager.upgradeUsingClerk();},
},
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Upgrade via Clerk currently exposes a cancel-loop failure mode.

From the linked flow (CommandsManager.upgradeUsingClerk + WindowManager.openLoginWindow), closing the login window resolves null, but upgrade logic keeps reopening login in a while (!authToken) loop. This makes this menu action non-cancelable for users.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@gui-js/apps/minsky-electron/src/app/managers/ApplicationMenuManager.ts`
around lines 131 - 133, The "Upgrade via Clerk" menu action currently spins in a
non-cancelable loop; modify CommandsManager.upgradeUsingClerk to stop reopening
the login when WindowManager.openLoginWindow returns null (user
closed/cancelled) by handling a null/undefined authToken result and aborting the
upgrade flow (or returning an explicit cancellation) instead of looping forever;
optionally replace the infinite while (!authToken) with a finite retry policy or
a user confirmation prompt before retrying so the operation can be cancelled by
the user.

Comment on lines +1385 to +1398
case 'linux':
state={system: 'linux', distro: '', version: '',arch:'', previous: ''};
// figure out distro and version from /etc/os-release
let aexec=promisify(exec);
let osRelease='/etc/os-release';
if (existsSync(process.resourcesPath+'/os-release'))
osRelease=process.resourcesPath+'/os-release';
let distroInfo=await aexec(`grep ^ID= ${osRelease}`);
// value may or may not be quoted
let extractor=/.*=['"]?([^'"\n]*)['"]?/;
state.distro=extractor.exec(distroInfo.stdout)[1];
distroInfo=await aexec(`grep ^VERSION_ID= ${osRelease}`);
state.version=extractor.exec(distroInfo.stdout)[1];
break;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Locate the CommandsManager.ts file
find . -name "CommandsManager.ts" -type f

Repository: highperformancecoder/minsky

Length of output: 136


🏁 Script executed:

#!/bin/bash
# Once we find the file, read the specific lines mentioned
# First, let's find the file
FILE=$(find . -name "CommandsManager.ts" -type f)
if [ -n "$FILE" ]; then
  echo "File found at: $FILE"
  # Get total line count to understand file size
  wc -l "$FILE"
  # Read lines 1380-1410 to capture context around the target lines
  sed -n '1380,1410p' "$FILE"
fi

Repository: highperformancecoder/minsky

Length of output: 1540


🌐 Web query:

Biome noSwitchDeclarations rule TypeScript JavaScript

💡 Result:

The noSwitchDeclarations rule in Biome is a correctness linter rule that disallows lexical declarations (const, let, function, class) directly in switch clauses. This prevents declarations from being accessible across the entire switch statement, where they might be used uninitialized in other cases. It applies to JavaScript and super languages like TypeScript. Configuration in biome.json: { "linter": { "rules": { "correctness": { "noSwitchDeclarations": "error" } } } } Invalid examples: switch (foo) { case 0: const x = 1; // Error: accessible in other cases break; } Valid fix: Wrap in block switch (foo) { case 0: { const x = 1; break; } } The rule is fixable (suggests wrapping in block) and sources from ESLint's no-case-declarations.

Citations:


Wrap the Linux case in braces.

The let declarations in this clause violate Biome's noSwitchDeclarations rule because they are scoped across the entire switch statement instead of being local to the case. Adding braces keeps the bindings local and resolves the lint error.

🧩 Suggested fix
-    case 'linux':
-      state={system: 'linux', distro: '', version: '',arch:'', previous: ''};
-      // figure out distro and version from /etc/os-release
-      let aexec=promisify(exec);
-      let osRelease='/etc/os-release';
-      if (existsSync(process.resourcesPath+'/os-release'))
-        osRelease=process.resourcesPath+'/os-release';
-      let distroInfo=await aexec(`grep ^ID= ${osRelease}`);
-      // value may or may not be quoted
-      let extractor=/.*=['"]?([^'"\n]*)['"]?/;
-      state.distro=extractor.exec(distroInfo.stdout)[1];
-      distroInfo=await aexec(`grep ^VERSION_ID= ${osRelease}`);
-      state.version=extractor.exec(distroInfo.stdout)[1];
-      break;
+    case 'linux': {
+      state={system: 'linux', distro: '', version: '',arch:'', previous: ''};
+      // figure out distro and version from /etc/os-release
+      const aexec=promisify(exec);
+      let osRelease='/etc/os-release';
+      if (existsSync(process.resourcesPath+'/os-release'))
+        osRelease=process.resourcesPath+'/os-release';
+      let distroInfo=await aexec(`grep ^ID= ${osRelease}`);
+      // value may or may not be quoted
+      const extractor=/.*=['"]?([^'"\n]*)['"]?/;
+      state.distro=extractor.exec(distroInfo.stdout)[1];
+      distroInfo=await aexec(`grep ^VERSION_ID= ${osRelease}`);
+      state.version=extractor.exec(distroInfo.stdout)[1];
+      break;
+    }
📝 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
case 'linux':
state={system: 'linux', distro: '', version: '',arch:'', previous: ''};
// figure out distro and version from /etc/os-release
let aexec=promisify(exec);
let osRelease='/etc/os-release';
if (existsSync(process.resourcesPath+'/os-release'))
osRelease=process.resourcesPath+'/os-release';
let distroInfo=await aexec(`grep ^ID= ${osRelease}`);
// value may or may not be quoted
let extractor=/.*=['"]?([^'"\n]*)['"]?/;
state.distro=extractor.exec(distroInfo.stdout)[1];
distroInfo=await aexec(`grep ^VERSION_ID= ${osRelease}`);
state.version=extractor.exec(distroInfo.stdout)[1];
break;
case 'linux': {
state={system: 'linux', distro: '', version: '',arch:'', previous: ''};
// figure out distro and version from /etc/os-release
const aexec=promisify(exec);
let osRelease='/etc/os-release';
if (existsSync(process.resourcesPath+'/os-release'))
osRelease=process.resourcesPath+'/os-release';
let distroInfo=await aexec(`grep ^ID= ${osRelease}`);
// value may or may not be quoted
const extractor=/.*=['"]?([^'"\n]*)['"]?/;
state.distro=extractor.exec(distroInfo.stdout)[1];
distroInfo=await aexec(`grep ^VERSION_ID= ${osRelease}`);
state.version=extractor.exec(distroInfo.stdout)[1];
break;
}
🧰 Tools
🪛 Biome (2.4.9)

[error] 1388-1388: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

(lint/correctness/noSwitchDeclarations)


[error] 1389-1389: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

(lint/correctness/noSwitchDeclarations)


[error] 1392-1392: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

(lint/correctness/noSwitchDeclarations)


[error] 1394-1394: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

(lint/correctness/noSwitchDeclarations)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@gui-js/apps/minsky-electron/src/app/managers/CommandsManager.ts` around lines
1385 - 1398, The `case 'linux'` block in CommandsManager.ts declares lets
(aexec, osRelease, distroInfo, extractor, state assignment) that leak across the
switch and trigger the noSwitchDeclarations lint rule; fix it by wrapping the
entire case 'linux' body in { ... } so those let/const bindings are block-scoped
and local to that case (ensure you move the existing code for case 'linux'
including state={...}, promisify(exec) assignment, os-release selection, grep
calls and extractor usage inside the new braces and keep the final break).

Comment on lines +1399 to +1405
default:
dialog.showMessageBoxSync(WindowManager.getMainWindow(),{
message: `In app update is not available for your operating system yet, please check back later`,
type: 'error',
});
window.close();
return;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Remove the out-of-scope window.close() call.

There is no local window binding here, so the unsupported-platform branch throws instead of exiting cleanly after showing the dialog.

🧹 Suggested fix
     default:
       dialog.showMessageBoxSync(WindowManager.getMainWindow(),{
             message: `In app update is not available for your operating system yet, please check back later`,
             type: 'error',
       });
-      window.close();
       return;
-      break;
     }
📝 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
default:
dialog.showMessageBoxSync(WindowManager.getMainWindow(),{
message: `In app update is not available for your operating system yet, please check back later`,
type: 'error',
});
window.close();
return;
default:
dialog.showMessageBoxSync(WindowManager.getMainWindow(),{
message: `In app update is not available for your operating system yet, please check back later`,
type: 'error',
});
return;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@gui-js/apps/minsky-electron/src/app/managers/CommandsManager.ts` around lines
1399 - 1405, The default (unsupported-platform) branch in CommandsManager.ts
calls an out-of-scope window.close(), which throws; remove that call and ensure
the flow exits cleanly after
dialog.showMessageBoxSync(WindowManager.getMainWindow(), ...) — either delete
the window.close() line or replace it with a valid call on the WindowManager
(e.g., WindowManager.getMainWindow().close() or app.quit()) so the dialog is
shown and the process exits without referencing an undefined window identifier.

Comment on lines +1469 to +1488
static async getRelease(product: string, previous: boolean, token: string) {
let state=await CommandsManager.buildState(previous);
let query=`product=${product}&os=${state.system}&arch=${state.arch}&distro=${state.distro}&distro_version=${state.version}`;
try {
if (previous) {
let releases=JSON.parse(await callBackendAPI(`${backendAPI}/releases?${query}`, token));
let prevRelease;
for (let release of releases)
if (semVerLess(release.version, state.previous))
prevRelease=release;
if (prevRelease) return prevRelease.download_url;
// if not, then treat the request as latest
}
let release=JSON.parse(await callBackendAPI(`${backendAPI}/releases/latest?${query}`, token));
return release?.release?.download_url;
}
catch (error) {
console.error(error);
return "";
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don't collapse backend/auth failures into a falsy URL.

release?.release?.download_url and the catch path both turn failures into falsy values. upgradeUsingClerk() then shows the "Everything's up to date" dialog on bad tokens and backend outages instead of prompting re-auth or retry.

🧰 Tools
🪛 GitHub Check: CodeQL

[warning] 1486-1486: Log injection
Log entry depends on a user-provided value.
Log entry depends on a user-provided value.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@gui-js/apps/minsky-electron/src/app/managers/CommandsManager.ts` around lines
1469 - 1488, getRelease currently swallows backend/auth failures by returning a
falsy URL (via optional chaining and a catch that returns ""), so
upgradeUsingClerk sees "no update" instead of surfacing errors; update
getRelease (the static method) to stop collapsing errors: remove or change the
catch to rethrow the caught error (or throw a new Error with context) so
auth/backend failures propagate, and explicitly validate the parsed response
(check that the parsed object from callBackendAPI and
release.release.download_url exist) before returning the URL — only return an
empty string when you deterministically know there is no release (e.g., no
prevRelease found when previous==true), but do not suppress exceptions from
callBackendAPI or malformed responses.

defaultModelDirectory: string;
defaultDataDirectory: string;
ravelPlugin: string; // used for post installation installation of Ravel
authToken?: string;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Avoid persisting JWT bearer tokens in plain electron-store.

Line 22 adds long-lived token storage in the regular app store, which is readable from disk and weakens account security. Store auth tokens in OS credential storage (e.g., keychain/credential vault) and keep only non-sensitive session state in electron-store.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@gui-js/apps/minsky-electron/src/app/managers/StoreManager.ts` at line 22, The
StoreManager currently persists a sensitive authToken property in
electron-store; instead remove authToken from the persisted store schema and
move storage to the OS credential vault (e.g., use keytar). Update StoreManager
to stop writing/reading authToken from electron-store: add async methods like
getAuthToken/setAuthToken/deleteAuthToken that call keytar (or another OS
credential API), migrate any existing token on startup by reading the old
electron-store value, saving it to keytar, and deleting it from electron-store,
and keep only non-sensitive session state in electron-store.

Comment on lines +31 to +32
// Pending resolver for the auth-token promise created by openLoginWindow()
static _resolveAuthToken: ((token: string | null) => void) | null = null;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Reuse the in-flight login promise/window.

A second openLoginWindow() call replaces _resolveAuthToken before the first promise settles. The first caller then waits forever, and the earlier popup becomes orphaned. This is reachable because both the upgrade flow and the menu can open the login window. Return the existing pending promise instead of overwriting the resolver.

Also applies to: 387-398

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@gui-js/apps/minsky-electron/src/app/managers/WindowManager.ts` around lines
31 - 32, openLoginWindow currently overwrites the shared resolver
_resolveAuthToken when called a second time, orphaning the first caller; fix by
detecting an in-flight auth promise/resolver and returning that existing promise
instead of creating a new one, and only create/assign a new _resolveAuthToken
and _authTokenPromise when none exists; ensure you also reuse any existing
_loginWindow instead of creating a second popup (see references to
openLoginWindow, _resolveAuthToken, _authTokenPromise and _loginWindow and the
similar logic around lines 387-398) so multiple callers share the same
promise/window rather than replacing the resolver.

Comment on lines +6 to +8
export const environment = {
clerkPublishableKey: 'pk_test_cG9zaXRpdmUtcGhvZW5peC04NS5jbGVyay5hY2NvdW50cy5kZXYk',
};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Consolidate Clerk key into the existing app config surface.

Line 6 introduces a second config object (environment) instead of extending AppConfig, which risks this value never being consumed consistently. With ClerkService still hardcoding the key, this new value is currently ineffective and can drift.

Proposed fix
 export const AppConfig = {
   production: false,
   environment: 'LOCAL',
+  clerkPublishableKey: 'pk_test_cG9zaXRpdmUtcGhvZW5peC04NS5jbGVyay5hY2NvdW50cy5kZXYk',
 };
-
-export const environment = {
-  clerkPublishableKey: 'pk_test_cG9zaXRpdmUtcGhvZW5peC04NS5jbGVyay5hY2NvdW50cy5kZXYk',
-};
🧰 Tools
🪛 Betterleaks (1.1.1)

[high] 7-7: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@gui-js/apps/minsky-web/src/environments/environment.ts` around lines 6 - 8,
You added a standalone environment object with clerkPublishableKey instead of
adding it to the existing AppConfig, so the value can be unused; modify the code
to move clerkPublishableKey into the existing AppConfig interface/object (extend
the AppConfig declaration to include clerkPublishableKey and set its value
there) and remove the separate environment export, then update ClerkService to
read the key from the injected AppConfig (use the AppConfig property rather than
a hardcoded string) so the publishable key is consistently sourced from the app
config.

Comment on lines +18 to +20
const publishableKey = 'pk_test_cG9zaXRpdmUtcGhvZW5peC04NS5jbGVyay5hY2NvdW50cy5kZXYk';

this.clerk = new Clerk(publishableKey);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don't hard-code the Clerk tenant in a shared library.

This locks every build to one test publishable key and bypasses the configuration work added elsewhere in the PR. Please inject the publishable key from app-level config instead of embedding it here.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@gui-js/libs/core/src/lib/services/clerk/clerk.service.ts` around lines 18 -
20, The publishable key is hard-coded as publishableKey and passed to new
Clerk(...) in ClerkService; instead accept the publishable key from the
app-level configuration (e.g. inject a ConfigService or add a constructor
parameter) and use that value when instantiating Clerk (this.clerk = new
Clerk(config.get('CLERK_PUBLISHABLE_KEY'))), including a clear error or fallback
if the config value is missing; update the ClerkService
constructor/initialization to read the injected config and remove the hard-coded
literal.

Comment on lines +54 to +73
async onSubmit() {
if (this.loginForm.invalid) return;

this.isLoading = true;
this.errorMessage = '';

try {
await this.clerkService.signInWithEmailPassword(
this.loginForm.value.email,
this.loginForm.value.password
);
this.isAuthenticated = true;
await this.clerkService.sendTokenToElectron();
} catch (err: any) {
this.errorMessage = err?.errors?.[0]?.message ?? err?.message ?? 'Authentication failed.';
} finally {
this.isLoading = false;
}
this.electronService.closeWindow();
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Gate window closing to successful flows and Electron runtime only.

Line [72] and Line [86] close the window unconditionally. That both (1) closes on failed auth/sign-out (user can’t retry) and (2) can crash in non-Electron runtime because ElectronService.closeWindow() is not guarded (gui-js/libs/core/src/lib/services/electron/electron.service.ts, Line 51).

Proposed fix
 async onSubmit() {
   if (this.loginForm.invalid) return;

   this.isLoading = true;
   this.errorMessage = '';
+  let isSuccess = false;

   try {
     await this.clerkService.signInWithEmailPassword(
       this.loginForm.value.email,
       this.loginForm.value.password
     );
     this.isAuthenticated = true;
     await this.clerkService.sendTokenToElectron();
+    isSuccess = true;
   } catch (err: any) {
     this.errorMessage = err?.errors?.[0]?.message ?? err?.message ?? 'Authentication failed.';
   } finally {
     this.isLoading = false;
   }
-  this.electronService.closeWindow();
+  if (isSuccess && this.electronService.isElectron) {
+    await this.electronService.closeWindow();
+  }
 }

 async onSignOut() {
   this.isLoading = true;
   this.errorMessage = '';
+  let isSuccess = false;
   try {
     await this.clerkService.signOut();
     this.isAuthenticated = false;
+    isSuccess = true;
   } catch (err: any) {
     this.errorMessage = err?.message ?? 'Sign out failed.';
   } finally {
     this.isLoading = false;
   }
-  this.electronService.closeWindow();
+  if (isSuccess && this.electronService.isElectron) {
+    await this.electronService.closeWindow();
+  }
 }

Also applies to: 75-87

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@gui-js/libs/ui-components/src/lib/login/login.component.ts` around lines 54 -
73, The window is closed unconditionally after onSubmit which prevents retries
on failure and can crash outside Electron; change logic so
electronService.closeWindow() is only called for successful flows and only when
running in Electron: move the closeWindow call into the successful branch after
setting isAuthenticated (inside the try after sendTokenToElectron) and call it
guarded by the Electron runtime check (e.g., electronService.isElectron() or
electronService?.isElectron) or by checking that electronService.closeWindow
exists; apply the same pattern to the signOut flow (the signOut handler) so
signOut only calls electronService.closeWindow() when running in Electron and
after a successful sign-out.

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.

3 participants