Browse Source

WIP: Improve SQLite initialization and error handling

- Implement XDG Base Directory Specification for data storage
  - Use $XDG_DATA_HOME (defaults to ~/.local/share) for data files
  - Add proper directory permissions (700) for security
  - Fallback to ~/.timesafari if XDG paths fail

- Add graceful degradation for SQLite failures
  - Allow app to boot even if SQLite initialization fails
  - Track and expose initialization errors via IPC
  - Add availability checks to all SQLite operations
  - Improve error reporting and logging

- Security improvements
  - Set secure permissions (700) on data directories
  - Verify directory permissions on existing paths
  - Add proper error handling for permission issues

TODO:
- Fix database creation
- Add retry logic for initialization
- Add reinitialization capability
- Add more detailed error reporting
- Consider fallback storage options
pull/134/head
Matthew Raymer 1 week ago
parent
commit
900c2521c7
  1. 55
      electron/.gitignore
  2. BIN
      electron/assets/appIcon.ico
  3. BIN
      electron/assets/appIcon.png
  4. BIN
      electron/assets/splash.gif
  5. BIN
      electron/assets/splash.png
  6. 63
      electron/capacitor.config.json
  7. 28
      electron/electron-builder.config.json
  8. 75
      electron/live-runner.js
  9. 5243
      electron/package-lock.json
  10. 51
      electron/package.json
  11. 10
      electron/resources/electron-publisher-custom.js
  12. 110
      electron/src/index.ts
  13. 76
      electron/src/preload.ts
  14. 6
      electron/src/rt/electron-plugins.js
  15. 88
      electron/src/rt/electron-rt.ts
  16. 356
      electron/src/rt/sqlite-init.ts
  17. 244
      electron/src/setup.ts
  18. 18
      electron/tsconfig.json
  19. 81
      package-lock.json
  20. 24
      package.json

55
electron/.gitignore

@ -0,0 +1,55 @@
# NPM renames .gitignore to .npmignore
# In order to prevent that, we remove the initial "."
# And the CLI then renames it
app
node_modules
build
dist
logs
# Node.js dependencies
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# Capacitor build outputs
web/
ios/
android/
electron/app/
# Capacitor SQLite plugin data (important!)
capacitor-sqlite/
# TypeScript / build output
dist/
build/
*.log
# Development / IDE files
.env.local
.env.development.local
.env.test.local
.env.production.local
# VS Code
.vscode/
!.vscode/extensions.json
# JetBrains IDEs (IntelliJ, WebStorm, etc.)
.idea/
*.iml
*.iws
# macOS specific
.DS_Store
*.swp
*~
*.tmp
# Windows specific
Thumbs.db
ehthumbs.db
Desktop.ini
$RECYCLE.BIN/

BIN
electron/assets/appIcon.ico

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

BIN
electron/assets/appIcon.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

BIN
electron/assets/splash.gif

Binary file not shown.

After

Width:  |  Height:  |  Size: 159 KiB

BIN
electron/assets/splash.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

63
electron/capacitor.config.json

@ -0,0 +1,63 @@
{
"appId": "com.timesafari.app",
"appName": "TimeSafari",
"webDir": "dist",
"bundledWebRuntime": false,
"server": {
"cleartext": true,
"androidScheme": "https"
},
"plugins": {
"App": {
"appUrlOpen": {
"handlers": [
{
"url": "timesafari://*",
"autoVerify": true
}
]
}
},
"SQLite": {
"iosDatabaseLocation": "Library/CapacitorDatabase",
"iosIsEncryption": true,
"iosBiometric": {
"biometricAuth": true,
"biometricTitle": "Biometric login for TimeSafari"
},
"androidIsEncryption": true,
"androidBiometric": {
"biometricAuth": true,
"biometricTitle": "Biometric login for TimeSafari"
}
},
"CapacitorSQLite": {
"electronIsEncryption": false,
"electronMacLocation": "~/Library/Application Support/TimeSafari",
"electronWindowsLocation": "C:\\ProgramData\\TimeSafari",
"electronLinuxLocation": "~/.local/share/TimeSafari"
}
},
"ios": {
"contentInset": "always",
"allowsLinkPreview": true,
"scrollEnabled": true,
"limitsNavigationsToAppBoundDomains": true,
"backgroundColor": "#ffffff",
"allowNavigation": [
"*.timesafari.app",
"*.jsdelivr.net",
"api.endorser.ch"
]
},
"android": {
"allowMixedContent": false,
"captureInput": true,
"webContentsDebuggingEnabled": false,
"allowNavigation": [
"*.timesafari.app",
"*.jsdelivr.net",
"api.endorser.ch"
]
}
}

28
electron/electron-builder.config.json

@ -0,0 +1,28 @@
{
"appId": "com.yourdoamnin.yourapp",
"directories": {
"buildResources": "resources"
},
"files": [
"assets/**/*",
"build/**/*",
"capacitor.config.*",
"app/**/*"
],
"publish": {
"provider": "github"
},
"nsis": {
"allowElevation": true,
"oneClick": false,
"allowToChangeInstallationDirectory": true
},
"win": {
"target": "nsis",
"icon": "assets/appIcon.ico"
},
"mac": {
"category": "your.app.category.type",
"target": "dmg"
}
}

75
electron/live-runner.js

@ -0,0 +1,75 @@
/* eslint-disable no-undef */
/* eslint-disable @typescript-eslint/no-var-requires */
const cp = require('child_process');
const chokidar = require('chokidar');
const electron = require('electron');
let child = null;
const npmCmd = process.platform === 'win32' ? 'npm.cmd' : 'npm';
const reloadWatcher = {
debouncer: null,
ready: false,
watcher: null,
restarting: false,
};
///*
function runBuild() {
return new Promise((resolve, _reject) => {
let tempChild = cp.spawn(npmCmd, ['run', 'build']);
tempChild.once('exit', () => {
resolve();
});
tempChild.stdout.pipe(process.stdout);
});
}
//*/
async function spawnElectron() {
if (child !== null) {
child.stdin.pause();
child.kill();
child = null;
await runBuild();
}
child = cp.spawn(electron, ['--inspect=5858', './']);
child.on('exit', () => {
if (!reloadWatcher.restarting) {
process.exit(0);
}
});
child.stdout.pipe(process.stdout);
}
function setupReloadWatcher() {
reloadWatcher.watcher = chokidar
.watch('./src/**/*', {
ignored: /[/\\]\./,
persistent: true,
})
.on('ready', () => {
reloadWatcher.ready = true;
})
.on('all', (_event, _path) => {
if (reloadWatcher.ready) {
clearTimeout(reloadWatcher.debouncer);
reloadWatcher.debouncer = setTimeout(async () => {
console.log('Restarting');
reloadWatcher.restarting = true;
await spawnElectron();
reloadWatcher.restarting = false;
reloadWatcher.ready = false;
clearTimeout(reloadWatcher.debouncer);
reloadWatcher.debouncer = null;
reloadWatcher.watcher = null;
setupReloadWatcher();
}, 500);
}
});
}
(async () => {
await runBuild();
await spawnElectron();
setupReloadWatcher();
})();

5243
electron/package-lock.json

File diff suppressed because it is too large

51
electron/package.json

@ -0,0 +1,51 @@
{
"name": "TimeSafari",
"version": "1.0.0",
"description": "TimeSafari Electron App",
"author": {
"name": "",
"email": ""
},
"repository": {
"type": "git",
"url": ""
},
"license": "MIT",
"main": "build/src/index.js",
"scripts": {
"build": "tsc && electron-rebuild",
"electron:start-live": "node ./live-runner.js",
"electron:start": "npm run build && electron --inspect=5858 ./",
"electron:pack": "npm run build && electron-builder build --dir -c ./electron-builder.config.json",
"electron:make": "npm run build && electron-builder build -c ./electron-builder.config.json -p always"
},
"dependencies": {
"@capacitor-community/electron": "^5.0.0",
"@capacitor-community/sqlite": "^6.0.2",
"better-sqlite3-multiple-ciphers": "^11.10.0",
"chokidar": "~3.5.3",
"crypto": "^1.0.1",
"crypto-js": "^4.2.0",
"electron-is-dev": "~2.0.0",
"electron-json-storage": "^4.6.0",
"electron-serve": "~1.1.0",
"electron-unhandled": "~4.0.1",
"electron-updater": "^5.3.0",
"electron-window-state": "^5.0.3",
"jszip": "^3.10.1",
"node-fetch": "^2.6.7"
},
"devDependencies": {
"@types/better-sqlite3": "^7.6.13",
"@types/crypto-js": "^4.2.2",
"@types/electron-json-storage": "^4.5.4",
"electron": "^26.2.2",
"electron-builder": "~23.6.0",
"source-map-support": "^0.5.21",
"typescript": "^5.0.4"
},
"keywords": [
"capacitor",
"electron"
]
}

10
electron/resources/electron-publisher-custom.js

@ -0,0 +1,10 @@
/* eslint-disable no-undef */
/* eslint-disable @typescript-eslint/no-var-requires */
const electronPublish = require('electron-publish');
class Publisher extends electronPublish.Publisher {
async upload(task) {
console.log('electron-publisher-custom', task.file);
}
}
module.exports = Publisher;

110
electron/src/index.ts

@ -0,0 +1,110 @@
import type { CapacitorElectronConfig } from '@capacitor-community/electron';
import { getCapacitorElectronConfig, setupElectronDeepLinking } from '@capacitor-community/electron';
import type { MenuItemConstructorOptions } from 'electron';
import { app, MenuItem } from 'electron';
import electronIsDev from 'electron-is-dev';
import unhandled from 'electron-unhandled';
import { autoUpdater } from 'electron-updater';
import { ElectronCapacitorApp, setupContentSecurityPolicy, setupReloadWatcher } from './setup';
import { initializeSQLite, setupSQLiteHandlers } from './rt/sqlite-init';
// Graceful handling of unhandled errors.
unhandled();
// Define our menu templates (these are optional)
const trayMenuTemplate: (MenuItemConstructorOptions | MenuItem)[] = [new MenuItem({ label: 'Quit App', role: 'quit' })];
const appMenuBarMenuTemplate: (MenuItemConstructorOptions | MenuItem)[] = [
{ role: process.platform === 'darwin' ? 'appMenu' : 'fileMenu' },
{ role: 'viewMenu' },
];
// Get Config options from capacitor.config
const capacitorFileConfig: CapacitorElectronConfig = getCapacitorElectronConfig();
// Initialize our app. You can pass menu templates into the app here.
const myCapacitorApp = new ElectronCapacitorApp(capacitorFileConfig, trayMenuTemplate, appMenuBarMenuTemplate);
// If deeplinking is enabled then we will set it up here.
if (capacitorFileConfig.electron?.deepLinkingEnabled) {
setupElectronDeepLinking(myCapacitorApp, {
customProtocol: capacitorFileConfig.electron.deepLinkingCustomProtocol ?? 'mycapacitorapp',
});
}
// If we are in Dev mode, use the file watcher components.
if (electronIsDev) {
setupReloadWatcher(myCapacitorApp);
}
// Run Application
(async () => {
try {
// Wait for electron app to be ready.
await app.whenReady();
// Security - Set Content-Security-Policy based on whether or not we are in dev mode.
setupContentSecurityPolicy(myCapacitorApp.getCustomURLScheme());
// Initialize SQLite and register handlers BEFORE app initialization
console.log('[Main] Starting SQLite initialization...');
try {
// Register handlers first to prevent "no handler" errors
setupSQLiteHandlers();
console.log('[Main] SQLite handlers registered');
// Then initialize the plugin
await initializeSQLite();
console.log('[Main] SQLite plugin initialized successfully');
} catch (error) {
console.error('[Main] Failed to initialize SQLite:', error);
// Don't proceed with app initialization if SQLite fails
throw new Error(`SQLite initialization failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
// Initialize our app, build windows, and load content.
console.log('[Main] Starting app initialization...');
await myCapacitorApp.init();
console.log('[Main] App initialization complete');
// Check for updates if we are in a packaged app.
if (!electronIsDev) {
console.log('[Main] Checking for updates...');
autoUpdater.checkForUpdatesAndNotify();
}
} catch (error) {
console.error('[Main] Fatal error during app initialization:', error);
// Ensure we notify the user before quitting
const mainWindow = myCapacitorApp.getMainWindow();
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send('app-error', {
type: 'initialization',
error: error instanceof Error ? error.message : 'Unknown error'
});
// Give the window time to show the error
setTimeout(() => app.quit(), 5000);
} else {
app.quit();
}
}
})();
// Handle when all of our windows are close (platforms have their own expectations).
app.on('window-all-closed', function () {
// On OS X it is common for applications and their menu bar
// to stay active until the user quits explicitly with Cmd + Q
if (process.platform !== 'darwin') {
app.quit();
}
});
// When the dock icon is clicked.
app.on('activate', async function () {
// On OS X it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (myCapacitorApp.getMainWindow().isDestroyed()) {
await myCapacitorApp.init();
}
});
// Place all ipc or other electron api calls and custom functionality under this line

76
electron/src/preload.ts

@ -0,0 +1,76 @@
/**
* Preload script for Electron
* Sets up context bridge and handles security policies
* @author Matthew Raymer
*/
import { contextBridge, ipcRenderer } from 'electron';
// Enable source maps in development
if (process.env.NODE_ENV === 'development') {
require('source-map-support').install();
}
// Log that preload is running
console.log('[Preload] Script starting...');
// Create a proxy for the CapacitorSQLite plugin
const createSQLiteProxy = () => {
const MAX_RETRIES = 5;
const RETRY_DELAY = 1000; // 1 second
const withRetry = async (operation: string, ...args: any[]) => {
let lastError;
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
try {
return await ipcRenderer.invoke(`sqlite-${operation}`, ...args);
} catch (error) {
lastError = error;
console.warn(`[Preload] SQLite operation ${operation} failed (attempt ${attempt}/${MAX_RETRIES}), retrying...`, error);
if (attempt < MAX_RETRIES) {
await new Promise(resolve => setTimeout(resolve, RETRY_DELAY));
}
}
}
throw new Error(`SQLite operation ${operation} failed after ${MAX_RETRIES} attempts: ${lastError?.message || 'Unknown error'}`);
};
return {
echo: (value: string) => withRetry('echo', value),
createConnection: (options: any) => withRetry('create-connection', options),
closeConnection: (options: any) => withRetry('close-connection', options),
execute: (options: any) => withRetry('execute', options),
query: (options: any) => withRetry('query', options),
isAvailable: () => withRetry('is-available'),
getPlatform: () => Promise.resolve('electron')
};
};
// Set up context bridge for secure IPC communication
contextBridge.exposeInMainWorld('electronAPI', {
// SQLite operations
sqlite: createSQLiteProxy(),
// Database status events
onDatabaseStatus: (callback: (status: { status: string; error?: string }) => void) => {
ipcRenderer.on('database-status', (_event, status) => callback(status));
return () => {
ipcRenderer.removeAllListeners('database-status');
};
}
});
// Expose CapacitorSQLite globally for the plugin system
contextBridge.exposeInMainWorld('CapacitorSQLite', createSQLiteProxy());
// Handle uncaught errors
window.addEventListener('unhandledrejection', (event) => {
console.error('[Preload] Unhandled promise rejection:', event.reason);
});
window.addEventListener('error', (event) => {
console.error('[Preload] Unhandled error:', event.error);
});
// Log that preload is complete
console.log('[Preload] Script complete');

6
electron/src/rt/electron-plugins.js

@ -0,0 +1,6 @@
/* eslint-disable @typescript-eslint/no-var-requires */
const CapacitorCommunitySqlite = require('../../../node_modules/@capacitor-community/sqlite/electron/dist/plugin.js');
module.exports = {
CapacitorCommunitySqlite,
}

88
electron/src/rt/electron-rt.ts

@ -0,0 +1,88 @@
import { randomBytes } from 'crypto';
import { ipcRenderer, contextBridge } from 'electron';
import { EventEmitter } from 'events';
////////////////////////////////////////////////////////
// eslint-disable-next-line @typescript-eslint/no-var-requires
const plugins = require('./electron-plugins');
const randomId = (length = 5) => randomBytes(length).toString('hex');
const contextApi: {
[plugin: string]: { [functionName: string]: () => Promise<any> };
} = {};
Object.keys(plugins).forEach((pluginKey) => {
Object.keys(plugins[pluginKey])
.filter((className) => className !== 'default')
.forEach((classKey) => {
const functionList = Object.getOwnPropertyNames(plugins[pluginKey][classKey].prototype).filter(
(v) => v !== 'constructor'
);
if (!contextApi[classKey]) {
contextApi[classKey] = {};
}
functionList.forEach((functionName) => {
if (!contextApi[classKey][functionName]) {
contextApi[classKey][functionName] = (...args) => ipcRenderer.invoke(`${classKey}-${functionName}`, ...args);
}
});
// Events
if (plugins[pluginKey][classKey].prototype instanceof EventEmitter) {
const listeners: { [key: string]: { type: string; listener: (...args: any[]) => void } } = {};
const listenersOfTypeExist = (type) =>
!!Object.values(listeners).find((listenerObj) => listenerObj.type === type);
Object.assign(contextApi[classKey], {
addListener(type: string, callback: (...args) => void) {
const id = randomId();
// Deduplicate events
if (!listenersOfTypeExist(type)) {
ipcRenderer.send(`event-add-${classKey}`, type);
}
const eventHandler = (_, ...args) => callback(...args);
ipcRenderer.addListener(`event-${classKey}-${type}`, eventHandler);
listeners[id] = { type, listener: eventHandler };
return id;
},
removeListener(id: string) {
if (!listeners[id]) {
throw new Error('Invalid id');
}
const { type, listener } = listeners[id];
ipcRenderer.removeListener(`event-${classKey}-${type}`, listener);
delete listeners[id];
if (!listenersOfTypeExist(type)) {
ipcRenderer.send(`event-remove-${classKey}-${type}`);
}
},
removeAllListeners(type: string) {
Object.entries(listeners).forEach(([id, listenerObj]) => {
if (!type || listenerObj.type === type) {
ipcRenderer.removeListener(`event-${classKey}-${listenerObj.type}`, listenerObj.listener);
ipcRenderer.send(`event-remove-${classKey}-${listenerObj.type}`);
delete listeners[id];
}
});
},
});
}
});
});
contextBridge.exposeInMainWorld('CapacitorCustomPlatform', {
name: 'electron',
plugins: contextApi,
});
////////////////////////////////////////////////////////

356
electron/src/rt/sqlite-init.ts

@ -0,0 +1,356 @@
/**
* SQLite initialization for Capacitor Electron
* Handles database path setup, plugin initialization, and IPC handlers
*
* Database Path Handling:
* - Uses XDG Base Directory Specification for data storage
* - Uses modern plugin conventions (Capacitor 6+ / Plugin 6.x+)
* - Supports custom database names without enforced extensions
* - Maintains backward compatibility with legacy paths
*
* XDG Base Directory Specification:
* - Uses $XDG_DATA_HOME (defaults to ~/.local/share) for data files
* - Falls back to legacy path if XDG environment variables are not set
*
* @author Matthew Raymer
*/
import { app, ipcMain } from 'electron';
import { CapacitorSQLite } from '@capacitor-community/sqlite/electron/dist/plugin.js';
import fs from 'fs';
import path from 'path';
import os from 'os';
// Simple logger implementation with structured logging
const logger = {
log: (...args: unknown[]) => console.log('[SQLite]', ...args),
error: (...args: unknown[]) => console.error('[SQLite]', ...args),
info: (...args: unknown[]) => console.info('[SQLite]', ...args),
warn: (...args: unknown[]) => console.warn('[SQLite]', ...args),
debug: (...args: unknown[]) => console.debug('[SQLite]', ...args),
};
// Database path resolution utilities following XDG Base Directory Specification
const getAppDataPath = async (): Promise<string> => {
try {
// Get XDG_DATA_HOME or fallback to default
const xdgDataHome = process.env.XDG_DATA_HOME || path.join(os.homedir(), '.local', 'share');
logger.info('XDG_DATA_HOME:', xdgDataHome);
// Create app data directory following XDG spec
const appDataDir = path.join(xdgDataHome, 'timesafari');
logger.info('App data directory:', appDataDir);
// Ensure directory exists with proper permissions (700)
if (!fs.existsSync(appDataDir)) {
await fs.promises.mkdir(appDataDir, {
recursive: true,
mode: 0o700 // rwx------ for security
});
} else {
// Ensure existing directory has correct permissions
await fs.promises.chmod(appDataDir, 0o700);
}
return appDataDir;
} catch (error) {
logger.error('Error getting app data path:', error);
// Fallback to legacy path in user's home
const fallbackDir = path.join(os.homedir(), '.timesafari');
logger.warn('Using fallback app data directory:', fallbackDir);
try {
if (!fs.existsSync(fallbackDir)) {
await fs.promises.mkdir(fallbackDir, {
recursive: true,
mode: 0o700
});
} else {
await fs.promises.chmod(fallbackDir, 0o700);
}
} catch (mkdirError) {
logger.error('Failed to create fallback directory:', mkdirError);
throw new Error('Could not create any suitable data directory');
}
return fallbackDir;
}
};
// Initialize database paths
let dbPath: string | undefined;
let dbDir: string | undefined;
let dbPathInitialized = false;
let dbPathInitializationPromise: Promise<void> | null = null;
const initializeDatabasePaths = async (): Promise<void> => {
if (dbPathInitializationPromise) return dbPathInitializationPromise;
if (dbPathInitialized) return;
dbPathInitializationPromise = (async () => {
try {
// Get the app data directory in user's home
const absolutePath = await getAppDataPath();
logger.info('Absolute database path:', absolutePath);
// For the plugin, we need to use a path relative to the electron folder
// So we'll use a relative path from the electron folder to the home directory
const electronPath = app.getAppPath();
const relativeToElectron = path.relative(electronPath, absolutePath);
dbDir = relativeToElectron;
logger.info('Database directory (relative to electron):', dbDir);
// Ensure directory exists using absolute path
if (!fs.existsSync(absolutePath)) {
await fs.promises.mkdir(absolutePath, { recursive: true });
}
// Use modern plugin conventions - no enforced extension
const baseName = 'timesafariSQLite';
dbPath = path.join(dbDir, baseName);
logger.info('Database path initialized (relative to electron):', dbPath);
// Verify we can write to the directory using absolute path
const testFile = path.join(absolutePath, '.write-test');
try {
await fs.promises.writeFile(testFile, 'test');
await fs.promises.unlink(testFile);
} catch (error) {
throw new Error(`Cannot write to database directory: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
// Set environment variable for the plugin using relative path
process.env.CAPACITOR_SQLITE_DB_PATH = dbDir;
logger.info('Set CAPACITOR_SQLITE_DB_PATH:', process.env.CAPACITOR_SQLITE_DB_PATH);
dbPathInitialized = true;
} catch (error) {
logger.error('Failed to initialize database paths:', error);
throw error;
} finally {
dbPathInitializationPromise = null;
}
})();
return dbPathInitializationPromise;
};
// Track initialization state
let initializationError: Error | null = null;
// Initialize SQLite plugin
let sqlitePlugin: any = null;
let sqliteInitialized = false;
let sqliteInitializationPromise: Promise<void> | null = null;
export async function initializeSQLite(): Promise<void> {
if (sqliteInitializationPromise) {
logger.info('SQLite initialization already in progress, waiting...');
return sqliteInitializationPromise;
}
if (sqliteInitialized) {
logger.info('SQLite already initialized');
return;
}
sqliteInitializationPromise = (async () => {
try {
logger.info('Starting SQLite plugin initialization...');
// Initialize database paths first
await initializeDatabasePaths();
if (!dbPath || !dbDir) {
throw new Error('Database path not initialized');
}
// Create plugin instance
logger.info('Creating SQLite plugin instance...');
sqlitePlugin = new CapacitorSQLite();
if (!sqlitePlugin) {
throw new Error('Failed to create SQLite plugin instance');
}
// Test the plugin
logger.info('Testing SQLite plugin...');
const echoResult = await sqlitePlugin.echo({ value: 'test' });
if (!echoResult || echoResult.value !== 'test') {
throw new Error('SQLite plugin echo test failed');
}
logger.info('SQLite plugin echo test successful');
// Initialize database connection using modern plugin conventions
const connectionOptions = {
database: 'timesafariSQLite',
version: 1,
readOnly: false,
encryption: 'no-encryption',
useNative: true,
mode: 'rwc',
location: dbDir // Use path relative to electron folder
};
logger.info('Creating initial database connection with options:', {
...connectionOptions,
expectedPath: dbPath // Path relative to electron folder
});
const db = await sqlitePlugin.createConnection(connectionOptions);
if (!db || typeof db !== 'object') {
throw new Error(`Failed to create database connection - invalid response. Path: ${dbPath}`);
}
// Verify connection with a simple query
logger.info('Verifying database connection...');
try {
const result = await db.query({ statement: 'SELECT 1 as test;' });
if (!result || !result.values || result.values.length === 0) {
throw new Error('Database connection test query returned no results');
}
logger.info('Database connection verified successfully');
} catch (error) {
throw new Error(`Database connection test failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
sqliteInitialized = true;
logger.info('SQLite plugin initialization completed successfully');
} catch (error) {
logger.error('SQLite plugin initialization failed:', error);
// Store the error but don't throw
initializationError = error instanceof Error ? error : new Error(String(error));
// Reset state on failure but allow app to continue
sqlitePlugin = null;
sqliteInitialized = false;
} finally {
sqliteInitializationPromise = null;
}
})();
return sqliteInitializationPromise;
}
// Set up IPC handlers
export function setupSQLiteHandlers(): void {
// Remove any existing handlers to prevent duplicates
try {
ipcMain.removeHandler('sqlite-is-available');
ipcMain.removeHandler('sqlite-echo');
ipcMain.removeHandler('sqlite-create-connection');
ipcMain.removeHandler('sqlite-execute');
ipcMain.removeHandler('sqlite-query');
ipcMain.removeHandler('sqlite-close-connection');
ipcMain.removeHandler('sqlite-get-error');
} catch (error) {
logger.warn('Error removing existing handlers:', error);
}
// Register all handlers before any are called
ipcMain.handle('sqlite-is-available', async () => {
try {
// Check both plugin instance and initialization state
return sqlitePlugin !== null && sqliteInitialized;
} catch (error) {
logger.error('Error in sqlite-is-available:', error);
return false;
}
});
// Add handler to get initialization error
ipcMain.handle('sqlite-get-error', async () => {
return initializationError ? {
message: initializationError.message,
stack: initializationError.stack,
name: initializationError.name
} : null;
});
// Update other handlers to handle unavailable state gracefully
ipcMain.handle('sqlite-echo', async (_event, value) => {
try {
if (!sqlitePlugin || !sqliteInitialized) {
throw new Error('SQLite plugin not available');
}
return await sqlitePlugin.echo({ value });
} catch (error) {
logger.error('Error in sqlite-echo:', error);
throw error;
}
});
ipcMain.handle('sqlite-create-connection', async (_event, options) => {
try {
if (!sqlitePlugin || !sqliteInitialized) {
throw new Error('SQLite plugin not available');
}
if (!dbPath || !dbDir) {
throw new Error('Database path not initialized');
}
// Use modern plugin conventions for connection options
const connectionOptions = {
...options,
database: 'timesafariSQLite',
readOnly: false,
mode: 'rwc',
encryption: 'no-encryption',
useNative: true,
location: dbDir // Use path relative to electron folder
};
logger.info('Creating database connection with options:', connectionOptions);
const result = await sqlitePlugin.createConnection(connectionOptions);
if (!result || typeof result !== 'object') {
throw new Error('Failed to create database connection - invalid response');
}
return result;
} catch (error) {
logger.error('Error in sqlite-create-connection:', error);
throw error;
}
});
ipcMain.handle('sqlite-execute', async (_event, options) => {
try {
if (!sqlitePlugin || !sqliteInitialized) {
throw new Error('SQLite plugin not available');
}
return await sqlitePlugin.execute(options);
} catch (error) {
logger.error('Error in sqlite-execute:', error);
throw error;
}
});
ipcMain.handle('sqlite-query', async (_event, options) => {
try {
if (!sqlitePlugin || !sqliteInitialized) {
throw new Error('SQLite plugin not available');
}
return await sqlitePlugin.query(options);
} catch (error) {
logger.error('Error in sqlite-query:', error);
throw error;
}
});
ipcMain.handle('sqlite-close-connection', async (_event, options) => {
try {
if (!sqlitePlugin || !sqliteInitialized) {
throw new Error('SQLite plugin not available');
}
return await sqlitePlugin.closeConnection(options);
} catch (error) {
logger.error('Error in sqlite-close-connection:', error);
throw error;
}
});
logger.info('SQLite IPC handlers registered successfully');
}

244
electron/src/setup.ts

@ -0,0 +1,244 @@
import type { CapacitorElectronConfig } from '@capacitor-community/electron';
import {
CapElectronEventEmitter,
CapacitorSplashScreen,
setupCapacitorElectronPlugins,
} from '@capacitor-community/electron';
import chokidar from 'chokidar';
import type { MenuItemConstructorOptions } from 'electron';
import { app, BrowserWindow, Menu, MenuItem, nativeImage, Tray, session } from 'electron';
import electronIsDev from 'electron-is-dev';
import electronServe from 'electron-serve';
import windowStateKeeper from 'electron-window-state';
import { join } from 'path';
// Define components for a watcher to detect when the webapp is changed so we can reload in Dev mode.
const reloadWatcher = {
debouncer: null,
ready: false,
watcher: null,
};
export function setupReloadWatcher(electronCapacitorApp: ElectronCapacitorApp): void {
reloadWatcher.watcher = chokidar
.watch(join(app.getAppPath(), 'app'), {
ignored: /[/\\]\./,
persistent: true,
})
.on('ready', () => {
reloadWatcher.ready = true;
})
.on('all', (_event, _path) => {
if (reloadWatcher.ready) {
clearTimeout(reloadWatcher.debouncer);
reloadWatcher.debouncer = setTimeout(async () => {
electronCapacitorApp.getMainWindow().webContents.reload();
reloadWatcher.ready = false;
clearTimeout(reloadWatcher.debouncer);
reloadWatcher.debouncer = null;
reloadWatcher.watcher = null;
setupReloadWatcher(electronCapacitorApp);
}, 1500);
}
});
}
// Define our class to manage our app.
export class ElectronCapacitorApp {
private MainWindow: BrowserWindow | null = null;
private SplashScreen: CapacitorSplashScreen | null = null;
private TrayIcon: Tray | null = null;
private CapacitorFileConfig: CapacitorElectronConfig;
private TrayMenuTemplate: (MenuItem | MenuItemConstructorOptions)[] = [
new MenuItem({ label: 'Quit App', role: 'quit' }),
];
private AppMenuBarMenuTemplate: (MenuItem | MenuItemConstructorOptions)[] = [
{ role: process.platform === 'darwin' ? 'appMenu' : 'fileMenu' },
{ role: 'viewMenu' },
];
private mainWindowState;
private loadWebApp;
private customScheme: string;
constructor(
capacitorFileConfig: CapacitorElectronConfig,
trayMenuTemplate?: (MenuItemConstructorOptions | MenuItem)[],
appMenuBarMenuTemplate?: (MenuItemConstructorOptions | MenuItem)[]
) {
this.CapacitorFileConfig = capacitorFileConfig;
this.customScheme = this.CapacitorFileConfig.electron?.customUrlScheme ?? 'capacitor-electron';
if (trayMenuTemplate) {
this.TrayMenuTemplate = trayMenuTemplate;
}
if (appMenuBarMenuTemplate) {
this.AppMenuBarMenuTemplate = appMenuBarMenuTemplate;
}
// Setup our web app loader, this lets us load apps like react, vue, and angular without changing their build chains.
this.loadWebApp = electronServe({
directory: join(app.getAppPath(), 'app'),
scheme: this.customScheme,
});
}
// Helper function to load in the app.
private async loadMainWindow(thisRef: any) {
await thisRef.loadWebApp(thisRef.MainWindow);
}
// Expose the mainWindow ref for use outside of the class.
getMainWindow(): BrowserWindow {
return this.MainWindow;
}
getCustomURLScheme(): string {
return this.customScheme;
}
async init(): Promise<void> {
const icon = nativeImage.createFromPath(
join(app.getAppPath(), 'assets', process.platform === 'win32' ? 'appIcon.ico' : 'appIcon.png')
);
this.mainWindowState = windowStateKeeper({
defaultWidth: 1000,
defaultHeight: 800,
});
// Setup preload script path and construct our main window.
const preloadPath = join(app.getAppPath(), 'build', 'src', 'preload.js');
this.MainWindow = new BrowserWindow({
icon,
show: false,
x: this.mainWindowState.x,
y: this.mainWindowState.y,
width: this.mainWindowState.width,
height: this.mainWindowState.height,
webPreferences: {
nodeIntegration: true,
contextIsolation: true,
// Use preload to inject the electron varriant overrides for capacitor plugins.
// preload: join(app.getAppPath(), "node_modules", "@capacitor-community", "electron", "dist", "runtime", "electron-rt.js"),
preload: preloadPath,
},
});
this.mainWindowState.manage(this.MainWindow);
if (this.CapacitorFileConfig.backgroundColor) {
this.MainWindow.setBackgroundColor(this.CapacitorFileConfig.electron.backgroundColor);
}
// If we close the main window with the splashscreen enabled we need to destory the ref.
this.MainWindow.on('closed', () => {
if (this.SplashScreen?.getSplashWindow() && !this.SplashScreen.getSplashWindow().isDestroyed()) {
this.SplashScreen.getSplashWindow().close();
}
});
// When the tray icon is enabled, setup the options.
if (this.CapacitorFileConfig.electron?.trayIconAndMenuEnabled) {
this.TrayIcon = new Tray(icon);
this.TrayIcon.on('double-click', () => {
if (this.MainWindow) {
if (this.MainWindow.isVisible()) {
this.MainWindow.hide();
} else {
this.MainWindow.show();
this.MainWindow.focus();
}
}
});
this.TrayIcon.on('click', () => {
if (this.MainWindow) {
if (this.MainWindow.isVisible()) {
this.MainWindow.hide();
} else {
this.MainWindow.show();
this.MainWindow.focus();
}
}
});
this.TrayIcon.setToolTip(app.getName());
this.TrayIcon.setContextMenu(Menu.buildFromTemplate(this.TrayMenuTemplate));
}
// Setup the main manu bar at the top of our window.
Menu.setApplicationMenu(Menu.buildFromTemplate(this.AppMenuBarMenuTemplate));
// If the splashscreen is enabled, show it first while the main window loads then switch it out for the main window, or just load the main window from the start.
if (this.CapacitorFileConfig.electron?.splashScreenEnabled) {
this.SplashScreen = new CapacitorSplashScreen({
imageFilePath: join(
app.getAppPath(),
'assets',
this.CapacitorFileConfig.electron?.splashScreenImageName ?? 'splash.png'
),
windowWidth: 400,
windowHeight: 400,
});
this.SplashScreen.init(this.loadMainWindow, this);
} else {
this.loadMainWindow(this);
}
// Security
this.MainWindow.webContents.setWindowOpenHandler((details) => {
if (!details.url.includes(this.customScheme)) {
return { action: 'deny' };
} else {
return { action: 'allow' };
}
});
this.MainWindow.webContents.on('will-navigate', (event, _newURL) => {
if (!this.MainWindow.webContents.getURL().includes(this.customScheme)) {
event.preventDefault();
}
});
// Link electron plugins into the system.
setupCapacitorElectronPlugins();
// When the web app is loaded we hide the splashscreen if needed and show the mainwindow.
this.MainWindow.webContents.on('dom-ready', () => {
if (this.CapacitorFileConfig.electron?.splashScreenEnabled) {
this.SplashScreen.getSplashWindow().hide();
}
if (!this.CapacitorFileConfig.electron?.hideMainWindowOnLaunch) {
this.MainWindow.show();
}
setTimeout(() => {
if (electronIsDev) {
this.MainWindow.webContents.openDevTools();
}
CapElectronEventEmitter.emit('CAPELECTRON_DeeplinkListenerInitialized', '');
}, 400);
});
}
}
// Set a CSP up for our application based on the custom scheme
export function setupContentSecurityPolicy(customScheme: string): void {
session.defaultSession.webRequest.onHeadersReceived((details, callback) => {
callback({
responseHeaders: {
...details.responseHeaders,
'Content-Security-Policy': [
// Base CSP for both dev and prod
`default-src ${customScheme}://* 'unsafe-inline' data:;`,
// Allow Google Fonts
`style-src ${customScheme}://* 'unsafe-inline' https://fonts.googleapis.com;`,
`font-src ${customScheme}://* https://fonts.gstatic.com;`,
// Allow images and media
`img-src ${customScheme}://* data: https:;`,
// Allow connections to HTTPS resources
`connect-src ${customScheme}://* https:;`,
// Add dev-specific policies
...(electronIsDev ? [
`script-src ${customScheme}://* 'unsafe-inline' 'unsafe-eval' devtools://*;`,
`default-src ${customScheme}://* 'unsafe-inline' devtools://* 'unsafe-eval' data:;`
] : [])
].join(' ')
},
});
});
}

18
electron/tsconfig.json

@ -0,0 +1,18 @@
{
"compileOnSave": true,
"include": ["./src/**/*", "./capacitor.config.ts", "./capacitor.config.js"],
"compilerOptions": {
"outDir": "./build",
"importHelpers": true,
"target": "ES2020",
"module": "CommonJS",
"moduleResolution": "node",
"esModuleInterop": true,
"typeRoots": ["./node_modules/@types"],
"allowJs": true,
"rootDir": ".",
"skipLibCheck": true,
"resolveJsonModule": true
}
}

81
package-lock.json

@ -19,8 +19,8 @@
"@capacitor/ios": "^6.2.0", "@capacitor/ios": "^6.2.0",
"@capacitor/share": "^6.0.3", "@capacitor/share": "^6.0.3",
"@capawesome/capacitor-file-picker": "^6.2.0", "@capawesome/capacitor-file-picker": "^6.2.0",
"@dicebear/collection": "^5.4.1", "@dicebear/collection": "^5.4.3",
"@dicebear/core": "^5.4.1", "@dicebear/core": "^5.4.3",
"@ethersproject/hdnode": "^5.7.0", "@ethersproject/hdnode": "^5.7.0",
"@ethersproject/wallet": "^5.8.0", "@ethersproject/wallet": "^5.8.0",
"@fortawesome/fontawesome-svg-core": "^6.5.1", "@fortawesome/fontawesome-svg-core": "^6.5.1",
@ -31,7 +31,7 @@
"@peculiar/asn1-schema": "^2.3.8", "@peculiar/asn1-schema": "^2.3.8",
"@pvermeer/dexie-encrypted-addon": "^3.0.0", "@pvermeer/dexie-encrypted-addon": "^3.0.0",
"@simplewebauthn/browser": "^10.0.0", "@simplewebauthn/browser": "^10.0.0",
"@simplewebauthn/server": "^10.0.0", "@simplewebauthn/server": "^10.0.1",
"@tweenjs/tween.js": "^21.1.1", "@tweenjs/tween.js": "^21.1.1",
"@types/qrcode": "^1.5.5", "@types/qrcode": "^1.5.5",
"@veramo/core": "^5.6.0", "@veramo/core": "^5.6.0",
@ -57,22 +57,22 @@
"did-resolver": "^4.1.0", "did-resolver": "^4.1.0",
"dotenv": "^16.0.3", "dotenv": "^16.0.3",
"electron-json-storage": "^4.6.0", "electron-json-storage": "^4.6.0",
"ethereum-cryptography": "^2.1.3", "ethereum-cryptography": "^2.2.1",
"ethereumjs-util": "^7.1.5", "ethereumjs-util": "^7.1.5",
"jdenticon": "^3.2.0", "jdenticon": "^3.3.0",
"js-generate-password": "^0.1.9", "js-generate-password": "^0.1.9",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"jsqr": "^1.4.0", "jsqr": "^1.4.0",
"leaflet": "^1.9.4", "leaflet": "^1.9.4",
"localstorage-slim": "^2.7.0", "localstorage-slim": "^2.7.0",
"lru-cache": "^10.2.0", "lru-cache": "^10.4.3",
"luxon": "^3.4.4", "luxon": "^3.4.4",
"merkletreejs": "^0.3.11", "merkletreejs": "^0.3.11",
"nostr-tools": "^2.10.4", "nostr-tools": "^2.13.1",
"notiwind": "^2.0.2", "notiwind": "^2.0.2",
"papaparse": "^5.4.1", "papaparse": "^5.4.1",
"pina": "^0.20.2204228", "pina": "^0.20.2204228",
"pinia-plugin-persistedstate": "^3.2.1", "pinia-plugin-persistedstate": "^3.2.3",
"qr-code-generator-vue3": "^1.4.21", "qr-code-generator-vue3": "^1.4.21",
"qrcode": "^1.5.4", "qrcode": "^1.5.4",
"ramda": "^0.29.1", "ramda": "^0.29.1",
@ -88,12 +88,13 @@
"vue-axios": "^3.5.2", "vue-axios": "^3.5.2",
"vue-facing-decorator": "^3.0.4", "vue-facing-decorator": "^3.0.4",
"vue-picture-cropper": "^0.7.0", "vue-picture-cropper": "^0.7.0",
"vue-qrcode-reader": "^5.5.3", "vue-qrcode-reader": "^5.7.2",
"vue-router": "^4.5.0", "vue-router": "^4.5.0",
"web-did-resolver": "^2.0.27", "web-did-resolver": "^2.0.30",
"zod": "^3.24.2" "zod": "^3.24.2"
}, },
"devDependencies": { "devDependencies": {
"@capacitor-community/electron": "^5.0.1",
"@capacitor/assets": "^3.0.5", "@capacitor/assets": "^3.0.5",
"@playwright/test": "^1.45.2", "@playwright/test": "^1.45.2",
"@types/dom-webcodecs": "^0.1.7", "@types/dom-webcodecs": "^0.1.7",
@ -108,7 +109,7 @@
"@types/ua-parser-js": "^0.7.39", "@types/ua-parser-js": "^0.7.39",
"@typescript-eslint/eslint-plugin": "^6.21.0", "@typescript-eslint/eslint-plugin": "^6.21.0",
"@typescript-eslint/parser": "^6.21.0", "@typescript-eslint/parser": "^6.21.0",
"@vitejs/plugin-vue": "^5.2.1", "@vitejs/plugin-vue": "^5.2.4",
"@vue/eslint-config-typescript": "^11.0.3", "@vue/eslint-config-typescript": "^11.0.3",
"autoprefixer": "^10.4.19", "autoprefixer": "^10.4.19",
"browserify-fs": "^1.0.0", "browserify-fs": "^1.0.0",
@ -128,6 +129,7 @@
"postcss": "^8.4.38", "postcss": "^8.4.38",
"prettier": "^3.2.5", "prettier": "^3.2.5",
"rimraf": "^6.0.1", "rimraf": "^6.0.1",
"source-map-support": "^0.5.21",
"tailwindcss": "^3.4.1", "tailwindcss": "^3.4.1",
"typescript": "~5.2.2", "typescript": "~5.2.2",
"vite": "^5.2.0", "vite": "^5.2.0",
@ -2514,6 +2516,40 @@
"node": ">=8.9" "node": ">=8.9"
} }
}, },
"node_modules/@capacitor-community/electron": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/@capacitor-community/electron/-/electron-5.0.1.tgz",
"integrity": "sha512-4/x12ycTq0Kq8JIn/BmIBdFVP5Cqw8iA6SU6YfFjmONfjW3OELwsB3zwLxOwAjLxnjyCMOBHl4ci9E5jLgZgAQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@capacitor/cli": ">=5.4.0",
"@capacitor/core": ">=5.4.0",
"@ionic/utils-fs": "~3.1.6",
"chalk": "^4.1.2",
"electron-is-dev": "~2.0.0",
"events": "~3.3.0",
"fs-extra": "~11.1.1",
"keyv": "^4.5.2",
"mime-types": "~2.1.35",
"ora": "^5.4.1"
}
},
"node_modules/@capacitor-community/electron/node_modules/fs-extra": {
"version": "11.1.1",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.1.1.tgz",
"integrity": "sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"graceful-fs": "^4.2.0",
"jsonfile": "^6.0.1",
"universalify": "^2.0.0"
},
"engines": {
"node": ">=14.14"
}
},
"node_modules/@capacitor-community/sqlite": { "node_modules/@capacitor-community/sqlite": {
"version": "6.0.2", "version": "6.0.2",
"resolved": "https://registry.npmjs.org/@capacitor-community/sqlite/-/sqlite-6.0.2.tgz", "resolved": "https://registry.npmjs.org/@capacitor-community/sqlite/-/sqlite-6.0.2.tgz",
@ -15261,6 +15297,16 @@
"node": ">=12" "node": ">=12"
} }
}, },
"node_modules/electron-is-dev": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/electron-is-dev/-/electron-is-dev-2.0.0.tgz",
"integrity": "sha512-3X99K852Yoqu9AcW50qz3ibYBWY79/pBhlMCab8ToEWS48R0T9tyxRiQhwylE7zQdXrMnx2JKqUJyMPmt5FBqA==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/electron-json-storage": { "node_modules/electron-json-storage": {
"version": "4.6.0", "version": "4.6.0",
"resolved": "https://registry.npmjs.org/electron-json-storage/-/electron-json-storage-4.6.0.tgz", "resolved": "https://registry.npmjs.org/electron-json-storage/-/electron-json-storage-4.6.0.tgz",
@ -23081,9 +23127,9 @@
} }
}, },
"node_modules/nostr-tools": { "node_modules/nostr-tools": {
"version": "2.13.0", "version": "2.13.1",
"resolved": "https://registry.npmjs.org/nostr-tools/-/nostr-tools-2.13.0.tgz", "resolved": "https://registry.npmjs.org/nostr-tools/-/nostr-tools-2.13.1.tgz",
"integrity": "sha512-A1arGsvpULqVK0NmZQqK1imwaCiPm8gcG/lo+cTax2NbNqBEYsuplbqAFdVqcGHEopmkByYbTwF76x25+oEbew==", "integrity": "sha512-EKcicym1ree14m8lU0b6sZIf46NcxOHvGwUWWAuxjhutOmJM5Pc9ylR1YqxbgpqDQpkEYQwv/d3GupK5CJj9ow==",
"license": "Unlicense", "license": "Unlicense",
"dependencies": { "dependencies": {
"@noble/ciphers": "^0.5.1", "@noble/ciphers": "^0.5.1",
@ -23091,9 +23137,7 @@
"@noble/hashes": "1.3.1", "@noble/hashes": "1.3.1",
"@scure/base": "1.1.1", "@scure/base": "1.1.1",
"@scure/bip32": "1.3.1", "@scure/bip32": "1.3.1",
"@scure/bip39": "1.2.1" "@scure/bip39": "1.2.1",
},
"optionalDependencies": {
"nostr-wasm": "0.1.0" "nostr-wasm": "0.1.0"
}, },
"peerDependencies": { "peerDependencies": {
@ -23205,8 +23249,7 @@
"version": "0.1.0", "version": "0.1.0",
"resolved": "https://registry.npmjs.org/nostr-wasm/-/nostr-wasm-0.1.0.tgz", "resolved": "https://registry.npmjs.org/nostr-wasm/-/nostr-wasm-0.1.0.tgz",
"integrity": "sha512-78BTryCLcLYv96ONU8Ws3Q1JzjlAt+43pWQhIl86xZmWeegYCNLPml7yQ+gG3vR6V5h4XGj+TxO+SS5dsThQIA==", "integrity": "sha512-78BTryCLcLYv96ONU8Ws3Q1JzjlAt+43pWQhIl86xZmWeegYCNLPml7yQ+gG3vR6V5h4XGj+TxO+SS5dsThQIA==",
"license": "MIT", "license": "MIT"
"optional": true
}, },
"node_modules/notiwind": { "node_modules/notiwind": {
"version": "2.1.0", "version": "2.1.0",

24
package.json

@ -58,8 +58,8 @@
"@capacitor/ios": "^6.2.0", "@capacitor/ios": "^6.2.0",
"@capacitor/share": "^6.0.3", "@capacitor/share": "^6.0.3",
"@capawesome/capacitor-file-picker": "^6.2.0", "@capawesome/capacitor-file-picker": "^6.2.0",
"@dicebear/collection": "^5.4.1", "@dicebear/collection": "^5.4.3",
"@dicebear/core": "^5.4.1", "@dicebear/core": "^5.4.3",
"@ethersproject/hdnode": "^5.7.0", "@ethersproject/hdnode": "^5.7.0",
"@ethersproject/wallet": "^5.8.0", "@ethersproject/wallet": "^5.8.0",
"@fortawesome/fontawesome-svg-core": "^6.5.1", "@fortawesome/fontawesome-svg-core": "^6.5.1",
@ -70,7 +70,7 @@
"@peculiar/asn1-schema": "^2.3.8", "@peculiar/asn1-schema": "^2.3.8",
"@pvermeer/dexie-encrypted-addon": "^3.0.0", "@pvermeer/dexie-encrypted-addon": "^3.0.0",
"@simplewebauthn/browser": "^10.0.0", "@simplewebauthn/browser": "^10.0.0",
"@simplewebauthn/server": "^10.0.0", "@simplewebauthn/server": "^10.0.1",
"@tweenjs/tween.js": "^21.1.1", "@tweenjs/tween.js": "^21.1.1",
"@types/qrcode": "^1.5.5", "@types/qrcode": "^1.5.5",
"@veramo/core": "^5.6.0", "@veramo/core": "^5.6.0",
@ -96,22 +96,22 @@
"did-resolver": "^4.1.0", "did-resolver": "^4.1.0",
"dotenv": "^16.0.3", "dotenv": "^16.0.3",
"electron-json-storage": "^4.6.0", "electron-json-storage": "^4.6.0",
"ethereum-cryptography": "^2.1.3", "ethereum-cryptography": "^2.2.1",
"ethereumjs-util": "^7.1.5", "ethereumjs-util": "^7.1.5",
"jdenticon": "^3.2.0", "jdenticon": "^3.3.0",
"js-generate-password": "^0.1.9", "js-generate-password": "^0.1.9",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"jsqr": "^1.4.0", "jsqr": "^1.4.0",
"leaflet": "^1.9.4", "leaflet": "^1.9.4",
"localstorage-slim": "^2.7.0", "localstorage-slim": "^2.7.0",
"lru-cache": "^10.2.0", "lru-cache": "^10.4.3",
"luxon": "^3.4.4", "luxon": "^3.4.4",
"merkletreejs": "^0.3.11", "merkletreejs": "^0.3.11",
"nostr-tools": "^2.10.4", "nostr-tools": "^2.13.1",
"notiwind": "^2.0.2", "notiwind": "^2.0.2",
"papaparse": "^5.4.1", "papaparse": "^5.4.1",
"pina": "^0.20.2204228", "pina": "^0.20.2204228",
"pinia-plugin-persistedstate": "^3.2.1", "pinia-plugin-persistedstate": "^3.2.3",
"qr-code-generator-vue3": "^1.4.21", "qr-code-generator-vue3": "^1.4.21",
"qrcode": "^1.5.4", "qrcode": "^1.5.4",
"ramda": "^0.29.1", "ramda": "^0.29.1",
@ -127,12 +127,13 @@
"vue-axios": "^3.5.2", "vue-axios": "^3.5.2",
"vue-facing-decorator": "^3.0.4", "vue-facing-decorator": "^3.0.4",
"vue-picture-cropper": "^0.7.0", "vue-picture-cropper": "^0.7.0",
"vue-qrcode-reader": "^5.5.3", "vue-qrcode-reader": "^5.7.2",
"vue-router": "^4.5.0", "vue-router": "^4.5.0",
"web-did-resolver": "^2.0.27", "web-did-resolver": "^2.0.30",
"zod": "^3.24.2" "zod": "^3.24.2"
}, },
"devDependencies": { "devDependencies": {
"@capacitor-community/electron": "^5.0.1",
"@capacitor/assets": "^3.0.5", "@capacitor/assets": "^3.0.5",
"@playwright/test": "^1.45.2", "@playwright/test": "^1.45.2",
"@types/dom-webcodecs": "^0.1.7", "@types/dom-webcodecs": "^0.1.7",
@ -147,7 +148,7 @@
"@types/ua-parser-js": "^0.7.39", "@types/ua-parser-js": "^0.7.39",
"@typescript-eslint/eslint-plugin": "^6.21.0", "@typescript-eslint/eslint-plugin": "^6.21.0",
"@typescript-eslint/parser": "^6.21.0", "@typescript-eslint/parser": "^6.21.0",
"@vitejs/plugin-vue": "^5.2.1", "@vitejs/plugin-vue": "^5.2.4",
"@vue/eslint-config-typescript": "^11.0.3", "@vue/eslint-config-typescript": "^11.0.3",
"autoprefixer": "^10.4.19", "autoprefixer": "^10.4.19",
"browserify-fs": "^1.0.0", "browserify-fs": "^1.0.0",
@ -167,6 +168,7 @@
"postcss": "^8.4.38", "postcss": "^8.4.38",
"prettier": "^3.2.5", "prettier": "^3.2.5",
"rimraf": "^6.0.1", "rimraf": "^6.0.1",
"source-map-support": "^0.5.21",
"tailwindcss": "^3.4.1", "tailwindcss": "^3.4.1",
"typescript": "~5.2.2", "typescript": "~5.2.2",
"vite": "^5.2.0", "vite": "^5.2.0",

Loading…
Cancel
Save