Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions gui-js/apps/minsky-electron/src/app/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,12 @@ export default class App {
await HelpFilesManager.initialize(helpFilesFolder);
App.initMainWindow();
let ravelPlugin=StoreManager.store.get('ravelPlugin');
if (ravelPlugin) {
if (ravelPlugin?.download_url) {
// if this is set (after a full reinstall of Minsky, kick off updating the Ravel plugin)
App.mainWindow.webContents.downloadURL(ravelPlugin);
App.mainWindow.webContents.session.on('will-download',CommandsManager.downloadRavel);
App.mainWindow.webContents.downloadURL(ravelPlugin.download_url);
App.mainWindow.webContents.session.on('will-download',(event,item,webContents)=>{
CommandsManager.downloadRavel(event,item,webContents,ravelPlugin);
});
StoreManager.store.set('ravelPlugin','');
Comment thread
highperformancecoder marked this conversation as resolved.
Outdated
}
Comment thread
highperformancecoder marked this conversation as resolved.
// check if ravel is approaching its expiry date, and nag user to upgrade if so
Expand Down
104 changes: 86 additions & 18 deletions gui-js/apps/minsky-electron/src/app/managers/CommandsManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
CanvasItem,
ClassType,
CSVDialog,
DownloadDetails,
events,
Functions,
HandleDimensionPayload,
Expand All @@ -27,6 +28,8 @@ import {exec,spawn} from 'child_process';
import decompress from 'decompress';
import {promisify} from 'util';
import {net, safeStorage } from 'electron';
import { createReadStream, rmSync } from 'fs';
import { createHash, verify } from 'crypto';

function semVer(version: string) {
const pattern=/(\d+)\.(\d+)\.(\d+)/;
Expand Down Expand Up @@ -88,6 +91,37 @@ try {
}
}

const publicKey=Buffer.from('\n-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEA3v8OynE8ZrGrXR062RtF37xLxRBlvBEy8/6os7Z7P1c=\n-----END PUBLIC KEY-----');

async function verifyFile(filePath: string, signature: string): Promise<boolean> {
return new Promise((resolve, reject) => {
// 1. Create a SHA-512 hash stream (Standard for Ed25519ph)
const hash = createHash('sha512');
const stream = createReadStream(filePath);

stream.on('data', (chunk) => hash.update(chunk));
stream.on('error', (err) => reject(err));

stream.on('end', () => {
const digest = hash.digest();

// 2. Verify using the 'ed25519' algorithm with the digest
// In Node.js, for Ed25519ph, we pass the digest and set dsaEncoding
Comment thread
highperformancecoder marked this conversation as resolved.
Outdated
const isValid = verify(
undefined,
digest,
{
key: publicKey,
format: 'pem',
},

Copilot AI Apr 21, 2026

Copy link

Choose a reason for hiding this comment

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

crypto.verify does not accept a { key, format } object; format is not a valid property for VerifyKeyObjectInput and will fail TypeScript excess-property checks (and may be ignored/unsupported at runtime). Pass the PEM as the key directly (string/Buffer/KeyObject), or create a KeyObject via createPublicKey and pass that.

Suggested change
// In Node.js, for Ed25519ph, we pass the digest and set dsaEncoding
const isValid = verify(
undefined,
digest,
{
key: publicKey,
format: 'pem',
},
// Pass the PEM-encoded public key directly to crypto.verify
const isValid = verify(
undefined,
digest,
publicKey,

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

According to the doc, passing an Object implicitly creates a KeyObject from createPublicKey. What is wrong with that?

Buffer.from(signature,'base64')
);

resolve(isValid);
});
});
}
Comment thread
highperformancecoder marked this conversation as resolved.

export class CommandsManager {
static activeGodleyWindowItems = new Map<string, CanvasItem>();
static currentMinskyModelFilePath: string;
Expand Down Expand Up @@ -1197,7 +1231,7 @@ export class CommandsManager {
}

// handler for downloading Ravel and installing it
static downloadRavel(event,item,webContents) {
static downloadRavel(event,item,webContents,asset: DownloadDetails=null) {

switch (process.platform) {
case 'win32':
Expand All @@ -1218,10 +1252,23 @@ export class CommandsManager {
let progress=new ProgressBar({text:"Downloading Ravel",value: 0, indeterminate:false, closeOnComplete: true,});

// handler for when download completed
item.once('done', (event,state)=>{
item.once('done', async (event,state)=>{
progress.close();

if (state==='completed') {
// validate signature
if (asset &&
(!asset.signature || asset.signature_algorithm!=='ed25519' ||
!await verifyFile(item.getSavePath(), asset.signature))) {
Comment thread
highperformancecoder marked this conversation as resolved.
Outdated
await rmSync(item.getSavePath()); // failed validation, remove
dialog.showMessageBoxSync(WindowManager.getMainWindow(),{
Comment thread
highperformancecoder marked this conversation as resolved.
Outdated
message: 'Download has invalid signature, removed for safety',
type: 'error',
});
webContents.close();
return;
}

if (state==='completed') {
dialog.showMessageBoxSync(WindowManager.getMainWindow(),{
message: 'Ravel plugin updated successfully - restart Ravel to use',
type: 'info',
Expand Down Expand Up @@ -1252,16 +1299,26 @@ export class CommandsManager {
}

// handler for downloading Minsky
static downloadMinsky(event,item,webContents) {
static downloadMinsky(event,item,webContents,asset: DownloadDetails=null) {
item.setSavePath(join(tmpdir(),item.getFilename()));

let progress=new ProgressBar({text:"Downloading Ravel application",value: 0, indeterminate:false, closeOnComplete: true,});

// handler for when download completed
item.once('done', (event,state)=>{
item.once('done', async (event,state)=>{
progress.close();


if (state==='completed') {

// validate signature
if (asset &&
(!asset.signature || asset.signature_algorithm!=='ed25519' ||
!await verifyFile(item.getSavePath(), asset.signature))) {
await rmSync(item.getSavePath()); // failed validation, remove
throw 'Download has invalid signature, removed for safety';
Comment thread
highperformancecoder marked this conversation as resolved.
Outdated

Copilot AI Apr 21, 2026

Copy link

Choose a reason for hiding this comment

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

Throwing a string (throw '...') loses stack/context and makes error handling inconsistent. Throw an Error instance instead so callers (and the catch block) get a meaningful message + stack trace.

Suggested change
throw 'Download has invalid signature, removed for safety';
throw new Error('Download has invalid signature, removed for safety');

Copilot uses AI. Check for mistakes.
}

Comment thread
highperformancecoder marked this conversation as resolved.
switch (process.platform) {
case 'win32':
spawn(item.getSavePath(),{detached: true, stdio: 'ignore'});
Expand Down Expand Up @@ -1470,21 +1527,21 @@ export class CommandsManager {


// gets release URL for current system from Ravelation.net backend
static async getRelease(product: string, previous: boolean, token: string) {
static async getRelease(product: string, previous: boolean, token: string): Promise<DownloadDetails> {
let state=await CommandsManager.buildState(previous);
if (!state) return '';
if (!state) return null;
let query=`product=${product}&os=${state.system}&arch=${state.arch}&distro=${state.distro}&distro_version=${state.version}`;
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 (prevRelease) return prevRelease;
// if not, then treat the request as latest
}
let release=JSON.parse(await callBackendAPI(`${backendAPI}/releases/latest?${query}`, token));
return release?.release?.download_url;
return release?.release;
}
Comment thread
highperformancecoder marked this conversation as resolved.
Outdated

static stashClerkToken(token: string) {
Expand All @@ -1510,22 +1567,33 @@ export class CommandsManager {
token=safeStorage.decryptString(Buffer.from(token, 'latin1'));

const window=WindowManager.getMainWindow();
let minskyAsset;
try {
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,token));
// rewrite any redirect
if (ravelAsset?.download_url)
ravelAsset.download_url=await getFinalUrl(ravelAsset.download_url,token);


if (minskyAsset?.download_url) {
if (ravelAsset?.download_url) {
// stash ravel upgrade to be installed on next startup
StoreManager.store.set('ravelPlugin',ravelAsset);
}
window.webContents.session.on('will-download',this.downloadMinsky);
window.webContents.downloadURL(await getFinalUrl(minskyAsset,token));

window.webContents.session.on('will-download',(event,item,webContents)=>{
this.downloadMinsky(event,item,webContents,minskyAsset);
});
window.webContents.downloadURL(await getFinalUrl(minskyAsset.download_url,token));
return;
} else if (ravelAsset) {
window.webContents.session.on('will-download',this.downloadRavel);
window.webContents.downloadURL(await getFinalUrl(ravelAsset,token));
} else if (ravelAsset?.download_url) {
window.webContents.session.on('will-download',(event,item,webContents)=>{
this.downloadRavel(event,item,webContents,ravelAsset);
});
window.webContents.downloadURL(ravelAsset.download_url);
return;
}
dialog.showMessageBoxSync(WindowManager.getMainWindow(),{
Expand Down
6 changes: 3 additions & 3 deletions gui-js/apps/minsky-electron/src/app/managers/StoreManager.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { defaultBackgroundColor } from '@minsky/shared';
import { defaultBackgroundColor, DownloadDetails } from '@minsky/shared';
import Store from 'electron-store';
import {homedir} from 'node:os';

Expand All @@ -18,7 +18,7 @@ interface MinskyStore {
preferences: MinskyPreferences;
defaultModelDirectory: string;
defaultDataDirectory: string;
ravelPlugin: string; // used for post installation installation of Ravel
ravelPlugin: DownloadDetails; // used for post installation installation of Ravel

Copilot AI Apr 21, 2026

Copy link

Choose a reason for hiding this comment

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

ravelPlugin is declared as DownloadDetails, but the store defaults set it to null (and other code clears it to a string). Please make the store type reflect reality (e.g., DownloadDetails | null, or make it optional and use delete), so TypeScript and persisted data stay consistent.

Suggested change
ravelPlugin: DownloadDetails; // used for post installation installation of Ravel
ravelPlugin: DownloadDetails | null; // used for post installation installation of Ravel

Copilot uses AI. Check for mistakes.

Copilot AI Apr 21, 2026

Copy link

Choose a reason for hiding this comment

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

Typo in comment: "post installation installation" repeats a word; please correct it to avoid confusion.

Suggested change
ravelPlugin: DownloadDetails; // used for post installation installation of Ravel
ravelPlugin: DownloadDetails; // used for post installation of Ravel

Copilot uses AI. Check for mistakes.
Comment thread
highperformancecoder marked this conversation as resolved.
Outdated
authToken?: string;
}

Expand All @@ -39,7 +39,7 @@ class StoreManager {
font: "",
numBackups: 1,
},
ravelPlugin: '',
ravelPlugin: null,
},
});
}
Expand Down
6 changes: 6 additions & 0 deletions gui-js/libs/shared/src/lib/interfaces/Interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,12 @@ export interface CreateWindowPayload {
raiseIfPresent?: boolean; ///< if true, then raise an existing window instead of creating a duplicate
}

export interface DownloadDetails {
download_url: string;
signature?: string;
signature_algorithm?: string;
}


export interface TypeValueName {
type : string,
Expand Down
Loading