From 13a1d2dc88c884c940b9774b57272788ab89380a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 31 Mar 2026 03:10:59 +0000 Subject: [PATCH 01/12] Initial plan From 6e0e17ed494bbbaaafc11bb97f0ae80de56448a1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 31 Mar 2026 03:16:50 +0000 Subject: [PATCH 02/12] Add Clerk authentication integration with Angular login component and 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> --- .../src/app/events/electron.events.ts | 9 ++ .../src/app/managers/StoreManager.ts | 1 + .../minsky-web/src/app/app-routing.module.ts | 7 +- .../src/environments/environment.web.ts | 1 + gui-js/libs/core/src/index.ts | 3 +- .../src/lib/services/clerk/clerk.service.ts | 72 ++++++++++++++++ .../shared/src/lib/constants/constants.ts | 3 +- gui-js/libs/ui-components/src/index.ts | 1 + .../src/lib/login/login.component.html | 35 ++++++++ .../src/lib/login/login.component.scss | 31 +++++++ .../src/lib/login/login.component.ts | 85 +++++++++++++++++++ gui-js/package.json | 17 ++-- 12 files changed, 256 insertions(+), 9 deletions(-) create mode 100644 gui-js/libs/core/src/lib/services/clerk/clerk.service.ts create mode 100644 gui-js/libs/ui-components/src/lib/login/login.component.html create mode 100644 gui-js/libs/ui-components/src/lib/login/login.component.scss create mode 100644 gui-js/libs/ui-components/src/lib/login/login.component.ts diff --git a/gui-js/apps/minsky-electron/src/app/events/electron.events.ts b/gui-js/apps/minsky-electron/src/app/events/electron.events.ts index 3bcb1f178..fca0210fa 100644 --- a/gui-js/apps/minsky-electron/src/app/events/electron.events.ts +++ b/gui-js/apps/minsky-electron/src/app/events/electron.events.ts @@ -288,3 +288,12 @@ ipcMain.handle(events.OPEN_URL, (event,options)=> { let window=WindowManager.createWindow(options); window.loadURL(options.url); }); + +ipcMain.handle(events.SET_AUTH_TOKEN, async (event, token: string | null) => { + if (token) { + StoreManager.store.set('authToken', token); + } else { + StoreManager.store.delete('authToken'); + } + return { success: true }; +}); diff --git a/gui-js/apps/minsky-electron/src/app/managers/StoreManager.ts b/gui-js/apps/minsky-electron/src/app/managers/StoreManager.ts index 2447ab5f8..6ff09307a 100644 --- a/gui-js/apps/minsky-electron/src/app/managers/StoreManager.ts +++ b/gui-js/apps/minsky-electron/src/app/managers/StoreManager.ts @@ -19,6 +19,7 @@ interface MinskyStore { defaultModelDirectory: string; defaultDataDirectory: string; ravelPlugin: string; // used for post installation installation of Ravel + authToken?: string; } class StoreManager { diff --git a/gui-js/apps/minsky-web/src/app/app-routing.module.ts b/gui-js/apps/minsky-web/src/app/app-routing.module.ts index 9262601e4..044a5a711 100644 --- a/gui-js/apps/minsky-web/src/app/app-routing.module.ts +++ b/gui-js/apps/minsky-web/src/app/app-routing.module.ts @@ -28,7 +28,8 @@ import { EditHandleDescriptionComponent, EditHandleDimensionComponent, PickSlicesComponent, - LockHandlesComponent + LockHandlesComponent, + LoginComponent, } from '@minsky/ui-components'; const routes: Routes = [ @@ -149,6 +150,10 @@ const routes: Routes = [ path: 'headless/variable-pane', component: VariablePaneComponent, }, + { + path: 'login', + component: LoginComponent, + }, { path: '**', component: PageNotFoundComponent, diff --git a/gui-js/apps/minsky-web/src/environments/environment.web.ts b/gui-js/apps/minsky-web/src/environments/environment.web.ts index 25a302f01..a08806d68 100644 --- a/gui-js/apps/minsky-web/src/environments/environment.web.ts +++ b/gui-js/apps/minsky-web/src/environments/environment.web.ts @@ -6,4 +6,5 @@ export const AppConfig = { production: false, environment: 'DEV', + clerkPublishableKey: '', }; diff --git a/gui-js/libs/core/src/index.ts b/gui-js/libs/core/src/index.ts index 618104a39..ecca496d7 100644 --- a/gui-js/libs/core/src/index.ts +++ b/gui-js/libs/core/src/index.ts @@ -3,4 +3,5 @@ export * from './lib/component/dialog/dialog.component'; export * from './lib/services/communication/communication.service'; export * from './lib/services/electron/electron.service'; export * from './lib/services/WindowUtility/window-utility.service'; -export * from './lib/services/TextInputUtilities'; \ No newline at end of file +export * from './lib/services/TextInputUtilities'; +export * from './lib/services/clerk/clerk.service'; \ No newline at end of file diff --git a/gui-js/libs/core/src/lib/services/clerk/clerk.service.ts b/gui-js/libs/core/src/lib/services/clerk/clerk.service.ts new file mode 100644 index 000000000..f82544463 --- /dev/null +++ b/gui-js/libs/core/src/lib/services/clerk/clerk.service.ts @@ -0,0 +1,72 @@ +import { Injectable } from '@angular/core'; +import { ElectronService } from '../electron/electron.service'; +import { events } from '@minsky/shared'; + +@Injectable({ + providedIn: 'root', +}) +export class ClerkService { + private clerk: any = null; + private initialized = false; + + constructor(private electronService: ElectronService) {} + + async initialize(): Promise { + if (this.initialized) return; + + const publishableKey = (window as any).__clerkPublishableKey + ?? (typeof process !== 'undefined' && process.env?.['CLERK_PUBLISHABLE_KEY']) + ?? ''; + + if (!publishableKey) { + console.warn( + 'ClerkService: No publishable key found in window.__clerkPublishableKey or ' + + 'CLERK_PUBLISHABLE_KEY environment variable. Authentication will not be available.' + ); + return; + } + + const { default: Clerk } = await import('@clerk/clerk-js'); + this.clerk = new Clerk(publishableKey); + await this.clerk.load(); + this.initialized = true; + } + + async isSignedIn(): Promise { + if (!this.clerk) return false; + return !!this.clerk.user; + } + + async getToken(): Promise { + if (!this.clerk?.session) return null; + return await this.clerk.session.getToken(); + } + + async signInWithEmailPassword(email: string | null | undefined, password: string | null | undefined): Promise { + if (!this.clerk) throw new Error('Clerk is not initialized.'); + if (!email || !password) throw new Error('Email and password are required.'); + const result = await this.clerk.client.signIn.create({ + identifier: email, + password, + }); + if (result.status === 'complete') { + await this.clerk.setActive({ session: result.createdSessionId }); + } else { + throw new Error('Sign-in was not completed. Additional steps may be required.'); + } + } + + async signOut(): Promise { + if (!this.clerk) throw new Error('Clerk is not initialized.'); + await this.clerk.signOut(); + if (this.electronService.isElectron) { + await this.electronService.invoke(events.SET_AUTH_TOKEN, null); + } + } + + async sendTokenToElectron(): Promise { + if (!this.electronService.isElectron) return; + const token = await this.getToken(); + await this.electronService.invoke(events.SET_AUTH_TOKEN, token); + } +} diff --git a/gui-js/libs/shared/src/lib/constants/constants.ts b/gui-js/libs/shared/src/lib/constants/constants.ts index f83e7ac6b..b72a14ae4 100644 --- a/gui-js/libs/shared/src/lib/constants/constants.ts +++ b/gui-js/libs/shared/src/lib/constants/constants.ts @@ -66,7 +66,8 @@ export const events = { UPDATE_BOOKMARK_LIST: 'update-bookmark-list', UPDATE_PREFERENCES: 'update-preferences', ZOOM: 'zoom', - LOG_MESSAGE: 'log-message' + LOG_MESSAGE: 'log-message', + SET_AUTH_TOKEN: 'set-auth-token' }; // add non exposed commands here to get intellisense on the terminal popup diff --git a/gui-js/libs/ui-components/src/index.ts b/gui-js/libs/ui-components/src/index.ts index 20009ee5a..4f7c36f8d 100644 --- a/gui-js/libs/ui-components/src/index.ts +++ b/gui-js/libs/ui-components/src/index.ts @@ -31,4 +31,5 @@ export * from './lib/rename-all-instances/rename-all-instances.component'; export * from './lib/summary/summary.component'; export * from './lib/variable-pane/variable-pane.component'; export * from './lib/wiring/wiring.component'; +export * from './lib/login/login.component'; diff --git a/gui-js/libs/ui-components/src/lib/login/login.component.html b/gui-js/libs/ui-components/src/lib/login/login.component.html new file mode 100644 index 000000000..4f26bbcc8 --- /dev/null +++ b/gui-js/libs/ui-components/src/lib/login/login.component.html @@ -0,0 +1,35 @@ + diff --git a/gui-js/libs/ui-components/src/lib/login/login.component.scss b/gui-js/libs/ui-components/src/lib/login/login.component.scss new file mode 100644 index 000000000..41fd46be5 --- /dev/null +++ b/gui-js/libs/ui-components/src/lib/login/login.component.scss @@ -0,0 +1,31 @@ +.login-container { + display: flex; + flex-direction: column; + align-items: center; + padding: 24px; + max-width: 400px; + margin: 0 auto; + + h2 { + margin-bottom: 16px; + } +} + +.full-width { + width: 100%; + margin-bottom: 12px; +} + +.error-message { + color: red; + margin-bottom: 8px; +} + +.signed-in-message { + margin-bottom: 16px; +} + +mat-spinner { + display: inline-block; + margin-right: 8px; +} diff --git a/gui-js/libs/ui-components/src/lib/login/login.component.ts b/gui-js/libs/ui-components/src/lib/login/login.component.ts new file mode 100644 index 000000000..312d52227 --- /dev/null +++ b/gui-js/libs/ui-components/src/lib/login/login.component.ts @@ -0,0 +1,85 @@ +import { Component, OnInit } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormControl, FormGroup, Validators, ReactiveFormsModule } from '@angular/forms'; +import { MatButtonModule } from '@angular/material/button'; +import { MatInputModule } from '@angular/material/input'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; +import { ClerkService } from '@minsky/core'; + +@Component({ + selector: 'minsky-login', + templateUrl: './login.component.html', + styleUrls: ['./login.component.scss'], + standalone: true, + imports: [ + CommonModule, + ReactiveFormsModule, + MatButtonModule, + MatInputModule, + MatFormFieldModule, + MatProgressSpinnerModule, + ], +}) +export class LoginComponent implements OnInit { + loginForm = new FormGroup({ + email: new FormControl('', [Validators.required, Validators.email]), + password: new FormControl('', [Validators.required]), + }); + + isLoading = false; + errorMessage = ''; + isAuthenticated = false; + + constructor(private clerkService: ClerkService) {} + + async ngOnInit() { + try { + await this.clerkService.initialize(); + this.isAuthenticated = await this.clerkService.isSignedIn(); + } catch (err) { + this.errorMessage = 'Failed to initialize authentication.'; + } + } + + get email() { + return this.loginForm.get('email'); + } + + get password() { + return this.loginForm.get('password'); + } + + 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; + } + } + + async onSignOut() { + this.isLoading = true; + this.errorMessage = ''; + try { + await this.clerkService.signOut(); + this.isAuthenticated = false; + } catch (err: any) { + this.errorMessage = err?.message ?? 'Sign out failed.'; + } finally { + this.isLoading = false; + } + } +} diff --git a/gui-js/package.json b/gui-js/package.json index aba645417..6c317a619 100644 --- a/gui-js/package.json +++ b/gui-js/package.json @@ -1,6 +1,6 @@ { "name": "ravel", - "version":"3.25.1", + "version": "3.25.1", "author": "High Performance Coders", "description": "Graphical dynamical systems simulator oriented towards economics", "repository": { @@ -23,10 +23,14 @@ }, "win": { "icon": "apps/minsky-electron/src/assets/RavelLogo.ico", - "target": [{ - "target": "nsis", - "arch": ["x64"] - }], + "target": [ + { + "target": "nsis", + "arch": [ + "x64" + ] + } + ], "verifyUpdateCodeSignature": false }, "nsis": { @@ -186,6 +190,8 @@ "@angular/forms": "^20.3.17", "@angular/platform-browser-dynamic": "^20.3.17", "@angular/router": "^20.3.17", + "@clerk/clerk-js": "^6.3.3", + "JSONStream": "^1.3.5", "bindings": "^1.5.0", "call-bind-apply-helpers": "^1.0.2", "decompress": "^4.2.1", @@ -194,7 +200,6 @@ "electron-store": "^8.2.0", "fullname": "^5.0.0", "glob": "^11.1.0", - "JSONStream": "^1.3.5", "mathjax": "^3.2.2", "readable-stream": "^4.7.0", "typescript": "5.8.3", From 3ebc40476d2242fd4347d0076d42e33e33467942 Mon Sep 17 00:00:00 2001 From: Russell Standish Date: Tue, 31 Mar 2026 15:54:06 +1100 Subject: [PATCH 03/12] Added a "login to clerk" menu item, and added a clerk publishable key. --- .../src/app/managers/ApplicationMenuManager.ts | 13 +++++++++++++ .../minsky-web/src/environments/environment.ts | 4 ++++ .../src/lib/services/clerk/clerk.service.ts | 18 ++++++++---------- 3 files changed, 25 insertions(+), 10 deletions(-) diff --git a/gui-js/apps/minsky-electron/src/app/managers/ApplicationMenuManager.ts b/gui-js/apps/minsky-electron/src/app/managers/ApplicationMenuManager.ts index 36ac74a22..c8fe005c5 100644 --- a/gui-js/apps/minsky-electron/src/app/managers/ApplicationMenuManager.ts +++ b/gui-js/apps/minsky-electron/src/app/managers/ApplicationMenuManager.ts @@ -127,6 +127,19 @@ export class ApplicationMenuManager { window.loadURL('https://www.patreon.com/logout'); }, }, + { + label: 'Login via Clerk', + click() { + let window=WindowManager.createWindow({ + width: 420, + height: 500, + title: '', + modal: false, + }); + window.loadURL('#/login'); + }, + }, + { label: 'New System', accelerator: 'CmdOrCtrl + Shift + N', diff --git a/gui-js/apps/minsky-web/src/environments/environment.ts b/gui-js/apps/minsky-web/src/environments/environment.ts index 1ffba9cad..3142d3432 100644 --- a/gui-js/apps/minsky-web/src/environments/environment.ts +++ b/gui-js/apps/minsky-web/src/environments/environment.ts @@ -2,3 +2,7 @@ export const AppConfig = { production: false, environment: 'LOCAL', }; + +export const environment = { + clerkPublishableKey: 'pk_test_cG9zaXRpdmUtcGhvZW5peC04NS5jbGVyay5hY2NvdW50cy5kZXYk', +}; diff --git a/gui-js/libs/core/src/lib/services/clerk/clerk.service.ts b/gui-js/libs/core/src/lib/services/clerk/clerk.service.ts index 3cef94adf..4f2538fcf 100644 --- a/gui-js/libs/core/src/lib/services/clerk/clerk.service.ts +++ b/gui-js/libs/core/src/lib/services/clerk/clerk.service.ts @@ -16,17 +16,15 @@ export class ClerkService { async initialize(): Promise { if (this.initialized) return; - const publishableKey = (window as any).__clerkPublishableKey - ?? (typeof process !== 'undefined' && process.env?.['CLERK_PUBLISHABLE_KEY']) - ?? ''; + const publishableKey = 'pk_test_cG9zaXRpdmUtcGhvZW5peC04NS5jbGVyay5hY2NvdW50cy5kZXYk'; - if (!publishableKey) { - console.warn( - 'ClerkService: No publishable key found in window.__clerkPublishableKey or ' + - 'CLERK_PUBLISHABLE_KEY environment variable. Authentication will not be available.' - ); - return; - } +// if (!publishableKey) { +// console.warn( +// 'ClerkService: No publishable key found in window.__clerkPublishableKey or ' + +// 'CLERK_PUBLISHABLE_KEY environment variable. Authentication will not be available.' +// ); +// return; +// } this.clerk = new Clerk(publishableKey); await this.clerk.load(); From 9d1d2615ed7e4c250be12f388903750a9ce6612a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 31 Mar 2026 04:58:20 +0000 Subject: [PATCH 04/12] Fix login window not rendering by using createPopupWindowWithRouting Agent-Logs-Url: https://github.com/highperformancecoder/minsky/sessions/ac00893a-9811-4d4f-8ef1-9b889eb6d5cc Co-authored-by: highperformancecoder <3075825+highperformancecoder@users.noreply.github.com> --- .../app/managers/ApplicationMenuManager.ts | 6 +++--- .../src/lib/services/clerk/clerk.service.ts | 19 ++++++++++--------- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/gui-js/apps/minsky-electron/src/app/managers/ApplicationMenuManager.ts b/gui-js/apps/minsky-electron/src/app/managers/ApplicationMenuManager.ts index c8fe005c5..467b6d45c 100644 --- a/gui-js/apps/minsky-electron/src/app/managers/ApplicationMenuManager.ts +++ b/gui-js/apps/minsky-electron/src/app/managers/ApplicationMenuManager.ts @@ -130,13 +130,13 @@ export class ApplicationMenuManager { { label: 'Login via Clerk', click() { - let window=WindowManager.createWindow({ + WindowManager.createPopupWindowWithRouting({ width: 420, height: 500, - title: '', + title: 'Login', modal: false, + url: '#/login', }); - window.loadURL('#/login'); }, }, diff --git a/gui-js/libs/core/src/lib/services/clerk/clerk.service.ts b/gui-js/libs/core/src/lib/services/clerk/clerk.service.ts index 4f2538fcf..439ee6f05 100644 --- a/gui-js/libs/core/src/lib/services/clerk/clerk.service.ts +++ b/gui-js/libs/core/src/lib/services/clerk/clerk.service.ts @@ -2,7 +2,6 @@ import { Injectable } from '@angular/core'; import { ElectronService } from '../electron/electron.service'; import { events } from '@minsky/shared'; import { Clerk } from '@clerk/clerk-js'; -// const { default: Clerk } = await import('@clerk/clerk-js'); @Injectable({ providedIn: 'root', @@ -16,15 +15,17 @@ export class ClerkService { async initialize(): Promise { if (this.initialized) return; - const publishableKey = 'pk_test_cG9zaXRpdmUtcGhvZW5peC04NS5jbGVyay5hY2NvdW50cy5kZXYk'; + const publishableKey = (window as any).__clerkPublishableKey + ?? (typeof process !== 'undefined' && process.env?.['CLERK_PUBLISHABLE_KEY']) + ?? ''; -// if (!publishableKey) { -// console.warn( -// 'ClerkService: No publishable key found in window.__clerkPublishableKey or ' + -// 'CLERK_PUBLISHABLE_KEY environment variable. Authentication will not be available.' -// ); -// return; -// } + if (!publishableKey) { + console.warn( + 'ClerkService: No publishable key found in window.__clerkPublishableKey or ' + + 'CLERK_PUBLISHABLE_KEY environment variable. Authentication will not be available.' + ); + return; + } this.clerk = new Clerk(publishableKey); await this.clerk.load(); From 62a9eadb6ae667289f0f2d89f29543952e44e696 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 31 Mar 2026 05:21:07 +0000 Subject: [PATCH 05/12] Fix login window showing app shell (header/tabs) by excluding login route 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> --- gui-js/apps/minsky-web/src/app/app.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gui-js/apps/minsky-web/src/app/app.component.html b/gui-js/apps/minsky-web/src/app/app.component.html index 01065ffe5..e1e681d35 100644 --- a/gui-js/apps/minsky-web/src/app/app.component.html +++ b/gui-js/apps/minsky-web/src/app/app.component.html @@ -5,7 +5,7 @@ } @if (!loading) { - @if (router.url.includes('headless');) { + @if (router.url.includes('headless') || router.url.includes('login')) { } @else { From f9b417d612cb0d71ec9d3ab60ce504bd7cc9af68 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 31 Mar 2026 05:28:54 +0000 Subject: [PATCH 06/12] Move login route to headless/login for consistency with other popup windows Agent-Logs-Url: https://github.com/highperformancecoder/minsky/sessions/0bde9861-8dca-48e7-965d-c608f6258171 Co-authored-by: highperformancecoder <3075825+highperformancecoder@users.noreply.github.com> --- .../minsky-electron/src/app/managers/ApplicationMenuManager.ts | 2 +- gui-js/apps/minsky-web/src/app/app-routing.module.ts | 2 +- gui-js/apps/minsky-web/src/app/app.component.html | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/gui-js/apps/minsky-electron/src/app/managers/ApplicationMenuManager.ts b/gui-js/apps/minsky-electron/src/app/managers/ApplicationMenuManager.ts index 467b6d45c..aa54a60fc 100644 --- a/gui-js/apps/minsky-electron/src/app/managers/ApplicationMenuManager.ts +++ b/gui-js/apps/minsky-electron/src/app/managers/ApplicationMenuManager.ts @@ -135,7 +135,7 @@ export class ApplicationMenuManager { height: 500, title: 'Login', modal: false, - url: '#/login', + url: '#/headless/login', }); }, }, diff --git a/gui-js/apps/minsky-web/src/app/app-routing.module.ts b/gui-js/apps/minsky-web/src/app/app-routing.module.ts index 044a5a711..1ee26de2c 100644 --- a/gui-js/apps/minsky-web/src/app/app-routing.module.ts +++ b/gui-js/apps/minsky-web/src/app/app-routing.module.ts @@ -151,7 +151,7 @@ const routes: Routes = [ component: VariablePaneComponent, }, { - path: 'login', + path: 'headless/login', component: LoginComponent, }, { diff --git a/gui-js/apps/minsky-web/src/app/app.component.html b/gui-js/apps/minsky-web/src/app/app.component.html index e1e681d35..ca19e9439 100644 --- a/gui-js/apps/minsky-web/src/app/app.component.html +++ b/gui-js/apps/minsky-web/src/app/app.component.html @@ -5,7 +5,7 @@ } @if (!loading) { - @if (router.url.includes('headless') || router.url.includes('login')) { + @if (router.url.includes('headless')) { } @else { From 6f7259df6bb82d115663dd14153ec2aedd2339ab Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 31 Mar 2026 05:45:41 +0000 Subject: [PATCH 07/12] Add openLoginWindow() promise that resolves when auth token is stored Agent-Logs-Url: https://github.com/highperformancecoder/minsky/sessions/f860580e-1854-46ba-96ed-117e3d0cfdce Co-authored-by: highperformancecoder <3075825+highperformancecoder@users.noreply.github.com> --- .../src/app/events/electron.events.ts | 36 +++++++++++++++++++ .../app/managers/ApplicationMenuManager.ts | 11 ++---- 2 files changed, 39 insertions(+), 8 deletions(-) diff --git a/gui-js/apps/minsky-electron/src/app/events/electron.events.ts b/gui-js/apps/minsky-electron/src/app/events/electron.events.ts index fca0210fa..8f5a48a9f 100644 --- a/gui-js/apps/minsky-electron/src/app/events/electron.events.ts +++ b/gui-js/apps/minsky-electron/src/app/events/electron.events.ts @@ -39,6 +39,38 @@ export default class ElectronEvents { } } +// Pending resolver for the auth-token promise created by openLoginWindow() +let _resolveAuthToken: ((token: string | null) => void) | null = null; + +/** + * Open the Clerk login popup and return a Promise that resolves with the JWT + * token once the user successfully authenticates (or null if the window is + * closed before authentication completes). + */ +export function openLoginWindow(): Promise { + const promise = new Promise((resolve) => { + _resolveAuthToken = resolve; + }); + + const loginWindow = WindowManager.createPopupWindowWithRouting({ + width: 420, + height: 500, + title: 'Login', + modal: false, + url: '#/headless/login', + }); + + // Resolve with null if the user closes the window before authenticating + loginWindow.once('closed', () => { + if (_resolveAuthToken) { + _resolveAuthToken(null); + _resolveAuthToken = null; + } + }); + + return promise; +} + ipcMain.handle(events.LOG_MESSAGE, async (event, message: string)=>{ return await CppClass.logMessage(message); }); @@ -295,5 +327,9 @@ ipcMain.handle(events.SET_AUTH_TOKEN, async (event, token: string | null) => { } else { StoreManager.store.delete('authToken'); } + if (_resolveAuthToken) { + _resolveAuthToken(token); + _resolveAuthToken = null; + } return { success: true }; }); diff --git a/gui-js/apps/minsky-electron/src/app/managers/ApplicationMenuManager.ts b/gui-js/apps/minsky-electron/src/app/managers/ApplicationMenuManager.ts index aa54a60fc..144142219 100644 --- a/gui-js/apps/minsky-electron/src/app/managers/ApplicationMenuManager.ts +++ b/gui-js/apps/minsky-electron/src/app/managers/ApplicationMenuManager.ts @@ -17,6 +17,7 @@ import { StoreManager } from './StoreManager'; import { WindowManager } from './WindowManager'; import { BookmarkManager } from './BookmarkManager'; import { RecordingManager } from './RecordingManager'; +import { openLoginWindow } from '../events/electron.events'; //TODO:: Remove hardcoding of popup dimensions @@ -129,14 +130,8 @@ export class ApplicationMenuManager { }, { label: 'Login via Clerk', - click() { - WindowManager.createPopupWindowWithRouting({ - width: 420, - height: 500, - title: 'Login', - modal: false, - url: '#/headless/login', - }); + async click() { + await openLoginWindow(); }, }, From 60f50ea80651875e6b36d1edd037202e439511b7 Mon Sep 17 00:00:00 2001 From: Russell Standish Date: Tue, 31 Mar 2026 19:14:42 +1100 Subject: [PATCH 08/12] Rearrange promise from events to WindowManager --- .../src/app/events/electron.events.ts | 38 ++----------------- .../app/managers/ApplicationMenuManager.ts | 3 +- .../src/app/managers/WindowManager.ts | 27 +++++++++++++ .../src/lib/services/clerk/clerk.service.ts | 12 +----- 4 files changed, 32 insertions(+), 48 deletions(-) diff --git a/gui-js/apps/minsky-electron/src/app/events/electron.events.ts b/gui-js/apps/minsky-electron/src/app/events/electron.events.ts index 8f5a48a9f..968c29432 100644 --- a/gui-js/apps/minsky-electron/src/app/events/electron.events.ts +++ b/gui-js/apps/minsky-electron/src/app/events/electron.events.ts @@ -39,38 +39,6 @@ export default class ElectronEvents { } } -// Pending resolver for the auth-token promise created by openLoginWindow() -let _resolveAuthToken: ((token: string | null) => void) | null = null; - -/** - * Open the Clerk login popup and return a Promise that resolves with the JWT - * token once the user successfully authenticates (or null if the window is - * closed before authentication completes). - */ -export function openLoginWindow(): Promise { - const promise = new Promise((resolve) => { - _resolveAuthToken = resolve; - }); - - const loginWindow = WindowManager.createPopupWindowWithRouting({ - width: 420, - height: 500, - title: 'Login', - modal: false, - url: '#/headless/login', - }); - - // Resolve with null if the user closes the window before authenticating - loginWindow.once('closed', () => { - if (_resolveAuthToken) { - _resolveAuthToken(null); - _resolveAuthToken = null; - } - }); - - return promise; -} - ipcMain.handle(events.LOG_MESSAGE, async (event, message: string)=>{ return await CppClass.logMessage(message); }); @@ -327,9 +295,9 @@ ipcMain.handle(events.SET_AUTH_TOKEN, async (event, token: string | null) => { } else { StoreManager.store.delete('authToken'); } - if (_resolveAuthToken) { - _resolveAuthToken(token); - _resolveAuthToken = null; + if (WindowManager._resolveAuthToken) { + WindowManager._resolveAuthToken(token); + WindowManager._resolveAuthToken = null; } return { success: true }; }); diff --git a/gui-js/apps/minsky-electron/src/app/managers/ApplicationMenuManager.ts b/gui-js/apps/minsky-electron/src/app/managers/ApplicationMenuManager.ts index 144142219..e61af5177 100644 --- a/gui-js/apps/minsky-electron/src/app/managers/ApplicationMenuManager.ts +++ b/gui-js/apps/minsky-electron/src/app/managers/ApplicationMenuManager.ts @@ -17,7 +17,6 @@ import { StoreManager } from './StoreManager'; import { WindowManager } from './WindowManager'; import { BookmarkManager } from './BookmarkManager'; import { RecordingManager } from './RecordingManager'; -import { openLoginWindow } from '../events/electron.events'; //TODO:: Remove hardcoding of popup dimensions @@ -131,7 +130,7 @@ export class ApplicationMenuManager { { label: 'Login via Clerk', async click() { - await openLoginWindow(); + await WindowManager.openLoginWindow(); }, }, diff --git a/gui-js/apps/minsky-electron/src/app/managers/WindowManager.ts b/gui-js/apps/minsky-electron/src/app/managers/WindowManager.ts index f97f76e31..c7a1bdb89 100644 --- a/gui-js/apps/minsky-electron/src/app/managers/WindowManager.ts +++ b/gui-js/apps/minsky-electron/src/app/managers/WindowManager.ts @@ -28,6 +28,8 @@ export class WindowManager { static canvasWidth: number; static scaleFactor: number; static currentTab=minsky.canvas as RenderNativeWindow; + // Pending resolver for the auth-token promise created by openLoginWindow() + static _resolveAuthToken: ((token: string | null) => void) | null = null; static activeWindows = new Map(); private static uidToWindowMap = new Map(); @@ -381,4 +383,29 @@ export class WindowManager { catch (err) {} // absorb any exceptions due to windows disappearing } } + + static async openLoginWindow() { + const promise = new Promise((resolve) => { + WindowManager._resolveAuthToken = resolve; + }); + + const loginWindow = WindowManager.createPopupWindowWithRouting({ + width: 420, + height: 500, + title: 'Login', + modal: false, + url: '#/headless/login', + }); + + // Resolve with null if the user closes the window before authenticating + loginWindow.once('closed', () => { + if (WindowManager._resolveAuthToken) { + WindowManager._resolveAuthToken(null); + WindowManager._resolveAuthToken = null; + } + }); + + return promise; +} + } diff --git a/gui-js/libs/core/src/lib/services/clerk/clerk.service.ts b/gui-js/libs/core/src/lib/services/clerk/clerk.service.ts index 439ee6f05..a4119d03c 100644 --- a/gui-js/libs/core/src/lib/services/clerk/clerk.service.ts +++ b/gui-js/libs/core/src/lib/services/clerk/clerk.service.ts @@ -15,17 +15,7 @@ export class ClerkService { async initialize(): Promise { if (this.initialized) return; - const publishableKey = (window as any).__clerkPublishableKey - ?? (typeof process !== 'undefined' && process.env?.['CLERK_PUBLISHABLE_KEY']) - ?? ''; - - if (!publishableKey) { - console.warn( - 'ClerkService: No publishable key found in window.__clerkPublishableKey or ' + - 'CLERK_PUBLISHABLE_KEY environment variable. Authentication will not be available.' - ); - return; - } + const publishableKey = 'pk_test_cG9zaXRpdmUtcGhvZW5peC04NS5jbGVyay5hY2NvdW50cy5kZXYk'; this.clerk = new Clerk(publishableKey); await this.clerk.load(); From 8da2877511f4f157d4366a0f4f056a2a40d84129 Mon Sep 17 00:00:00 2001 From: Russell Standish Date: Tue, 31 Mar 2026 19:21:59 +1100 Subject: [PATCH 09/12] Close login dialog window once submitted. --- gui-js/libs/ui-components/src/lib/login/login.component.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/gui-js/libs/ui-components/src/lib/login/login.component.ts b/gui-js/libs/ui-components/src/lib/login/login.component.ts index 312d52227..7ddf298f8 100644 --- a/gui-js/libs/ui-components/src/lib/login/login.component.ts +++ b/gui-js/libs/ui-components/src/lib/login/login.component.ts @@ -6,6 +6,7 @@ import { MatInputModule } from '@angular/material/input'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { ClerkService } from '@minsky/core'; +import { ElectronService } from '@minsky/core'; @Component({ selector: 'minsky-login', @@ -31,7 +32,7 @@ export class LoginComponent implements OnInit { errorMessage = ''; isAuthenticated = false; - constructor(private clerkService: ClerkService) {} + constructor(private clerkService: ClerkService, private electronService: ElectronService) {} async ngOnInit() { try { @@ -68,6 +69,7 @@ export class LoginComponent implements OnInit { } finally { this.isLoading = false; } + this.electronService.closeWindow(); } async onSignOut() { @@ -81,5 +83,6 @@ export class LoginComponent implements OnInit { } finally { this.isLoading = false; } + this.electronService.closeWindow(); } } From 59e8663f394dd209b05f60af510d3adab82d0b4d Mon Sep 17 00:00:00 2001 From: Russell Standish Date: Wed, 1 Apr 2026 17:50:54 +1100 Subject: [PATCH 10/12] Got the upgradeUsingClerk process working. --- .../app/managers/ApplicationMenuManager.ts | 12 +- .../src/app/managers/CommandsManager.ts | 200 ++++++++++++++---- 2 files changed, 170 insertions(+), 42 deletions(-) diff --git a/gui-js/apps/minsky-electron/src/app/managers/ApplicationMenuManager.ts b/gui-js/apps/minsky-electron/src/app/managers/ApplicationMenuManager.ts index e61af5177..a8e455e39 100644 --- a/gui-js/apps/minsky-electron/src/app/managers/ApplicationMenuManager.ts +++ b/gui-js/apps/minsky-electron/src/app/managers/ApplicationMenuManager.ts @@ -128,12 +128,14 @@ export class ApplicationMenuManager { }, }, { - label: 'Login via Clerk', - async click() { - await WindowManager.openLoginWindow(); - }, + label: 'Upgrade via Clerk', + click() {CommandsManager.upgradeUsingClerk();}, + }, + { + label: 'Logout Clerk', + click() {WindowManager.openLoginWindow();}, }, - + { label: 'New System', accelerator: 'CmdOrCtrl + Shift + N', diff --git a/gui-js/apps/minsky-electron/src/app/managers/CommandsManager.ts b/gui-js/apps/minsky-electron/src/app/managers/CommandsManager.ts index cb10e0d9b..4c890dff8 100644 --- a/gui-js/apps/minsky-electron/src/app/managers/CommandsManager.ts +++ b/gui-js/apps/minsky-electron/src/app/managers/CommandsManager.ts @@ -26,6 +26,67 @@ import ProgressBar from 'electron-progressbar'; import {exec,spawn} from 'child_process'; import decompress from 'decompress'; import {promisify} from 'util'; +import {net} from 'electron'; + +function semVer(version: string) { + const pattern=/(\d+)\.(\d+)\.(\d+)/; + let [,major,minor,patch]=pattern.exec(version); + return {major: +major,minor: +minor, patch: +patch}; +} +function semVerLess(x: string, y: string): boolean { + let xver=semVer(x), yver=semVer(y); + return xver.major((resolve, reject)=> { + let request=net.request(options); + request.setHeader('Authorization',`Bearer ${token}`); + request.on('response', (response)=>{ + let chunks=[]; + response.on('data', (chunk)=>{chunks.push(chunk);}); + response.on('end', ()=>resolve(Buffer.concat(chunks).toString())); + response.on('error',()=>reject(response.statusMessage)); + }); + request.on('error',(err)=>reject(err.toString())); + request.end(); + }); +} + +// to handle redirects +async function getFinalUrl(initialUrl, token) { +try { + const response = await fetch(initialUrl, { + method: 'GET', + headers: { + 'Authorization': `Bearer ${token}` + }, + redirect: 'manual' // This tells fetch NOT to follow the link automatically + }); + + // 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 (response.ok) return initialUrl; + + throw new Error(`Server responded with ${response.status}`); + } catch (error) { + // If redirect: 'manual' is used, fetch might throw a 'TypeError' + // when it hits the redirect—this is actually what we want to catch. + console.error("Fetch encountered the redirect/error:", error); + throw error; + } +} export class CommandsManager { static activeGodleyWindowItems = new Map(); @@ -1135,8 +1196,12 @@ export class CommandsManager { RecentFilesManager.updateNumberOfRecentFilesToDisplay(); } + //static activeDownloads=new Set(); + // handler for downloading Ravel and installing it static downloadRavel(event,item,webContents) { + //CommandsManager.activeDownloads.add(item); + switch (process.platform) { case 'win32': const savePath=dirname(process.execPath)+'/libravel.dll'; @@ -1158,7 +1223,8 @@ export class CommandsManager { // handler for when download completed item.once('done', (event,state)=>{ progress.close(); - + //CommandsManager.activeDownloads.delete(item); + if (state==='completed') { dialog.showMessageBoxSync(WindowManager.getMainWindow(),{ message: 'Ravel plugin updated successfully - restart Ravel to use', @@ -1308,6 +1374,45 @@ export class CommandsManager { modal: false, }); } + + // return information about the current system + static async buildState(previous: boolean) { + // need to pass what platform we are + let state; + switch (process.platform) { + case 'win32': + state={system: 'windows', distro: '', version: '', arch:'', previous: ''}; + break; + case 'darwin': + state={system: 'macos', distro: '', version: '', arch: `${process.arch}`, previous: ''}; + break; + 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; + 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; + } + if (await minsky.ravelAvailable() && previous) + state.previous=/[^:]*/.exec(await minsky.ravelVersion())[0]; + return state; + } static async upgrade(installCase: InstallCase=InstallCase.theLot) { const window=this.createDownloadWindow(); @@ -1344,7 +1449,7 @@ export class CommandsManager { } if (ravelFile) { // currently on latest, so reinstall ravel - window.webContents.session.on('will-download',this.downloadRavel); + window.webContents.session.on('will-download',this.downloadRavel); window.webContents.downloadURL(ravelFile); return; } @@ -1358,43 +1463,64 @@ export class CommandsManager { }); let clientId='-PiL7snNmZL_BlLJTPm62SHBcFTMG5d46m2336r118mfrp6sz4ty0g-thbKAs76c'; - // need to pass what platform we are - let state; - switch (process.platform) { - case 'win32': - state={system: 'windows', distro: '', version: '', arch:'', previous: ''}; - break; - case 'darwin': - state={system: 'macos', distro: '', version: '', arch: `${process.arch}`, previous: ''}; - break; - 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; - 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; - } - if (await minsky.ravelAvailable() && installCase===InstallCase.previousRavel) - state.previous=/[^:]*/.exec(await minsky.ravelVersion())[0]; - let encodedState=encodeURI(JSON.stringify(state)); + let encodedState=encodeURI(JSON.stringify(await CommandsManager.buildState(installCase==InstallCase.previousRavel))); // load patreon's login page window.loadURL(`https://www.patreon.com/oauth2/authorize?response_type=code&client_id=${clientId}&redirect_uri=https://ravelation.net/ravel-downloader.cgi&scope=identity%20identity%5Bemail%5D&state=${encodedState}`); } + + + // gets release URL for current system from Ravelation.net backend + 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 ""; + } + } + static async upgradeUsingClerk(installCase: InstallCase=InstallCase.theLot) { + while (!StoreManager.store.get('authToken')) + await WindowManager.openLoginWindow(); + let token=StoreManager.store.get('authToken'); + + const window=WindowManager.getMainWindow();//this.createDownloadWindow(); + let minskyAsset; + if (installCase===InstallCase.theLot) + minskyAsset=await CommandsManager.getRelease('minsky', false, token); + let ravelAsset=await CommandsManager.getRelease('ravel', installCase===InstallCase.previousRavel, token); + + if (minskyAsset) { + if (ravelAsset) { // stash ravel upgrade to be installed on next startup + StoreManager.store.set('ravelPlugin',await getFinalUrl(ravelAsset)); + } + window.webContents.session.on('will-download',this.downloadMinsky); + 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)); + return; + } + dialog.showMessageBoxSync(WindowManager.getMainWindow(),{ + message: "Everything's up to date, nothing to do.\n"+ + "If you're trying to download the Ravel plugin, please ensure you are logged into an account subscribed to Ravel Fan or Explorer tiers.", + type: 'info', + }); + //window.close(); + } + } From 6539e6e7d1e2554abd6c423df80a9719f12ee138 Mon Sep 17 00:00:00 2001 From: Russell Standish Date: Wed, 1 Apr 2026 17:52:09 +1100 Subject: [PATCH 11/12] Dead code removal --- .../apps/minsky-electron/src/app/managers/CommandsManager.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/gui-js/apps/minsky-electron/src/app/managers/CommandsManager.ts b/gui-js/apps/minsky-electron/src/app/managers/CommandsManager.ts index 4c890dff8..5e5e8aef9 100644 --- a/gui-js/apps/minsky-electron/src/app/managers/CommandsManager.ts +++ b/gui-js/apps/minsky-electron/src/app/managers/CommandsManager.ts @@ -1497,7 +1497,7 @@ export class CommandsManager { await WindowManager.openLoginWindow(); let token=StoreManager.store.get('authToken'); - const window=WindowManager.getMainWindow();//this.createDownloadWindow(); + const window=WindowManager.getMainWindow(); let minskyAsset; if (installCase===InstallCase.theLot) minskyAsset=await CommandsManager.getRelease('minsky', false, token); @@ -1520,7 +1520,6 @@ export class CommandsManager { "If you're trying to download the Ravel plugin, please ensure you are logged into an account subscribed to Ravel Fan or Explorer tiers.", type: 'info', }); - //window.close(); } } From 8663d100d6f278c1a9c4afe27e491034cd47fbac Mon Sep 17 00:00:00 2001 From: Russell Standish Date: Wed, 1 Apr 2026 17:54:12 +1100 Subject: [PATCH 12/12] More dead code removal. --- .../apps/minsky-electron/src/app/managers/CommandsManager.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/gui-js/apps/minsky-electron/src/app/managers/CommandsManager.ts b/gui-js/apps/minsky-electron/src/app/managers/CommandsManager.ts index 5e5e8aef9..c5f11f05c 100644 --- a/gui-js/apps/minsky-electron/src/app/managers/CommandsManager.ts +++ b/gui-js/apps/minsky-electron/src/app/managers/CommandsManager.ts @@ -1196,11 +1196,8 @@ export class CommandsManager { RecentFilesManager.updateNumberOfRecentFilesToDisplay(); } - //static activeDownloads=new Set(); - // handler for downloading Ravel and installing it static downloadRavel(event,item,webContents) { - //CommandsManager.activeDownloads.add(item); switch (process.platform) { case 'win32': @@ -1223,7 +1220,6 @@ export class CommandsManager { // handler for when download completed item.once('done', (event,state)=>{ progress.close(); - //CommandsManager.activeDownloads.delete(item); if (state==='completed') { dialog.showMessageBoxSync(WindowManager.getMainWindow(),{