Add Electron wrapper for NovaTune Player desktop app with secure token storage (project)
Package the NovaTune Player as a desktop application using Electron with security hardening and OS keychain integration.
This skill adds an Electron wrapper to apps/player for:
apps/player-electron/
├── package.json
├── electron-builder.yml
├── tsconfig.json
├── src/
│ ├── main/
│ │ ├── index.ts
│ │ ├── preload.ts
│ │ ├── ipc-handlers.ts
│ │ └── secure-storage.ts
│ └── renderer/
│ └── (symlink or copy of apps/player/dist)
└── resources/
├── icon.icns (macOS)
├── icon.ico (Windows)
└── icon.png (Linux)
import { app, BrowserWindow, ipcMain } from 'electron';
import { join } from 'path';
import { setupIpcHandlers } from './ipc-handlers';
const isDev = process.env.NODE_ENV === 'development';
const createWindow = () => {
const win = new BrowserWindow({
width: 1200,
height: 800,
minWidth: 800,
minHeight: 600,
webPreferences: {
preload: join(__dirname, 'preload.js'),
contextIsolation: true,
nodeIntegration: false,
sandbox: true,
webSecurity: true,
},
titleBarStyle: 'hiddenInset', // macOS
frame: true,
backgroundColor: '#1a1a1a',
});
// Content Security Policy
win.webContents.session.webRequest.onHeadersReceived((details, callback) => {
callback({
responseHeaders: {
...details.responseHeaders,
'Content-Security-Policy': [
"default-src 'self'",
"script-src 'self'",
"style-src 'self' 'unsafe-inline'",
"img-src 'self' data: blob: https:",
"media-src 'self' blob: https:",
"connect-src 'self' http://localhost:* https://*.novatune.dev",
].join('; '),
},
});
});
if (isDev) {
win.loadURL('http://localhost:5173');
win.webContents.openDevTools();
} else {
win.loadFile(join(__dirname, '../renderer/index.html'));
}
return win;
};
app.whenReady().then(() => {
setupIpcHandlers();
createWindow();
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
});
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
});
import { contextBridge, ipcRenderer } from 'electron';
// Expose secure API to renderer
contextBridge.exposeInMainWorld('electronAPI', {
// Secure storage
getSecureToken: (): Promise<string | null> =>
ipcRenderer.invoke('secure-storage:get'),
setSecureToken: (token: string): Promise<void> =>
ipcRenderer.invoke('secure-storage:set', token),
deleteSecureToken: (): Promise<void> =>
ipcRenderer.invoke('secure-storage:delete'),
// App info
getAppVersion: (): Promise<string> =>
ipcRenderer.invoke('app:version'),
// Platform
getPlatform: (): NodeJS.Platform => process.platform,
});
// Type declaration for renderer
declare global {
interface Window {
electronAPI: {
getSecureToken: () => Promise<string | null>;
setSecureToken: (token: string) => Promise<void>;
deleteSecureToken: () => Promise<void>;
getAppVersion: () => Promise<string>;
getPlatform: () => NodeJS.Platform;
};
}
}
import keytar from 'keytar';
const SERVICE_NAME = 'NovaTune';
const ACCOUNT_NAME = 'refresh_token';
export const secureStorage = {
async get(): Promise<string | null> {
try {
return await keytar.getPassword(SERVICE_NAME, ACCOUNT_NAME);
} catch (error) {
console.error('Failed to get secure token:', error);
return null;
}
},
async set(token: string): Promise<void> {
try {
await keytar.setPassword(SERVICE_NAME, ACCOUNT_NAME, token);
} catch (error) {
console.error('Failed to set secure token:', error);
throw error;
}
},
async delete(): Promise<void> {
try {
await keytar.deletePassword(SERVICE_NAME, ACCOUNT_NAME);
} catch (error) {
console.error('Failed to delete secure token:', error);
}
},
};
import { ipcMain, app } from 'electron';
import { secureStorage } from './secure-storage';
export function setupIpcHandlers(): void {
// Secure storage handlers
ipcMain.handle('secure-storage:get', async () => {
return secureStorage.get();
});
ipcMain.handle('secure-storage:set', async (_, token: string) => {
return secureStorage.set(token);
});
ipcMain.handle('secure-storage:delete', async () => {
return secureStorage.delete();
});
// App info
ipcMain.handle('app:version', () => {
return app.getVersion();
});
}
// Platform-aware token storage
export interface TokenStorage {
getRefreshToken(): Promise<string | null>;
setRefreshToken(token: string): Promise<void>;
clearRefreshToken(): Promise<void>;
}
// Detect platform and return appropriate storage
export function createTokenStorage(): TokenStorage {
// Electron
if (typeof window !== 'undefined' && window.electronAPI) {
return {
getRefreshToken: () => window.electronAPI.getSecureToken(),
setRefreshToken: (token) => window.electronAPI.setSecureToken(token),
clearRefreshToken: () => window.electronAPI.deleteSecureToken(),
};
}
// Web fallback
return {
getRefreshToken: () => Promise.resolve(localStorage.getItem('refresh_token')),
setRefreshToken: (token) => {
localStorage.setItem('refresh_token', token);
return Promise.resolve();
},
clearRefreshToken: () => {
localStorage.removeItem('refresh_token');
return Promise.resolve();
},
};
}
{
"name": "novatune-player-desktop",
"version": "1.0.0",
"description": "NovaTune Music Player for Desktop",
"main": "dist/main/index.js",
"scripts": {
"dev": "concurrently \"pnpm dev:main\" \"pnpm dev:renderer\"",
"dev:main": "tsc -p tsconfig.main.json -w",
"dev:renderer": "pnpm --filter player dev",
"build": "pnpm build:renderer && pnpm build:main && electron-builder",
"build:main": "tsc -p tsconfig.main.json",
"build:renderer": "pnpm --filter player build && cp -r ../player/dist dist/renderer",
"pack": "electron-builder --dir",
"dist": "electron-builder"
},
"dependencies": {
"keytar": "^7.9.0"
},
"devDependencies": {
"electron": "^31.0.0",
"electron-builder": "^24.13.0",
"concurrently": "^8.2.0",
"typescript": "^5.6.0",
"@types/node": "^20.14.0"
},
"build": {
"extends": "./electron-builder.yml"
}
}
appId: dev.novatune.player
productName: NovaTune