Skip to content

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

Draft
Copilot wants to merge 19 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 19 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.

@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

Important

Review skipped

Bot user detected.

To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b302b0e9-0f2f-41c7-bfc5-43044a36689c

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 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 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 +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

@coderabbitai coderabbitai bot Apr 2, 2026

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.

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.

openLoginWindow does return the promise. _resolveToken stores the returned token. It shouldn't matter if this gets overwritten, as node does this atomically, IIUC.

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.

@highperformancecoder, you're absolutely right that Node.js's single-threaded event loop makes the assignment itself atomic — no torn writes or data races there.

The concern is a logical one rather than a concurrency/atomicity one. Here's what happens with two sequential (but overlapping) calls:

  1. Call 1: promise1 is created; its internal resolve1 is stored in _resolveAuthToken. promise1 is returned to caller 1.
  2. Call 2: _resolveAuthToken is overwritten with resolve2. At this point, resolve1 is no longer reachable from anywhere — the only reference to it was _resolveAuthToken, and that's gone.
  3. When the IPC handler fires _resolveAuthToken(token), it calls resolve2promise2 settles. ✅
  4. promise1 never settles — caller 1 awaits it forever. 🔴

The window1.once('closed') handler also checks _resolveAuthToken, which by now either points to resolve2 or is already null, so it doesn't help promise1 either.

That said, if the application ensures only one login window can be opened at a time (e.g., it's purely user-triggered via a single menu item and the menu is disabled while the window is open), this is low-risk in practice. If you're confident that's the case, the current code is fine for practical purposes.


🧠 Learnings used
Learnt from: highperformancecoder
Repo: highperformancecoder/minsky PR: 630
File: RESTService/addon.cc:255-264
Timestamp: 2026-03-22T22:13:42.606Z
Learning: In Minsky (model/renderNativeWindow.cc, model/minsky.cc), `RenderNativeWindow::requestRedraw()` may only ever be called on the minsky thread (not from the RK/simulation thread). The simulation thread already explicitly avoids calling `requestRedraw()` via the guard `if (!RKThreadRunning) canvas.requestRedraw()` at model/minsky.cc:1276. If this invariant holds everywhere, the data race on `nativeWindowsToRedraw` reported in PR `#630` does not exist, and the swap-before-lock pattern introduced by that PR (releasing `nativeWindowsToRedrawMutex` before acquiring `minskyCmdMutex`) creates a new use-after-free hazard that is worse than the original code.

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.

_resolveAuthToken is only overwritten when the promise resolves. The promise is not stored there. Let's say promise1 resolves first, then _resolveAuthToken is updated to that value. promise 2 is still alive, and being awaited on. Meanwhile, login window 1 proceeds, as it has the token. Then promise 2 resolves, _resolveAuthToken is updated with the exact same token, and the second upgradeUsingClerk method continues, duplicating everything the first method has done. I don't see the problem here.

Copilot AI and others added 3 commits April 9, 2026 00:55
Agent-Logs-Url: https://github.com/highperformancecoder/minsky/sessions/fee97c92-7978-4c1d-8195-ace7a1267507

Co-authored-by: highperformancecoder <3075825+highperformancecoder@users.noreply.github.com>
Agent-Logs-Url: https://github.com/highperformancecoder/minsky/sessions/198fce2f-6377-4ea0-a84d-1c4fe6545b1b

Co-authored-by: highperformancecoder <3075825+highperformancecoder@users.noreply.github.com>
…erk-session-login-window

Preserve Clerk session between login window opens
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