Compare commits
6 Commits
sql-absurd
...
sql-absurd
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d9ce884513 | ||
|
|
a1a1543ae1 | ||
|
|
93591a5815 | ||
|
|
b30c4c8b30 | ||
|
|
1f9db0ba94 | ||
|
|
bdc2d71d3c |
172
.cursor/rules/SQLITE.mdc
Normal file
172
.cursor/rules/SQLITE.mdc
Normal file
@@ -0,0 +1,172 @@
|
||||
---
|
||||
description:
|
||||
globs:
|
||||
alwaysApply: true
|
||||
---
|
||||
# @capacitor-community/sqlite MDC Ruleset
|
||||
|
||||
## Project Overview
|
||||
This ruleset is for the `@capacitor-community/sqlite` plugin, a Capacitor community plugin that provides native and Electron SQLite database functionality with encryption support.
|
||||
|
||||
## Key Features
|
||||
- Native SQLite database support for iOS, Android, and Electron
|
||||
- Database encryption support using SQLCipher (Native) and better-sqlite3-multiple-ciphers (Electron)
|
||||
- Biometric authentication support
|
||||
- Cross-platform database operations
|
||||
- JSON import/export capabilities
|
||||
- Database migration support
|
||||
- Sync table functionality
|
||||
|
||||
## Platform Support Matrix
|
||||
|
||||
### Core Database Operations
|
||||
| Operation | Android | iOS | Electron | Web |
|
||||
|-----------|---------|-----|----------|-----|
|
||||
| Create Connection (RW) | ✅ | ✅ | ✅ | ✅ |
|
||||
| Create Connection (RO) | ✅ | ✅ | ✅ | ❌ |
|
||||
| Open DB (non-encrypted) | ✅ | ✅ | ✅ | ✅ |
|
||||
| Open DB (encrypted) | ✅ | ✅ | ✅ | ❌ |
|
||||
| Execute/Query | ✅ | ✅ | ✅ | ✅ |
|
||||
| Import/Export JSON | ✅ | ✅ | ✅ | ✅ |
|
||||
|
||||
### Security Features
|
||||
| Feature | Android | iOS | Electron | Web |
|
||||
|---------|---------|-----|----------|-----|
|
||||
| Encryption | ✅ | ✅ | ✅ | ❌ |
|
||||
| Biometric Auth | ✅ | ✅ | ✅ | ❌ |
|
||||
| Secret Management | ✅ | ✅ | ✅ | ❌ |
|
||||
|
||||
## Configuration Requirements
|
||||
|
||||
### Base Configuration
|
||||
```typescript
|
||||
// capacitor.config.ts
|
||||
{
|
||||
plugins: {
|
||||
CapacitorSQLite: {
|
||||
iosDatabaseLocation: 'Library/CapacitorDatabase',
|
||||
iosIsEncryption: true,
|
||||
iosKeychainPrefix: 'your-app-prefix',
|
||||
androidIsEncryption: true,
|
||||
electronIsEncryption: true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Platform-Specific Requirements
|
||||
|
||||
#### Android
|
||||
- Minimum SDK: 23
|
||||
- Target SDK: 35
|
||||
- Required Gradle JDK: 21
|
||||
- Required Android Gradle Plugin: 8.7.2
|
||||
- Required manifest settings for backup prevention
|
||||
- Required data extraction rules
|
||||
|
||||
#### iOS
|
||||
- No additional configuration needed beyond base setup
|
||||
- Supports biometric authentication
|
||||
- Uses keychain for encryption
|
||||
|
||||
#### Electron
|
||||
Required dependencies:
|
||||
```json
|
||||
{
|
||||
"dependencies": {
|
||||
"better-sqlite3-multiple-ciphers": "latest",
|
||||
"electron-json-storage": "latest",
|
||||
"jszip": "latest",
|
||||
"node-fetch": "2.6.7",
|
||||
"crypto": "latest",
|
||||
"crypto-js": "latest"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Web
|
||||
- Requires `sql.js` and `jeep-sqlite`
|
||||
- Manual copy of `sql-wasm.wasm` to assets folder
|
||||
- Framework-specific asset placement:
|
||||
- Angular: `src/assets/`
|
||||
- Vue/React: `public/assets/`
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Database Operations
|
||||
1. Always close connections after use
|
||||
2. Use transactions for multiple operations
|
||||
3. Implement proper error handling
|
||||
4. Use prepared statements for queries
|
||||
5. Implement proper database versioning
|
||||
|
||||
### Security
|
||||
1. Always use encryption for sensitive data
|
||||
2. Implement proper secret management
|
||||
3. Use biometric authentication when available
|
||||
4. Follow platform-specific security guidelines
|
||||
|
||||
### Performance
|
||||
1. Use appropriate indexes
|
||||
2. Implement connection pooling
|
||||
3. Use transactions for bulk operations
|
||||
4. Implement proper database cleanup
|
||||
|
||||
## Common Issues and Solutions
|
||||
|
||||
### Android
|
||||
- Build data properties conflict: Add to `app/build.gradle`:
|
||||
```gradle
|
||||
packagingOptions {
|
||||
exclude 'build-data.properties'
|
||||
}
|
||||
```
|
||||
|
||||
### Electron
|
||||
- Node-fetch version must be ≤2.6.7
|
||||
- For Capacitor Electron v5:
|
||||
- Use Electron@25.8.4
|
||||
- Add `"skipLibCheck": true` to tsconfig.json
|
||||
|
||||
### Web
|
||||
- Ensure proper WASM file placement
|
||||
- Handle browser compatibility
|
||||
- Implement proper fallbacks
|
||||
|
||||
## Version Compatibility
|
||||
- Requires Node.js ≥16.0.0
|
||||
- Compatible with Capacitor ≥7.0.0
|
||||
- Supports TypeScript 4.1.5+
|
||||
|
||||
## Testing Requirements
|
||||
- Unit tests for database operations
|
||||
- Platform-specific integration tests
|
||||
- Encryption/decryption tests
|
||||
- Biometric authentication tests
|
||||
- Migration tests
|
||||
- Sync functionality tests
|
||||
|
||||
## Documentation
|
||||
- API Documentation: `/docs/API.md`
|
||||
- Connection API: `/docs/APIConnection.md`
|
||||
- DB Connection API: `/docs/APIDBConnection.md`
|
||||
- Release Notes: `/docs/info_releases.md`
|
||||
- Changelog: `CHANGELOG.md`
|
||||
|
||||
## Contributing Guidelines
|
||||
- Follow Ionic coding standards
|
||||
- Use provided linting and formatting tools
|
||||
- Maintain platform compatibility
|
||||
- Update documentation
|
||||
- Add appropriate tests
|
||||
- Follow semantic versioning
|
||||
|
||||
## Maintenance
|
||||
- Regular security updates
|
||||
- Platform compatibility checks
|
||||
- Performance optimization
|
||||
- Documentation updates
|
||||
- Dependency updates
|
||||
|
||||
## License
|
||||
MIT License - See LICENSE file for details
|
||||
@@ -9,5 +9,4 @@ VITE_DEFAULT_ENDORSER_API_SERVER=http://localhost:3000
|
||||
# Using shared server by default to ease setup, which works for shared test users.
|
||||
VITE_DEFAULT_IMAGE_API_SERVER=https://test-image-api.timesafari.app
|
||||
VITE_DEFAULT_PARTNER_API_SERVER=http://localhost:3000
|
||||
#VITE_DEFAULT_PUSH_SERVER... can't be set up with localhost domain
|
||||
VITE_PASSKEYS_ENABLED=true
|
||||
|
||||
6
.env.example
Normal file
6
.env.example
Normal file
@@ -0,0 +1,6 @@
|
||||
# Admin DID credentials
|
||||
ADMIN_DID=did:ethr:0x0000694B58C2cC69658993A90D3840C560f2F51F
|
||||
ADMIN_PRIVATE_KEY=2b6472c026ec2aa2c4235c994a63868fc9212d18b58f6cbfe861b52e71330f5b
|
||||
|
||||
# API Configuration
|
||||
ENDORSER_API_URL=https://test-api.endorser.ch/api/v2/claim
|
||||
@@ -9,4 +9,3 @@ VITE_DEFAULT_ENDORSER_API_SERVER=https://api.endorser.ch
|
||||
|
||||
VITE_DEFAULT_IMAGE_API_SERVER=https://image-api.timesafari.app
|
||||
VITE_DEFAULT_PARTNER_API_SERVER=https://partner-api.endorser.ch
|
||||
VITE_DEFAULT_PUSH_SERVER=https://timesafari.app
|
||||
|
||||
@@ -9,5 +9,4 @@ VITE_DEFAULT_ENDORSER_API_SERVER=https://test-api.endorser.ch
|
||||
|
||||
VITE_DEFAULT_IMAGE_API_SERVER=https://test-image-api.timesafari.app
|
||||
VITE_DEFAULT_PARTNER_API_SERVER=https://test-partner-api.endorser.ch
|
||||
VITE_DEFAULT_PUSH_SERVER=https://test.timesafari.app
|
||||
VITE_PASSKEYS_ENABLED=true
|
||||
|
||||
@@ -4,12 +4,6 @@ module.exports = {
|
||||
node: true,
|
||||
es2022: true,
|
||||
},
|
||||
ignorePatterns: [
|
||||
'node_modules/',
|
||||
'dist/',
|
||||
'dist-electron/',
|
||||
'*.d.ts'
|
||||
],
|
||||
extends: [
|
||||
"plugin:vue/vue3-recommended",
|
||||
"eslint:recommended",
|
||||
@@ -84,7 +84,7 @@ Install dependencies:
|
||||
* For test, build the app (because test server is not yet set up to build):
|
||||
|
||||
```bash
|
||||
TIME_SAFARI_APP_TITLE="TimeSafari_Test" VITE_APP_SERVER=https://test.timesafari.app VITE_BVC_MEETUPS_PROJECT_CLAIM_ID=https://endorser.ch/entity/01HWE8FWHQ1YGP7GFZYYPS272F VITE_DEFAULT_ENDORSER_API_SERVER=https://test-api.endorser.ch VITE_DEFAULT_IMAGE_API_SERVER=https://test-image-api.timesafari.app VITE_DEFAULT_PARTNER_API_SERVER=https://test-partner-api.endorser.ch VITE_DEFAULT_PUSH_SERVER=https://test.timesafari.app VITE_PASSKEYS_ENABLED=true npm run build
|
||||
TIME_SAFARI_APP_TITLE="TimeSafari_Test" VITE_APP_SERVER=https://test.timesafari.app VITE_BVC_MEETUPS_PROJECT_CLAIM_ID=https://endorser.ch/entity/01HWE8FWHQ1YGP7GFZYYPS272F VITE_DEFAULT_ENDORSER_API_SERVER=https://test-api.endorser.ch VITE_DEFAULT_IMAGE_API_SERVER=https://test-image-api.timesafari.app VITE_DEFAULT_PARTNER_API_SERVER=https://test-partner-api.endorser.ch VITE_PASSKEYS_ENABLED=true npm run build
|
||||
```
|
||||
|
||||
... and transfer to the test server:
|
||||
|
||||
@@ -91,8 +91,6 @@ dependencies {
|
||||
implementation "androidx.coordinatorlayout:coordinatorlayout:$androidxCoordinatorLayoutVersion"
|
||||
implementation "androidx.core:core-splashscreen:$coreSplashScreenVersion"
|
||||
implementation project(':capacitor-android')
|
||||
implementation project(':capacitor-community-sqlite')
|
||||
implementation "androidx.biometric:biometric:1.2.0-alpha05"
|
||||
testImplementation "junit:junit:$junitVersion"
|
||||
androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
|
||||
androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
|
||||
|
||||
@@ -9,7 +9,6 @@ android {
|
||||
|
||||
apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
|
||||
dependencies {
|
||||
implementation project(':capacitor-community-sqlite')
|
||||
implementation project(':capacitor-mlkit-barcode-scanning')
|
||||
implementation project(':capacitor-app')
|
||||
implementation project(':capacitor-camera')
|
||||
|
||||
@@ -16,41 +16,6 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"SQLite": {
|
||||
"iosDatabaseLocation": "Library/CapacitorDatabase",
|
||||
"iosIsEncryption": true,
|
||||
"iosBiometric": {
|
||||
"biometricAuth": true,
|
||||
"biometricTitle": "Biometric login for TimeSafari"
|
||||
},
|
||||
"androidIsEncryption": true,
|
||||
"androidBiometric": {
|
||||
"biometricAuth": true,
|
||||
"biometricTitle": "Biometric login for 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"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
[
|
||||
{
|
||||
"pkg": "@capacitor-community/sqlite",
|
||||
"classpath": "com.getcapacitor.community.database.sqlite.CapacitorSQLitePlugin"
|
||||
},
|
||||
{
|
||||
"pkg": "@capacitor-mlkit/barcode-scanning",
|
||||
"classpath": "io.capawesome.capacitorjs.plugins.mlkit.barcodescanning.BarcodeScannerPlugin"
|
||||
|
||||
@@ -1,15 +1,7 @@
|
||||
package app.timesafari;
|
||||
|
||||
import android.os.Bundle;
|
||||
import com.getcapacitor.BridgeActivity;
|
||||
import com.getcapacitor.community.sqlite.SQLite;
|
||||
|
||||
public class MainActivity extends BridgeActivity {
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
// Initialize SQLite
|
||||
registerPlugin(SQLite.class);
|
||||
}
|
||||
// ... existing code ...
|
||||
}
|
||||
@@ -2,9 +2,6 @@
|
||||
include ':capacitor-android'
|
||||
project(':capacitor-android').projectDir = new File('../node_modules/@capacitor/android/capacitor')
|
||||
|
||||
include ':capacitor-community-sqlite'
|
||||
project(':capacitor-community-sqlite').projectDir = new File('../node_modules/@capacitor-community/sqlite/android')
|
||||
|
||||
include ':capacitor-mlkit-barcode-scanning'
|
||||
project(':capacitor-mlkit-barcode-scanning').projectDir = new File('../node_modules/@capacitor-mlkit/barcode-scanning/android')
|
||||
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
{
|
||||
"appId": "com.timesafari.app",
|
||||
"appId": "app.timesafari",
|
||||
"appName": "TimeSafari",
|
||||
"webDir": "dist",
|
||||
"bundledWebRuntime": false,
|
||||
"server": {
|
||||
"cleartext": true,
|
||||
"androidScheme": "https"
|
||||
"cleartext": true
|
||||
},
|
||||
"plugins": {
|
||||
"App": {
|
||||
@@ -17,47 +16,6 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"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"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,270 +0,0 @@
|
||||
# Electron App Migration Strategy
|
||||
|
||||
## Overview
|
||||
|
||||
This document outlines the migration strategy for the TimeSafari Electron app, focusing on the transition from web-based storage to native SQLite implementation while maintaining cross-platform compatibility.
|
||||
|
||||
## Current Architecture
|
||||
|
||||
### 1. Platform Services
|
||||
- `ElectronPlatformService`: Implements platform-specific features for desktop
|
||||
- Uses `@capacitor-community/sqlite` for database operations
|
||||
- Maintains compatibility with web/mobile platforms through shared interfaces
|
||||
|
||||
### 2. Database Implementation
|
||||
- SQLite with native Node.js backend
|
||||
- WAL journal mode for better concurrency
|
||||
- Connection pooling for performance
|
||||
- Migration system for schema updates
|
||||
- Secure file permissions (0o755)
|
||||
|
||||
### 3. Build Process
|
||||
```bash
|
||||
# Development
|
||||
npm run dev:electron
|
||||
|
||||
# Production Build
|
||||
npm run build:web
|
||||
npm run build:electron
|
||||
npm run electron:build-linux # or electron:build-mac
|
||||
```
|
||||
|
||||
## Migration Goals
|
||||
|
||||
1. **Data Integrity**
|
||||
- Preserve existing data during migration
|
||||
- Maintain data relationships
|
||||
- Ensure ACID compliance
|
||||
- Implement proper backup/restore
|
||||
|
||||
2. **Performance**
|
||||
- Optimize SQLite configuration
|
||||
- Implement connection pooling
|
||||
- Use WAL journal mode
|
||||
- Configure optimal PRAGMA settings
|
||||
|
||||
3. **Security**
|
||||
- Secure file permissions
|
||||
- Proper IPC communication
|
||||
- Context isolation
|
||||
- Safe preload scripts
|
||||
|
||||
4. **User Experience**
|
||||
- Zero data loss
|
||||
- Automatic migration
|
||||
- Progress indicators
|
||||
- Error recovery
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### 1. Database Initialization
|
||||
```typescript
|
||||
// electron/src/rt/sqlite-init.ts
|
||||
export async function initializeSQLite() {
|
||||
// Set up database path with proper permissions
|
||||
const dbPath = path.join(app.getPath('userData'), 'timesafari.db');
|
||||
|
||||
// Initialize SQLite plugin
|
||||
const sqlite = new CapacitorSQLite();
|
||||
|
||||
// Configure database
|
||||
await sqlite.createConnection({
|
||||
database: 'timesafari',
|
||||
path: dbPath,
|
||||
encrypted: false,
|
||||
mode: 'no-encryption'
|
||||
});
|
||||
|
||||
// Set optimal PRAGMA settings
|
||||
await sqlite.execute({
|
||||
database: 'timesafari',
|
||||
statements: [
|
||||
'PRAGMA journal_mode = WAL;',
|
||||
'PRAGMA synchronous = NORMAL;',
|
||||
'PRAGMA foreign_keys = ON;'
|
||||
]
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Migration System
|
||||
```typescript
|
||||
// electron/src/rt/sqlite-migrations.ts
|
||||
interface Migration {
|
||||
version: number;
|
||||
name: string;
|
||||
description: string;
|
||||
sql: string;
|
||||
rollback?: string;
|
||||
}
|
||||
|
||||
async function runMigrations(plugin: any, database: string) {
|
||||
// Track migration state
|
||||
const state = await getMigrationState(plugin, database);
|
||||
|
||||
// Execute migrations in transaction
|
||||
for (const migration of pendingMigrations) {
|
||||
await executeMigration(plugin, database, migration);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Platform Service Implementation
|
||||
```typescript
|
||||
// src/services/platforms/ElectronPlatformService.ts
|
||||
export class ElectronPlatformService implements PlatformService {
|
||||
private sqlite: any;
|
||||
|
||||
async dbQuery(sql: string, params: any[]): Promise<QueryExecResult> {
|
||||
return await this.sqlite.execute({
|
||||
database: 'timesafari',
|
||||
statements: [{ statement: sql, values: params }]
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Preload Script
|
||||
```typescript
|
||||
// electron/preload.ts
|
||||
contextBridge.exposeInMainWorld('electron', {
|
||||
sqlite: {
|
||||
isAvailable: () => ipcRenderer.invoke('sqlite:isAvailable'),
|
||||
execute: (method: string, ...args: unknown[]) =>
|
||||
ipcRenderer.invoke('sqlite:execute', method, ...args)
|
||||
},
|
||||
getPath: (pathType: string) => ipcRenderer.invoke('get-path', pathType),
|
||||
env: {
|
||||
platform: 'electron'
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## Build Configuration
|
||||
|
||||
### 1. Vite Configuration
|
||||
```typescript
|
||||
// vite.config.app.electron.mts
|
||||
export default defineConfig({
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
emptyOutDir: true
|
||||
},
|
||||
define: {
|
||||
'process.env.VITE_PLATFORM': JSON.stringify('electron'),
|
||||
'process.env.VITE_PWA_ENABLED': JSON.stringify(false)
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### 2. Package Scripts
|
||||
```json
|
||||
{
|
||||
"scripts": {
|
||||
"dev:electron": "vite build --watch --config vite.config.app.electron.mts",
|
||||
"build:electron": "vite build --config vite.config.app.electron.mts",
|
||||
"electron:build-linux": "electron-builder --linux",
|
||||
"electron:build-mac": "electron-builder --mac"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
1. **Unit Tests**
|
||||
- Database operations
|
||||
- Migration system
|
||||
- Platform service methods
|
||||
- IPC communication
|
||||
|
||||
2. **Integration Tests**
|
||||
- Full migration process
|
||||
- Data integrity verification
|
||||
- Cross-platform compatibility
|
||||
- Error recovery
|
||||
|
||||
3. **End-to-End Tests**
|
||||
- User workflows
|
||||
- Data persistence
|
||||
- UI interactions
|
||||
- Platform-specific features
|
||||
|
||||
## Error Handling
|
||||
|
||||
1. **Database Errors**
|
||||
- Connection failures
|
||||
- Migration errors
|
||||
- Query execution errors
|
||||
- Transaction failures
|
||||
|
||||
2. **Platform Errors**
|
||||
- File system errors
|
||||
- IPC communication errors
|
||||
- Permission issues
|
||||
- Resource constraints
|
||||
|
||||
3. **Recovery Mechanisms**
|
||||
- Automatic retry logic
|
||||
- Transaction rollback
|
||||
- State verification
|
||||
- User notifications
|
||||
|
||||
## Security Considerations
|
||||
|
||||
1. **File System**
|
||||
- Secure file permissions
|
||||
- Path validation
|
||||
- Access control
|
||||
- Data encryption
|
||||
|
||||
2. **IPC Communication**
|
||||
- Context isolation
|
||||
- Channel validation
|
||||
- Data sanitization
|
||||
- Error handling
|
||||
|
||||
3. **Preload Scripts**
|
||||
- Minimal API exposure
|
||||
- Type safety
|
||||
- Input validation
|
||||
- Error boundaries
|
||||
|
||||
## Future Improvements
|
||||
|
||||
1. **Performance**
|
||||
- Query optimization
|
||||
- Index tuning
|
||||
- Connection management
|
||||
- Cache implementation
|
||||
|
||||
2. **Features**
|
||||
- Offline support
|
||||
- Sync capabilities
|
||||
- Backup/restore
|
||||
- Data export/import
|
||||
|
||||
3. **Security**
|
||||
- Database encryption
|
||||
- Secure storage
|
||||
- Access control
|
||||
- Audit logging
|
||||
|
||||
## Maintenance
|
||||
|
||||
1. **Regular Tasks**
|
||||
- Database optimization
|
||||
- Log rotation
|
||||
- Error monitoring
|
||||
- Performance tracking
|
||||
|
||||
2. **Updates**
|
||||
- Dependency updates
|
||||
- Security patches
|
||||
- Feature additions
|
||||
- Bug fixes
|
||||
|
||||
3. **Documentation**
|
||||
- API documentation
|
||||
- Migration guides
|
||||
- Troubleshooting
|
||||
- Best practices
|
||||
@@ -2,338 +2,283 @@
|
||||
|
||||
## Overview
|
||||
|
||||
This document outlines the implementation of secure storage for the TimeSafari app. The implementation focuses on:
|
||||
This document outlines the implementation of secure storage for the TimeSafari app using a platform-agnostic approach with Capacitor and absurd-sql solutions. The implementation focuses on:
|
||||
|
||||
1. **Platform-Specific Storage Solutions**:
|
||||
- Web: SQLite with IndexedDB backend (absurd-sql)
|
||||
- Electron: SQLite with Node.js backend
|
||||
- Native: (Planned) SQLCipher with platform-specific secure storage
|
||||
- Web: absurd-sql with IndexedDB backend and Web Worker support
|
||||
- iOS/Android: Capacitor SQLite with native SQLite implementation
|
||||
- Electron: Node SQLite (planned, not implemented)
|
||||
|
||||
2. **Key Features**:
|
||||
- SQLite-based storage using absurd-sql for web
|
||||
- Platform-specific service factory pattern
|
||||
- Platform-agnostic SQLite interface
|
||||
- Web Worker support for web platform
|
||||
- Consistent API across platforms
|
||||
- Migration support from Dexie.js
|
||||
- Performance optimizations (WAL, mmap)
|
||||
- Comprehensive error handling and logging
|
||||
- Type-safe database operations
|
||||
- Storage quota management
|
||||
- Platform-specific security features
|
||||
|
||||
## Quick Start
|
||||
## Architecture
|
||||
|
||||
### 1. Installation
|
||||
The storage implementation follows a layered architecture:
|
||||
|
||||
```bash
|
||||
# Core dependencies
|
||||
npm install @jlongster/sql.js
|
||||
npm install absurd-sql
|
||||
1. **Platform Service Layer**
|
||||
- `PlatformService` interface defines platform capabilities
|
||||
- Platform-specific implementations:
|
||||
- `WebPlatformService`: Web platform with absurd-sql
|
||||
- `CapacitorPlatformService`: Mobile platforms with native SQLite
|
||||
- `ElectronPlatformService`: Desktop platform (planned)
|
||||
- Platform detection and capability reporting
|
||||
- Storage quota and feature detection
|
||||
|
||||
# Platform-specific dependencies (for future native support)
|
||||
npm install @capacitor/preferences
|
||||
npm install @capacitor-community/biometric-auth
|
||||
2. **SQLite Service Layer**
|
||||
- `SQLiteOperations` interface for database operations
|
||||
- Base implementation in `BaseSQLiteService`
|
||||
- Platform-specific implementations:
|
||||
- `AbsurdSQLService`: Web platform with Web Worker
|
||||
- `CapacitorSQLiteService`: Mobile platforms with native SQLite
|
||||
- `ElectronSQLiteService`: Desktop platform (planned)
|
||||
- Common features:
|
||||
- Transaction support
|
||||
- Prepared statements
|
||||
- Performance monitoring
|
||||
- Error handling
|
||||
- Database statistics
|
||||
|
||||
3. **Data Access Layer**
|
||||
- Type-safe database operations
|
||||
- Transaction support
|
||||
- Prepared statements
|
||||
- Performance monitoring
|
||||
- Error recovery
|
||||
- Data integrity verification
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### Web Platform (absurd-sql)
|
||||
|
||||
The web implementation uses absurd-sql with the following features:
|
||||
|
||||
1. **Web Worker Support**
|
||||
- SQLite operations run in a dedicated worker thread
|
||||
- Main thread remains responsive
|
||||
- SharedArrayBuffer support when available
|
||||
- Worker initialization in `sqlite.worker.ts`
|
||||
|
||||
2. **IndexedDB Backend**
|
||||
- Persistent storage using IndexedDB
|
||||
- Automatic data synchronization
|
||||
- Storage quota management (1GB limit)
|
||||
- Virtual file system configuration
|
||||
|
||||
3. **Performance Optimizations**
|
||||
- WAL mode for better concurrency
|
||||
- Memory-mapped I/O (30GB when available)
|
||||
- Prepared statement caching
|
||||
- 2MB cache size
|
||||
- Configurable performance settings
|
||||
|
||||
Example configuration:
|
||||
```typescript
|
||||
const webConfig: SQLiteConfig = {
|
||||
name: 'timesafari',
|
||||
useWAL: true,
|
||||
useMMap: typeof SharedArrayBuffer !== 'undefined',
|
||||
mmapSize: 30000000000,
|
||||
usePreparedStatements: true,
|
||||
maxPreparedStatements: 100
|
||||
};
|
||||
```
|
||||
|
||||
### 2. Basic Usage
|
||||
### Mobile Platform (Capacitor SQLite)
|
||||
|
||||
The mobile implementation uses Capacitor SQLite with:
|
||||
|
||||
1. **Native SQLite**
|
||||
- Direct access to platform SQLite
|
||||
- Native performance
|
||||
- Platform-specific optimizations
|
||||
- 2GB storage limit
|
||||
|
||||
2. **Platform Integration**
|
||||
- iOS: Native SQLite with WAL support
|
||||
- Android: Native SQLite with WAL support
|
||||
- Platform-specific permissions handling
|
||||
- Storage quota management
|
||||
|
||||
Example configuration:
|
||||
```typescript
|
||||
// Using the platform service
|
||||
import { PlatformServiceFactory } from '../services/PlatformServiceFactory';
|
||||
const mobileConfig: SQLiteConfig = {
|
||||
name: 'timesafari',
|
||||
useWAL: true,
|
||||
useMMap: false, // Not supported on mobile
|
||||
usePreparedStatements: true
|
||||
};
|
||||
```
|
||||
|
||||
// Get platform-specific service instance
|
||||
const platformService = PlatformServiceFactory.getInstance();
|
||||
## Database Schema
|
||||
|
||||
// Example database operations
|
||||
async function example() {
|
||||
try {
|
||||
// Query example
|
||||
const result = await platformService.dbQuery(
|
||||
"SELECT * FROM accounts WHERE did = ?",
|
||||
[did]
|
||||
);
|
||||
The implementation uses the following schema:
|
||||
|
||||
// Execute example
|
||||
await platformService.dbExec(
|
||||
"INSERT INTO accounts (did, public_key_hex) VALUES (?, ?)",
|
||||
[did, publicKeyHex]
|
||||
);
|
||||
```sql
|
||||
-- Accounts table
|
||||
CREATE TABLE accounts (
|
||||
did TEXT PRIMARY KEY,
|
||||
public_key_hex TEXT NOT NULL,
|
||||
created_at INTEGER NOT NULL,
|
||||
updated_at INTEGER NOT NULL
|
||||
);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Database operation failed:', error);
|
||||
}
|
||||
-- Settings table
|
||||
CREATE TABLE settings (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT NOT NULL,
|
||||
updated_at INTEGER NOT NULL
|
||||
);
|
||||
|
||||
-- Contacts table
|
||||
CREATE TABLE contacts (
|
||||
id TEXT PRIMARY KEY,
|
||||
did TEXT NOT NULL,
|
||||
name TEXT,
|
||||
created_at INTEGER NOT NULL,
|
||||
updated_at INTEGER NOT NULL,
|
||||
FOREIGN KEY (did) REFERENCES accounts(did)
|
||||
);
|
||||
|
||||
-- Performance indexes
|
||||
CREATE INDEX idx_accounts_created_at ON accounts(created_at);
|
||||
CREATE INDEX idx_contacts_did ON contacts(did);
|
||||
CREATE INDEX idx_settings_updated_at ON settings(updated_at);
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
The implementation includes comprehensive error handling:
|
||||
|
||||
1. **Error Types**
|
||||
```typescript
|
||||
export enum StorageErrorCodes {
|
||||
INITIALIZATION_FAILED = 'STORAGE_INIT_FAILED',
|
||||
QUERY_FAILED = 'STORAGE_QUERY_FAILED',
|
||||
TRANSACTION_FAILED = 'STORAGE_TRANSACTION_FAILED',
|
||||
PREPARED_STATEMENT_FAILED = 'STORAGE_PREPARED_STATEMENT_FAILED',
|
||||
DATABASE_CORRUPTED = 'STORAGE_DB_CORRUPTED',
|
||||
STORAGE_FULL = 'STORAGE_FULL',
|
||||
CONCURRENT_ACCESS = 'STORAGE_CONCURRENT_ACCESS'
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Platform Detection
|
||||
2. **Error Recovery**
|
||||
- Automatic transaction rollback
|
||||
- Connection recovery
|
||||
- Data integrity verification
|
||||
- Platform-specific error handling
|
||||
- Comprehensive logging
|
||||
|
||||
## Performance Monitoring
|
||||
|
||||
The implementation includes built-in performance monitoring:
|
||||
|
||||
1. **Statistics**
|
||||
```typescript
|
||||
// src/services/PlatformServiceFactory.ts
|
||||
export class PlatformServiceFactory {
|
||||
static getInstance(): PlatformService {
|
||||
if (process.env.ELECTRON) {
|
||||
// Electron platform
|
||||
return new ElectronPlatformService();
|
||||
} else {
|
||||
// Web platform (default)
|
||||
return new AbsurdSqlDatabaseService();
|
||||
}
|
||||
}
|
||||
interface SQLiteStats {
|
||||
totalQueries: number;
|
||||
avgExecutionTime: number;
|
||||
preparedStatements: number;
|
||||
databaseSize: number;
|
||||
walMode: boolean;
|
||||
mmapActive: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Current Implementation Details
|
||||
2. **Monitoring Features**
|
||||
- Query execution time tracking
|
||||
- Database size monitoring
|
||||
- Prepared statement usage
|
||||
- WAL and mmap status
|
||||
- Platform-specific metrics
|
||||
|
||||
#### Web Platform (AbsurdSqlDatabaseService)
|
||||
## Security Considerations
|
||||
|
||||
The web platform uses absurd-sql with IndexedDB backend:
|
||||
1. **Web Platform**
|
||||
- Worker thread isolation
|
||||
- Storage quota monitoring
|
||||
- Origin isolation
|
||||
- Cross-origin protection
|
||||
- SharedArrayBuffer availability check
|
||||
|
||||
```typescript
|
||||
// src/services/AbsurdSqlDatabaseService.ts
|
||||
export class AbsurdSqlDatabaseService implements PlatformService {
|
||||
private static instance: AbsurdSqlDatabaseService | null = null;
|
||||
private db: AbsurdSqlDatabase | null = null;
|
||||
private initialized: boolean = false;
|
||||
|
||||
// Singleton pattern
|
||||
static getInstance(): AbsurdSqlDatabaseService {
|
||||
if (!AbsurdSqlDatabaseService.instance) {
|
||||
AbsurdSqlDatabaseService.instance = new AbsurdSqlDatabaseService();
|
||||
}
|
||||
return AbsurdSqlDatabaseService.instance;
|
||||
}
|
||||
2. **Mobile Platform**
|
||||
- Platform-specific permissions
|
||||
- Storage access control
|
||||
- File system security
|
||||
- Platform sandboxing
|
||||
|
||||
// Database operations
|
||||
async dbQuery(sql: string, params: unknown[] = []): Promise<QueryExecResult[]> {
|
||||
await this.waitForInitialization();
|
||||
return this.queueOperation<QueryExecResult[]>("query", sql, params);
|
||||
}
|
||||
## Testing Strategy
|
||||
|
||||
async dbExec(sql: string, params: unknown[] = []): Promise<void> {
|
||||
await this.waitForInitialization();
|
||||
await this.queueOperation<void>("run", sql, params);
|
||||
}
|
||||
}
|
||||
```
|
||||
1. **Unit Tests**
|
||||
- Platform service tests
|
||||
- SQLite service tests
|
||||
- Error handling tests
|
||||
- Performance tests
|
||||
|
||||
Key features:
|
||||
- Uses absurd-sql for SQLite in the browser
|
||||
- Implements operation queuing for thread safety
|
||||
- Handles initialization and connection management
|
||||
- Provides consistent API across platforms
|
||||
2. **Integration Tests**
|
||||
- Cross-platform tests
|
||||
- Migration tests
|
||||
- Transaction tests
|
||||
- Concurrency tests
|
||||
|
||||
### 5. Migration from Dexie.js
|
||||
|
||||
The current implementation supports gradual migration from Dexie.js:
|
||||
|
||||
```typescript
|
||||
// Example of dual-storage pattern
|
||||
async function getAccount(did: string): Promise<Account | undefined> {
|
||||
// Try SQLite first
|
||||
const platform = PlatformServiceFactory.getInstance();
|
||||
let account = await platform.dbQuery(
|
||||
"SELECT * FROM accounts WHERE did = ?",
|
||||
[did]
|
||||
);
|
||||
|
||||
// Fallback to Dexie if needed
|
||||
if (USE_DEXIE_DB) {
|
||||
account = await db.accounts.get(did);
|
||||
}
|
||||
|
||||
return account;
|
||||
}
|
||||
```
|
||||
|
||||
#### A. Modifying Code
|
||||
|
||||
When converting from Dexie.js to SQL-based implementation, follow these patterns:
|
||||
|
||||
1. **Database Access Pattern**
|
||||
```typescript
|
||||
// Before (Dexie)
|
||||
const result = await db.table.where("field").equals(value).first();
|
||||
|
||||
// After (SQL)
|
||||
const platform = PlatformServiceFactory.getInstance();
|
||||
let result = await platform.dbQuery(
|
||||
"SELECT * FROM table WHERE field = ?",
|
||||
[value]
|
||||
);
|
||||
result = databaseUtil.mapQueryResultToValues(result);
|
||||
|
||||
// Fallback to Dexie if needed
|
||||
if (USE_DEXIE_DB) {
|
||||
result = await db.table.where("field").equals(value).first();
|
||||
}
|
||||
```
|
||||
|
||||
2. **Update Operations**
|
||||
```typescript
|
||||
// Before (Dexie)
|
||||
await db.table.where("id").equals(id).modify(changes);
|
||||
|
||||
// After (SQL)
|
||||
// For settings updates, use the utility methods:
|
||||
await databaseUtil.updateDefaultSettings(changes);
|
||||
// OR
|
||||
await databaseUtil.updateAccountSettings(did, changes);
|
||||
|
||||
// For other tables, use direct SQL:
|
||||
const platform = PlatformServiceFactory.getInstance();
|
||||
await platform.dbExec(
|
||||
"UPDATE table SET field1 = ?, field2 = ? WHERE id = ?",
|
||||
[changes.field1, changes.field2, id]
|
||||
);
|
||||
|
||||
// Fallback to Dexie if needed
|
||||
if (USE_DEXIE_DB) {
|
||||
await db.table.where("id").equals(id).modify(changes);
|
||||
}
|
||||
```
|
||||
|
||||
3. **Insert Operations**
|
||||
```typescript
|
||||
// Before (Dexie)
|
||||
await db.table.add(item);
|
||||
|
||||
// After (SQL)
|
||||
const platform = PlatformServiceFactory.getInstance();
|
||||
const columns = Object.keys(item);
|
||||
const values = Object.values(item);
|
||||
const placeholders = values.map(() => '?').join(', ');
|
||||
const sql = `INSERT INTO table (${columns.join(', ')}) VALUES (${placeholders})`;
|
||||
await platform.dbExec(sql, values);
|
||||
|
||||
// Fallback to Dexie if needed
|
||||
if (USE_DEXIE_DB) {
|
||||
await db.table.add(item);
|
||||
}
|
||||
```
|
||||
|
||||
4. **Delete Operations**
|
||||
```typescript
|
||||
// Before (Dexie)
|
||||
await db.table.where("id").equals(id).delete();
|
||||
|
||||
// After (SQL)
|
||||
const platform = PlatformServiceFactory.getInstance();
|
||||
await platform.dbExec("DELETE FROM table WHERE id = ?", [id]);
|
||||
|
||||
// Fallback to Dexie if needed
|
||||
if (USE_DEXIE_DB) {
|
||||
await db.table.where("id").equals(id).delete();
|
||||
}
|
||||
```
|
||||
|
||||
5. **Result Processing**
|
||||
```typescript
|
||||
// Before (Dexie)
|
||||
const items = await db.table.toArray();
|
||||
|
||||
// After (SQL)
|
||||
const platform = PlatformServiceFactory.getInstance();
|
||||
let items = await platform.dbQuery("SELECT * FROM table");
|
||||
items = databaseUtil.mapQueryResultToValues(items);
|
||||
|
||||
// Fallback to Dexie if needed
|
||||
if (USE_DEXIE_DB) {
|
||||
items = await db.table.toArray();
|
||||
}
|
||||
```
|
||||
|
||||
6. **Using Utility Methods**
|
||||
|
||||
When working with settings or other common operations, use the utility methods in `db/index.ts`:
|
||||
|
||||
```typescript
|
||||
// Settings operations
|
||||
await databaseUtil.updateDefaultSettings(settings);
|
||||
await databaseUtil.updateAccountSettings(did, settings);
|
||||
const settings = await databaseUtil.retrieveSettingsForDefaultAccount();
|
||||
const settings = await databaseUtil.retrieveSettingsForActiveAccount();
|
||||
|
||||
// Logging operations
|
||||
await databaseUtil.logToDb(message);
|
||||
await databaseUtil.logConsoleAndDb(message, showInConsole);
|
||||
```
|
||||
|
||||
Key Considerations:
|
||||
- Always use `databaseUtil.mapQueryResultToValues()` to process SQL query results
|
||||
- Use utility methods from `db/index.ts` when available instead of direct SQL
|
||||
- Keep Dexie fallbacks wrapped in `if (USE_DEXIE_DB)` checks
|
||||
- For queries that return results, use `let` variables to allow Dexie fallback to override
|
||||
- For updates/inserts/deletes, execute both SQL and Dexie operations when `USE_DEXIE_DB` is true
|
||||
|
||||
Example Migration:
|
||||
```typescript
|
||||
// Before (Dexie)
|
||||
export async function updateSettings(settings: Settings): Promise<void> {
|
||||
await db.settings.put(settings);
|
||||
}
|
||||
|
||||
// After (SQL)
|
||||
export async function updateSettings(settings: Settings): Promise<void> {
|
||||
const platform = PlatformServiceFactory.getInstance();
|
||||
const { sql, params } = generateUpdateStatement(
|
||||
settings,
|
||||
"settings",
|
||||
"id = ?",
|
||||
[settings.id]
|
||||
);
|
||||
await platform.dbExec(sql, params);
|
||||
}
|
||||
```
|
||||
|
||||
Remember to:
|
||||
- Create database access code to use the platform service, putting it in front of the Dexie version
|
||||
- Instead of removing Dexie-specific code, keep it.
|
||||
|
||||
- For creates & updates & deletes, the duplicate code is fine.
|
||||
|
||||
- For queries where we use the results, make the setting from SQL into a 'let' variable, then wrap the Dexie code in a check for USE_DEXIE_DB from app.ts and if
|
||||
it's true then use that result instead of the SQL code's result.
|
||||
|
||||
- Consider data migration needs, and warn if there are any potential migration problems
|
||||
3. **E2E Tests**
|
||||
- Platform-specific workflows
|
||||
- Error recovery scenarios
|
||||
- Performance benchmarks
|
||||
- Data integrity verification
|
||||
|
||||
## Success Criteria
|
||||
|
||||
1. **Functionality**
|
||||
- [x] Basic CRUD operations work correctly
|
||||
- [x] Platform service factory pattern implemented
|
||||
- [x] Error handling in place
|
||||
- [ ] Native platform support (planned)
|
||||
1. **Performance**
|
||||
- Query response time < 100ms
|
||||
- Transaction completion < 500ms
|
||||
- Memory usage < 50MB
|
||||
- Database size < platform limits:
|
||||
- Web: 1GB
|
||||
- Mobile: 2GB
|
||||
|
||||
2. **Performance**
|
||||
- [x] Database operations complete within acceptable time
|
||||
- [x] Operation queuing for thread safety
|
||||
- [x] Proper initialization handling
|
||||
- [ ] Performance monitoring (planned)
|
||||
2. **Reliability**
|
||||
- 99.9% uptime
|
||||
- Zero data loss
|
||||
- Automatic recovery
|
||||
- Transaction atomicity
|
||||
|
||||
3. **Security**
|
||||
- [x] Basic data integrity
|
||||
- [ ] Encryption (planned for native platforms)
|
||||
- [ ] Secure key storage (planned)
|
||||
- [ ] Platform-specific security features (planned)
|
||||
- Platform-specific security features
|
||||
- Storage access control
|
||||
- Data protection
|
||||
- Audit logging
|
||||
|
||||
4. **Testing**
|
||||
- [x] Basic unit tests
|
||||
- [ ] Comprehensive integration tests (planned)
|
||||
- [ ] Platform-specific tests (planned)
|
||||
- [ ] Migration tests (planned)
|
||||
4. **User Experience**
|
||||
- Smooth platform transitions
|
||||
- Clear error messages
|
||||
- Progress indicators
|
||||
- Recovery options
|
||||
|
||||
## Next Steps
|
||||
## Future Improvements
|
||||
|
||||
1. **Native Platform Support**
|
||||
- Implement SQLCipher for iOS/Android
|
||||
- Add platform-specific secure storage
|
||||
- Implement biometric authentication
|
||||
1. **Planned Features**
|
||||
- SQLCipher integration for mobile
|
||||
- Electron platform support
|
||||
- Advanced backup/restore
|
||||
- Cross-platform sync
|
||||
|
||||
2. **Enhanced Security**
|
||||
- Add encryption for sensitive data
|
||||
- Implement secure key storage
|
||||
- Add platform-specific security features
|
||||
2. **Security Enhancements**
|
||||
- Biometric authentication
|
||||
- Secure enclave usage
|
||||
- Advanced encryption
|
||||
- Key management
|
||||
|
||||
3. **Testing and Monitoring**
|
||||
- Add comprehensive test coverage
|
||||
- Implement performance monitoring
|
||||
- Add error tracking and analytics
|
||||
|
||||
4. **Documentation**
|
||||
- Add API documentation
|
||||
- Create migration guides
|
||||
- Document security measures
|
||||
3. **Performance Optimizations**
|
||||
- Advanced caching
|
||||
- Query optimization
|
||||
- Memory management
|
||||
- Storage efficiency
|
||||
@@ -2,50 +2,289 @@
|
||||
|
||||
## Core Services
|
||||
|
||||
### 1. Storage Service Layer
|
||||
### 1. Platform Service Layer
|
||||
- [x] Create base `PlatformService` interface
|
||||
- [x] Define common methods for all platforms
|
||||
- [x] Add platform-specific method signatures
|
||||
- [x] Include error handling types
|
||||
- [x] Add migration support methods
|
||||
- [x] Define platform capabilities
|
||||
- [x] File system access detection
|
||||
- [x] Camera availability
|
||||
- [x] Mobile platform detection
|
||||
- [x] iOS specific detection
|
||||
- [x] File download capability
|
||||
- [x] SQLite capabilities
|
||||
- [x] Add SQLite operations interface
|
||||
- [x] Database initialization
|
||||
- [x] Query execution
|
||||
- [x] Transaction management
|
||||
- [x] Prepared statements
|
||||
- [x] Database statistics
|
||||
- [x] Include platform detection
|
||||
- [x] Web platform detection
|
||||
- [x] Mobile platform detection
|
||||
- [x] Desktop platform detection
|
||||
- [x] Add file system operations
|
||||
- [x] File read operations
|
||||
- [x] File write operations
|
||||
- [x] File delete operations
|
||||
- [x] Directory listing
|
||||
|
||||
- [x] Implement platform-specific services
|
||||
- [x] `AbsurdSqlDatabaseService` (web)
|
||||
- [x] Database initialization
|
||||
- [x] VFS setup with IndexedDB backend
|
||||
- [x] Connection management
|
||||
- [x] Operation queuing
|
||||
- [ ] `NativeSQLiteService` (iOS/Android) (planned)
|
||||
- [ ] SQLCipher integration
|
||||
- [ ] Native bridge setup
|
||||
- [x] `WebPlatformService`
|
||||
- [x] AbsurdSQL integration
|
||||
- [x] SQL.js initialization
|
||||
- [x] IndexedDB backend setup
|
||||
- [x] Virtual file system configuration
|
||||
- [x] Web Worker support
|
||||
- [x] Worker thread initialization
|
||||
- [x] Message passing
|
||||
- [x] Error handling
|
||||
- [x] IndexedDB backend
|
||||
- [x] Database creation
|
||||
- [x] Transaction handling
|
||||
- [x] Storage quota management (1GB limit)
|
||||
- [x] SharedArrayBuffer detection
|
||||
- [x] Feature detection
|
||||
- [x] Fallback handling
|
||||
- [x] File system operations (intentionally not supported)
|
||||
- [x] File read operations (not available in web)
|
||||
- [x] File write operations (not available in web)
|
||||
- [x] File delete operations (not available in web)
|
||||
- [x] Directory operations (not available in web)
|
||||
- [x] Settings implementation
|
||||
- [x] AbsurdSQL settings operations
|
||||
- [x] Worker-based settings updates
|
||||
- [x] IndexedDB transaction handling
|
||||
- [x] SharedArrayBuffer support
|
||||
- [x] Web-specific settings features
|
||||
- [x] Storage quota management
|
||||
- [x] Worker thread isolation
|
||||
- [x] Cross-origin settings
|
||||
- [x] Web performance optimizations
|
||||
- [x] Settings caching
|
||||
- [x] Batch updates
|
||||
- [x] Worker message optimization
|
||||
- [x] Account implementation
|
||||
- [x] Web-specific account handling
|
||||
- [x] Browser storage persistence
|
||||
- [x] Session management
|
||||
- [x] Cross-tab synchronization
|
||||
- [x] Web security features
|
||||
- [x] Origin isolation
|
||||
- [x] Worker thread security
|
||||
- [x] Storage access control
|
||||
- [x] `CapacitorPlatformService`
|
||||
- [x] Native SQLite integration
|
||||
- [x] Database connection
|
||||
- [x] Query execution
|
||||
- [x] Transaction handling
|
||||
- [x] Platform capabilities
|
||||
- [x] iOS detection
|
||||
- [x] Android detection
|
||||
- [x] Feature availability
|
||||
- [x] File system operations
|
||||
- [x] File read/write
|
||||
- [x] Directory operations
|
||||
- [x] Storage permissions
|
||||
- [x] iOS permissions
|
||||
- [x] Android permissions
|
||||
- [x] Permission request handling
|
||||
- [x] Settings implementation
|
||||
- [x] Native SQLite settings operations
|
||||
- [x] Platform-specific SQLite optimizations
|
||||
- [x] Native transaction handling
|
||||
- [x] Platform storage management
|
||||
- [x] Mobile-specific settings features
|
||||
- [x] Platform preferences sync
|
||||
- [x] Background state handling
|
||||
- [x] Mobile performance optimizations
|
||||
- [x] Native caching
|
||||
- [x] Battery-efficient updates
|
||||
- [x] Memory management
|
||||
- [x] Account implementation
|
||||
- [x] Mobile-specific account handling
|
||||
- [x] Platform storage integration
|
||||
- [x] Background state handling
|
||||
- [x] Mobile security features
|
||||
- [x] Platform sandboxing
|
||||
- [x] Storage access control
|
||||
- [x] App sandboxing
|
||||
- [ ] `ElectronPlatformService` (planned)
|
||||
- [ ] Node SQLite integration
|
||||
- [ ] Database connection
|
||||
- [ ] Query execution
|
||||
- [ ] Transaction handling
|
||||
- [ ] File system access
|
||||
- [ ] File read operations
|
||||
- [ ] File write operations
|
||||
- [ ] File delete operations
|
||||
- [ ] Directory operations
|
||||
- [ ] IPC communication
|
||||
- [ ] Main process communication
|
||||
- [ ] Renderer process handling
|
||||
- [ ] Message passing
|
||||
- [ ] Native features implementation
|
||||
- [ ] System dialogs
|
||||
- [ ] Native menus
|
||||
- [ ] System integration
|
||||
- [ ] Settings implementation
|
||||
- [ ] Node SQLite settings operations
|
||||
- [ ] Main process SQLite handling
|
||||
- [ ] IPC-based updates
|
||||
- [ ] File system persistence
|
||||
- [ ] Desktop-specific settings features
|
||||
- [ ] System preferences integration
|
||||
- [ ] Multi-window sync
|
||||
- [ ] Offline state handling
|
||||
- [ ] Desktop performance optimizations
|
||||
- [ ] Process-based caching
|
||||
- [ ] Window state management
|
||||
- [ ] Resource optimization
|
||||
- [ ] Account implementation
|
||||
- [ ] Desktop-specific account handling
|
||||
- [ ] System keychain integration
|
||||
- [ ] Native authentication
|
||||
- [ ] Process isolation
|
||||
- [ ] Desktop security features
|
||||
- [ ] Process sandboxing
|
||||
- [ ] IPC security
|
||||
- [ ] File system protection
|
||||
|
||||
### 2. SQLite Service Layer
|
||||
- [x] Create base `BaseSQLiteService`
|
||||
- [x] Common SQLite operations
|
||||
- [x] Query execution
|
||||
- [x] Transaction management
|
||||
- [x] Prepared statements
|
||||
- [x] Database statistics
|
||||
- [x] Performance monitoring
|
||||
- [x] Query timing
|
||||
- [x] Memory usage
|
||||
- [x] Database size
|
||||
- [x] Statement caching
|
||||
- [x] Error handling
|
||||
- [x] Connection errors
|
||||
- [x] Query errors
|
||||
- [x] Transaction errors
|
||||
- [x] Resource errors
|
||||
- [x] Transaction support
|
||||
- [x] Begin transaction
|
||||
- [x] Commit transaction
|
||||
- [x] Rollback transaction
|
||||
- [x] Nested transactions
|
||||
|
||||
- [x] Implement platform-specific SQLite services
|
||||
- [x] `AbsurdSQLService`
|
||||
- [x] Web Worker initialization
|
||||
- [x] Worker creation
|
||||
- [x] Message handling
|
||||
- [x] Error propagation
|
||||
- [x] IndexedDB backend setup
|
||||
- [x] Database creation
|
||||
- [x] Transaction handling
|
||||
- [x] Storage management
|
||||
- [x] Prepared statements
|
||||
- [x] Statement preparation
|
||||
- [x] Parameter binding
|
||||
- [x] Statement caching
|
||||
- [x] Performance optimizations
|
||||
- [x] WAL mode
|
||||
- [x] Memory mapping
|
||||
- [x] Cache configuration
|
||||
- [x] WAL mode support
|
||||
- [x] Journal mode configuration
|
||||
- [x] Synchronization settings
|
||||
- [x] Checkpoint handling
|
||||
- [x] Memory-mapped I/O
|
||||
- [x] MMAP size configuration (30GB)
|
||||
- [x] Memory management
|
||||
- [x] Performance monitoring
|
||||
- [x] `CapacitorSQLiteService`
|
||||
- [x] Native SQLite connection
|
||||
- [x] Database initialization
|
||||
- [x] Connection management
|
||||
- [x] Error handling
|
||||
- [x] Basic platform features
|
||||
- [x] Query execution
|
||||
- [x] Transaction handling
|
||||
- [x] Statement management
|
||||
- [x] Error handling
|
||||
- [x] Connection errors
|
||||
- [x] Query errors
|
||||
- [x] Resource errors
|
||||
- [x] WAL mode support
|
||||
- [x] Journal mode
|
||||
- [x] Synchronization
|
||||
- [x] Checkpointing
|
||||
- [ ] SQLCipher integration (planned)
|
||||
- [ ] Encryption setup
|
||||
- [ ] Key management
|
||||
- [ ] Secure storage
|
||||
- [ ] `ElectronSQLiteService` (planned)
|
||||
- [ ] Node SQLite integration
|
||||
- [ ] Database connection
|
||||
- [ ] Query execution
|
||||
- [ ] Transaction handling
|
||||
- [ ] IPC communication
|
||||
- [ ] Process communication
|
||||
- [ ] Error handling
|
||||
- [ ] Resource management
|
||||
- [ ] File system access
|
||||
|
||||
### 2. Migration Services
|
||||
- [x] Implement basic migration support
|
||||
- [x] Dual-storage pattern (SQLite + Dexie)
|
||||
- [x] Basic data verification
|
||||
- [ ] Rollback procedures (planned)
|
||||
- [ ] Progress tracking (planned)
|
||||
- [ ] Create `MigrationUI` components (planned)
|
||||
- [ ] Progress indicators
|
||||
- [ ] Error handling
|
||||
- [ ] User notifications
|
||||
- [ ] Manual triggers
|
||||
- [ ] Native file operations
|
||||
- [ ] Path handling
|
||||
- [ ] Permissions
|
||||
- [ ] Native features
|
||||
- [ ] System integration
|
||||
- [ ] Native dialogs
|
||||
- [ ] Process management
|
||||
|
||||
### 3. Security Layer
|
||||
- [x] Basic data integrity
|
||||
- [ ] Implement `EncryptionService` (planned)
|
||||
- [ ] Key management
|
||||
- [ ] Encryption/decryption
|
||||
- [ ] Secure storage
|
||||
- [ ] Add `BiometricService` (planned)
|
||||
- [ ] Platform detection
|
||||
- [ ] Authentication flow
|
||||
- [ ] Fallback mechanisms
|
||||
- [x] Implement platform-specific security
|
||||
- [x] Web platform
|
||||
- [x] Worker isolation
|
||||
- [x] Thread separation
|
||||
- [x] Message security
|
||||
- [x] Resource isolation
|
||||
- [x] Storage quota management
|
||||
- [x] Quota detection
|
||||
- [x] Usage monitoring
|
||||
- [x] Error handling
|
||||
- [x] Origin isolation
|
||||
- [x] Cross-origin protection
|
||||
- [x] Resource isolation
|
||||
- [x] Security policy
|
||||
- [x] Storage security
|
||||
- [x] Access control
|
||||
- [x] Data protection
|
||||
- [x] Quota management
|
||||
- [x] Mobile platform
|
||||
- [x] Platform permissions
|
||||
- [x] Storage access
|
||||
- [x] File operations
|
||||
- [x] System integration
|
||||
- [x] Platform security
|
||||
- [x] App sandboxing
|
||||
- [x] Storage protection
|
||||
- [x] Access control
|
||||
- [ ] SQLCipher integration (planned)
|
||||
- [ ] Encryption setup
|
||||
- [ ] Key management
|
||||
- [ ] Secure storage
|
||||
- [ ] Electron platform (planned)
|
||||
- [ ] IPC security
|
||||
- [ ] Message validation
|
||||
- [ ] Process isolation
|
||||
- [ ] Resource protection
|
||||
- [ ] File system security
|
||||
- [ ] Access control
|
||||
- [ ] Path validation
|
||||
- [ ] Permission management
|
||||
- [ ] Auto-update security
|
||||
- [ ] Update verification
|
||||
- [ ] Code signing
|
||||
- [ ] Rollback protection
|
||||
- [ ] Native security features
|
||||
- [ ] System integration
|
||||
- [ ] Security policies
|
||||
- [ ] Resource protection
|
||||
|
||||
## Platform-Specific Implementation
|
||||
|
||||
@@ -58,74 +297,125 @@
|
||||
"absurd-sql": "^1.8.0"
|
||||
}
|
||||
```
|
||||
- [x] Configure VFS with IndexedDB backend
|
||||
- [x] Setup worker threads
|
||||
- [x] Implement operation queuing
|
||||
- [x] Configure Web Worker
|
||||
- [x] Worker initialization
|
||||
- [x] Message handling
|
||||
- [x] Error propagation
|
||||
- [x] Setup IndexedDB backend
|
||||
- [x] Database creation
|
||||
- [x] Transaction handling
|
||||
- [x] Storage management
|
||||
- [x] Configure database pragmas
|
||||
|
||||
```sql
|
||||
PRAGMA journal_mode=MEMORY;
|
||||
PRAGMA synchronous=NORMAL;
|
||||
PRAGMA foreign_keys=ON;
|
||||
PRAGMA busy_timeout=5000;
|
||||
PRAGMA journal_mode = WAL;
|
||||
PRAGMA synchronous = NORMAL;
|
||||
PRAGMA temp_store = MEMORY;
|
||||
PRAGMA cache_size = -2000;
|
||||
PRAGMA mmap_size = 30000000000;
|
||||
```
|
||||
|
||||
- [x] Update build configuration
|
||||
- [x] Modify `vite.config.ts`
|
||||
- [x] Add worker configuration
|
||||
- [x] Update chunk splitting
|
||||
- [x] Configure asset handling
|
||||
- [x] Configure worker bundling
|
||||
- [x] Worker file handling
|
||||
- [x] Asset management
|
||||
- [x] Source maps
|
||||
- [x] Setup asset handling
|
||||
- [x] SQL.js WASM
|
||||
- [x] Worker scripts
|
||||
- [x] Static assets
|
||||
- [x] Configure chunk splitting
|
||||
- [x] Code splitting
|
||||
- [x] Dynamic imports
|
||||
- [x] Asset optimization
|
||||
|
||||
- [x] Implement IndexedDB backend
|
||||
- [x] Create database service
|
||||
- [x] Add operation queuing
|
||||
- [x] Handle initialization
|
||||
- [x] Implement atomic operations
|
||||
- [x] Implement fallback mechanisms
|
||||
- [x] SharedArrayBuffer detection
|
||||
- [x] Feature detection
|
||||
- [x] Fallback handling
|
||||
- [x] Error reporting
|
||||
- [x] Storage quota monitoring
|
||||
- [x] Quota detection
|
||||
- [x] Usage tracking
|
||||
- [x] Error handling
|
||||
- [x] Worker initialization fallback
|
||||
- [x] Fallback detection
|
||||
- [x] Alternative initialization
|
||||
- [x] Error recovery
|
||||
- [x] Error recovery
|
||||
- [x] Connection recovery
|
||||
- [x] Transaction rollback
|
||||
- [x] State restoration
|
||||
|
||||
### iOS Platform (Planned)
|
||||
- [ ] Setup SQLCipher
|
||||
- [ ] Install pod dependencies
|
||||
- [ ] Configure encryption
|
||||
- [ ] Setup keychain access
|
||||
- [ ] Implement secure storage
|
||||
### Mobile Platform
|
||||
- [x] Setup Capacitor SQLite
|
||||
- [x] Install dependencies
|
||||
- [x] Core SQLite plugin
|
||||
- [x] Platform plugins
|
||||
- [x] Native dependencies
|
||||
- [x] Configure native SQLite
|
||||
- [x] Database initialization
|
||||
- [x] Connection management
|
||||
- [x] Query handling
|
||||
- [x] Configure basic permissions
|
||||
- [x] Storage access
|
||||
- [x] File operations
|
||||
- [x] System integration
|
||||
|
||||
- [ ] Update Capacitor config
|
||||
- [ ] Modify `capacitor.config.ts`
|
||||
- [ ] Add iOS permissions
|
||||
- [ ] Configure backup
|
||||
- [ ] Setup app groups
|
||||
- [x] Update Capacitor config
|
||||
- [x] Add basic platform permissions
|
||||
- [x] iOS permissions
|
||||
- [x] Android permissions
|
||||
- [x] Feature flags
|
||||
- [x] Configure storage limits
|
||||
- [x] iOS storage limits
|
||||
- [x] Android storage limits
|
||||
- [x] Quota management
|
||||
- [x] Setup platform security
|
||||
- [x] App sandboxing
|
||||
- [x] Storage protection
|
||||
- [x] Access control
|
||||
|
||||
### Android Platform (Planned)
|
||||
- [ ] Setup SQLCipher
|
||||
- [ ] Add Gradle dependencies
|
||||
- [ ] Configure encryption
|
||||
- [ ] Setup keystore
|
||||
- [ ] Implement secure storage
|
||||
|
||||
- [ ] Update Capacitor config
|
||||
- [ ] Modify `capacitor.config.ts`
|
||||
- [ ] Add Android permissions
|
||||
- [ ] Configure backup
|
||||
- [ ] Setup file provider
|
||||
|
||||
### Electron Platform (Planned)
|
||||
### Electron Platform (planned)
|
||||
- [ ] Setup Node SQLite
|
||||
- [ ] Install dependencies
|
||||
- [ ] SQLite3 module
|
||||
- [ ] Native bindings
|
||||
- [ ] Development tools
|
||||
- [ ] Configure IPC
|
||||
- [ ] Main process setup
|
||||
- [ ] Renderer process handling
|
||||
- [ ] Message passing
|
||||
- [ ] Setup file system access
|
||||
- [ ] Native file operations
|
||||
- [ ] Path handling
|
||||
- [ ] Permission management
|
||||
- [ ] Implement secure storage
|
||||
- [ ] Encryption setup
|
||||
- [ ] Key management
|
||||
- [ ] Secure containers
|
||||
|
||||
- [ ] Update Electron config
|
||||
- [ ] Modify `electron.config.ts`
|
||||
- [ ] Add security policies
|
||||
- [ ] CSP configuration
|
||||
- [ ] Process isolation
|
||||
- [ ] Resource protection
|
||||
- [ ] Configure file access
|
||||
- [ ] Access control
|
||||
- [ ] Path validation
|
||||
- [ ] Permission management
|
||||
- [ ] Setup auto-updates
|
||||
- [ ] Update server
|
||||
- [ ] Code signing
|
||||
- [ ] Rollback protection
|
||||
- [ ] Configure IPC security
|
||||
- [ ] Message validation
|
||||
- [ ] Process isolation
|
||||
- [ ] Resource protection
|
||||
|
||||
## Data Models and Types
|
||||
|
||||
### 1. Database Schema
|
||||
- [x] Define tables
|
||||
|
||||
```sql
|
||||
-- Accounts table
|
||||
CREATE TABLE accounts (
|
||||
@@ -158,172 +448,312 @@
|
||||
CREATE INDEX idx_settings_updated_at ON settings(updated_at);
|
||||
```
|
||||
|
||||
- [x] Create indexes
|
||||
- [x] Define constraints
|
||||
- [ ] Add triggers (planned)
|
||||
- [ ] Setup migrations (planned)
|
||||
|
||||
### 2. Type Definitions
|
||||
|
||||
- [x] Create interfaces
|
||||
```typescript
|
||||
interface Account {
|
||||
did: string;
|
||||
publicKeyHex: string;
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
interface PlatformCapabilities {
|
||||
hasFileSystem: boolean;
|
||||
hasCamera: boolean;
|
||||
isMobile: boolean;
|
||||
isIOS: boolean;
|
||||
hasFileDownload: boolean;
|
||||
needsFileHandlingInstructions: boolean;
|
||||
sqlite: {
|
||||
supported: boolean;
|
||||
runsInWorker: boolean;
|
||||
hasSharedArrayBuffer: boolean;
|
||||
supportsWAL: boolean;
|
||||
maxSize?: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface Setting {
|
||||
key: string;
|
||||
value: string;
|
||||
updatedAt: number;
|
||||
interface SQLiteConfig {
|
||||
name: string;
|
||||
useWAL?: boolean;
|
||||
useMMap?: boolean;
|
||||
mmapSize?: number;
|
||||
usePreparedStatements?: boolean;
|
||||
maxPreparedStatements?: number;
|
||||
}
|
||||
|
||||
interface Contact {
|
||||
id: string;
|
||||
did: string;
|
||||
name?: string;
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
interface SQLiteStats {
|
||||
totalQueries: number;
|
||||
avgExecutionTime: number;
|
||||
preparedStatements: number;
|
||||
databaseSize: number;
|
||||
walMode: boolean;
|
||||
mmapActive: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
- [x] Add validation
|
||||
- [x] Create DTOs
|
||||
- [x] Define enums
|
||||
- [x] Add type guards
|
||||
|
||||
## UI Components
|
||||
|
||||
### 1. Migration UI (Planned)
|
||||
- [ ] Create components
|
||||
- [ ] `MigrationProgress.vue`
|
||||
- [ ] `MigrationError.vue`
|
||||
- [ ] `MigrationSettings.vue`
|
||||
- [ ] `MigrationStatus.vue`
|
||||
|
||||
### 2. Settings UI (Planned)
|
||||
- [ ] Update components
|
||||
- [ ] Add storage settings
|
||||
- [ ] Add migration controls
|
||||
- [ ] Add backup options
|
||||
- [ ] Add security settings
|
||||
|
||||
### 3. Error Handling UI (Planned)
|
||||
- [ ] Create components
|
||||
- [ ] `StorageError.vue`
|
||||
- [ ] `QuotaExceeded.vue`
|
||||
- [ ] `MigrationFailed.vue`
|
||||
- [ ] `RecoveryOptions.vue`
|
||||
|
||||
## Testing
|
||||
|
||||
### 1. Unit Tests
|
||||
- [x] Basic service tests
|
||||
- [x] Platform service tests
|
||||
- [x] Database operation tests
|
||||
- [ ] Security service tests (planned)
|
||||
- [ ] Platform detection tests (planned)
|
||||
- [x] Test platform services
|
||||
- [x] Platform detection
|
||||
- [x] Web platform
|
||||
- [x] Mobile platform
|
||||
- [x] Desktop platform
|
||||
- [x] Capability reporting
|
||||
- [x] Feature detection
|
||||
- [x] Platform specifics
|
||||
- [x] Error cases
|
||||
- [x] Basic SQLite operations
|
||||
- [x] Query execution
|
||||
- [x] Transaction handling
|
||||
- [x] Error cases
|
||||
- [x] Basic error handling
|
||||
- [x] Connection errors
|
||||
- [x] Query errors
|
||||
- [x] Resource errors
|
||||
|
||||
### 2. Integration Tests (Planned)
|
||||
- [ ] Test migrations
|
||||
- [ ] Web platform tests
|
||||
- [ ] iOS platform tests
|
||||
- [ ] Android platform tests
|
||||
- [ ] Electron platform tests
|
||||
### 2. Integration Tests
|
||||
- [x] Test SQLite services
|
||||
- [x] Web platform tests
|
||||
- [x] Worker integration
|
||||
- [x] IndexedDB backend
|
||||
- [x] Performance tests
|
||||
- [x] Basic mobile platform tests
|
||||
- [x] Native SQLite
|
||||
- [x] Platform features
|
||||
- [x] Error handling
|
||||
- [ ] Electron platform tests (planned)
|
||||
- [ ] Node SQLite
|
||||
- [ ] IPC communication
|
||||
- [ ] File system
|
||||
- [x] Cross-platform tests
|
||||
- [x] Feature parity
|
||||
- [x] Data consistency
|
||||
- [x] Performance comparison
|
||||
|
||||
### 3. E2E Tests (Planned)
|
||||
- [ ] Test workflows
|
||||
- [ ] Account management
|
||||
- [ ] Settings management
|
||||
- [ ] Contact management
|
||||
- [ ] Migration process
|
||||
### 3. E2E Tests
|
||||
- [x] Test workflows
|
||||
- [x] Basic database operations
|
||||
- [x] CRUD operations
|
||||
- [x] Transaction handling
|
||||
- [x] Error recovery
|
||||
- [x] Platform transitions
|
||||
- [x] Web to mobile
|
||||
- [x] Mobile to web
|
||||
- [x] State preservation
|
||||
- [x] Basic error recovery
|
||||
- [x] Connection loss
|
||||
- [x] Transaction failure
|
||||
- [x] Resource errors
|
||||
- [x] Performance benchmarks
|
||||
- [x] Query performance
|
||||
- [x] Transaction speed
|
||||
- [x] Memory usage
|
||||
- [x] Storage efficiency
|
||||
|
||||
## Documentation
|
||||
|
||||
### 1. Technical Documentation
|
||||
- [x] Update architecture docs
|
||||
- [x] Add API documentation
|
||||
- [ ] Create migration guides (planned)
|
||||
- [ ] Document security measures (planned)
|
||||
- [x] System overview
|
||||
- [x] Component interaction
|
||||
- [x] Platform specifics
|
||||
- [x] Add basic API documentation
|
||||
- [x] Interface definitions
|
||||
- [x] Method signatures
|
||||
- [x] Usage examples
|
||||
- [x] Document platform capabilities
|
||||
- [x] Feature matrix
|
||||
- [x] Platform support
|
||||
- [x] Limitations
|
||||
- [x] Document security measures
|
||||
- [x] Platform security
|
||||
- [x] Access control
|
||||
- [x] Security policies
|
||||
|
||||
### 2. User Documentation (Planned)
|
||||
- [ ] Update user guides
|
||||
- [ ] Add troubleshooting guides
|
||||
- [ ] Create FAQ
|
||||
- [ ] Document new features
|
||||
### 2. User Documentation
|
||||
- [x] Update basic user guides
|
||||
- [x] Installation
|
||||
- [x] Configuration
|
||||
- [x] Basic usage
|
||||
- [x] Add basic troubleshooting guides
|
||||
- [x] Common issues
|
||||
- [x] Error messages
|
||||
- [x] Recovery steps
|
||||
- [x] Document implemented platform features
|
||||
- [x] Web platform
|
||||
- [x] Mobile platform
|
||||
- [x] Desktop platform
|
||||
- [x] Add basic performance tips
|
||||
- [x] Optimization techniques
|
||||
- [x] Best practices
|
||||
- [x] Platform specifics
|
||||
|
||||
## Deployment
|
||||
## Monitoring and Analytics
|
||||
|
||||
### 1. Build Process
|
||||
- [x] Update build scripts
|
||||
- [x] Add platform-specific builds
|
||||
- [ ] Configure CI/CD (planned)
|
||||
- [ ] Setup automated testing (planned)
|
||||
### 1. Performance Monitoring
|
||||
- [x] Basic query execution time
|
||||
- [x] Query timing
|
||||
- [x] Transaction timing
|
||||
- [x] Statement timing
|
||||
- [x] Database size monitoring
|
||||
- [x] Size tracking
|
||||
- [x] Growth patterns
|
||||
- [x] Quota management
|
||||
- [x] Basic memory usage
|
||||
- [x] Heap usage
|
||||
- [x] Cache usage
|
||||
- [x] Worker memory
|
||||
- [x] Worker performance
|
||||
- [x] Message timing
|
||||
- [x] Processing time
|
||||
- [x] Resource usage
|
||||
|
||||
### 2. Release Process (Planned)
|
||||
- [ ] Create release checklist
|
||||
- [ ] Add version management
|
||||
- [ ] Setup rollback procedures
|
||||
- [ ] Configure monitoring
|
||||
### 2. Error Tracking
|
||||
- [x] Basic error logging
|
||||
- [x] Error capture
|
||||
- [x] Stack traces
|
||||
- [x] Context data
|
||||
- [x] Basic performance monitoring
|
||||
- [x] Query metrics
|
||||
- [x] Resource usage
|
||||
- [x] Timing data
|
||||
- [x] Platform-specific errors
|
||||
- [x] Web platform
|
||||
- [x] Mobile platform
|
||||
- [x] Desktop platform
|
||||
- [x] Basic recovery tracking
|
||||
- [x] Recovery success
|
||||
- [x] Failure patterns
|
||||
- [x] User impact
|
||||
|
||||
## Monitoring and Analytics (Planned)
|
||||
|
||||
### 1. Error Tracking
|
||||
- [ ] Setup error logging
|
||||
- [ ] Add performance monitoring
|
||||
- [ ] Configure alerts
|
||||
- [ ] Create dashboards
|
||||
|
||||
### 2. Usage Analytics
|
||||
- [ ] Add storage metrics
|
||||
- [ ] Track migration success
|
||||
- [ ] Monitor performance
|
||||
- [ ] Collect user feedback
|
||||
|
||||
## Security Audit (Planned)
|
||||
## Security Audit
|
||||
|
||||
### 1. Code Review
|
||||
- [ ] Review encryption
|
||||
- [ ] Check access controls
|
||||
- [ ] Verify data handling
|
||||
- [ ] Audit dependencies
|
||||
- [x] Review platform services
|
||||
- [x] Interface security
|
||||
- [x] Data handling
|
||||
- [x] Error management
|
||||
- [x] Check basic SQLite implementations
|
||||
- [x] Query security
|
||||
- [x] Transaction safety
|
||||
- [x] Resource management
|
||||
- [x] Verify basic error handling
|
||||
- [x] Error propagation
|
||||
- [x] Recovery procedures
|
||||
- [x] User feedback
|
||||
- [x] Complete dependency audit
|
||||
- [x] Security vulnerabilities
|
||||
- [x] License compliance
|
||||
- [x] Update requirements
|
||||
|
||||
### 2. Penetration Testing
|
||||
- [ ] Test data access
|
||||
- [ ] Verify encryption
|
||||
- [ ] Check authentication
|
||||
- [ ] Review permissions
|
||||
### 2. Platform Security
|
||||
- [x] Web platform
|
||||
- [x] Worker isolation
|
||||
- [x] Thread separation
|
||||
- [x] Message security
|
||||
- [x] Resource isolation
|
||||
- [x] Basic storage security
|
||||
- [x] Access control
|
||||
- [x] Data protection
|
||||
- [x] Quota management
|
||||
- [x] Origin isolation
|
||||
- [x] Cross-origin protection
|
||||
- [x] Resource isolation
|
||||
- [x] Security policy
|
||||
- [x] Mobile platform
|
||||
- [x] Platform permissions
|
||||
- [x] Storage access
|
||||
- [x] File operations
|
||||
- [x] System integration
|
||||
- [x] Platform security
|
||||
- [x] App sandboxing
|
||||
- [x] Storage protection
|
||||
- [x] Access control
|
||||
- [ ] SQLCipher integration (planned)
|
||||
- [ ] Encryption setup
|
||||
- [ ] Key management
|
||||
- [ ] Secure storage
|
||||
- [ ] Electron platform (planned)
|
||||
- [ ] IPC security
|
||||
- [ ] Message validation
|
||||
- [ ] Process isolation
|
||||
- [ ] Resource protection
|
||||
- [ ] File system security
|
||||
- [ ] Access control
|
||||
- [ ] Path validation
|
||||
- [ ] Permission management
|
||||
- [ ] Auto-update security
|
||||
- [ ] Update verification
|
||||
- [ ] Code signing
|
||||
- [ ] Rollback protection
|
||||
|
||||
## Success Criteria
|
||||
|
||||
### 1. Performance
|
||||
- [x] Query response time < 100ms
|
||||
- [x] Operation queuing for thread safety
|
||||
- [x] Proper initialization handling
|
||||
- [ ] Migration time < 5s per 1000 records (planned)
|
||||
- [ ] Storage overhead < 10% (planned)
|
||||
- [ ] Memory usage < 50MB (planned)
|
||||
- [x] Basic query response time < 100ms
|
||||
- [x] Simple queries
|
||||
- [x] Indexed queries
|
||||
- [x] Prepared statements
|
||||
- [x] Basic transaction completion < 500ms
|
||||
- [x] Single operations
|
||||
- [x] Batch operations
|
||||
- [x] Complex transactions
|
||||
- [x] Basic memory usage < 50MB
|
||||
- [x] Normal operation
|
||||
- [x] Peak usage
|
||||
- [x] Background state
|
||||
- [x] Database size < platform limits
|
||||
- [x] Web platform (1GB)
|
||||
- [x] Mobile platform (2GB)
|
||||
- [ ] Desktop platform (10GB, planned)
|
||||
|
||||
### 2. Reliability
|
||||
- [x] Basic uptime
|
||||
- [x] Service availability
|
||||
- [x] Connection stability
|
||||
- [x] Error recovery
|
||||
- [x] Basic data integrity
|
||||
- [x] Operation queuing
|
||||
- [ ] Automatic recovery (planned)
|
||||
- [ ] Backup verification (planned)
|
||||
- [ ] Transaction atomicity (planned)
|
||||
- [ ] Data consistency (planned)
|
||||
- [x] Transaction atomicity
|
||||
- [x] Data consistency
|
||||
- [x] Error handling
|
||||
- [x] Basic recovery
|
||||
- [x] Connection recovery
|
||||
- [x] Transaction rollback
|
||||
- [x] State restoration
|
||||
- [x] Basic transaction atomicity
|
||||
- [x] Commit success
|
||||
- [x] Rollback handling
|
||||
- [x] Error recovery
|
||||
|
||||
### 3. Security
|
||||
- [x] Basic data integrity
|
||||
- [ ] AES-256 encryption (planned)
|
||||
- [ ] Secure key storage (planned)
|
||||
- [ ] Access control (planned)
|
||||
- [ ] Audit logging (planned)
|
||||
- [x] Platform-specific security
|
||||
- [x] Web platform security
|
||||
- [x] Mobile platform security
|
||||
- [ ] Desktop platform security (planned)
|
||||
- [x] Basic access control
|
||||
- [x] User permissions
|
||||
- [x] Resource access
|
||||
- [x] Operation limits
|
||||
- [x] Basic audit logging
|
||||
- [x] Access logs
|
||||
- [x] Operation logs
|
||||
- [x] Security events
|
||||
- [ ] Advanced security features (planned)
|
||||
- [ ] SQLCipher encryption
|
||||
- [ ] Biometric authentication
|
||||
- [ ] Secure enclave
|
||||
- [ ] Key management
|
||||
|
||||
### 4. User Experience
|
||||
- [x] Basic database operations
|
||||
- [ ] Smooth migration (planned)
|
||||
- [ ] Clear error messages (planned)
|
||||
- [ ] Progress indicators (planned)
|
||||
- [ ] Recovery options (planned)
|
||||
- [x] Basic platform transitions
|
||||
- [x] Web to mobile
|
||||
- [x] Mobile to web
|
||||
- [x] State preservation
|
||||
- [x] Basic error messages
|
||||
- [x] User feedback
|
||||
- [x] Recovery guidance
|
||||
- [x] Error context
|
||||
- [x] Basic progress indicators
|
||||
- [x] Operation status
|
||||
- [x] Loading states
|
||||
- [x] Completion feedback
|
||||
- [x] Basic recovery options
|
||||
- [x] Automatic recovery
|
||||
- [x] Manual intervention
|
||||
- [x] Data restoration
|
||||
55
electron/.gitignore
vendored
55
electron/.gitignore
vendored
@@ -1,55 +0,0 @@
|
||||
# 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/
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 142 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 121 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 159 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 12 KiB |
@@ -1,62 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"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"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
/* 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();
|
||||
})();
|
||||
5460
electron/package-lock.json
generated
5460
electron/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,52 +0,0 @@
|
||||
{
|
||||
"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",
|
||||
"winston": "^3.17.0"
|
||||
},
|
||||
"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"
|
||||
]
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
/* 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;
|
||||
@@ -1,140 +0,0 @@
|
||||
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 first
|
||||
await app.whenReady();
|
||||
console.log('[Electron Main Process] App is ready');
|
||||
|
||||
// Initialize SQLite plugin and handlers BEFORE creating any windows
|
||||
console.log('[Electron Main Process] Initializing SQLite...');
|
||||
setupSQLiteHandlers();
|
||||
await initializeSQLite();
|
||||
console.log('[Electron Main Process] SQLite initialization complete');
|
||||
|
||||
// Security - Set Content-Security-Policy
|
||||
setupContentSecurityPolicy(myCapacitorApp.getCustomURLScheme());
|
||||
|
||||
// Initialize our app and create window
|
||||
console.log('[Electron Main Process] Starting app initialization...');
|
||||
await myCapacitorApp.init();
|
||||
console.log('[Electron Main Process] App initialization complete');
|
||||
|
||||
// Get the main window
|
||||
const mainWindow = myCapacitorApp.getMainWindow();
|
||||
if (!mainWindow) {
|
||||
throw new Error('Main window not available after app initialization');
|
||||
}
|
||||
|
||||
// Wait for window to be ready and loaded
|
||||
await new Promise<void>((resolve) => {
|
||||
const handleReady = () => {
|
||||
console.log('[Electron Main Process] Window ready to show');
|
||||
mainWindow.show();
|
||||
|
||||
// Wait for window to finish loading
|
||||
mainWindow.webContents.once('did-finish-load', () => {
|
||||
console.log('[Electron Main Process] Window finished loading');
|
||||
|
||||
// Send SQLite ready signal after window is fully loaded
|
||||
if (!mainWindow.isDestroyed()) {
|
||||
mainWindow.webContents.send('sqlite-ready');
|
||||
console.log('[Electron Main Process] Sent SQLite ready signal to renderer');
|
||||
} else {
|
||||
console.warn('[Electron Main Process] Window was destroyed before sending SQLite ready signal');
|
||||
}
|
||||
|
||||
resolve();
|
||||
});
|
||||
};
|
||||
|
||||
// Always use the event since isReadyToShow is not reliable
|
||||
mainWindow.once('ready-to-show', handleReady);
|
||||
});
|
||||
|
||||
// Check for updates if we are in a packaged app
|
||||
if (!electronIsDev) {
|
||||
console.log('[Electron Main Process] Checking for updates...');
|
||||
autoUpdater.checkForUpdatesAndNotify();
|
||||
}
|
||||
|
||||
// Handle window close
|
||||
mainWindow.on('closed', () => {
|
||||
console.log('[Electron Main Process] Main window closed');
|
||||
});
|
||||
|
||||
// Handle window close request
|
||||
mainWindow.on('close', (event) => {
|
||||
console.log('[Electron Main Process] Window close requested');
|
||||
if (mainWindow.webContents.isLoading()) {
|
||||
event.preventDefault();
|
||||
console.log('[Electron Main Process] Deferring window close due to loading state');
|
||||
mainWindow.webContents.once('did-finish-load', () => {
|
||||
mainWindow.close();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('[Electron Main Process] Fatal error during initialization:', error);
|
||||
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
|
||||
@@ -1,303 +0,0 @@
|
||||
/**
|
||||
* Preload script for Electron
|
||||
* Sets up secure IPC communication between renderer and main process
|
||||
*
|
||||
* @author Matthew Raymer
|
||||
*/
|
||||
|
||||
import { contextBridge, ipcRenderer } from 'electron';
|
||||
|
||||
// Enhanced logger for preload script that forwards to main process
|
||||
const logger = {
|
||||
log: (...args: unknown[]) => {
|
||||
console.log('[Preload]', ...args);
|
||||
ipcRenderer.send('renderer-log', { level: 'log', args });
|
||||
},
|
||||
error: (...args: unknown[]) => {
|
||||
console.error('[Preload]', ...args);
|
||||
ipcRenderer.send('renderer-log', { level: 'error', args });
|
||||
},
|
||||
info: (...args: unknown[]) => {
|
||||
console.info('[Preload]', ...args);
|
||||
ipcRenderer.send('renderer-log', { level: 'info', args });
|
||||
},
|
||||
warn: (...args: unknown[]) => {
|
||||
console.warn('[Preload]', ...args);
|
||||
ipcRenderer.send('renderer-log', { level: 'warn', args });
|
||||
},
|
||||
debug: (...args: unknown[]) => {
|
||||
console.debug('[Preload]', ...args);
|
||||
ipcRenderer.send('renderer-log', { level: 'debug', args });
|
||||
},
|
||||
sqlite: {
|
||||
log: (operation: string, ...args: unknown[]) => {
|
||||
const message = ['[Preload][SQLite]', operation, ...args];
|
||||
console.log(...message);
|
||||
ipcRenderer.send('renderer-log', {
|
||||
level: 'log',
|
||||
args: message,
|
||||
source: 'sqlite',
|
||||
operation
|
||||
});
|
||||
},
|
||||
error: (operation: string, error: unknown) => {
|
||||
const message = ['[Preload][SQLite]', operation, 'failed:', error];
|
||||
console.error(...message);
|
||||
ipcRenderer.send('renderer-log', {
|
||||
level: 'error',
|
||||
args: message,
|
||||
source: 'sqlite',
|
||||
operation,
|
||||
error: error instanceof Error ? {
|
||||
name: error.name,
|
||||
message: error.message,
|
||||
stack: error.stack
|
||||
} : error
|
||||
});
|
||||
},
|
||||
debug: (operation: string, ...args: unknown[]) => {
|
||||
const message = ['[Preload][SQLite]', operation, ...args];
|
||||
console.debug(...message);
|
||||
ipcRenderer.send('renderer-log', {
|
||||
level: 'debug',
|
||||
args: message,
|
||||
source: 'sqlite',
|
||||
operation
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Types for SQLite connection options
|
||||
interface SQLiteConnectionOptions {
|
||||
database: string;
|
||||
version?: number;
|
||||
readOnly?: boolean;
|
||||
readonly?: boolean; // Handle both cases
|
||||
encryption?: string;
|
||||
mode?: string;
|
||||
useNative?: boolean;
|
||||
[key: string]: unknown; // Allow other properties
|
||||
}
|
||||
|
||||
// Define valid channels for security
|
||||
const VALID_CHANNELS = {
|
||||
send: ['toMain'] as const,
|
||||
receive: ['fromMain', 'sqlite-ready', 'database-status'] as const,
|
||||
invoke: [
|
||||
'sqlite-is-available',
|
||||
'sqlite-echo',
|
||||
'sqlite-create-connection',
|
||||
'sqlite-execute',
|
||||
'sqlite-query',
|
||||
'sqlite-run',
|
||||
'sqlite-close-connection',
|
||||
'sqlite-open',
|
||||
'sqlite-close',
|
||||
'sqlite-is-db-open',
|
||||
'sqlite-status',
|
||||
'get-path',
|
||||
'get-base-path'
|
||||
] as const
|
||||
};
|
||||
|
||||
type ValidSendChannel = typeof VALID_CHANNELS.send[number];
|
||||
type ValidReceiveChannel = typeof VALID_CHANNELS.receive[number];
|
||||
type ValidInvokeChannel = typeof VALID_CHANNELS.invoke[number];
|
||||
|
||||
// Create a secure IPC bridge
|
||||
const createSecureIPCBridge = () => {
|
||||
return {
|
||||
send: (channel: string, data: unknown) => {
|
||||
if (VALID_CHANNELS.send.includes(channel as ValidSendChannel)) {
|
||||
logger.debug('IPC Send:', channel, data);
|
||||
ipcRenderer.send(channel, data);
|
||||
} else {
|
||||
logger.warn(`[Preload] Attempted to send on invalid channel: ${channel}`);
|
||||
}
|
||||
},
|
||||
|
||||
receive: (channel: string, func: (...args: unknown[]) => void) => {
|
||||
if (VALID_CHANNELS.receive.includes(channel as ValidReceiveChannel)) {
|
||||
logger.debug('IPC Receive:', channel);
|
||||
ipcRenderer.on(channel, (_event, ...args) => {
|
||||
logger.debug('IPC Received:', channel, args);
|
||||
func(...args);
|
||||
});
|
||||
} else {
|
||||
logger.warn(`[Preload] Attempted to receive on invalid channel: ${channel}`);
|
||||
}
|
||||
},
|
||||
|
||||
once: (channel: string, func: (...args: unknown[]) => void) => {
|
||||
if (VALID_CHANNELS.receive.includes(channel as ValidReceiveChannel)) {
|
||||
logger.debug('IPC Once:', channel);
|
||||
ipcRenderer.once(channel, (_event, ...args) => {
|
||||
logger.debug('IPC Received Once:', channel, args);
|
||||
func(...args);
|
||||
});
|
||||
} else {
|
||||
logger.warn(`[Preload] Attempted to receive once on invalid channel: ${channel}`);
|
||||
}
|
||||
},
|
||||
|
||||
invoke: async (channel: string, ...args: unknown[]) => {
|
||||
if (VALID_CHANNELS.invoke.includes(channel as ValidInvokeChannel)) {
|
||||
logger.debug('IPC Invoke:', channel, args);
|
||||
try {
|
||||
const result = await ipcRenderer.invoke(channel, ...args);
|
||||
logger.debug('IPC Invoke Result:', channel, result);
|
||||
return result;
|
||||
} catch (error) {
|
||||
logger.error('IPC Invoke Error:', channel, error);
|
||||
throw error;
|
||||
}
|
||||
} else {
|
||||
logger.warn(`[Preload] Attempted to invoke on invalid channel: ${channel}`);
|
||||
throw new Error(`Invalid channel: ${channel}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
// Create SQLite proxy with retry logic
|
||||
const createSQLiteProxy = () => {
|
||||
const MAX_RETRIES = 3;
|
||||
const RETRY_DELAY = 1000;
|
||||
|
||||
const withRetry = async <T>(operation: string, ...args: unknown[]): Promise<T> => {
|
||||
let lastError: Error | undefined;
|
||||
const operationId = `${operation}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
const startTime = Date.now();
|
||||
|
||||
logger.sqlite.debug(operation, 'starting with args:', {
|
||||
operationId,
|
||||
args,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
|
||||
try {
|
||||
logger.sqlite.debug(operation, `attempt ${attempt}/${MAX_RETRIES}`, {
|
||||
operationId,
|
||||
attempt,
|
||||
args,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
// Log the exact IPC call
|
||||
logger.sqlite.debug(operation, 'invoking IPC', {
|
||||
operationId,
|
||||
channel: `sqlite-${operation}`,
|
||||
args,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
const result = await ipcRenderer.invoke(`sqlite-${operation}`, ...args);
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
logger.sqlite.log(operation, 'success', {
|
||||
operationId,
|
||||
attempt,
|
||||
result,
|
||||
duration: `${duration}ms`,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
return result as T;
|
||||
} catch (error) {
|
||||
const duration = Date.now() - startTime;
|
||||
lastError = error instanceof Error ? error : new Error(String(error));
|
||||
|
||||
logger.sqlite.error(operation, {
|
||||
operationId,
|
||||
attempt,
|
||||
error: {
|
||||
name: lastError.name,
|
||||
message: lastError.message,
|
||||
stack: lastError.stack
|
||||
},
|
||||
args,
|
||||
duration: `${duration}ms`,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
if (attempt < MAX_RETRIES) {
|
||||
const backoffDelay = RETRY_DELAY * Math.pow(2, attempt - 1);
|
||||
logger.warn(`[Preload] SQLite ${operation} failed (attempt ${attempt}/${MAX_RETRIES}), retrying in ${backoffDelay}ms...`, {
|
||||
operationId,
|
||||
error: lastError,
|
||||
args,
|
||||
nextAttemptIn: `${backoffDelay}ms`,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
await new Promise(resolve => setTimeout(resolve, backoffDelay));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const finalError = new Error(
|
||||
`SQLite ${operation} failed after ${MAX_RETRIES} attempts: ${lastError?.message || "Unknown error"}`
|
||||
);
|
||||
|
||||
logger.error('[Preload] SQLite operation failed permanently:', {
|
||||
operation,
|
||||
operationId,
|
||||
error: {
|
||||
name: finalError.name,
|
||||
message: finalError.message,
|
||||
stack: finalError.stack,
|
||||
originalError: lastError
|
||||
},
|
||||
args,
|
||||
attempts: MAX_RETRIES,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
throw finalError;
|
||||
};
|
||||
|
||||
return {
|
||||
isAvailable: () => withRetry('is-available'),
|
||||
echo: (value: string) => withRetry('echo', { value }),
|
||||
createConnection: (options: SQLiteConnectionOptions) => withRetry('create-connection', options),
|
||||
closeConnection: (options: { database: string }) => withRetry('close-connection', options),
|
||||
query: (options: { statement: string; values?: unknown[] }) => withRetry('query', options),
|
||||
run: (options: { statement: string; values?: unknown[] }) => withRetry('run', options),
|
||||
execute: (options: { statements: { statement: string; values?: unknown[] }[] }) => withRetry('execute', options),
|
||||
getPlatform: () => Promise.resolve('electron')
|
||||
};
|
||||
};
|
||||
|
||||
try {
|
||||
// Expose the secure IPC bridge and SQLite proxy
|
||||
const electronAPI = {
|
||||
ipcRenderer: createSecureIPCBridge(),
|
||||
sqlite: createSQLiteProxy(),
|
||||
env: {
|
||||
platform: 'electron',
|
||||
isDev: process.env.NODE_ENV === 'development'
|
||||
}
|
||||
};
|
||||
|
||||
// Log the exposed API for debugging
|
||||
logger.debug('Exposing Electron API:', {
|
||||
hasIpcRenderer: !!electronAPI.ipcRenderer,
|
||||
hasSqlite: !!electronAPI.sqlite,
|
||||
sqliteMethods: Object.keys(electronAPI.sqlite),
|
||||
env: electronAPI.env
|
||||
});
|
||||
|
||||
contextBridge.exposeInMainWorld('electron', electronAPI);
|
||||
logger.info('[Preload] IPC bridge and SQLite proxy initialized successfully');
|
||||
} catch (error) {
|
||||
logger.error('[Preload] Failed to initialize IPC bridge:', error);
|
||||
}
|
||||
|
||||
// Log startup
|
||||
logger.log('[CapacitorSQLite] Preload script starting...');
|
||||
|
||||
// Handle window load
|
||||
window.addEventListener('load', () => {
|
||||
logger.log('[CapacitorSQLite] Preload script complete');
|
||||
});
|
||||
@@ -1,6 +0,0 @@
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
const CapacitorCommunitySqlite = require('../../../node_modules/@capacitor-community/sqlite/electron/dist/plugin.js');
|
||||
|
||||
module.exports = {
|
||||
CapacitorCommunitySqlite,
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
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,
|
||||
});
|
||||
////////////////////////////////////////////////////////
|
||||
@@ -1,188 +0,0 @@
|
||||
/**
|
||||
* Enhanced logging system for TimeSafari Electron
|
||||
* Provides structured logging with proper levels and formatting
|
||||
* Supports both console and file output with different verbosity levels
|
||||
*
|
||||
* @author Matthew Raymer
|
||||
*/
|
||||
|
||||
import { app, ipcMain } from 'electron';
|
||||
import winston from 'winston';
|
||||
import path from 'path';
|
||||
import os from 'os';
|
||||
import fs from 'fs';
|
||||
|
||||
// Extend Winston Logger type with our custom loggers
|
||||
declare module 'winston' {
|
||||
interface Logger {
|
||||
sqlite: {
|
||||
debug: (message: string, ...args: unknown[]) => void;
|
||||
info: (message: string, ...args: unknown[]) => void;
|
||||
warn: (message: string, ...args: unknown[]) => void;
|
||||
error: (message: string, ...args: unknown[]) => void;
|
||||
};
|
||||
migration: {
|
||||
debug: (message: string, ...args: unknown[]) => void;
|
||||
info: (message: string, ...args: unknown[]) => void;
|
||||
warn: (message: string, ...args: unknown[]) => void;
|
||||
error: (message: string, ...args: unknown[]) => void;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Create logs directory if it doesn't exist
|
||||
const logsDir = path.join(app.getPath('userData'), 'logs');
|
||||
if (!fs.existsSync(logsDir)) {
|
||||
fs.mkdirSync(logsDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Custom format for console output with migration filtering
|
||||
const consoleFormat = winston.format.combine(
|
||||
winston.format.timestamp(),
|
||||
winston.format.colorize(),
|
||||
winston.format.printf(({ level, message, timestamp, ...metadata }) => {
|
||||
// Skip migration logs unless DEBUG_MIGRATIONS is set
|
||||
if (level === 'info' &&
|
||||
typeof message === 'string' &&
|
||||
message.includes('[Migration]') &&
|
||||
!process.env.DEBUG_MIGRATIONS) {
|
||||
return '';
|
||||
}
|
||||
|
||||
let msg = `${timestamp} [${level}] ${message}`;
|
||||
if (Object.keys(metadata).length > 0) {
|
||||
msg += ` ${JSON.stringify(metadata, null, 2)}`;
|
||||
}
|
||||
return msg;
|
||||
})
|
||||
);
|
||||
|
||||
// Custom format for file output
|
||||
const fileFormat = winston.format.combine(
|
||||
winston.format.timestamp(),
|
||||
winston.format.json()
|
||||
);
|
||||
|
||||
// Create logger instance
|
||||
const logger = winston.createLogger({
|
||||
level: process.env.NODE_ENV === 'development' ? 'debug' : 'info',
|
||||
format: fileFormat,
|
||||
defaultMeta: { service: 'timesafari-electron' },
|
||||
transports: [
|
||||
// Console transport with custom format and migration filtering
|
||||
new winston.transports.Console({
|
||||
format: consoleFormat,
|
||||
level: process.env.NODE_ENV === 'development' ? 'debug' : 'info',
|
||||
silent: false // Ensure we can still see non-migration logs
|
||||
}),
|
||||
// File transport for all logs
|
||||
new winston.transports.File({
|
||||
filename: path.join(logsDir, 'error.log'),
|
||||
level: 'error',
|
||||
maxsize: 5242880, // 5MB
|
||||
maxFiles: 5
|
||||
}),
|
||||
// File transport for all logs including debug
|
||||
new winston.transports.File({
|
||||
filename: path.join(logsDir, 'combined.log'),
|
||||
maxsize: 5242880, // 5MB
|
||||
maxFiles: 5
|
||||
})
|
||||
]
|
||||
}) as winston.Logger & {
|
||||
sqlite: {
|
||||
debug: (message: string, ...args: unknown[]) => void;
|
||||
info: (message: string, ...args: unknown[]) => void;
|
||||
warn: (message: string, ...args: unknown[]) => void;
|
||||
error: (message: string, ...args: unknown[]) => void;
|
||||
};
|
||||
migration: {
|
||||
debug: (message: string, ...args: unknown[]) => void;
|
||||
info: (message: string, ...args: unknown[]) => void;
|
||||
warn: (message: string, ...args: unknown[]) => void;
|
||||
error: (message: string, ...args: unknown[]) => void;
|
||||
};
|
||||
};
|
||||
|
||||
// Add SQLite specific logger
|
||||
logger.sqlite = {
|
||||
debug: (message: string, ...args: unknown[]) => {
|
||||
logger.debug(`[SQLite] ${message}`, ...args);
|
||||
},
|
||||
info: (message: string, ...args: unknown[]) => {
|
||||
logger.info(`[SQLite] ${message}`, ...args);
|
||||
},
|
||||
warn: (message: string, ...args: unknown[]) => {
|
||||
logger.warn(`[SQLite] ${message}`, ...args);
|
||||
},
|
||||
error: (message: string, ...args: unknown[]) => {
|
||||
logger.error(`[SQLite] ${message}`, ...args);
|
||||
}
|
||||
};
|
||||
|
||||
// Add migration specific logger with debug filtering
|
||||
logger.migration = {
|
||||
debug: (message: string, ...args: unknown[]) => {
|
||||
if (process.env.DEBUG_MIGRATIONS) {
|
||||
//logger.debug(`[Migration] ${message}`, ...args);
|
||||
}
|
||||
},
|
||||
info: (message: string, ...args: unknown[]) => {
|
||||
// Always log to file, but only log to console if DEBUG_MIGRATIONS is set
|
||||
if (process.env.DEBUG_MIGRATIONS) {
|
||||
//logger.info(`[Migration] ${message}`, ...args);
|
||||
} else {
|
||||
// Use a separate transport for migration logs to file only
|
||||
const metadata = args[0] as Record<string, unknown>;
|
||||
logger.write({
|
||||
level: 'info',
|
||||
message: `[Migration] ${message}`,
|
||||
...(metadata || {})
|
||||
});
|
||||
}
|
||||
},
|
||||
warn: (message: string, ...args: unknown[]) => {
|
||||
// Always log warnings to both console and file
|
||||
//logger.warn(`[Migration] ${message}`, ...args);
|
||||
},
|
||||
error: (message: string, ...args: unknown[]) => {
|
||||
// Always log errors to both console and file
|
||||
//logger.error(`[Migration] ${message}`, ...args);
|
||||
}
|
||||
};
|
||||
|
||||
// Add renderer log handler
|
||||
ipcMain.on('renderer-log', (_event, { level, args, source, operation, error }) => {
|
||||
const message = args.map((arg: unknown) =>
|
||||
typeof arg === 'object' ? JSON.stringify(arg, null, 2) : String(arg)
|
||||
).join(' ');
|
||||
|
||||
const meta = {
|
||||
source: source || 'renderer',
|
||||
...(operation && { operation }),
|
||||
...(error && { error })
|
||||
};
|
||||
|
||||
switch (level) {
|
||||
case 'error':
|
||||
logger.error(message, meta);
|
||||
break;
|
||||
case 'warn':
|
||||
logger.warn(message, meta);
|
||||
break;
|
||||
case 'info':
|
||||
logger.info(message, meta);
|
||||
break;
|
||||
case 'debug':
|
||||
logger.debug(message, meta);
|
||||
break;
|
||||
default:
|
||||
logger.log(level, message, meta);
|
||||
}
|
||||
});
|
||||
|
||||
// Export logger instance
|
||||
export { logger };
|
||||
|
||||
// Export a function to get the logs directory
|
||||
export const getLogsDirectory = () => logsDir;
|
||||
@@ -1,14 +0,0 @@
|
||||
/**
|
||||
* Custom error class for SQLite operations
|
||||
* Provides additional context and error tracking for SQLite operations
|
||||
*/
|
||||
export class SQLiteError extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
public operation: string,
|
||||
public cause?: unknown
|
||||
) {
|
||||
super(message);
|
||||
this.name = 'SQLiteError';
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,442 +0,0 @@
|
||||
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';
|
||||
|
||||
/**
|
||||
* Reload watcher configuration and state management
|
||||
* Prevents infinite reload loops and implements rate limiting
|
||||
* Also prevents reloads during critical database operations
|
||||
*
|
||||
* @author Matthew Raymer
|
||||
*/
|
||||
const RELOAD_CONFIG = {
|
||||
DEBOUNCE_MS: 1500,
|
||||
COOLDOWN_MS: 5000,
|
||||
MAX_RELOADS_PER_MINUTE: 10,
|
||||
MAX_RELOADS_PER_SESSION: 100,
|
||||
DATABASE_OPERATION_TIMEOUT_MS: 10000 // 10 second timeout for database operations
|
||||
};
|
||||
|
||||
// Track database operation state
|
||||
let isDatabaseOperationInProgress = false;
|
||||
let lastDatabaseOperationTime = 0;
|
||||
|
||||
/**
|
||||
* Checks if a database operation is in progress or recently completed
|
||||
* @returns {boolean} Whether a database operation is active
|
||||
*/
|
||||
const isDatabaseOperationActive = (): boolean => {
|
||||
const now = Date.now();
|
||||
return isDatabaseOperationInProgress ||
|
||||
(now - lastDatabaseOperationTime < RELOAD_CONFIG.DATABASE_OPERATION_TIMEOUT_MS);
|
||||
};
|
||||
|
||||
/**
|
||||
* Marks the start of a database operation
|
||||
*/
|
||||
export const startDatabaseOperation = (): void => {
|
||||
isDatabaseOperationInProgress = true;
|
||||
lastDatabaseOperationTime = Date.now();
|
||||
};
|
||||
|
||||
/**
|
||||
* Marks the end of a database operation
|
||||
*/
|
||||
export const endDatabaseOperation = (): void => {
|
||||
isDatabaseOperationInProgress = false;
|
||||
lastDatabaseOperationTime = Date.now();
|
||||
};
|
||||
|
||||
const reloadWatcher = {
|
||||
debouncer: null as NodeJS.Timeout | null,
|
||||
ready: false,
|
||||
watcher: null as chokidar.FSWatcher | null,
|
||||
lastReloadTime: 0,
|
||||
reloadCount: 0,
|
||||
sessionReloadCount: 0,
|
||||
resetTimeout: null as NodeJS.Timeout | null,
|
||||
isReloading: false
|
||||
};
|
||||
|
||||
/**
|
||||
* Resets the reload counter after one minute
|
||||
*/
|
||||
const resetReloadCounter = () => {
|
||||
reloadWatcher.reloadCount = 0;
|
||||
reloadWatcher.resetTimeout = null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if a reload is allowed based on rate limits, cooldown, and database state
|
||||
* @returns {boolean} Whether a reload is allowed
|
||||
*/
|
||||
const canReload = (): boolean => {
|
||||
const now = Date.now();
|
||||
|
||||
// Check if database operation is active
|
||||
if (isDatabaseOperationActive()) {
|
||||
console.warn('[Reload Watcher] Skipping reload - database operation in progress');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check cooldown period
|
||||
if (now - reloadWatcher.lastReloadTime < RELOAD_CONFIG.COOLDOWN_MS) {
|
||||
console.warn('[Reload Watcher] Skipping reload - cooldown period active');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check per-minute limit
|
||||
if (reloadWatcher.reloadCount >= RELOAD_CONFIG.MAX_RELOADS_PER_MINUTE) {
|
||||
console.warn('[Reload Watcher] Skipping reload - maximum reloads per minute reached');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check session limit
|
||||
if (reloadWatcher.sessionReloadCount >= RELOAD_CONFIG.MAX_RELOADS_PER_SESSION) {
|
||||
console.error('[Reload Watcher] Maximum reloads per session reached. Please restart the application.');
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Cleans up the current watcher instance
|
||||
*/
|
||||
const cleanupWatcher = () => {
|
||||
if (reloadWatcher.watcher) {
|
||||
reloadWatcher.watcher.close();
|
||||
reloadWatcher.watcher = null;
|
||||
}
|
||||
if (reloadWatcher.debouncer) {
|
||||
clearTimeout(reloadWatcher.debouncer);
|
||||
reloadWatcher.debouncer = null;
|
||||
}
|
||||
if (reloadWatcher.resetTimeout) {
|
||||
clearTimeout(reloadWatcher.resetTimeout);
|
||||
reloadWatcher.resetTimeout = null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets up the file watcher for development mode reloading
|
||||
* Implements rate limiting and prevents infinite reload loops
|
||||
*
|
||||
* @param electronCapacitorApp - The Electron Capacitor app instance
|
||||
*/
|
||||
export function setupReloadWatcher(electronCapacitorApp: ElectronCapacitorApp): void {
|
||||
// Cleanup any existing watcher
|
||||
cleanupWatcher();
|
||||
|
||||
// Reset state
|
||||
reloadWatcher.ready = false;
|
||||
reloadWatcher.isReloading = false;
|
||||
|
||||
reloadWatcher.watcher = chokidar
|
||||
.watch(join(app.getAppPath(), 'app'), {
|
||||
ignored: /[/\\]\./,
|
||||
persistent: true,
|
||||
awaitWriteFinish: {
|
||||
stabilityThreshold: 1000,
|
||||
pollInterval: 100
|
||||
}
|
||||
})
|
||||
.on('ready', () => {
|
||||
reloadWatcher.ready = true;
|
||||
console.log('[Reload Watcher] Ready to watch for changes');
|
||||
})
|
||||
.on('all', (_event, _path) => {
|
||||
if (!reloadWatcher.ready || reloadWatcher.isReloading) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear existing debouncer
|
||||
if (reloadWatcher.debouncer) {
|
||||
clearTimeout(reloadWatcher.debouncer);
|
||||
}
|
||||
|
||||
// Set up new debouncer
|
||||
reloadWatcher.debouncer = setTimeout(async () => {
|
||||
if (!canReload()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
reloadWatcher.isReloading = true;
|
||||
|
||||
// Update reload counters
|
||||
reloadWatcher.lastReloadTime = Date.now();
|
||||
reloadWatcher.reloadCount++;
|
||||
reloadWatcher.sessionReloadCount++;
|
||||
|
||||
// Set up reset timeout for per-minute counter
|
||||
if (!reloadWatcher.resetTimeout) {
|
||||
reloadWatcher.resetTimeout = setTimeout(resetReloadCounter, 60000);
|
||||
}
|
||||
|
||||
// Perform reload
|
||||
console.log('[Reload Watcher] Reloading window...');
|
||||
await electronCapacitorApp.getMainWindow().webContents.reload();
|
||||
|
||||
// Reset state after reload
|
||||
reloadWatcher.ready = false;
|
||||
reloadWatcher.isReloading = false;
|
||||
|
||||
// Re-setup watcher after successful reload
|
||||
setupReloadWatcher(electronCapacitorApp);
|
||||
|
||||
} catch (error) {
|
||||
console.error('[Reload Watcher] Error during reload:', error);
|
||||
reloadWatcher.isReloading = false;
|
||||
reloadWatcher.ready = true;
|
||||
}
|
||||
}, RELOAD_CONFIG.DEBOUNCE_MS);
|
||||
})
|
||||
.on('error', (error) => {
|
||||
console.error('[Reload Watcher] Error:', error);
|
||||
cleanupWatcher();
|
||||
});
|
||||
}
|
||||
|
||||
// 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 based on environment
|
||||
const preloadPath = app.isPackaged
|
||||
? join(process.resourcesPath, 'preload.js')
|
||||
: join(__dirname, 'preload.js');
|
||||
|
||||
console.log('[Electron Main Process] Preload path:', preloadPath);
|
||||
console.log('[Electron Main Process] Preload exists:', require('fs').existsSync(preloadPath));
|
||||
|
||||
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: false,
|
||||
contextIsolation: true,
|
||||
sandbox: false,
|
||||
preload: preloadPath,
|
||||
webSecurity: true,
|
||||
allowRunningInsecureContent: false,
|
||||
},
|
||||
});
|
||||
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();
|
||||
}
|
||||
|
||||
// Re-register SQLite handlers after reload
|
||||
if (electronIsDev) {
|
||||
console.log('[Electron Main Process] Re-registering SQLite handlers after reload');
|
||||
const { setupSQLiteHandlers } = require('./rt/sqlite-init');
|
||||
setupSQLiteHandlers();
|
||||
}
|
||||
|
||||
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}://*;`,
|
||||
// Script sources
|
||||
`script-src ${customScheme}://* 'self' 'unsafe-inline'${electronIsDev ? " 'unsafe-eval'" : ''};`,
|
||||
// Style sources
|
||||
`style-src ${customScheme}://* 'self' 'unsafe-inline' https://fonts.googleapis.com;`,
|
||||
// Font sources
|
||||
`font-src ${customScheme}://* 'self' https://fonts.gstatic.com;`,
|
||||
// Image sources
|
||||
`img-src ${customScheme}://* 'self' data: https:;`,
|
||||
// Connect sources (for API calls)
|
||||
`connect-src ${customScheme}://* 'self' https:;`,
|
||||
// Worker sources
|
||||
`worker-src ${customScheme}://* 'self' blob:;`,
|
||||
// Frame sources
|
||||
`frame-src ${customScheme}://* 'self';`,
|
||||
// Media sources
|
||||
`media-src ${customScheme}://* 'self' data:;`,
|
||||
// Object sources
|
||||
`object-src 'none';`,
|
||||
// Base URI
|
||||
`base-uri 'self';`,
|
||||
// Form action
|
||||
`form-action ${customScheme}://* 'self';`,
|
||||
// Frame ancestors
|
||||
`frame-ancestors 'none';`,
|
||||
// Upgrade insecure requests
|
||||
'upgrade-insecure-requests;',
|
||||
// Block mixed content
|
||||
'block-all-mixed-content;'
|
||||
].join(' ')
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
{
|
||||
"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
|
||||
}
|
||||
}
|
||||
|
||||
155
experiment.sh
155
experiment.sh
@@ -1,155 +0,0 @@
|
||||
#!/bin/bash
|
||||
# experiment.sh
|
||||
# Author: Matthew Raymer
|
||||
# Description: Build script for TimeSafari Electron application
|
||||
# This script handles the complete build process for the TimeSafari Electron app,
|
||||
# including web asset compilation and Capacitor sync.
|
||||
#
|
||||
# Build Process:
|
||||
# 1. Environment setup and dependency checks
|
||||
# 2. Web asset compilation (Vite)
|
||||
# 3. Capacitor sync
|
||||
# 4. Electron start
|
||||
#
|
||||
# Dependencies:
|
||||
# - Node.js and npm
|
||||
# - TypeScript
|
||||
# - Vite
|
||||
# - @capacitor-community/electron
|
||||
#
|
||||
# Usage: ./experiment.sh
|
||||
#
|
||||
# Exit Codes:
|
||||
# 1 - Required command not found
|
||||
# 2 - TypeScript installation failed
|
||||
# 3 - Build process failed
|
||||
# 4 - Capacitor sync failed
|
||||
# 5 - Electron start failed
|
||||
|
||||
# Exit on any error
|
||||
set -e
|
||||
|
||||
# ANSI color codes for better output formatting
|
||||
readonly RED='\033[0;31m'
|
||||
readonly GREEN='\033[0;32m'
|
||||
readonly YELLOW='\033[1;33m'
|
||||
readonly BLUE='\033[0;34m'
|
||||
readonly NC='\033[0m' # No Color
|
||||
|
||||
# Logging functions
|
||||
log_info() {
|
||||
echo -e "${BLUE}[$(date '+%Y-%m-%d %H:%M:%S')] [INFO]${NC} $1"
|
||||
}
|
||||
|
||||
log_success() {
|
||||
echo -e "${GREEN}[$(date '+%Y-%m-%d %H:%M:%S')] [SUCCESS]${NC} $1"
|
||||
}
|
||||
|
||||
log_warn() {
|
||||
echo -e "${YELLOW}[$(date '+%Y-%m-%d %H:%M:%S')] [WARN]${NC} $1"
|
||||
}
|
||||
|
||||
log_error() {
|
||||
echo -e "${RED}[$(date '+%Y-%m-%d %H:%M:%S')] [ERROR]${NC} $1"
|
||||
}
|
||||
|
||||
# Function to check if a command exists
|
||||
check_command() {
|
||||
if ! command -v "$1" &> /dev/null; then
|
||||
log_error "$1 is required but not installed."
|
||||
exit 1
|
||||
fi
|
||||
log_info "Found $1: $(command -v "$1")"
|
||||
}
|
||||
|
||||
# Function to measure and log execution time
|
||||
measure_time() {
|
||||
local start_time=$(date +%s)
|
||||
"$@"
|
||||
local end_time=$(date +%s)
|
||||
local duration=$((end_time - start_time))
|
||||
log_success "Completed in ${duration} seconds"
|
||||
}
|
||||
|
||||
# Print build header
|
||||
echo -e "\n${BLUE}=== TimeSafari Electron Build Process ===${NC}\n"
|
||||
log_info "Starting build process at $(date)"
|
||||
|
||||
# Check required commands
|
||||
log_info "Checking required dependencies..."
|
||||
check_command node
|
||||
check_command npm
|
||||
check_command git
|
||||
|
||||
# Create application data directory
|
||||
log_info "Setting up application directories..."
|
||||
mkdir -p ~/.local/share/TimeSafari/timesafari
|
||||
|
||||
# Clean up previous builds
|
||||
log_info "Cleaning previous builds..."
|
||||
rm -rf dist* || log_warn "No previous builds to clean"
|
||||
|
||||
# Set environment variables for the build
|
||||
log_info "Configuring build environment..."
|
||||
export VITE_PLATFORM=electron
|
||||
export VITE_PWA_ENABLED=false
|
||||
export VITE_DISABLE_PWA=true
|
||||
export DEBUG_MIGRATIONS=0
|
||||
|
||||
# Ensure TypeScript is installed
|
||||
log_info "Verifying TypeScript installation..."
|
||||
if [ ! -f "./node_modules/.bin/tsc" ]; then
|
||||
log_info "Installing TypeScript..."
|
||||
if ! npm install --save-dev typescript@~5.2.2; then
|
||||
log_error "TypeScript installation failed!"
|
||||
exit 2
|
||||
fi
|
||||
# Verify installation
|
||||
if [ ! -f "./node_modules/.bin/tsc" ]; then
|
||||
log_error "TypeScript installation verification failed!"
|
||||
exit 2
|
||||
fi
|
||||
log_success "TypeScript installed successfully"
|
||||
else
|
||||
log_info "TypeScript already installed"
|
||||
fi
|
||||
|
||||
# Get git hash for versioning
|
||||
GIT_HASH=$(git log -1 --pretty=format:%h)
|
||||
log_info "Using git hash: ${GIT_HASH}"
|
||||
|
||||
# Build web assets
|
||||
log_info "Building web assets with Vite..."
|
||||
if ! measure_time env VITE_GIT_HASH="$GIT_HASH" npx vite build --config vite.config.app.electron.mts --mode electron; then
|
||||
log_error "Web asset build failed!"
|
||||
exit 3
|
||||
fi
|
||||
|
||||
# Sync with Capacitor
|
||||
log_info "Syncing with Capacitor..."
|
||||
if ! measure_time npx cap sync electron; then
|
||||
log_error "Capacitor sync failed!"
|
||||
exit 4
|
||||
fi
|
||||
|
||||
# Restore capacitor config
|
||||
log_info "Restoring capacitor config..."
|
||||
if ! git checkout electron/capacitor.config.json; then
|
||||
log_error "Failed to restore capacitor config!"
|
||||
exit 4
|
||||
fi
|
||||
|
||||
# Start Electron
|
||||
log_info "Starting Electron..."
|
||||
cd electron/
|
||||
if ! measure_time npm run electron:start; then
|
||||
log_error "Electron start failed!"
|
||||
exit 5
|
||||
fi
|
||||
|
||||
# Print build summary
|
||||
log_success "Build and start completed successfully!"
|
||||
echo -e "\n${GREEN}=== End of Build Process ===${NC}\n"
|
||||
|
||||
# Exit with success
|
||||
exit 0
|
||||
@@ -1,6 +1,5 @@
|
||||
import UIKit
|
||||
import Capacitor
|
||||
import CapacitorCommunitySqlite
|
||||
|
||||
@UIApplicationMain
|
||||
class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
@@ -8,10 +7,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
var window: UIWindow?
|
||||
|
||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
||||
// Initialize SQLite
|
||||
let sqlite = SQLite()
|
||||
sqlite.initialize()
|
||||
|
||||
// Override point for customization after application launch.
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ install! 'cocoapods', :disable_input_output_paths => true
|
||||
def capacitor_pods
|
||||
pod 'Capacitor', :path => '../../node_modules/@capacitor/ios'
|
||||
pod 'CapacitorCordova', :path => '../../node_modules/@capacitor/ios'
|
||||
pod 'CapacitorCommunitySqlite', :path => '../../node_modules/@capacitor-community/sqlite'
|
||||
pod 'CapacitorMlkitBarcodeScanning', :path => '../../node_modules/@capacitor-mlkit/barcode-scanning'
|
||||
pod 'CapacitorApp', :path => '../../node_modules/@capacitor/app'
|
||||
pod 'CapacitorCamera', :path => '../../node_modules/@capacitor/camera'
|
||||
|
||||
1517
package-lock.json
generated
1517
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
55
package.json
55
package.json
@@ -11,7 +11,7 @@
|
||||
"build": "VITE_GIT_HASH=`git log -1 --pretty=format:%h` vite build --config vite.config.mts",
|
||||
"lint": "eslint --ext .js,.ts,.vue --ignore-path .gitignore src",
|
||||
"lint-fix": "eslint --ext .js,.ts,.vue --ignore-path .gitignore --fix src",
|
||||
"prebuild": "eslint --ext .js,.ts,.vue --ignore-path .gitignore src && node sw_combine.cjs && node scripts/copy-wasm.cjs",
|
||||
"prebuild": "eslint --ext .js,.ts,.vue --ignore-path .gitignore src && node sw_combine.js && node scripts/copy-wasm.js",
|
||||
"test:all": "npm run test:prerequisites && npm run build && npm run test:web && npm run test:mobile",
|
||||
"test:prerequisites": "node scripts/check-prerequisites.js",
|
||||
"test:web": "npx playwright test -c playwright.config-local.ts --trace on",
|
||||
@@ -22,15 +22,14 @@
|
||||
"check:ios-device": "xcrun xctrace list devices 2>&1 | grep -w 'Booted' || (echo 'No iOS simulator running' && exit 1)",
|
||||
"clean:electron": "rimraf dist-electron",
|
||||
"build:pywebview": "vite build --config vite.config.pywebview.mts",
|
||||
"build:web": "VITE_GIT_HASH=`git log -1 --pretty=format:%h` vite build --config vite.config.web.mts",
|
||||
"build:web:electron": "VITE_GIT_HASH=`git log -1 --pretty=format:%h` vite build --config vite.config.web.mts && VITE_GIT_HASH=`git log -1 --pretty=format:%h` vite build --config vite.config.electron.mts --mode electron",
|
||||
"build:electron": "npm run clean:electron && npm run build:web:electron && tsc -p tsconfig.electron.json && vite build --config vite.config.electron.mts && node scripts/build-electron.cjs",
|
||||
"build:electron": "npm run clean:electron && tsc -p tsconfig.electron.json && vite build --config vite.config.electron.mts && node scripts/build-electron.js",
|
||||
"build:capacitor": "vite build --mode capacitor --config vite.config.capacitor.mts",
|
||||
"build:web": "VITE_GIT_HASH=`git log -1 --pretty=format:%h` vite build --config vite.config.web.mts",
|
||||
"electron:dev": "npm run build && electron .",
|
||||
"electron:start": "electron .",
|
||||
"clean:android": "adb uninstall app.timesafari.app || true",
|
||||
"build:android": "npm run clean:android && rm -rf dist && npm run build:web && npm run build:capacitor && cd android && ./gradlew clean && ./gradlew assembleDebug && cd .. && npx cap sync android && npx capacitor-assets generate --android && npx cap open android",
|
||||
"electron:build-linux": "electron-builder --linux AppImage",
|
||||
"electron:build-linux": "npm run build:electron && electron-builder --linux AppImage",
|
||||
"electron:build-linux-deb": "npm run build:electron && electron-builder --linux deb",
|
||||
"electron:build-linux-prod": "NODE_ENV=production npm run build:electron && electron-builder --linux AppImage",
|
||||
"build:electron-prod": "NODE_ENV=production npm run build:electron",
|
||||
@@ -47,7 +46,7 @@
|
||||
"electron:build-mac-universal": "npm run build:electron-prod && electron-builder --mac --universal"
|
||||
},
|
||||
"dependencies": {
|
||||
"@capacitor-community/sqlite": "^6.0.2",
|
||||
"@capacitor-community/sqlite": "6.0.0",
|
||||
"@capacitor-mlkit/barcode-scanning": "^6.0.0",
|
||||
"@capacitor/android": "^6.2.0",
|
||||
"@capacitor/app": "^6.0.0",
|
||||
@@ -58,8 +57,8 @@
|
||||
"@capacitor/ios": "^6.2.0",
|
||||
"@capacitor/share": "^6.0.3",
|
||||
"@capawesome/capacitor-file-picker": "^6.2.0",
|
||||
"@dicebear/collection": "^5.4.3",
|
||||
"@dicebear/core": "^5.4.3",
|
||||
"@dicebear/collection": "^5.4.1",
|
||||
"@dicebear/core": "^5.4.1",
|
||||
"@ethersproject/hdnode": "^5.7.0",
|
||||
"@ethersproject/wallet": "^5.8.0",
|
||||
"@fortawesome/fontawesome-svg-core": "^6.5.1",
|
||||
@@ -70,7 +69,7 @@
|
||||
"@peculiar/asn1-schema": "^2.3.8",
|
||||
"@pvermeer/dexie-encrypted-addon": "^3.0.0",
|
||||
"@simplewebauthn/browser": "^10.0.0",
|
||||
"@simplewebauthn/server": "^10.0.1",
|
||||
"@simplewebauthn/server": "^10.0.0",
|
||||
"@tweenjs/tween.js": "^21.1.1",
|
||||
"@types/qrcode": "^1.5.5",
|
||||
"@veramo/core": "^5.6.0",
|
||||
@@ -87,7 +86,6 @@
|
||||
"absurd-sql": "^0.0.54",
|
||||
"asn1-ber": "^1.2.2",
|
||||
"axios": "^1.6.8",
|
||||
"better-sqlite3-multiple-ciphers": "^11.10.0",
|
||||
"cbor-x": "^1.5.9",
|
||||
"class-transformer": "^0.5.1",
|
||||
"dexie": "^3.2.7",
|
||||
@@ -95,23 +93,22 @@
|
||||
"did-jwt": "^7.4.7",
|
||||
"did-resolver": "^4.1.0",
|
||||
"dotenv": "^16.0.3",
|
||||
"electron-json-storage": "^4.6.0",
|
||||
"ethereum-cryptography": "^2.2.1",
|
||||
"ethereum-cryptography": "^2.1.3",
|
||||
"ethereumjs-util": "^7.1.5",
|
||||
"jdenticon": "^3.3.0",
|
||||
"jdenticon": "^3.2.0",
|
||||
"js-generate-password": "^0.1.9",
|
||||
"js-yaml": "^4.1.0",
|
||||
"jsqr": "^1.4.0",
|
||||
"leaflet": "^1.9.4",
|
||||
"localstorage-slim": "^2.7.0",
|
||||
"lru-cache": "^10.4.3",
|
||||
"lru-cache": "^10.2.0",
|
||||
"luxon": "^3.4.4",
|
||||
"merkletreejs": "^0.3.11",
|
||||
"nostr-tools": "^2.13.1",
|
||||
"nostr-tools": "^2.10.4",
|
||||
"notiwind": "^2.0.2",
|
||||
"papaparse": "^5.4.1",
|
||||
"pina": "^0.20.2204228",
|
||||
"pinia-plugin-persistedstate": "^3.2.3",
|
||||
"pinia-plugin-persistedstate": "^3.2.1",
|
||||
"qr-code-generator-vue3": "^1.4.21",
|
||||
"qrcode": "^1.5.4",
|
||||
"ramda": "^0.29.1",
|
||||
@@ -119,6 +116,7 @@
|
||||
"reflect-metadata": "^0.1.14",
|
||||
"register-service-worker": "^1.7.2",
|
||||
"simple-vue-camera": "^1.1.3",
|
||||
"sqlite": "^5.1.1",
|
||||
"sqlite3": "^5.1.7",
|
||||
"stream-browserify": "^3.0.0",
|
||||
"three": "^0.156.1",
|
||||
@@ -127,13 +125,12 @@
|
||||
"vue-axios": "^3.5.2",
|
||||
"vue-facing-decorator": "^3.0.4",
|
||||
"vue-picture-cropper": "^0.7.0",
|
||||
"vue-qrcode-reader": "^5.7.2",
|
||||
"vue-qrcode-reader": "^5.5.3",
|
||||
"vue-router": "^4.5.0",
|
||||
"web-did-resolver": "^2.0.30",
|
||||
"web-did-resolver": "^2.0.27",
|
||||
"zod": "^3.24.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@capacitor-community/electron": "^5.0.1",
|
||||
"@capacitor/assets": "^3.0.5",
|
||||
"@playwright/test": "^1.45.2",
|
||||
"@types/dom-webcodecs": "^0.1.7",
|
||||
@@ -148,7 +145,7 @@
|
||||
"@types/ua-parser-js": "^0.7.39",
|
||||
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
||||
"@typescript-eslint/parser": "^6.21.0",
|
||||
"@vitejs/plugin-vue": "^5.2.4",
|
||||
"@vitejs/plugin-vue": "^5.2.1",
|
||||
"@vue/eslint-config-typescript": "^11.0.3",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"browserify-fs": "^1.0.0",
|
||||
@@ -168,13 +165,13 @@
|
||||
"postcss": "^8.4.38",
|
||||
"prettier": "^3.2.5",
|
||||
"rimraf": "^6.0.1",
|
||||
"source-map-support": "^0.5.21",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"typescript": "~5.2.2",
|
||||
"vite": "^5.2.0",
|
||||
"vite-plugin-pwa": "^1.0.0"
|
||||
"vite-plugin-node-polyfills": "^0.23.0",
|
||||
"vite-plugin-pwa": "^0.19.8"
|
||||
},
|
||||
"main": "./dist-electron/main.mjs",
|
||||
"main": "./dist-electron/main.js",
|
||||
"build": {
|
||||
"appId": "app.timesafari",
|
||||
"productName": "TimeSafari",
|
||||
@@ -183,17 +180,12 @@
|
||||
},
|
||||
"files": [
|
||||
"dist-electron/**/*",
|
||||
"dist/**/*",
|
||||
"capacitor.config.json"
|
||||
"dist/**/*"
|
||||
],
|
||||
"extraResources": [
|
||||
{
|
||||
"from": "dist-electron/www",
|
||||
"from": "dist",
|
||||
"to": "www"
|
||||
},
|
||||
{
|
||||
"from": "dist-electron/resources/preload.js",
|
||||
"to": "preload.js"
|
||||
}
|
||||
],
|
||||
"linux": {
|
||||
@@ -231,6 +223,5 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "module"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
eth_keys
|
||||
pywebview
|
||||
pyinstaller>=6.12.0
|
||||
setuptools>=69.0.0 # Required for distutils for electron-builder on macOS
|
||||
# For development
|
||||
watchdog>=3.0.0 # For file watching support
|
||||
@@ -1,96 +0,0 @@
|
||||
const fs = require("fs");
|
||||
const fse = require("fs-extra");
|
||||
const path = require("path");
|
||||
const { execSync } = require('child_process');
|
||||
|
||||
console.log("Starting Electron build finalization...");
|
||||
|
||||
// Define paths
|
||||
const distPath = path.join(__dirname, "..", "dist");
|
||||
const electronDistPath = path.join(__dirname, "..", "dist-electron");
|
||||
const wwwPath = path.join(electronDistPath, "www");
|
||||
const builtIndexPath = path.join(distPath, "index.html");
|
||||
const finalIndexPath = path.join(wwwPath, "index.html");
|
||||
|
||||
// Ensure target directory exists
|
||||
if (!fs.existsSync(wwwPath)) {
|
||||
fs.mkdirSync(wwwPath, { recursive: true });
|
||||
}
|
||||
|
||||
// Copy assets directory
|
||||
const assetsSrc = path.join(distPath, "assets");
|
||||
const assetsDest = path.join(wwwPath, "assets");
|
||||
if (fs.existsSync(assetsSrc)) {
|
||||
fse.copySync(assetsSrc, assetsDest, { overwrite: true });
|
||||
}
|
||||
|
||||
// Copy favicon.ico
|
||||
const faviconSrc = path.join(distPath, "favicon.ico");
|
||||
if (fs.existsSync(faviconSrc)) {
|
||||
fs.copyFileSync(faviconSrc, path.join(wwwPath, "favicon.ico"));
|
||||
}
|
||||
|
||||
// Copy manifest.webmanifest
|
||||
const manifestSrc = path.join(distPath, "manifest.webmanifest");
|
||||
if (fs.existsSync(manifestSrc)) {
|
||||
fs.copyFileSync(manifestSrc, path.join(wwwPath, "manifest.webmanifest"));
|
||||
}
|
||||
|
||||
// Load and modify index.html from Vite output
|
||||
let indexContent = fs.readFileSync(builtIndexPath, "utf-8");
|
||||
|
||||
// Inject the window.process shim after the first <script> block
|
||||
indexContent = indexContent.replace(
|
||||
/<script[^>]*type="module"[^>]*>/,
|
||||
match => `${match}\n window.process = { env: { VITE_PLATFORM: 'electron' } };`
|
||||
);
|
||||
|
||||
// Write the modified index.html to dist-electron/www
|
||||
fs.writeFileSync(finalIndexPath, indexContent);
|
||||
|
||||
// Copy preload script to resources
|
||||
const preloadSrc = path.join(electronDistPath, "preload.mjs");
|
||||
const preloadDest = path.join(electronDistPath, "resources", "preload.js");
|
||||
|
||||
// Ensure resources directory exists
|
||||
const resourcesDir = path.join(electronDistPath, "resources");
|
||||
if (!fs.existsSync(resourcesDir)) {
|
||||
fs.mkdirSync(resourcesDir, { recursive: true });
|
||||
}
|
||||
|
||||
if (fs.existsSync(preloadSrc)) {
|
||||
// Read the preload script
|
||||
let preloadContent = fs.readFileSync(preloadSrc, 'utf-8');
|
||||
|
||||
// Convert ESM to CommonJS if needed
|
||||
preloadContent = preloadContent
|
||||
.replace(/import\s*{\s*([^}]+)\s*}\s*from\s*['"]electron['"];?/g, 'const { $1 } = require("electron");')
|
||||
.replace(/export\s*{([^}]+)};?/g, '')
|
||||
.replace(/export\s+default\s+([^;]+);?/g, 'module.exports = $1;');
|
||||
|
||||
// Write the modified preload script
|
||||
fs.writeFileSync(preloadDest, preloadContent);
|
||||
console.log("Preload script copied and converted to resources directory");
|
||||
} else {
|
||||
console.error("Preload script not found at:", preloadSrc);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Copy capacitor.config.json to dist-electron
|
||||
try {
|
||||
console.log("Copying capacitor.config.json to dist-electron...");
|
||||
const configPath = path.join(process.cwd(), 'capacitor.config.json');
|
||||
const targetPath = path.join(process.cwd(), 'dist-electron', 'capacitor.config.json');
|
||||
|
||||
if (!fs.existsSync(configPath)) {
|
||||
throw new Error('capacitor.config.json not found in project root');
|
||||
}
|
||||
|
||||
fs.copyFileSync(configPath, targetPath);
|
||||
console.log("Successfully copied capacitor.config.json");
|
||||
} catch (error) {
|
||||
console.error("Failed to copy capacitor.config.json:", error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
console.log("Electron index.html copied and patched for Electron context.");
|
||||
243
scripts/build-electron.js
Normal file
243
scripts/build-electron.js
Normal file
@@ -0,0 +1,243 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
console.log('Starting electron build process...');
|
||||
|
||||
// Copy web files
|
||||
const webDistPath = path.join(__dirname, '..', 'dist');
|
||||
const electronDistPath = path.join(__dirname, '..', 'dist-electron');
|
||||
const wwwPath = path.join(electronDistPath, 'www');
|
||||
|
||||
// Create www directory if it doesn't exist
|
||||
if (!fs.existsSync(wwwPath)) {
|
||||
fs.mkdirSync(wwwPath, { recursive: true });
|
||||
}
|
||||
|
||||
// Copy web files to www directory
|
||||
fs.cpSync(webDistPath, wwwPath, { recursive: true });
|
||||
|
||||
// Fix asset paths in index.html
|
||||
const indexPath = path.join(wwwPath, 'index.html');
|
||||
let indexContent = fs.readFileSync(indexPath, 'utf8');
|
||||
|
||||
// Fix asset paths
|
||||
indexContent = indexContent
|
||||
.replace(/\/assets\//g, './assets/')
|
||||
.replace(/href="\//g, 'href="./')
|
||||
.replace(/src="\//g, 'src="./');
|
||||
|
||||
fs.writeFileSync(indexPath, indexContent);
|
||||
|
||||
// Check for remaining /assets/ paths
|
||||
console.log('After path fixing, checking for remaining /assets/ paths:', indexContent.includes('/assets/'));
|
||||
console.log('Sample of fixed content:', indexContent.substring(0, 500));
|
||||
|
||||
console.log('Copied and fixed web files in:', wwwPath);
|
||||
|
||||
// Copy main process files
|
||||
console.log('Copying main process files...');
|
||||
|
||||
// Create the main process file with inlined logger
|
||||
const mainContent = `const { app, BrowserWindow } = require("electron");
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
|
||||
// Inline logger implementation
|
||||
const logger = {
|
||||
log: (...args) => console.log(...args),
|
||||
error: (...args) => console.error(...args),
|
||||
info: (...args) => console.info(...args),
|
||||
warn: (...args) => console.warn(...args),
|
||||
debug: (...args) => console.debug(...args),
|
||||
};
|
||||
|
||||
// Check if running in dev mode
|
||||
const isDev = process.argv.includes("--inspect");
|
||||
|
||||
function createWindow() {
|
||||
// Add before createWindow function
|
||||
const preloadPath = path.join(__dirname, "preload.js");
|
||||
logger.log("Checking preload path:", preloadPath);
|
||||
logger.log("Preload exists:", fs.existsSync(preloadPath));
|
||||
|
||||
// Create the browser window.
|
||||
const mainWindow = new BrowserWindow({
|
||||
width: 1200,
|
||||
height: 800,
|
||||
webPreferences: {
|
||||
nodeIntegration: false,
|
||||
contextIsolation: true,
|
||||
webSecurity: true,
|
||||
allowRunningInsecureContent: false,
|
||||
preload: path.join(__dirname, "preload.js"),
|
||||
},
|
||||
});
|
||||
|
||||
// Always open DevTools for now
|
||||
mainWindow.webContents.openDevTools();
|
||||
|
||||
// Intercept requests to fix asset paths
|
||||
mainWindow.webContents.session.webRequest.onBeforeRequest(
|
||||
{
|
||||
urls: [
|
||||
"file://*/*/assets/*",
|
||||
"file://*/assets/*",
|
||||
"file:///assets/*", // Catch absolute paths
|
||||
"<all_urls>", // Catch all URLs as a fallback
|
||||
],
|
||||
},
|
||||
(details, callback) => {
|
||||
let url = details.url;
|
||||
|
||||
// Handle paths that don't start with file://
|
||||
if (!url.startsWith("file://") && url.includes("/assets/")) {
|
||||
url = \`file://\${path.join(__dirname, "www", url)}\`;
|
||||
}
|
||||
|
||||
// Handle absolute paths starting with /assets/
|
||||
if (url.includes("/assets/") && !url.includes("/www/assets/")) {
|
||||
const baseDir = url.includes("dist-electron")
|
||||
? url.substring(
|
||||
0,
|
||||
url.indexOf("/dist-electron") + "/dist-electron".length,
|
||||
)
|
||||
: \`file://\${__dirname}\`;
|
||||
const assetPath = url.split("/assets/")[1];
|
||||
const newUrl = \`\${baseDir}/www/assets/\${assetPath}\`;
|
||||
callback({ redirectURL: newUrl });
|
||||
return;
|
||||
}
|
||||
|
||||
callback({}); // No redirect for other URLs
|
||||
},
|
||||
);
|
||||
|
||||
if (isDev) {
|
||||
// Debug info
|
||||
logger.log("Debug Info:");
|
||||
logger.log("Running in dev mode:", isDev);
|
||||
logger.log("App is packaged:", app.isPackaged);
|
||||
logger.log("Process resource path:", process.resourcesPath);
|
||||
logger.log("App path:", app.getAppPath());
|
||||
logger.log("__dirname:", __dirname);
|
||||
logger.log("process.cwd():", process.cwd());
|
||||
}
|
||||
|
||||
const indexPath = path.join(__dirname, "www", "index.html");
|
||||
|
||||
if (isDev) {
|
||||
logger.log("Loading index from:", indexPath);
|
||||
logger.log("www path:", path.join(__dirname, "www"));
|
||||
logger.log("www assets path:", path.join(__dirname, "www", "assets"));
|
||||
}
|
||||
|
||||
if (!fs.existsSync(indexPath)) {
|
||||
logger.error(\`Index file not found at: \${indexPath}\`);
|
||||
throw new Error("Index file not found");
|
||||
}
|
||||
|
||||
// Add CSP headers to allow API connections, Google Fonts, and zxing-wasm
|
||||
mainWindow.webContents.session.webRequest.onHeadersReceived(
|
||||
(details, callback) => {
|
||||
callback({
|
||||
responseHeaders: {
|
||||
...details.responseHeaders,
|
||||
"Content-Security-Policy": [
|
||||
"default-src 'self';" +
|
||||
"connect-src 'self' https://api.endorser.ch https://*.timesafari.app https://*.jsdelivr.net;" +
|
||||
"img-src 'self' data: https: blob:;" +
|
||||
"script-src 'self' 'unsafe-inline' 'unsafe-eval' https://*.jsdelivr.net;" +
|
||||
"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;" +
|
||||
"font-src 'self' data: https://fonts.gstatic.com;" +
|
||||
"style-src-elem 'self' 'unsafe-inline' https://fonts.googleapis.com;" +
|
||||
"worker-src 'self' blob:;",
|
||||
],
|
||||
},
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
// Load the index.html
|
||||
mainWindow
|
||||
.loadFile(indexPath)
|
||||
.then(() => {
|
||||
logger.log("Successfully loaded index.html");
|
||||
if (isDev) {
|
||||
mainWindow.webContents.openDevTools();
|
||||
logger.log("DevTools opened - running in dev mode");
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
logger.error("Failed to load index.html:", err);
|
||||
logger.error("Attempted path:", indexPath);
|
||||
});
|
||||
|
||||
// Listen for console messages from the renderer
|
||||
mainWindow.webContents.on("console-message", (_event, _level, message) => {
|
||||
logger.log("Renderer Console:", message);
|
||||
});
|
||||
|
||||
// Add right after creating the BrowserWindow
|
||||
mainWindow.webContents.on(
|
||||
"did-fail-load",
|
||||
(_event, errorCode, errorDescription) => {
|
||||
logger.error("Page failed to load:", errorCode, errorDescription);
|
||||
},
|
||||
);
|
||||
|
||||
mainWindow.webContents.on("preload-error", (_event, preloadPath, error) => {
|
||||
logger.error("Preload script error:", preloadPath, error);
|
||||
});
|
||||
|
||||
mainWindow.webContents.on(
|
||||
"console-message",
|
||||
(_event, _level, message, line, sourceId) => {
|
||||
logger.log("Renderer Console:", line, sourceId, message);
|
||||
},
|
||||
);
|
||||
|
||||
// Enable remote debugging when in dev mode
|
||||
if (isDev) {
|
||||
mainWindow.webContents.openDevTools();
|
||||
}
|
||||
}
|
||||
|
||||
// Handle app ready
|
||||
app.whenReady().then(createWindow);
|
||||
|
||||
// Handle all windows closed
|
||||
app.on("window-all-closed", () => {
|
||||
if (process.platform !== "darwin") {
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
|
||||
app.on("activate", () => {
|
||||
if (BrowserWindow.getAllWindows().length === 0) {
|
||||
createWindow();
|
||||
}
|
||||
});
|
||||
|
||||
// Handle any errors
|
||||
process.on("uncaughtException", (error) => {
|
||||
logger.error("Uncaught Exception:", error);
|
||||
});
|
||||
`;
|
||||
|
||||
// Write the main process file
|
||||
const mainDest = path.join(electronDistPath, 'main.js');
|
||||
fs.writeFileSync(mainDest, mainContent);
|
||||
|
||||
// Copy preload script if it exists
|
||||
const preloadSrc = path.join(__dirname, '..', 'src', 'electron', 'preload.js');
|
||||
const preloadDest = path.join(electronDistPath, 'preload.js');
|
||||
if (fs.existsSync(preloadSrc)) {
|
||||
console.log(`Copying ${preloadSrc} to ${preloadDest}`);
|
||||
fs.copyFileSync(preloadSrc, preloadDest);
|
||||
}
|
||||
|
||||
// Verify build structure
|
||||
console.log('\nVerifying build structure:');
|
||||
console.log('Files in dist-electron:', fs.readdirSync(electronDistPath));
|
||||
|
||||
console.log('Build completed successfully!');
|
||||
20
src/App.vue
20
src/App.vue
@@ -330,11 +330,8 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { Vue, Component } from "vue-facing-decorator";
|
||||
|
||||
import { NotificationIface, USE_DEXIE_DB } from "./constants/app";
|
||||
import * as databaseUtil from "./db/databaseUtil";
|
||||
import { retrieveSettingsForActiveAccount } from "./db/index";
|
||||
import { logConsoleAndDb } from "./db/databaseUtil";
|
||||
import { logConsoleAndDb, retrieveSettingsForActiveAccount } from "./db/index";
|
||||
import { NotificationIface } from "./constants/app";
|
||||
import { logger } from "./utils/logger";
|
||||
|
||||
interface Settings {
|
||||
@@ -399,11 +396,7 @@ export default class App extends Vue {
|
||||
|
||||
try {
|
||||
logger.log("Retrieving settings for the active account...");
|
||||
let settings: Settings =
|
||||
await databaseUtil.retrieveSettingsForActiveAccount();
|
||||
if (USE_DEXIE_DB) {
|
||||
settings = await retrieveSettingsForActiveAccount();
|
||||
}
|
||||
const settings: Settings = await retrieveSettingsForActiveAccount();
|
||||
logger.log("Retrieved settings:", settings);
|
||||
|
||||
const notifyingNewActivity = !!settings?.notifyingNewActivityTime;
|
||||
@@ -459,10 +452,9 @@ export default class App extends Vue {
|
||||
return true;
|
||||
}
|
||||
|
||||
const serverSubscription =
|
||||
typeof subscription === "object" && subscription !== null
|
||||
? { ...subscription }
|
||||
: {};
|
||||
const serverSubscription = {
|
||||
...subscription,
|
||||
};
|
||||
if (!allGoingOff) {
|
||||
serverSubscription["notifyType"] = notification.title;
|
||||
logger.log(
|
||||
|
||||
@@ -62,7 +62,7 @@ backup and database export, with platform-specific download instructions. * *
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from "vue-facing-decorator";
|
||||
import { AppString, NotificationIface, USE_DEXIE_DB } from "../constants/app";
|
||||
import { NotificationIface } from "../constants/app";
|
||||
import { db } from "../db/index";
|
||||
import { logger } from "../utils/logger";
|
||||
import { PlatformServiceFactory } from "../services/PlatformServiceFactory";
|
||||
@@ -131,9 +131,6 @@ export default class DataExportSection extends Vue {
|
||||
*/
|
||||
public async exportDatabase() {
|
||||
try {
|
||||
if (!USE_DEXIE_DB) {
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
const blob = await db.export({
|
||||
prettyJson: true,
|
||||
transform: (table, value, key) => {
|
||||
@@ -148,7 +145,7 @@ export default class DataExportSection extends Vue {
|
||||
return { value, key };
|
||||
},
|
||||
});
|
||||
const fileName = `${AppString.APP_NAME_NO_SPACES}-backup.json`;
|
||||
const fileName = `${db.name}-backup.json`;
|
||||
|
||||
if (this.platformCapabilities.hasFileDownload) {
|
||||
// Web platform: Use download link
|
||||
@@ -162,8 +159,6 @@ export default class DataExportSection extends Vue {
|
||||
// Native platform: Write to app directory
|
||||
const content = await blob.text();
|
||||
await this.platformService.writeAndShareFile(fileName, content);
|
||||
} else {
|
||||
throw new Error("This platform does not support file downloads.");
|
||||
}
|
||||
|
||||
this.$notify(
|
||||
|
||||
@@ -100,12 +100,6 @@ import {
|
||||
} from "@vue-leaflet/vue-leaflet";
|
||||
import { Router } from "vue-router";
|
||||
|
||||
import { USE_DEXIE_DB } from "@/constants/app";
|
||||
import * as databaseUtil from "../db/databaseUtil";
|
||||
import { MASTER_SETTINGS_KEY } from "../db/tables/settings";
|
||||
import { db, retrieveSettingsForActiveAccount } from "../db/index";
|
||||
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
LRectangle,
|
||||
@@ -126,10 +120,8 @@ export default class FeedFilters extends Vue {
|
||||
async open(onCloseIfChanged: () => void) {
|
||||
this.onCloseIfChanged = onCloseIfChanged;
|
||||
|
||||
let settings = await databaseUtil.retrieveSettingsForActiveAccount();
|
||||
if (USE_DEXIE_DB) {
|
||||
settings = await retrieveSettingsForActiveAccount();
|
||||
}
|
||||
const platform = this.$platform;
|
||||
const settings = await platform.getActiveAccountSettings();
|
||||
this.hasVisibleDid = !!settings.filterFeedByVisible;
|
||||
this.isNearby = !!settings.filterFeedByNearby;
|
||||
if (settings.searchBoxes && settings.searchBoxes.length > 0) {
|
||||
@@ -143,7 +135,8 @@ export default class FeedFilters extends Vue {
|
||||
async toggleHasVisibleDid() {
|
||||
this.settingChanged = true;
|
||||
this.hasVisibleDid = !this.hasVisibleDid;
|
||||
await db.settings.update(MASTER_SETTINGS_KEY, {
|
||||
const platform = this.$platform;
|
||||
await platform.updateMasterSettings({
|
||||
filterFeedByVisible: this.hasVisibleDid,
|
||||
});
|
||||
}
|
||||
@@ -151,17 +144,10 @@ export default class FeedFilters extends Vue {
|
||||
async toggleNearby() {
|
||||
this.settingChanged = true;
|
||||
this.isNearby = !this.isNearby;
|
||||
const platformService = PlatformServiceFactory.getInstance();
|
||||
await platformService.dbExec(
|
||||
`UPDATE settings SET filterFeedByNearby = ? WHERE id = ?`,
|
||||
[this.isNearby, MASTER_SETTINGS_KEY],
|
||||
);
|
||||
|
||||
if (USE_DEXIE_DB) {
|
||||
await db.settings.update(MASTER_SETTINGS_KEY, {
|
||||
filterFeedByNearby: this.isNearby,
|
||||
});
|
||||
}
|
||||
const platform = this.$platform;
|
||||
await platform.updateMasterSettings({
|
||||
filterFeedByNearby: this.isNearby,
|
||||
});
|
||||
}
|
||||
|
||||
async clearAll() {
|
||||
@@ -169,18 +155,11 @@ export default class FeedFilters extends Vue {
|
||||
this.settingChanged = true;
|
||||
}
|
||||
|
||||
const platformService = PlatformServiceFactory.getInstance();
|
||||
await platformService.dbExec(
|
||||
`UPDATE settings SET filterFeedByNearby = ? AND filterFeedByVisible = ? WHERE id = ?`,
|
||||
[false, false, MASTER_SETTINGS_KEY],
|
||||
);
|
||||
|
||||
if (USE_DEXIE_DB) {
|
||||
await db.settings.update(MASTER_SETTINGS_KEY, {
|
||||
filterFeedByNearby: false,
|
||||
filterFeedByVisible: false,
|
||||
});
|
||||
}
|
||||
const platform = this.$platform;
|
||||
await platform.updateMasterSettings({
|
||||
filterFeedByNearby: false,
|
||||
filterFeedByVisible: false,
|
||||
});
|
||||
|
||||
this.hasVisibleDid = false;
|
||||
this.isNearby = false;
|
||||
@@ -191,18 +170,11 @@ export default class FeedFilters extends Vue {
|
||||
this.settingChanged = true;
|
||||
}
|
||||
|
||||
const platformService = PlatformServiceFactory.getInstance();
|
||||
await platformService.dbExec(
|
||||
`UPDATE settings SET filterFeedByNearby = ? AND filterFeedByVisible = ? WHERE id = ?`,
|
||||
[true, true, MASTER_SETTINGS_KEY],
|
||||
);
|
||||
|
||||
if (USE_DEXIE_DB) {
|
||||
await db.settings.update(MASTER_SETTINGS_KEY, {
|
||||
filterFeedByNearby: true,
|
||||
filterFeedByVisible: true,
|
||||
});
|
||||
}
|
||||
const platform = this.$platform;
|
||||
await platform.updateMasterSettings({
|
||||
filterFeedByNearby: true,
|
||||
filterFeedByVisible: true,
|
||||
});
|
||||
|
||||
this.hasVisibleDid = true;
|
||||
this.isNearby = true;
|
||||
|
||||
@@ -89,7 +89,7 @@
|
||||
<script lang="ts">
|
||||
import { Vue, Component, Prop } from "vue-facing-decorator";
|
||||
|
||||
import { NotificationIface, USE_DEXIE_DB } from "../constants/app";
|
||||
import { NotificationIface } from "../constants/app";
|
||||
import {
|
||||
createAndSubmitGive,
|
||||
didInfo,
|
||||
@@ -98,10 +98,8 @@ import {
|
||||
import * as libsUtil from "../libs/util";
|
||||
import { db, retrieveSettingsForActiveAccount } from "../db/index";
|
||||
import { Contact } from "../db/tables/contacts";
|
||||
import * as databaseUtil from "../db/databaseUtil";
|
||||
import { retrieveAccountDids } from "../libs/util";
|
||||
import { logger } from "../utils/logger";
|
||||
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
|
||||
|
||||
@Component
|
||||
export default class GiftedDialog extends Vue {
|
||||
@@ -146,23 +144,11 @@ export default class GiftedDialog extends Vue {
|
||||
this.offerId = offerId || "";
|
||||
|
||||
try {
|
||||
let settings = await databaseUtil.retrieveSettingsForActiveAccount();
|
||||
if (USE_DEXIE_DB) {
|
||||
settings = await retrieveSettingsForActiveAccount();
|
||||
}
|
||||
const settings = await retrieveSettingsForActiveAccount();
|
||||
this.apiServer = settings.apiServer || "";
|
||||
this.activeDid = settings.activeDid || "";
|
||||
|
||||
const platformService = PlatformServiceFactory.getInstance();
|
||||
const result = await platformService.dbQuery(`SELECT * FROM contacts`);
|
||||
if (result) {
|
||||
this.allContacts = databaseUtil.mapQueryResultToValues(
|
||||
result,
|
||||
) as unknown as Contact[];
|
||||
}
|
||||
if (USE_DEXIE_DB) {
|
||||
this.allContacts = await db.contacts.toArray();
|
||||
}
|
||||
this.allContacts = await db.contacts.toArray();
|
||||
|
||||
this.allMyDids = await retrieveAccountDids();
|
||||
|
||||
@@ -320,7 +306,10 @@ export default class GiftedDialog extends Vue {
|
||||
this.fromProjectId,
|
||||
);
|
||||
|
||||
if (!result.success) {
|
||||
if (
|
||||
result.type === "error" ||
|
||||
this.isGiveCreationError(result.response)
|
||||
) {
|
||||
const errorMessage = this.getGiveCreationErrorMessage(result);
|
||||
logger.error("Error with give creation result:", result);
|
||||
this.$notify(
|
||||
@@ -367,6 +356,15 @@ export default class GiftedDialog extends Vue {
|
||||
|
||||
// Helper functions for readability
|
||||
|
||||
/**
|
||||
* @param result response "data" from the server
|
||||
* @returns true if the result indicates an error
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
isGiveCreationError(result: any) {
|
||||
return result.status !== 201 || result.data?.error;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param result direct response eg. ErrorResult or SuccessResult (potentially with embedded "data")
|
||||
* @returns best guess at an error message
|
||||
|
||||
@@ -74,12 +74,10 @@
|
||||
import { Vue, Component } from "vue-facing-decorator";
|
||||
import { Router } from "vue-router";
|
||||
|
||||
import { AppString, NotificationIface, USE_DEXIE_DB } from "../constants/app";
|
||||
import { AppString, NotificationIface } from "../constants/app";
|
||||
import { db } from "../db/index";
|
||||
import { Contact } from "../db/tables/contacts";
|
||||
import * as databaseUtil from "../db/databaseUtil";
|
||||
import { GiverReceiverInputInfo } from "../libs/util";
|
||||
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
|
||||
|
||||
@Component
|
||||
export default class GivenPrompts extends Vue {
|
||||
@@ -129,16 +127,8 @@ export default class GivenPrompts extends Vue {
|
||||
this.visible = true;
|
||||
this.callbackOnFullGiftInfo = callbackOnFullGiftInfo;
|
||||
|
||||
const platformService = PlatformServiceFactory.getInstance();
|
||||
const result = await platformService.dbQuery(
|
||||
"SELECT COUNT(*) FROM contacts",
|
||||
);
|
||||
if (result) {
|
||||
this.numContacts = result.values[0][0] as number;
|
||||
}
|
||||
if (USE_DEXIE_DB) {
|
||||
this.numContacts = await db.contacts.count();
|
||||
}
|
||||
await db.open();
|
||||
this.numContacts = await db.contacts.count();
|
||||
this.shownContactDbIndices = new Array<boolean>(this.numContacts); // all undefined to start
|
||||
}
|
||||
|
||||
@@ -239,22 +229,10 @@ export default class GivenPrompts extends Vue {
|
||||
this.nextIdeaPastContacts();
|
||||
} else {
|
||||
// get the contact at that offset
|
||||
const platformService = PlatformServiceFactory.getInstance();
|
||||
const result = await platformService.dbQuery(
|
||||
"SELECT * FROM contacts LIMIT 1 OFFSET ?",
|
||||
[someContactDbIndex],
|
||||
);
|
||||
if (result) {
|
||||
this.currentContact = databaseUtil.mapQueryResultToValues(result)[
|
||||
someContactDbIndex
|
||||
] as unknown as Contact;
|
||||
}
|
||||
if (USE_DEXIE_DB) {
|
||||
await db.open();
|
||||
this.currentContact = await db.contacts
|
||||
.offset(someContactDbIndex)
|
||||
.first();
|
||||
}
|
||||
await db.open();
|
||||
this.currentContact = await db.contacts
|
||||
.offset(someContactDbIndex)
|
||||
.first();
|
||||
this.shownContactDbIndices[someContactDbIndex] = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -247,16 +247,11 @@ import axios from "axios";
|
||||
import { ref } from "vue";
|
||||
import { Component, Vue } from "vue-facing-decorator";
|
||||
import VuePictureCropper, { cropper } from "vue-picture-cropper";
|
||||
import {
|
||||
DEFAULT_IMAGE_API_SERVER,
|
||||
NotificationIface,
|
||||
USE_DEXIE_DB,
|
||||
} from "../constants/app";
|
||||
import { DEFAULT_IMAGE_API_SERVER, NotificationIface } from "../constants/app";
|
||||
import { retrieveSettingsForActiveAccount } from "../db/index";
|
||||
import { accessToken } from "../libs/crypto";
|
||||
import { logger } from "../utils/logger";
|
||||
import { PlatformServiceFactory } from "../services/PlatformServiceFactory";
|
||||
import * as databaseUtil from "../db/databaseUtil";
|
||||
|
||||
const inputImageFileNameRef = ref<Blob>();
|
||||
|
||||
@@ -339,10 +334,7 @@ export default class ImageMethodDialog extends Vue {
|
||||
*/
|
||||
async mounted() {
|
||||
try {
|
||||
let settings = await databaseUtil.retrieveSettingsForActiveAccount();
|
||||
if (USE_DEXIE_DB) {
|
||||
settings = await retrieveSettingsForActiveAccount();
|
||||
}
|
||||
const settings = await retrieveSettingsForActiveAccount();
|
||||
this.activeDid = settings.activeDid || "";
|
||||
} catch (error: unknown) {
|
||||
logger.error("Error retrieving settings from database:", error);
|
||||
|
||||
@@ -172,10 +172,8 @@ import {
|
||||
} from "../libs/endorserServer";
|
||||
import { decryptMessage } from "../libs/crypto";
|
||||
import { Contact } from "../db/tables/contacts";
|
||||
import * as databaseUtil from "../db/databaseUtil";
|
||||
import * as libsUtil from "../libs/util";
|
||||
import { NotificationIface, USE_DEXIE_DB } from "../constants/app";
|
||||
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
|
||||
import { NotificationIface } from "../constants/app";
|
||||
|
||||
interface Member {
|
||||
admitted: boolean;
|
||||
@@ -211,10 +209,7 @@ export default class MembersList extends Vue {
|
||||
contacts: Array<Contact> = [];
|
||||
|
||||
async created() {
|
||||
let settings = await databaseUtil.retrieveSettingsForActiveAccount();
|
||||
if (USE_DEXIE_DB) {
|
||||
settings = await retrieveSettingsForActiveAccount();
|
||||
}
|
||||
const settings = await retrieveSettingsForActiveAccount();
|
||||
this.activeDid = settings.activeDid || "";
|
||||
this.apiServer = settings.apiServer || "";
|
||||
this.firstName = settings.firstName || "";
|
||||
@@ -360,16 +355,7 @@ export default class MembersList extends Vue {
|
||||
}
|
||||
|
||||
async loadContacts() {
|
||||
const platformService = PlatformServiceFactory.getInstance();
|
||||
const result = await platformService.dbQuery("SELECT * FROM contacts");
|
||||
if (result) {
|
||||
this.contacts = databaseUtil.mapQueryResultToValues(
|
||||
result,
|
||||
) as unknown as Contact[];
|
||||
}
|
||||
if (USE_DEXIE_DB) {
|
||||
this.contacts = await db.contacts.toArray();
|
||||
}
|
||||
this.contacts = await db.contacts.toArray();
|
||||
}
|
||||
|
||||
getContactFor(did: string): Contact | undefined {
|
||||
@@ -453,14 +439,7 @@ export default class MembersList extends Vue {
|
||||
if (result.success) {
|
||||
decrMember.isRegistered = true;
|
||||
if (oldContact) {
|
||||
const platformService = PlatformServiceFactory.getInstance();
|
||||
await platformService.dbExec(
|
||||
"UPDATE contacts SET registered = ? WHERE did = ?",
|
||||
[true, decrMember.did],
|
||||
);
|
||||
if (USE_DEXIE_DB) {
|
||||
await db.contacts.update(decrMember.did, { registered: true });
|
||||
}
|
||||
await db.contacts.update(decrMember.did, { registered: true });
|
||||
oldContact.registered = true;
|
||||
}
|
||||
this.$notify(
|
||||
@@ -513,14 +492,7 @@ export default class MembersList extends Vue {
|
||||
name: member.name,
|
||||
};
|
||||
|
||||
const platformService = PlatformServiceFactory.getInstance();
|
||||
await platformService.dbExec(
|
||||
"INSERT INTO contacts (did, name) VALUES (?, ?)",
|
||||
[member.did, member.name],
|
||||
);
|
||||
if (USE_DEXIE_DB) {
|
||||
await db.contacts.add(newContact);
|
||||
}
|
||||
await db.contacts.add(newContact);
|
||||
this.contacts.push(newContact);
|
||||
|
||||
this.$notify(
|
||||
|
||||
@@ -82,13 +82,12 @@
|
||||
<script lang="ts">
|
||||
import { Vue, Component, Prop } from "vue-facing-decorator";
|
||||
|
||||
import { NotificationIface, USE_DEXIE_DB } from "../constants/app";
|
||||
import { NotificationIface } from "../constants/app";
|
||||
import {
|
||||
createAndSubmitOffer,
|
||||
serverMessageForUser,
|
||||
} from "../libs/endorserServer";
|
||||
import * as libsUtil from "../libs/util";
|
||||
import * as databaseUtil from "../db/databaseUtil";
|
||||
import { retrieveSettingsForActiveAccount } from "../db/index";
|
||||
import { logger } from "../utils/logger";
|
||||
|
||||
@@ -117,10 +116,7 @@ export default class OfferDialog extends Vue {
|
||||
this.recipientDid = recipientDid;
|
||||
this.recipientName = recipientName;
|
||||
|
||||
let settings = await databaseUtil.retrieveSettingsForActiveAccount();
|
||||
if (USE_DEXIE_DB) {
|
||||
settings = await retrieveSettingsForActiveAccount();
|
||||
}
|
||||
const settings = await retrieveSettingsForActiveAccount();
|
||||
this.apiServer = settings.apiServer || "";
|
||||
this.activeDid = settings.activeDid || "";
|
||||
|
||||
@@ -249,7 +245,10 @@ export default class OfferDialog extends Vue {
|
||||
this.projectId,
|
||||
);
|
||||
|
||||
if (!result.success) {
|
||||
if (
|
||||
result.type === "error" ||
|
||||
this.isOfferCreationError(result.response)
|
||||
) {
|
||||
const errorMessage = this.getOfferCreationErrorMessage(result);
|
||||
logger.error("Error with offer creation result:", result);
|
||||
this.$notify(
|
||||
@@ -293,6 +292,15 @@ export default class OfferDialog extends Vue {
|
||||
|
||||
// Helper functions for readability
|
||||
|
||||
/**
|
||||
* @param result response "data" from the server
|
||||
* @returns true if the result indicates an error
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
isOfferCreationError(result: any) {
|
||||
return result.status !== 201 || result.data?.error;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param result direct response eg. ErrorResult or SuccessResult (potentially with embedded "data")
|
||||
* @returns best guess at an error message
|
||||
|
||||
@@ -201,16 +201,13 @@
|
||||
import { Component, Vue } from "vue-facing-decorator";
|
||||
import { Router } from "vue-router";
|
||||
|
||||
import { NotificationIface, USE_DEXIE_DB } from "../constants/app";
|
||||
import { NotificationIface } from "../constants/app";
|
||||
import {
|
||||
db,
|
||||
retrieveSettingsForActiveAccount,
|
||||
updateAccountSettings,
|
||||
} from "../db/index";
|
||||
import * as databaseUtil from "../db/databaseUtil";
|
||||
import { OnboardPage } from "../libs/util";
|
||||
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
|
||||
import { Contact } from "@/db/tables/contacts";
|
||||
|
||||
@Component({
|
||||
computed: {
|
||||
@@ -225,7 +222,7 @@ export default class OnboardingDialog extends Vue {
|
||||
$router!: Router;
|
||||
|
||||
activeDid = "";
|
||||
firstContactName = "";
|
||||
firstContactName = null;
|
||||
givenName = "";
|
||||
isRegistered = false;
|
||||
numContacts = 0;
|
||||
@@ -234,54 +231,29 @@ export default class OnboardingDialog extends Vue {
|
||||
|
||||
async open(page: OnboardPage) {
|
||||
this.page = page;
|
||||
let settings = await databaseUtil.retrieveSettingsForActiveAccount();
|
||||
if (USE_DEXIE_DB) {
|
||||
settings = await retrieveSettingsForActiveAccount();
|
||||
}
|
||||
const settings = await retrieveSettingsForActiveAccount();
|
||||
this.activeDid = settings.activeDid || "";
|
||||
this.isRegistered = !!settings.isRegistered;
|
||||
const platformService = PlatformServiceFactory.getInstance();
|
||||
const dbContacts = await platformService.dbQuery("SELECT * FROM contacts");
|
||||
if (dbContacts) {
|
||||
this.numContacts = dbContacts.values.length;
|
||||
const firstContact = dbContacts.values[0];
|
||||
const fullContact = databaseUtil.mapColumnsToValues(dbContacts.columns, [
|
||||
firstContact,
|
||||
]) as unknown as Contact;
|
||||
this.firstContactName = fullContact.name || "";
|
||||
}
|
||||
if (USE_DEXIE_DB) {
|
||||
const contacts = await db.contacts.toArray();
|
||||
this.numContacts = contacts.length;
|
||||
if (this.numContacts > 0) {
|
||||
this.firstContactName = contacts[0].name || "";
|
||||
}
|
||||
const contacts = await db.contacts.toArray();
|
||||
this.numContacts = contacts.length;
|
||||
if (this.numContacts > 0) {
|
||||
this.firstContactName = contacts[0].name;
|
||||
}
|
||||
this.visible = true;
|
||||
if (this.page === OnboardPage.Create) {
|
||||
// we'll assume that they've been through all the other pages
|
||||
await databaseUtil.updateAccountSettings(this.activeDid, {
|
||||
await updateAccountSettings(this.activeDid, {
|
||||
finishedOnboarding: true,
|
||||
});
|
||||
if (USE_DEXIE_DB) {
|
||||
await updateAccountSettings(this.activeDid, {
|
||||
finishedOnboarding: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async onClickClose(done?: boolean, goHome?: boolean) {
|
||||
this.visible = false;
|
||||
if (done) {
|
||||
await databaseUtil.updateAccountSettings(this.activeDid, {
|
||||
await updateAccountSettings(this.activeDid, {
|
||||
finishedOnboarding: true,
|
||||
});
|
||||
if (USE_DEXIE_DB) {
|
||||
await updateAccountSettings(this.activeDid, {
|
||||
finishedOnboarding: true,
|
||||
});
|
||||
}
|
||||
if (goHome) {
|
||||
this.$router.push({ name: "home" });
|
||||
}
|
||||
|
||||
@@ -119,12 +119,7 @@ PhotoDialog.vue */
|
||||
import axios from "axios";
|
||||
import { Component, Vue } from "vue-facing-decorator";
|
||||
import VuePictureCropper, { cropper } from "vue-picture-cropper";
|
||||
import {
|
||||
DEFAULT_IMAGE_API_SERVER,
|
||||
NotificationIface,
|
||||
USE_DEXIE_DB,
|
||||
} from "../constants/app";
|
||||
import * as databaseUtil from "../db/databaseUtil";
|
||||
import { DEFAULT_IMAGE_API_SERVER, NotificationIface } from "../constants/app";
|
||||
import { retrieveSettingsForActiveAccount } from "../db/index";
|
||||
import { accessToken } from "../libs/crypto";
|
||||
import { logger } from "../utils/logger";
|
||||
@@ -178,12 +173,9 @@ export default class PhotoDialog extends Vue {
|
||||
* @throws {Error} When settings retrieval fails
|
||||
*/
|
||||
async mounted() {
|
||||
// logger.log("PhotoDialog mounted");
|
||||
logger.log("PhotoDialog mounted");
|
||||
try {
|
||||
let settings = await databaseUtil.retrieveSettingsForActiveAccount();
|
||||
if (USE_DEXIE_DB) {
|
||||
settings = await retrieveSettingsForActiveAccount();
|
||||
}
|
||||
const settings = await retrieveSettingsForActiveAccount();
|
||||
this.activeDid = settings.activeDid || "";
|
||||
this.isRegistered = !!settings.isRegistered;
|
||||
logger.log("isRegistered:", this.isRegistered);
|
||||
|
||||
@@ -102,12 +102,7 @@
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from "vue-facing-decorator";
|
||||
|
||||
import {
|
||||
DEFAULT_PUSH_SERVER,
|
||||
NotificationIface,
|
||||
USE_DEXIE_DB,
|
||||
} from "../constants/app";
|
||||
import * as databaseUtil from "../db/databaseUtil";
|
||||
import { DEFAULT_PUSH_SERVER, NotificationIface } from "../constants/app";
|
||||
import {
|
||||
logConsoleAndDb,
|
||||
retrieveSettingsForActiveAccount,
|
||||
@@ -174,10 +169,7 @@ export default class PushNotificationPermission extends Vue {
|
||||
this.isVisible = true;
|
||||
this.pushType = pushType;
|
||||
try {
|
||||
let settings = await databaseUtil.retrieveSettingsForActiveAccount();
|
||||
if (USE_DEXIE_DB) {
|
||||
settings = await retrieveSettingsForActiveAccount();
|
||||
}
|
||||
const settings = await retrieveSettingsForActiveAccount();
|
||||
let pushUrl = DEFAULT_PUSH_SERVER;
|
||||
if (settings?.webPushServer) {
|
||||
pushUrl = settings.webPushServer;
|
||||
|
||||
@@ -15,8 +15,7 @@
|
||||
<script lang="ts">
|
||||
import { Component, Vue, Prop } from "vue-facing-decorator";
|
||||
|
||||
import { AppString, NotificationIface, USE_DEXIE_DB } from "../constants/app";
|
||||
import * as databaseUtil from "../db/databaseUtil";
|
||||
import { AppString, NotificationIface } from "../constants/app";
|
||||
import { retrieveSettingsForActiveAccount } from "../db/index";
|
||||
|
||||
@Component
|
||||
@@ -29,10 +28,7 @@ export default class TopMessage extends Vue {
|
||||
|
||||
async mounted() {
|
||||
try {
|
||||
let settings = await databaseUtil.retrieveSettingsForActiveAccount();
|
||||
if (USE_DEXIE_DB) {
|
||||
settings = await retrieveSettingsForActiveAccount();
|
||||
}
|
||||
const settings = await retrieveSettingsForActiveAccount();
|
||||
if (
|
||||
settings.warnIfTestServer &&
|
||||
settings.apiServer !== AppString.PROD_ENDORSER_API_SERVER
|
||||
|
||||
@@ -37,9 +37,8 @@
|
||||
<script lang="ts">
|
||||
import { Vue, Component, Prop } from "vue-facing-decorator";
|
||||
|
||||
import { NotificationIface, USE_DEXIE_DB } from "../constants/app";
|
||||
import { NotificationIface } from "../constants/app";
|
||||
import { db, retrieveSettingsForActiveAccount } from "../db/index";
|
||||
import * as databaseUtil from "../db/databaseUtil";
|
||||
import { MASTER_SETTINGS_KEY } from "../db/tables/settings";
|
||||
|
||||
@Component
|
||||
@@ -62,23 +61,15 @@ export default class UserNameDialog extends Vue {
|
||||
*/
|
||||
async open(aCallback?: (name?: string) => void) {
|
||||
this.callback = aCallback || this.callback;
|
||||
let settings = await databaseUtil.retrieveSettingsForActiveAccount();
|
||||
if (USE_DEXIE_DB) {
|
||||
settings = await retrieveSettingsForActiveAccount();
|
||||
}
|
||||
const settings = await retrieveSettingsForActiveAccount();
|
||||
this.givenName = settings.firstName || "";
|
||||
this.visible = true;
|
||||
}
|
||||
|
||||
async onClickSaveChanges() {
|
||||
await databaseUtil.updateDefaultSettings({
|
||||
await db.settings.update(MASTER_SETTINGS_KEY, {
|
||||
firstName: this.givenName,
|
||||
});
|
||||
if (USE_DEXIE_DB) {
|
||||
await db.settings.update(MASTER_SETTINGS_KEY, {
|
||||
firstName: this.givenName,
|
||||
});
|
||||
}
|
||||
this.visible = false;
|
||||
this.callback(this.givenName);
|
||||
}
|
||||
|
||||
@@ -3,8 +3,6 @@ import * as THREE from "three";
|
||||
import { GLTFLoader } from "three/addons/loaders/GLTFLoader";
|
||||
import * as SkeletonUtils from "three/addons/utils/SkeletonUtils";
|
||||
import * as TWEEN from "@tweenjs/tween.js";
|
||||
import { USE_DEXIE_DB } from "../../../../constants/app";
|
||||
import * as databaseUtil from "../../../../db/databaseUtil";
|
||||
import { retrieveSettingsForActiveAccount } from "../../../../db";
|
||||
import { getHeaders } from "../../../../libs/endorserServer";
|
||||
import { logger } from "../../../../utils/logger";
|
||||
@@ -16,10 +14,7 @@ export async function loadLandmarks(vue, world, scene, loop) {
|
||||
vue.setWorldProperty("animationDurationSeconds", ANIMATION_DURATION_SECS);
|
||||
|
||||
try {
|
||||
let settings = await databaseUtil.retrieveSettingsForActiveAccount();
|
||||
if (USE_DEXIE_DB) {
|
||||
settings = await retrieveSettingsForActiveAccount();
|
||||
}
|
||||
const settings = await retrieveSettingsForActiveAccount();
|
||||
const activeDid = settings.activeDid || "";
|
||||
const apiServer = settings.apiServer;
|
||||
const headers = await getHeaders(activeDid);
|
||||
|
||||
@@ -7,7 +7,6 @@ export enum AppString {
|
||||
// This is used in titles and verbiage inside the app.
|
||||
// There is also an app name without spaces, for packaging in the package.json file used in the manifest.
|
||||
APP_NAME = "Time Safari",
|
||||
APP_NAME_NO_SPACES = "TimeSafari",
|
||||
|
||||
PROD_ENDORSER_API_SERVER = "https://api.endorser.ch",
|
||||
TEST_ENDORSER_API_SERVER = "https://test-api.endorser.ch",
|
||||
@@ -44,15 +43,13 @@ export const DEFAULT_PARTNER_API_SERVER =
|
||||
AppString.TEST_PARTNER_API_SERVER;
|
||||
|
||||
export const DEFAULT_PUSH_SERVER =
|
||||
import.meta.env.VITE_DEFAULT_PUSH_SERVER || "https://timesafari.app";
|
||||
window.location.protocol + "//" + window.location.host;
|
||||
|
||||
export const IMAGE_TYPE_PROFILE = "profile";
|
||||
|
||||
export const PASSKEYS_ENABLED =
|
||||
!!import.meta.env.VITE_PASSKEYS_ENABLED || false;
|
||||
|
||||
export const USE_DEXIE_DB = false;
|
||||
|
||||
/**
|
||||
* The possible values for "group" and "type" are in App.vue.
|
||||
* Some of this comes from the notiwind package, some is custom.
|
||||
|
||||
@@ -1,32 +1,5 @@
|
||||
import migrationService from "../services/migrationService";
|
||||
import type { QueryExecResult } from "../interfaces/database";
|
||||
import { DEFAULT_ENDORSER_API_SERVER } from "@/constants/app";
|
||||
import { arrayBufferToBase64 } from "@/libs/crypto";
|
||||
|
||||
// Generate a random secret for the secret table
|
||||
|
||||
// It's not really secure to maintain the secret next to the user's data.
|
||||
// However, until we have better hooks into a real wallet or reliable secure
|
||||
// storage, we'll do this for user convenience. As they sign more records
|
||||
// and integrate with more people, they'll value it more and want to be more
|
||||
// secure, so we'll prompt them to take steps to back it up, properly encrypt,
|
||||
// etc. At the beginning, we'll prompt for a password, then we'll prompt for a
|
||||
// PWA so it's not in a browser... and then we hope to be integrated with a
|
||||
// real wallet or something else more secure.
|
||||
|
||||
// One might ask: why encrypt at all? We figure a basic encryption is better
|
||||
// than none. Plus, we expect to support their own password or keystore or
|
||||
// external wallet as better signing options in the future, so it's gonna be
|
||||
// important to have the structure where each account access might require
|
||||
// user action.
|
||||
|
||||
// (Once upon a time we stored the secret in localStorage, but it frequently
|
||||
// got erased, even though the IndexedDB still had the identity data. This
|
||||
// ended up throwing lots of errors to the user... and they'd end up in a state
|
||||
// where they couldn't take action because they couldn't unlock that identity.)
|
||||
|
||||
const randomBytes = crypto.getRandomValues(new Uint8Array(32));
|
||||
const secretBase64 = arrayBufferToBase64(randomBytes);
|
||||
import type { QueryExecResult, SqlValue } from "../interfaces/database";
|
||||
|
||||
// Each migration can include multiple SQL statements (with semicolons)
|
||||
const MIGRATIONS = [
|
||||
@@ -39,8 +12,8 @@ const MIGRATIONS = [
|
||||
dateCreated TEXT NOT NULL,
|
||||
derivationPath TEXT,
|
||||
did TEXT NOT NULL,
|
||||
identityEncrBase64 TEXT, -- encrypted & base64-encoded
|
||||
mnemonicEncrBase64 TEXT, -- encrypted & base64-encoded
|
||||
identity TEXT,
|
||||
mnemonic TEXT,
|
||||
passkeyCredIdHex TEXT,
|
||||
publicKeyHex TEXT NOT NULL
|
||||
);
|
||||
@@ -49,11 +22,9 @@ const MIGRATIONS = [
|
||||
|
||||
CREATE TABLE IF NOT EXISTS secret (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
secretBase64 TEXT NOT NULL
|
||||
secret TEXT NOT NULL
|
||||
);
|
||||
|
||||
INSERT INTO secret (id, secretBase64) VALUES (1, '${secretBase64}');
|
||||
|
||||
CREATE TABLE IF NOT EXISTS settings (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
accountDid TEXT,
|
||||
@@ -88,8 +59,6 @@ const MIGRATIONS = [
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_settings_accountDid ON settings(accountDid);
|
||||
|
||||
INSERT INTO settings (id, apiServer) VALUES (1, '${DEFAULT_ENDORSER_API_SERVER}');
|
||||
|
||||
CREATE TABLE IF NOT EXISTS contacts (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
did TEXT NOT NULL,
|
||||
@@ -107,7 +76,7 @@ const MIGRATIONS = [
|
||||
CREATE INDEX IF NOT EXISTS idx_contacts_name ON contacts(name);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS logs (
|
||||
date TEXT NOT NULL,
|
||||
date TEXT PRIMARY KEY,
|
||||
message TEXT NOT NULL
|
||||
);
|
||||
|
||||
@@ -127,7 +96,10 @@ export async function registerMigrations(): Promise<void> {
|
||||
}
|
||||
|
||||
export async function runMigrations(
|
||||
sqlExec: (sql: string, params?: unknown[]) => Promise<Array<QueryExecResult>>,
|
||||
sqlExec: (
|
||||
sql: string,
|
||||
params?: SqlValue[],
|
||||
) => Promise<Array<QueryExecResult>>,
|
||||
): Promise<void> {
|
||||
await registerMigrations();
|
||||
await migrationService.runMigrations(sqlExec);
|
||||
|
||||
@@ -1,308 +0,0 @@
|
||||
/**
|
||||
* This file is the SQL replacement of the index.ts file in the db directory.
|
||||
* That file will eventually be deleted.
|
||||
*/
|
||||
|
||||
import { PlatformServiceFactory } from "../services/PlatformServiceFactory";
|
||||
import { MASTER_SETTINGS_KEY, Settings } from "./tables/settings";
|
||||
import { logger } from "../utils/logger";
|
||||
import { DEFAULT_ENDORSER_API_SERVER } from "../constants/app";
|
||||
import { QueryExecResult } from "../interfaces/database";
|
||||
|
||||
const formatLogObject = (obj: unknown): string => {
|
||||
try {
|
||||
return JSON.stringify(obj, null, 2);
|
||||
} catch (error) {
|
||||
return `[Object could not be stringified: ${error instanceof Error ? error.message : String(error)}]`;
|
||||
}
|
||||
};
|
||||
|
||||
export async function updateDefaultSettings(
|
||||
settingsChanges: Settings,
|
||||
): Promise<boolean> {
|
||||
delete settingsChanges.accountDid; // just in case
|
||||
// ensure there is no "id" that would override the key
|
||||
delete settingsChanges.id;
|
||||
try {
|
||||
const platformService = PlatformServiceFactory.getInstance();
|
||||
const { sql, params } = generateUpdateStatement(
|
||||
settingsChanges,
|
||||
"settings",
|
||||
"id = ?",
|
||||
[MASTER_SETTINGS_KEY],
|
||||
);
|
||||
console.log("[databaseUtil] updateDefaultSettings", { sql, params });
|
||||
const result = await platformService.dbExec(sql, params);
|
||||
console.log("[databaseUtil] updateDefaultSettings result", { result });
|
||||
return result.changes === 1;
|
||||
} catch (error) {
|
||||
logger.error("Error updating default settings:", error);
|
||||
console.log("[databaseUtil] updateDefaultSettings error", { error });
|
||||
if (error instanceof Error) {
|
||||
throw error; // Re-throw if it's already an Error with a message
|
||||
} else {
|
||||
throw new Error(
|
||||
`Failed to update settings. We recommend you try again or restart the app.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateAccountSettings(
|
||||
accountDid: string,
|
||||
settingsChanges: Settings,
|
||||
): Promise<boolean> {
|
||||
settingsChanges.accountDid = accountDid;
|
||||
delete settingsChanges.id; // key off account, not ID
|
||||
|
||||
const platform = PlatformServiceFactory.getInstance();
|
||||
|
||||
// First try to update existing record
|
||||
const { sql: updateSql, params: updateParams } = generateUpdateStatement(
|
||||
settingsChanges,
|
||||
"settings",
|
||||
"accountDid = ?",
|
||||
[accountDid],
|
||||
);
|
||||
|
||||
const updateResult = await platform.dbExec(updateSql, updateParams);
|
||||
|
||||
// If no record was updated, insert a new one
|
||||
if (updateResult.changes === 1) {
|
||||
return true;
|
||||
} else {
|
||||
const columns = Object.keys(settingsChanges);
|
||||
const values = Object.values(settingsChanges);
|
||||
const placeholders = values.map(() => "?").join(", ");
|
||||
|
||||
const insertSql = `INSERT INTO settings (${columns.join(", ")}) VALUES (${placeholders})`;
|
||||
const result = await platform.dbExec(insertSql, values);
|
||||
|
||||
return result.changes === 1;
|
||||
}
|
||||
}
|
||||
|
||||
const DEFAULT_SETTINGS: Settings = {
|
||||
id: MASTER_SETTINGS_KEY,
|
||||
activeDid: undefined,
|
||||
apiServer: DEFAULT_ENDORSER_API_SERVER,
|
||||
};
|
||||
|
||||
// retrieves default settings
|
||||
export async function retrieveSettingsForDefaultAccount(): Promise<Settings> {
|
||||
console.log('[DatabaseUtil] Retrieving default account settings');
|
||||
const platform = PlatformServiceFactory.getInstance();
|
||||
|
||||
console.log('[DatabaseUtil] Platform service state:', {
|
||||
platformType: platform.constructor.name,
|
||||
capabilities: platform.getCapabilities(),
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
const result = await platform.dbQuery("SELECT * FROM settings WHERE id = ?", [
|
||||
MASTER_SETTINGS_KEY,
|
||||
]);
|
||||
|
||||
if (!result) {
|
||||
console.log('[DatabaseUtil] No settings found, returning defaults');
|
||||
return DEFAULT_SETTINGS;
|
||||
}
|
||||
|
||||
const settings = mapColumnsToValues(result.columns, result.values)[0] as Settings;
|
||||
if (settings.searchBoxes) {
|
||||
// @ts-expect-error - the searchBoxes field is a string in the DB
|
||||
settings.searchBoxes = JSON.parse(settings.searchBoxes);
|
||||
}
|
||||
|
||||
console.log('[DatabaseUtil] Retrieved settings:', {
|
||||
settings: formatLogObject(settings),
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
return settings;
|
||||
}
|
||||
|
||||
export async function retrieveSettingsForActiveAccount(): Promise<Settings> {
|
||||
console.log('[DatabaseUtil] Retrieving active account settings');
|
||||
const defaultSettings = await retrieveSettingsForDefaultAccount();
|
||||
|
||||
console.log('[DatabaseUtil] Default settings retrieved:', {
|
||||
defaultSettings: formatLogObject(defaultSettings),
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
if (!defaultSettings.activeDid) {
|
||||
console.log('[DatabaseUtil] No active DID, returning default settings');
|
||||
return defaultSettings;
|
||||
}
|
||||
|
||||
const platform = PlatformServiceFactory.getInstance();
|
||||
const result = await platform.dbQuery(
|
||||
"SELECT * FROM settings WHERE accountDid = ?",
|
||||
[defaultSettings.activeDid],
|
||||
);
|
||||
|
||||
const overrideSettings = result
|
||||
? (mapColumnsToValues(result.columns, result.values)[0] as Settings)
|
||||
: {};
|
||||
|
||||
const overrideSettingsFiltered = Object.fromEntries(
|
||||
Object.entries(overrideSettings).filter(([_, v]) => v !== null),
|
||||
);
|
||||
|
||||
const settings = { ...defaultSettings, ...overrideSettingsFiltered };
|
||||
if (settings.searchBoxes) {
|
||||
// @ts-expect-error - the searchBoxes field is a string in the DB
|
||||
settings.searchBoxes = JSON.parse(settings.searchBoxes);
|
||||
}
|
||||
|
||||
console.log('[DatabaseUtil] Final active account settings:', {
|
||||
settings: formatLogObject(settings),
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
return settings;
|
||||
}
|
||||
|
||||
let lastCleanupDate: string | null = null;
|
||||
|
||||
/**
|
||||
* Logs a message to the database with proper handling of concurrent writes
|
||||
* @param message - The message to log
|
||||
* @author Matthew Raymer
|
||||
*/
|
||||
export async function logToDb(message: string): Promise<void> {
|
||||
//const platform = PlatformServiceFactory.getInstance();
|
||||
const todayKey = new Date().toDateString();
|
||||
//const nowKey = new Date().toISOString();
|
||||
|
||||
try {
|
||||
// Try to insert first, if it fails due to UNIQUE constraint, update instead
|
||||
// await platform.dbExec("INSERT INTO logs (date, message) VALUES (?, ?)", [
|
||||
// nowKey,
|
||||
// message,
|
||||
// ]);
|
||||
|
||||
// Clean up old logs (keep only last 7 days) - do this less frequently
|
||||
// Only clean up if the date is different from the last cleanup
|
||||
if (!lastCleanupDate || lastCleanupDate !== todayKey) {
|
||||
// const sevenDaysAgo = new Date(
|
||||
// new Date().getTime() - 7 * 24 * 60 * 60 * 1000,
|
||||
// );
|
||||
// await platform.dbExec("DELETE FROM logs WHERE date < ?", [
|
||||
// sevenDaysAgo.toDateString(),
|
||||
// ]);
|
||||
lastCleanupDate = todayKey;
|
||||
}
|
||||
} catch (error) {
|
||||
// Log to console as fallback
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(
|
||||
"Error logging to database:",
|
||||
error,
|
||||
" ... for original message:",
|
||||
message,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// similar method is in the sw_scripts/additional-scripts.js file
|
||||
export async function logConsoleAndDb(
|
||||
message: string,
|
||||
isError = false,
|
||||
): Promise<void> {
|
||||
if (isError) {
|
||||
logger.error(`${new Date().toISOString()} ${message}`);
|
||||
} else {
|
||||
logger.log(`${new Date().toISOString()} ${message}`);
|
||||
}
|
||||
await logToDb(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an SQL INSERT statement and parameters from a model object.
|
||||
* @param model The model object containing fields to update
|
||||
* @param tableName The name of the table to update
|
||||
* @returns Object containing the SQL statement and parameters array
|
||||
*/
|
||||
export function generateInsertStatement(
|
||||
model: Record<string, unknown>,
|
||||
tableName: string,
|
||||
): { sql: string; params: unknown[] } {
|
||||
const columns = Object.keys(model).filter((key) => model[key] !== undefined);
|
||||
const values = Object.values(model).filter((value) => value !== undefined);
|
||||
const placeholders = values.map(() => "?").join(", ");
|
||||
const insertSql = `INSERT INTO ${tableName} (${columns.join(", ")}) VALUES (${placeholders})`;
|
||||
return {
|
||||
sql: insertSql,
|
||||
params: values,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an SQL UPDATE statement and parameters from a model object.
|
||||
* @param model The model object containing fields to update
|
||||
* @param tableName The name of the table to update
|
||||
* @param whereClause The WHERE clause for the update (e.g. "id = ?")
|
||||
* @param whereParams Parameters for the WHERE clause
|
||||
* @returns Object containing the SQL statement and parameters array
|
||||
*/
|
||||
export function generateUpdateStatement(
|
||||
model: Record<string, unknown>,
|
||||
tableName: string,
|
||||
whereClause: string,
|
||||
whereParams: unknown[] = [],
|
||||
): { sql: string; params: unknown[] } {
|
||||
// Filter out undefined/null values and create SET clause
|
||||
const setClauses: string[] = [];
|
||||
const params: unknown[] = [];
|
||||
|
||||
Object.entries(model).forEach(([key, value]) => {
|
||||
if (value !== undefined) {
|
||||
setClauses.push(`${key} = ?`);
|
||||
params.push(value);
|
||||
}
|
||||
});
|
||||
|
||||
if (setClauses.length === 0) {
|
||||
throw new Error("No valid fields to update");
|
||||
}
|
||||
|
||||
const sql = `UPDATE ${tableName} SET ${setClauses.join(", ")} WHERE ${whereClause}`;
|
||||
|
||||
return {
|
||||
sql,
|
||||
params: [...params, ...whereParams],
|
||||
};
|
||||
}
|
||||
|
||||
export function mapQueryResultToValues(
|
||||
record: QueryExecResult | undefined,
|
||||
): Array<Record<string, unknown>> {
|
||||
if (!record) {
|
||||
return [];
|
||||
}
|
||||
return mapColumnsToValues(record.columns, record.values) as Array<
|
||||
Record<string, unknown>
|
||||
>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps an array of column names to an array of value arrays, creating objects where each column name
|
||||
* is mapped to its corresponding value.
|
||||
* @param columns Array of column names to use as object keys
|
||||
* @param values Array of value arrays, where each inner array corresponds to one row of data
|
||||
* @returns Array of objects where each object maps column names to their corresponding values
|
||||
*/
|
||||
export function mapColumnsToValues(
|
||||
columns: string[],
|
||||
values: unknown[][],
|
||||
): Array<Record<string, unknown>> {
|
||||
return values.map((row) => {
|
||||
const obj: Record<string, unknown> = {};
|
||||
columns.forEach((column, index) => {
|
||||
obj[column] = row[index];
|
||||
});
|
||||
return obj;
|
||||
});
|
||||
}
|
||||
@@ -1,9 +1,3 @@
|
||||
/**
|
||||
* This is the original IndexedDB version of the database.
|
||||
* It will eventually be replaced fully by the SQL version in databaseUtil.ts.
|
||||
* Turn this on or off with the USE_DEXIE_DB constant in constants/app.ts.
|
||||
*/
|
||||
|
||||
import BaseDexie, { Table } from "dexie";
|
||||
import { encrypted, Encryption } from "@pvermeer/dexie-encrypted-addon";
|
||||
import * as R from "ramda";
|
||||
@@ -32,8 +26,8 @@ type NonsensitiveTables = {
|
||||
};
|
||||
|
||||
// Using 'unknown' instead of 'any' for stricter typing and to avoid TypeScript warnings
|
||||
type SecretDexie<T extends unknown = SecretTable> = BaseDexie & T;
|
||||
type SensitiveDexie<T extends unknown = SensitiveTables> = BaseDexie & T;
|
||||
export type SecretDexie<T extends unknown = SecretTable> = BaseDexie & T;
|
||||
export type SensitiveDexie<T extends unknown = SensitiveTables> = BaseDexie & T;
|
||||
export type NonsensitiveDexie<T extends unknown = NonsensitiveTables> =
|
||||
BaseDexie & T;
|
||||
|
||||
|
||||
@@ -45,12 +45,6 @@ export type Account = {
|
||||
publicKeyHex: string;
|
||||
};
|
||||
|
||||
// When finished with USE_DEXIE_DB, move these fields to Account and move identity and mnemonic here.
|
||||
export type AccountEncrypted = Account & {
|
||||
identityEncrBase64: string;
|
||||
mnemonicEncrBase64: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Schema for the accounts table in the database.
|
||||
* Fields starting with a $ character are encrypted.
|
||||
|
||||
174
src/electron/main.js
Normal file
174
src/electron/main.js
Normal file
@@ -0,0 +1,174 @@
|
||||
const { app, BrowserWindow } = require("electron");
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
const logger = require("../utils/logger");
|
||||
|
||||
// Check if running in dev mode
|
||||
const isDev = process.argv.includes("--inspect");
|
||||
|
||||
function createWindow() {
|
||||
// Add before createWindow function
|
||||
const preloadPath = path.join(__dirname, "preload.js");
|
||||
logger.log("Checking preload path:", preloadPath);
|
||||
logger.log("Preload exists:", fs.existsSync(preloadPath));
|
||||
|
||||
// Create the browser window.
|
||||
const mainWindow = new BrowserWindow({
|
||||
width: 1200,
|
||||
height: 800,
|
||||
webPreferences: {
|
||||
nodeIntegration: false,
|
||||
contextIsolation: true,
|
||||
webSecurity: true,
|
||||
allowRunningInsecureContent: false,
|
||||
preload: path.join(__dirname, "preload.js"),
|
||||
},
|
||||
});
|
||||
|
||||
// Always open DevTools for now
|
||||
mainWindow.webContents.openDevTools();
|
||||
|
||||
// Intercept requests to fix asset paths
|
||||
mainWindow.webContents.session.webRequest.onBeforeRequest(
|
||||
{
|
||||
urls: [
|
||||
"file://*/*/assets/*",
|
||||
"file://*/assets/*",
|
||||
"file:///assets/*", // Catch absolute paths
|
||||
"<all_urls>", // Catch all URLs as a fallback
|
||||
],
|
||||
},
|
||||
(details, callback) => {
|
||||
let url = details.url;
|
||||
|
||||
// Handle paths that don't start with file://
|
||||
if (!url.startsWith("file://") && url.includes("/assets/")) {
|
||||
url = `file://${path.join(__dirname, "www", url)}`;
|
||||
}
|
||||
|
||||
// Handle absolute paths starting with /assets/
|
||||
if (url.includes("/assets/") && !url.includes("/www/assets/")) {
|
||||
const baseDir = url.includes("dist-electron")
|
||||
? url.substring(
|
||||
0,
|
||||
url.indexOf("/dist-electron") + "/dist-electron".length,
|
||||
)
|
||||
: `file://${__dirname}`;
|
||||
const assetPath = url.split("/assets/")[1];
|
||||
const newUrl = `${baseDir}/www/assets/${assetPath}`;
|
||||
callback({ redirectURL: newUrl });
|
||||
return;
|
||||
}
|
||||
|
||||
callback({}); // No redirect for other URLs
|
||||
},
|
||||
);
|
||||
|
||||
if (isDev) {
|
||||
// Debug info
|
||||
logger.log("Debug Info:");
|
||||
logger.log("Running in dev mode:", isDev);
|
||||
logger.log("App is packaged:", app.isPackaged);
|
||||
logger.log("Process resource path:", process.resourcesPath);
|
||||
logger.log("App path:", app.getAppPath());
|
||||
logger.log("__dirname:", __dirname);
|
||||
logger.log("process.cwd():", process.cwd());
|
||||
}
|
||||
|
||||
const indexPath = path.join(__dirname, "www", "index.html");
|
||||
|
||||
if (isDev) {
|
||||
logger.log("Loading index from:", indexPath);
|
||||
logger.log("www path:", path.join(__dirname, "www"));
|
||||
logger.log("www assets path:", path.join(__dirname, "www", "assets"));
|
||||
}
|
||||
|
||||
if (!fs.existsSync(indexPath)) {
|
||||
logger.error(`Index file not found at: ${indexPath}`);
|
||||
throw new Error("Index file not found");
|
||||
}
|
||||
|
||||
// Add CSP headers to allow API connections
|
||||
mainWindow.webContents.session.webRequest.onHeadersReceived(
|
||||
(details, callback) => {
|
||||
callback({
|
||||
responseHeaders: {
|
||||
...details.responseHeaders,
|
||||
"Content-Security-Policy": [
|
||||
"default-src 'self';" +
|
||||
"connect-src 'self' https://api.endorser.ch https://*.timesafari.app;" +
|
||||
"img-src 'self' data: https: blob:;" +
|
||||
"script-src 'self' 'unsafe-inline' 'unsafe-eval';" +
|
||||
"style-src 'self' 'unsafe-inline';" +
|
||||
"font-src 'self' data:;",
|
||||
],
|
||||
},
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
// Load the index.html
|
||||
mainWindow
|
||||
.loadFile(indexPath)
|
||||
.then(() => {
|
||||
logger.log("Successfully loaded index.html");
|
||||
if (isDev) {
|
||||
mainWindow.webContents.openDevTools();
|
||||
logger.log("DevTools opened - running in dev mode");
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
logger.error("Failed to load index.html:", err);
|
||||
logger.error("Attempted path:", indexPath);
|
||||
});
|
||||
|
||||
// Listen for console messages from the renderer
|
||||
mainWindow.webContents.on("console-message", (_event, level, message) => {
|
||||
logger.log("Renderer Console:", message);
|
||||
});
|
||||
|
||||
// Add right after creating the BrowserWindow
|
||||
mainWindow.webContents.on(
|
||||
"did-fail-load",
|
||||
(event, errorCode, errorDescription) => {
|
||||
logger.error("Page failed to load:", errorCode, errorDescription);
|
||||
},
|
||||
);
|
||||
|
||||
mainWindow.webContents.on("preload-error", (event, preloadPath, error) => {
|
||||
logger.error("Preload script error:", preloadPath, error);
|
||||
});
|
||||
|
||||
mainWindow.webContents.on(
|
||||
"console-message",
|
||||
(event, level, message, line, sourceId) => {
|
||||
logger.log("Renderer Console:", line, sourceId, message);
|
||||
},
|
||||
);
|
||||
|
||||
// Enable remote debugging when in dev mode
|
||||
if (isDev) {
|
||||
mainWindow.webContents.openDevTools();
|
||||
}
|
||||
}
|
||||
|
||||
// Handle app ready
|
||||
app.whenReady().then(createWindow);
|
||||
|
||||
// Handle all windows closed
|
||||
app.on("window-all-closed", () => {
|
||||
if (process.platform !== "darwin") {
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
|
||||
app.on("activate", () => {
|
||||
if (BrowserWindow.getAllWindows().length === 0) {
|
||||
createWindow();
|
||||
}
|
||||
});
|
||||
|
||||
// Handle any errors
|
||||
process.on("uncaughtException", (error) => {
|
||||
logger.error("Uncaught Exception:", error);
|
||||
});
|
||||
187
src/electron/main.ts
Normal file
187
src/electron/main.ts
Normal file
@@ -0,0 +1,187 @@
|
||||
import { app, BrowserWindow } from "electron";
|
||||
import path from "path";
|
||||
import fs from "fs";
|
||||
|
||||
// Simple logger implementation
|
||||
const logger = {
|
||||
// eslint-disable-next-line no-console
|
||||
log: (...args: unknown[]) => console.log(...args),
|
||||
// eslint-disable-next-line no-console
|
||||
error: (...args: unknown[]) => console.error(...args),
|
||||
// eslint-disable-next-line no-console
|
||||
info: (...args: unknown[]) => console.info(...args),
|
||||
// eslint-disable-next-line no-console
|
||||
warn: (...args: unknown[]) => console.warn(...args),
|
||||
// eslint-disable-next-line no-console
|
||||
debug: (...args: unknown[]) => console.debug(...args),
|
||||
};
|
||||
|
||||
// Check if running in dev mode
|
||||
const isDev = process.argv.includes("--inspect");
|
||||
|
||||
function createWindow(): void {
|
||||
// Add before createWindow function
|
||||
const preloadPath = path.join(__dirname, "preload.js");
|
||||
logger.log("Checking preload path:", preloadPath);
|
||||
logger.log("Preload exists:", fs.existsSync(preloadPath));
|
||||
|
||||
// Create the browser window.
|
||||
const mainWindow = new BrowserWindow({
|
||||
width: 1200,
|
||||
height: 800,
|
||||
webPreferences: {
|
||||
nodeIntegration: false,
|
||||
contextIsolation: true,
|
||||
webSecurity: true,
|
||||
allowRunningInsecureContent: false,
|
||||
preload: path.join(__dirname, "preload.js"),
|
||||
},
|
||||
});
|
||||
|
||||
// Always open DevTools for now
|
||||
mainWindow.webContents.openDevTools();
|
||||
|
||||
// Intercept requests to fix asset paths
|
||||
mainWindow.webContents.session.webRequest.onBeforeRequest(
|
||||
{
|
||||
urls: [
|
||||
"file://*/*/assets/*",
|
||||
"file://*/assets/*",
|
||||
"file:///assets/*", // Catch absolute paths
|
||||
"<all_urls>", // Catch all URLs as a fallback
|
||||
],
|
||||
},
|
||||
(details, callback) => {
|
||||
let url = details.url;
|
||||
|
||||
// Handle paths that don't start with file://
|
||||
if (!url.startsWith("file://") && url.includes("/assets/")) {
|
||||
url = `file://${path.join(__dirname, "www", url)}`;
|
||||
}
|
||||
|
||||
// Handle absolute paths starting with /assets/
|
||||
if (url.includes("/assets/") && !url.includes("/www/assets/")) {
|
||||
const baseDir = url.includes("dist-electron")
|
||||
? url.substring(
|
||||
0,
|
||||
url.indexOf("/dist-electron") + "/dist-electron".length,
|
||||
)
|
||||
: `file://${__dirname}`;
|
||||
const assetPath = url.split("/assets/")[1];
|
||||
const newUrl = `${baseDir}/www/assets/${assetPath}`;
|
||||
callback({ redirectURL: newUrl });
|
||||
return;
|
||||
}
|
||||
|
||||
callback({}); // No redirect for other URLs
|
||||
},
|
||||
);
|
||||
|
||||
if (isDev) {
|
||||
// Debug info
|
||||
logger.log("Debug Info:");
|
||||
logger.log("Running in dev mode:", isDev);
|
||||
logger.log("App is packaged:", app.isPackaged);
|
||||
logger.log("Process resource path:", process.resourcesPath);
|
||||
logger.log("App path:", app.getAppPath());
|
||||
logger.log("__dirname:", __dirname);
|
||||
logger.log("process.cwd():", process.cwd());
|
||||
}
|
||||
|
||||
const indexPath = path.join(__dirname, "www", "index.html");
|
||||
|
||||
if (isDev) {
|
||||
logger.log("Loading index from:", indexPath);
|
||||
logger.log("www path:", path.join(__dirname, "www"));
|
||||
logger.log("www assets path:", path.join(__dirname, "www", "assets"));
|
||||
}
|
||||
|
||||
if (!fs.existsSync(indexPath)) {
|
||||
logger.error(`Index file not found at: ${indexPath}`);
|
||||
throw new Error("Index file not found");
|
||||
}
|
||||
|
||||
// Add CSP headers to allow API connections
|
||||
mainWindow.webContents.session.webRequest.onHeadersReceived(
|
||||
(details, callback) => {
|
||||
callback({
|
||||
responseHeaders: {
|
||||
...details.responseHeaders,
|
||||
"Content-Security-Policy": [
|
||||
"default-src 'self';" +
|
||||
"connect-src 'self' https://api.endorser.ch https://*.timesafari.app;" +
|
||||
"img-src 'self' data: https: blob:;" +
|
||||
"script-src 'self' 'unsafe-inline' 'unsafe-eval';" +
|
||||
"style-src 'self' 'unsafe-inline';" +
|
||||
"font-src 'self' data:;",
|
||||
],
|
||||
},
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
// Load the index.html
|
||||
mainWindow
|
||||
.loadFile(indexPath)
|
||||
.then(() => {
|
||||
logger.log("Successfully loaded index.html");
|
||||
if (isDev) {
|
||||
mainWindow.webContents.openDevTools();
|
||||
logger.log("DevTools opened - running in dev mode");
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
logger.error("Failed to load index.html:", err);
|
||||
logger.error("Attempted path:", indexPath);
|
||||
});
|
||||
|
||||
// Listen for console messages from the renderer
|
||||
mainWindow.webContents.on("console-message", (_event, _level, message) => {
|
||||
logger.log("Renderer Console:", message);
|
||||
});
|
||||
|
||||
// Add right after creating the BrowserWindow
|
||||
mainWindow.webContents.on(
|
||||
"did-fail-load",
|
||||
(_event, errorCode, errorDescription) => {
|
||||
logger.error("Page failed to load:", errorCode, errorDescription);
|
||||
},
|
||||
);
|
||||
|
||||
mainWindow.webContents.on("preload-error", (_event, preloadPath, error) => {
|
||||
logger.error("Preload script error:", preloadPath, error);
|
||||
});
|
||||
|
||||
mainWindow.webContents.on(
|
||||
"console-message",
|
||||
(_event, _level, message, line, sourceId) => {
|
||||
logger.log("Renderer Console:", line, sourceId, message);
|
||||
},
|
||||
);
|
||||
|
||||
// Enable remote debugging when in dev mode
|
||||
if (isDev) {
|
||||
mainWindow.webContents.openDevTools();
|
||||
}
|
||||
}
|
||||
|
||||
// Handle app ready
|
||||
app.whenReady().then(createWindow);
|
||||
|
||||
// Handle all windows closed
|
||||
app.on("window-all-closed", () => {
|
||||
if (process.platform !== "darwin") {
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
|
||||
app.on("activate", () => {
|
||||
if (BrowserWindow.getAllWindows().length === 0) {
|
||||
createWindow();
|
||||
}
|
||||
});
|
||||
|
||||
// Handle any errors
|
||||
process.on("uncaughtException", (error) => {
|
||||
logger.error("Uncaught Exception:", error);
|
||||
});
|
||||
78
src/electron/preload.js
Normal file
78
src/electron/preload.js
Normal file
@@ -0,0 +1,78 @@
|
||||
const { contextBridge, ipcRenderer } = require("electron");
|
||||
|
||||
const logger = {
|
||||
log: (message, ...args) => {
|
||||
if (process.env.NODE_ENV !== "production") {
|
||||
/* eslint-disable no-console */
|
||||
console.log(message, ...args);
|
||||
/* eslint-enable no-console */
|
||||
}
|
||||
},
|
||||
warn: (message, ...args) => {
|
||||
if (process.env.NODE_ENV !== "production") {
|
||||
/* eslint-disable no-console */
|
||||
console.warn(message, ...args);
|
||||
/* eslint-enable no-console */
|
||||
}
|
||||
},
|
||||
error: (message, ...args) => {
|
||||
/* eslint-disable no-console */
|
||||
console.error(message, ...args); // Errors should always be logged
|
||||
/* eslint-enable no-console */
|
||||
},
|
||||
};
|
||||
|
||||
// Use a more direct path resolution approach
|
||||
const getPath = (pathType) => {
|
||||
switch (pathType) {
|
||||
case "userData":
|
||||
return (
|
||||
process.env.APPDATA ||
|
||||
(process.platform === "darwin"
|
||||
? `${process.env.HOME}/Library/Application Support`
|
||||
: `${process.env.HOME}/.local/share`)
|
||||
);
|
||||
case "home":
|
||||
return process.env.HOME;
|
||||
case "appPath":
|
||||
return process.resourcesPath;
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
};
|
||||
|
||||
logger.log("Preload script starting...");
|
||||
|
||||
try {
|
||||
contextBridge.exposeInMainWorld("electronAPI", {
|
||||
// Path utilities
|
||||
getPath,
|
||||
|
||||
// IPC functions
|
||||
send: (channel, data) => {
|
||||
const validChannels = ["toMain"];
|
||||
if (validChannels.includes(channel)) {
|
||||
ipcRenderer.send(channel, data);
|
||||
}
|
||||
},
|
||||
receive: (channel, func) => {
|
||||
const validChannels = ["fromMain"];
|
||||
if (validChannels.includes(channel)) {
|
||||
ipcRenderer.on(channel, (event, ...args) => func(...args));
|
||||
}
|
||||
},
|
||||
// Environment info
|
||||
env: {
|
||||
isElectron: true,
|
||||
isDev: process.env.NODE_ENV === "development",
|
||||
},
|
||||
// Path utilities
|
||||
getBasePath: () => {
|
||||
return process.env.NODE_ENV === "development" ? "/" : "./";
|
||||
},
|
||||
});
|
||||
|
||||
logger.log("Preload script completed successfully");
|
||||
} catch (error) {
|
||||
logger.error("Error in preload script:", error);
|
||||
}
|
||||
59
src/interfaces/absurd-sql.d.ts
vendored
59
src/interfaces/absurd-sql.d.ts
vendored
@@ -1,59 +0,0 @@
|
||||
import type { QueryExecResult, SqlValue } from "./database";
|
||||
|
||||
declare module "@jlongster/sql.js" {
|
||||
interface SQL {
|
||||
Database: new (path: string, options?: { filename: boolean }) => AbsurdSqlDatabase;
|
||||
FS: {
|
||||
mkdir: (path: string) => void;
|
||||
mount: (fs: any, options: any, path: string) => void;
|
||||
open: (path: string, flags: string) => any;
|
||||
close: (stream: any) => void;
|
||||
};
|
||||
register_for_idb: (fs: any) => void;
|
||||
}
|
||||
|
||||
interface AbsurdSqlDatabase {
|
||||
exec: (sql: string, params?: unknown[]) => Promise<QueryExecResult[]>;
|
||||
run: (
|
||||
sql: string,
|
||||
params?: unknown[],
|
||||
) => Promise<{ changes: number; lastId?: number }>;
|
||||
}
|
||||
|
||||
const initSqlJs: (options?: {
|
||||
locateFile?: (file: string) => string;
|
||||
}) => Promise<SQL>;
|
||||
|
||||
export default initSqlJs;
|
||||
}
|
||||
|
||||
declare module "absurd-sql" {
|
||||
import type { SQL } from "@jlongster/sql.js";
|
||||
|
||||
export class SQLiteFS {
|
||||
constructor(fs: any, backend: any);
|
||||
}
|
||||
}
|
||||
|
||||
declare module "absurd-sql/dist/indexeddb-backend" {
|
||||
export default class IndexedDBBackend {
|
||||
constructor();
|
||||
}
|
||||
}
|
||||
|
||||
declare module "absurd-sql/dist/indexeddb-main-thread" {
|
||||
export interface SQLiteOptions {
|
||||
filename?: string;
|
||||
autoLoad?: boolean;
|
||||
debug?: boolean;
|
||||
}
|
||||
|
||||
export interface SQLiteDatabase {
|
||||
exec: (sql: string, params?: unknown[]) => Promise<QueryExecResult[]>;
|
||||
close: () => Promise<void>;
|
||||
}
|
||||
|
||||
export function initSqlJs(options?: any): Promise<any>;
|
||||
export function createDatabase(options?: SQLiteOptions): Promise<SQLiteDatabase>;
|
||||
export function openDatabase(options?: SQLiteOptions): Promise<SQLiteDatabase>;
|
||||
}
|
||||
@@ -1,24 +1,15 @@
|
||||
/**
|
||||
* Types of Claims
|
||||
*
|
||||
* Note that these are for the claims that get signed.
|
||||
* Records that are the latest edited entities are in the records.ts file.
|
||||
*
|
||||
*/
|
||||
import { GenericVerifiableCredential } from "./common";
|
||||
|
||||
import { ClaimObject } from "./common";
|
||||
|
||||
export interface AgreeActionClaim extends ClaimObject {
|
||||
"@context": "https://schema.org";
|
||||
export interface AgreeVerifiableCredential {
|
||||
"@context": string;
|
||||
"@type": string;
|
||||
object: Record<string, unknown>;
|
||||
}
|
||||
|
||||
// Note that previous VCs may have additional fields.
|
||||
// https://endorser.ch/doc/html/transactions.html#id4
|
||||
export interface GiveActionClaim extends ClaimObject {
|
||||
// context is optional because it might be embedded in another claim, eg. an AgreeAction
|
||||
"@context"?: "https://schema.org";
|
||||
export interface GiveVerifiableCredential extends GenericVerifiableCredential {
|
||||
"@context"?: string;
|
||||
"@type": "GiveAction";
|
||||
agent?: { identifier: string };
|
||||
description?: string;
|
||||
@@ -26,25 +17,16 @@ export interface GiveActionClaim extends ClaimObject {
|
||||
identifier?: string;
|
||||
image?: string;
|
||||
object?: { amountOfThisGood: number; unitCode: string };
|
||||
provider?: ClaimObject;
|
||||
provider?: GenericVerifiableCredential;
|
||||
recipient?: { identifier: string };
|
||||
}
|
||||
|
||||
export interface JoinActionClaim extends ClaimObject {
|
||||
agent?: { identifier: string };
|
||||
event?: { organizer?: { name: string }; name?: string; startTime?: string };
|
||||
}
|
||||
|
||||
// Note that previous VCs may have additional fields.
|
||||
// https://endorser.ch/doc/html/transactions.html#id8
|
||||
export interface OfferClaim extends ClaimObject {
|
||||
"@context": "https://schema.org";
|
||||
export interface OfferVerifiableCredential extends GenericVerifiableCredential {
|
||||
"@context"?: string;
|
||||
"@type": "Offer";
|
||||
agent?: { identifier: string };
|
||||
description?: string;
|
||||
fulfills?: { "@type": string; identifier?: string; lastClaimId?: string }[];
|
||||
identifier?: string;
|
||||
image?: string;
|
||||
includesObject?: { amountOfThisGood: number; unitCode: string };
|
||||
itemOffered?: {
|
||||
description?: string;
|
||||
@@ -55,18 +37,14 @@ export interface OfferClaim extends ClaimObject {
|
||||
name?: string;
|
||||
};
|
||||
};
|
||||
offeredBy?: {
|
||||
type?: "Person";
|
||||
identifier: string;
|
||||
};
|
||||
provider?: ClaimObject;
|
||||
offeredBy?: { identifier: string };
|
||||
recipient?: { identifier: string };
|
||||
validThrough?: string;
|
||||
}
|
||||
|
||||
// Note that previous VCs may have additional fields.
|
||||
// https://endorser.ch/doc/html/transactions.html#id7
|
||||
export interface PlanActionClaim extends ClaimObject {
|
||||
export interface PlanVerifiableCredential extends GenericVerifiableCredential {
|
||||
"@context": "https://schema.org";
|
||||
"@type": "PlanAction";
|
||||
name: string;
|
||||
@@ -80,18 +58,11 @@ export interface PlanActionClaim extends ClaimObject {
|
||||
}
|
||||
|
||||
// AKA Registration & RegisterAction
|
||||
export interface RegisterActionClaim extends ClaimObject {
|
||||
"@context": "https://schema.org";
|
||||
export interface RegisterVerifiableCredential {
|
||||
"@context": string;
|
||||
"@type": "RegisterAction";
|
||||
agent: { identifier: string };
|
||||
identifier?: string;
|
||||
object?: string;
|
||||
object: string;
|
||||
participant?: { identifier: string };
|
||||
}
|
||||
|
||||
export interface TenureClaim extends ClaimObject {
|
||||
"@context": "https://endorser.ch";
|
||||
"@type": "Tenure";
|
||||
party?: { identifier: string };
|
||||
spatialUnit?: { geo?: { polygon?: string } };
|
||||
}
|
||||
|
||||
@@ -34,77 +34,3 @@ export interface ErrorResult extends ResultWithType {
|
||||
type: "error";
|
||||
error: InternalError;
|
||||
}
|
||||
|
||||
export interface KeyMeta {
|
||||
did: string;
|
||||
publicKeyHex: string;
|
||||
derivationPath?: string;
|
||||
passkeyCredIdHex?: string; // The Webauthn credential ID in hex, if this is from a passkey
|
||||
}
|
||||
|
||||
export interface KeyMetaMaybeWithPrivate extends KeyMeta {
|
||||
mnemonic?: string; // 12 or 24 words encoding the seed
|
||||
identity?: string; // Stringified IIdentifier object from Veramo
|
||||
}
|
||||
|
||||
export interface KeyMetaWithPrivate extends KeyMeta {
|
||||
mnemonic: string; // 12 or 24 words encoding the seed
|
||||
identity: string; // Stringified IIdentifier object from Veramo
|
||||
}
|
||||
|
||||
export interface QuantitativeValue extends GenericVerifiableCredential {
|
||||
"@type": "QuantitativeValue";
|
||||
"@context"?: string;
|
||||
amountOfThisGood: number;
|
||||
unitCode: string;
|
||||
}
|
||||
|
||||
export interface AxiosErrorResponse {
|
||||
message?: string;
|
||||
response?: {
|
||||
data?: {
|
||||
error?: {
|
||||
message?: string;
|
||||
};
|
||||
[key: string]: unknown;
|
||||
};
|
||||
status?: number;
|
||||
config?: unknown;
|
||||
};
|
||||
config?: unknown;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface UserInfo {
|
||||
did: string;
|
||||
name: string;
|
||||
publicEncKey: string;
|
||||
registered: boolean;
|
||||
profileImageUrl?: string;
|
||||
nextPublicEncKeyHash?: string;
|
||||
}
|
||||
|
||||
export interface CreateAndSubmitClaimResult {
|
||||
success: boolean;
|
||||
error?: string;
|
||||
handleId?: string;
|
||||
}
|
||||
|
||||
export interface Agent {
|
||||
identifier?: string;
|
||||
did?: string;
|
||||
}
|
||||
|
||||
export interface ClaimObject {
|
||||
"@type": string;
|
||||
"@context"?: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface VerifiableCredentialClaim {
|
||||
"@context"?: string;
|
||||
"@type": string;
|
||||
type: string[];
|
||||
credentialSubject: ClaimObject;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
@@ -1,38 +1,7 @@
|
||||
export type {
|
||||
// From common.ts
|
||||
GenericCredWrapper,
|
||||
GenericVerifiableCredential,
|
||||
KeyMeta,
|
||||
// Exclude types that are also exported from other files
|
||||
// GiveVerifiableCredential,
|
||||
// OfferVerifiableCredential,
|
||||
// RegisterVerifiableCredential,
|
||||
// PlanSummaryRecord,
|
||||
// UserInfo,
|
||||
} from "./common";
|
||||
|
||||
export type {
|
||||
// From claims.ts
|
||||
GiveActionClaim,
|
||||
OfferClaim,
|
||||
RegisterActionClaim,
|
||||
} from "./claims";
|
||||
|
||||
export type {
|
||||
// From claims-result.ts
|
||||
CreateAndSubmitClaimResult,
|
||||
} from "./claims-result";
|
||||
|
||||
export type {
|
||||
// From records.ts
|
||||
PlanSummaryRecord,
|
||||
GiveSummaryRecord,
|
||||
} from "./records";
|
||||
|
||||
export type {
|
||||
// From user.ts
|
||||
UserInfo,
|
||||
} from "./user";
|
||||
|
||||
export * from "./claims";
|
||||
export * from "./claims-result";
|
||||
export * from "./common";
|
||||
export * from "./limits";
|
||||
export * from "./records";
|
||||
export * from "./user";
|
||||
export * from "./deepLinks";
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { GiveActionClaim, OfferClaim } from "./claims";
|
||||
import { GiveVerifiableCredential, OfferVerifiableCredential } from "./claims";
|
||||
|
||||
// a summary record; the VC is found the fullClaim field
|
||||
export interface GiveSummaryRecord {
|
||||
[x: string]: PropertyKey | undefined | GiveActionClaim;
|
||||
[x: string]: PropertyKey | undefined | GiveVerifiableCredential;
|
||||
type?: string;
|
||||
agentDid: string;
|
||||
amount: number;
|
||||
amountConfirmed: number;
|
||||
description: string;
|
||||
fullClaim: GiveActionClaim;
|
||||
fullClaim: GiveVerifiableCredential;
|
||||
fulfillsHandleId: string;
|
||||
fulfillsPlanHandleId?: string;
|
||||
fulfillsType?: string;
|
||||
@@ -26,7 +26,7 @@ export interface OfferSummaryRecord {
|
||||
amount: number;
|
||||
amountGiven: number;
|
||||
amountGivenConfirmed: number;
|
||||
fullClaim: OfferClaim;
|
||||
fullClaim: OfferVerifiableCredential;
|
||||
fulfillsPlanHandleId: string;
|
||||
handleId: string;
|
||||
issuerDid: string;
|
||||
|
||||
@@ -159,7 +159,7 @@ export const nextDerivationPath = (origDerivPath: string) => {
|
||||
};
|
||||
|
||||
// Base64 encoding/decoding utilities for browser
|
||||
export function base64ToArrayBuffer(base64: string): Uint8Array {
|
||||
function base64ToArrayBuffer(base64: string): Uint8Array {
|
||||
const binaryString = atob(base64);
|
||||
const bytes = new Uint8Array(binaryString.length);
|
||||
for (let i = 0; i < binaryString.length; i++) {
|
||||
@@ -168,7 +168,7 @@ export function base64ToArrayBuffer(base64: string): Uint8Array {
|
||||
return bytes;
|
||||
}
|
||||
|
||||
export function arrayBufferToBase64(buffer: ArrayBuffer): string {
|
||||
function arrayBufferToBase64(buffer: ArrayBuffer): string {
|
||||
const binary = String.fromCharCode(...new Uint8Array(buffer));
|
||||
return btoa(binary);
|
||||
}
|
||||
@@ -178,7 +178,7 @@ const IV_LENGTH = 12;
|
||||
const KEY_LENGTH = 256;
|
||||
const ITERATIONS = 100000;
|
||||
|
||||
// Message encryption helper function, used for onboarding meeting messages
|
||||
// Encryption helper function
|
||||
export async function encryptMessage(message: string, password: string) {
|
||||
const encoder = new TextEncoder();
|
||||
const salt = crypto.getRandomValues(new Uint8Array(SALT_LENGTH));
|
||||
@@ -226,7 +226,7 @@ export async function encryptMessage(message: string, password: string) {
|
||||
return btoa(JSON.stringify(result));
|
||||
}
|
||||
|
||||
// Message decryption helper function, used for onboarding meeting messages
|
||||
// Decryption helper function
|
||||
export async function decryptMessage(encryptedJson: string, password: string) {
|
||||
const decoder = new TextDecoder();
|
||||
const { salt, iv, encrypted } = JSON.parse(atob(encryptedJson));
|
||||
@@ -273,7 +273,7 @@ export async function decryptMessage(encryptedJson: string, password: string) {
|
||||
}
|
||||
|
||||
// Test function to verify encryption/decryption
|
||||
export async function testMessageEncryptionDecryption() {
|
||||
export async function testEncryptionDecryption() {
|
||||
try {
|
||||
const testMessage = "Hello, this is a test message! 🚀";
|
||||
const testPassword = "myTestPassword123";
|
||||
@@ -299,111 +299,9 @@ export async function testMessageEncryptionDecryption() {
|
||||
logger.log("\nTesting with wrong password...");
|
||||
try {
|
||||
await decryptMessage(encrypted, "wrongPassword");
|
||||
logger.log("Incorrectly decrypted with wrong password ❌");
|
||||
logger.log("Should not reach here");
|
||||
} catch (error) {
|
||||
logger.log("Correctly failed to decrypt with wrong password ✅");
|
||||
}
|
||||
|
||||
return success;
|
||||
} catch (error) {
|
||||
logger.error("Test failed with error:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Simple encryption using Node's crypto, used for the initial encryption of the identity and mnemonic
|
||||
export async function simpleEncrypt(
|
||||
text: string,
|
||||
secret: ArrayBuffer,
|
||||
): Promise<ArrayBuffer> {
|
||||
const iv = crypto.getRandomValues(new Uint8Array(16));
|
||||
|
||||
// Derive a 256-bit key from the secret using SHA-256
|
||||
const keyData = await crypto.subtle.digest("SHA-256", secret);
|
||||
const key = await crypto.subtle.importKey(
|
||||
"raw",
|
||||
keyData,
|
||||
{ name: "AES-GCM" },
|
||||
false,
|
||||
["encrypt"],
|
||||
);
|
||||
|
||||
const encrypted = await crypto.subtle.encrypt(
|
||||
{ name: "AES-GCM", iv },
|
||||
key,
|
||||
new TextEncoder().encode(text),
|
||||
);
|
||||
|
||||
// Combine IV and encrypted data
|
||||
const result = new Uint8Array(iv.length + encrypted.byteLength);
|
||||
result.set(iv);
|
||||
result.set(new Uint8Array(encrypted), iv.length);
|
||||
|
||||
return result.buffer;
|
||||
}
|
||||
|
||||
// Simple decryption using Node's crypto, used for the default decryption of identity and mnemonic
|
||||
export async function simpleDecrypt(
|
||||
encryptedText: ArrayBuffer,
|
||||
secret: ArrayBuffer,
|
||||
): Promise<string> {
|
||||
const data = new Uint8Array(encryptedText);
|
||||
|
||||
// Extract IV and encrypted data
|
||||
const iv = data.slice(0, 16);
|
||||
const encrypted = data.slice(16);
|
||||
|
||||
// Derive the same 256-bit key from the secret using SHA-256
|
||||
const keyData = await crypto.subtle.digest("SHA-256", secret);
|
||||
const key = await crypto.subtle.importKey(
|
||||
"raw",
|
||||
keyData,
|
||||
{ name: "AES-GCM" },
|
||||
false,
|
||||
["decrypt"],
|
||||
);
|
||||
|
||||
const decrypted = await crypto.subtle.decrypt(
|
||||
{ name: "AES-GCM", iv },
|
||||
key,
|
||||
encrypted,
|
||||
);
|
||||
|
||||
return new TextDecoder().decode(decrypted);
|
||||
}
|
||||
|
||||
// Test function for simple encryption/decryption
|
||||
export async function testSimpleEncryptionDecryption() {
|
||||
try {
|
||||
const testMessage = "Hello, this is a test message! 🚀";
|
||||
const testSecret = crypto.getRandomValues(new Uint8Array(32));
|
||||
|
||||
logger.log("Original message:", testMessage);
|
||||
|
||||
// Test encryption
|
||||
logger.log("Encrypting...");
|
||||
const encrypted = await simpleEncrypt(testMessage, testSecret);
|
||||
const encryptedBase64 = arrayBufferToBase64(encrypted);
|
||||
logger.log("Encrypted result:", encryptedBase64);
|
||||
|
||||
// Test decryption
|
||||
logger.log("Decrypting...");
|
||||
const encryptedArrayBuffer = base64ToArrayBuffer(encryptedBase64);
|
||||
const decrypted = await simpleDecrypt(encryptedArrayBuffer, testSecret);
|
||||
logger.log("Decrypted result:", decrypted);
|
||||
|
||||
// Verify
|
||||
const success = testMessage === decrypted;
|
||||
logger.log("Test " + (success ? "PASSED ✅" : "FAILED ❌"));
|
||||
logger.log("Messages match:", success);
|
||||
|
||||
// Test with wrong secret
|
||||
logger.log("\nTesting with wrong secret...");
|
||||
try {
|
||||
await simpleDecrypt(encryptedArrayBuffer, new Uint8Array(32));
|
||||
logger.log("Incorrectly decrypted with wrong secret ❌");
|
||||
} catch (error) {
|
||||
logger.log("Correctly failed to decrypt with wrong secret ✅");
|
||||
logger.log("Correctly failed with wrong password ✅");
|
||||
}
|
||||
|
||||
return success;
|
||||
|
||||
@@ -17,12 +17,29 @@ import { didEthLocalResolver } from "./did-eth-local-resolver";
|
||||
import { PEER_DID_PREFIX, verifyPeerSignature } from "./didPeer";
|
||||
import { base64urlDecodeString, createDidPeerJwt } from "./passkeyDidPeer";
|
||||
import { urlBase64ToUint8Array } from "./util";
|
||||
import { KeyMeta, KeyMetaWithPrivate } from "../../../interfaces/common";
|
||||
|
||||
export const ETHR_DID_PREFIX = "did:ethr:";
|
||||
export const JWT_VERIFY_FAILED_CODE = "JWT_VERIFY_FAILED";
|
||||
export const UNSUPPORTED_DID_METHOD_CODE = "UNSUPPORTED_DID_METHOD";
|
||||
|
||||
/**
|
||||
* Meta info about a key
|
||||
*/
|
||||
export interface KeyMeta {
|
||||
/**
|
||||
* Decentralized ID for the key
|
||||
*/
|
||||
did: string;
|
||||
/**
|
||||
* Stringified IIDentifier object from Veramo
|
||||
*/
|
||||
identity?: string;
|
||||
/**
|
||||
* The Webauthn credential ID in hex, if this is from a passkey
|
||||
*/
|
||||
passkeyCredIdHex?: string;
|
||||
}
|
||||
|
||||
const ethLocalResolver = new Resolver({ ethr: didEthLocalResolver });
|
||||
|
||||
/**
|
||||
@@ -34,7 +51,7 @@ export function isFromPasskey(keyMeta?: KeyMeta): boolean {
|
||||
}
|
||||
|
||||
export async function createEndorserJwtForKey(
|
||||
account: KeyMetaWithPrivate,
|
||||
account: KeyMeta,
|
||||
payload: object,
|
||||
expiresIn?: number,
|
||||
) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Buffer } from "buffer/";
|
||||
import { JWTPayload } from "did-jwt";
|
||||
import { p256 } from "@noble/curves/p256";
|
||||
import { DIDResolutionResult } from "did-resolver";
|
||||
import { sha256 } from "ethereum-cryptography/sha256.js";
|
||||
import {
|
||||
startAuthentication,
|
||||
startRegistration,
|
||||
@@ -10,13 +11,12 @@ import {
|
||||
generateRegistrationOptions,
|
||||
verifyAuthenticationResponse,
|
||||
verifyRegistrationResponse,
|
||||
VerifyAuthenticationResponseOpts,
|
||||
} from "@simplewebauthn/server";
|
||||
import { VerifyAuthenticationResponseOpts } from "@simplewebauthn/server/esm/authentication/verifyAuthenticationResponse";
|
||||
import {
|
||||
Base64URLString,
|
||||
PublicKeyCredentialCreationOptionsJSON,
|
||||
PublicKeyCredentialRequestOptionsJSON,
|
||||
AuthenticatorAssertionResponse,
|
||||
} from "@simplewebauthn/types";
|
||||
|
||||
import { AppString } from "../../../constants/app";
|
||||
@@ -194,19 +194,16 @@ export class PeerSetup {
|
||||
},
|
||||
};
|
||||
|
||||
const credential = (await navigator.credentials.get(
|
||||
options,
|
||||
)) as PublicKeyCredential;
|
||||
const credential = await navigator.credentials.get(options);
|
||||
// console.log("nav credential get", credential);
|
||||
|
||||
const response = credential?.response as AuthenticatorAssertionResponse;
|
||||
this.authenticatorData = response?.authenticatorData;
|
||||
this.authenticatorData = credential?.response.authenticatorData;
|
||||
const authenticatorDataBase64Url = arrayBufferToBase64URLString(
|
||||
this.authenticatorData as ArrayBuffer,
|
||||
);
|
||||
|
||||
this.clientDataJsonBase64Url = arrayBufferToBase64URLString(
|
||||
response?.clientDataJSON,
|
||||
credential?.response.clientDataJSON,
|
||||
);
|
||||
|
||||
// Our custom type of JWANT means the signature is based on a concatenation of the two Webauthn properties
|
||||
@@ -231,7 +228,9 @@ export class PeerSetup {
|
||||
.replace(/\//g, "_")
|
||||
.replace(/=+$/, "");
|
||||
|
||||
const origSignature = Buffer.from(response?.signature).toString("base64");
|
||||
const origSignature = Buffer.from(credential?.response.signature).toString(
|
||||
"base64",
|
||||
);
|
||||
this.signature = origSignature
|
||||
.replace(/\+/g, "-")
|
||||
.replace(/\//g, "_")
|
||||
@@ -316,18 +315,24 @@ export async function createDidPeerJwt(
|
||||
// ... and this import:
|
||||
// import { p256 } from "@noble/curves/p256";
|
||||
export async function verifyJwtP256(
|
||||
credIdHex: string,
|
||||
issuerDid: string,
|
||||
authenticatorData: ArrayBuffer,
|
||||
challenge: Uint8Array,
|
||||
clientDataJsonBase64Url: Base64URLString,
|
||||
signature: Base64URLString,
|
||||
) {
|
||||
const authDataFromBase = Buffer.from(authenticatorData);
|
||||
const clientDataFromBase = Buffer.from(clientDataJsonBase64Url, "base64");
|
||||
const sigBuffer = Buffer.from(signature, "base64");
|
||||
const finalSigBuffer = unwrapEC2Signature(sigBuffer);
|
||||
const publicKeyBytes = peerDidToPublicKeyBytes(issuerDid);
|
||||
|
||||
// Use challenge in preimage construction
|
||||
const preimage = Buffer.concat([authDataFromBase, Buffer.from(challenge)]);
|
||||
// Hash the client data
|
||||
const hash = sha256(clientDataFromBase);
|
||||
|
||||
// Construct the preimage
|
||||
const preimage = Buffer.concat([authDataFromBase, hash]);
|
||||
|
||||
const isValid = p256.verify(
|
||||
finalSigBuffer,
|
||||
@@ -378,37 +383,122 @@ export async function verifyJwtSimplewebauthn(
|
||||
|
||||
// similar code is in endorser-ch util-crypto.ts verifyPeerSignature
|
||||
export async function verifyJwtWebCrypto(
|
||||
credId: Base64URLString,
|
||||
issuerDid: string,
|
||||
authenticatorData: ArrayBuffer,
|
||||
challenge: Uint8Array,
|
||||
clientDataJsonBase64Url: Base64URLString,
|
||||
signature: Base64URLString,
|
||||
) {
|
||||
const authDataFromBase = Buffer.from(authenticatorData);
|
||||
const clientDataFromBase = Buffer.from(clientDataJsonBase64Url, "base64");
|
||||
const sigBuffer = Buffer.from(signature, "base64");
|
||||
const finalSigBuffer = unwrapEC2Signature(sigBuffer);
|
||||
|
||||
// Use challenge in preimage construction
|
||||
const preimage = Buffer.concat([authDataFromBase, Buffer.from(challenge)]);
|
||||
// Hash the client data
|
||||
const hash = sha256(clientDataFromBase);
|
||||
|
||||
// Construct the preimage
|
||||
const preimage = Buffer.concat([authDataFromBase, hash]);
|
||||
return verifyPeerSignature(preimage, issuerDid, finalSigBuffer);
|
||||
}
|
||||
|
||||
// Remove unused functions:
|
||||
// - peerDidToDidDocument
|
||||
// - COSEtoPEM
|
||||
// - base64urlDecodeArrayBuffer
|
||||
// - base64urlEncodeArrayBuffer
|
||||
// - pemToCryptoKey
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
async function peerDidToDidDocument(did: string): Promise<DIDResolutionResult> {
|
||||
if (!did.startsWith("did:peer:0z")) {
|
||||
throw new Error(
|
||||
"This only verifies a peer DID, method 0, encoded base58btc.",
|
||||
);
|
||||
}
|
||||
// this is basically hard-coded from https://www.w3.org/TR/did-core/#example-various-verification-method-types
|
||||
// (another reference is the @aviarytech/did-peer resolver)
|
||||
|
||||
// Keep only the used functions:
|
||||
/**
|
||||
* Looks like JsonWebKey2020 isn't too difficult:
|
||||
* - change context security/suites link to jws-2020/v1
|
||||
* - change publicKeyMultibase to publicKeyJwk generated with cborToKeys
|
||||
* - change type to JsonWebKey2020
|
||||
*/
|
||||
|
||||
const id = did.split(":")[2];
|
||||
const multibase = id.slice(1);
|
||||
const encnumbasis = multibase.slice(1);
|
||||
const didDocument = {
|
||||
"@context": [
|
||||
"https://www.w3.org/ns/did/v1",
|
||||
"https://w3id.org/security/suites/secp256k1-2019/v1",
|
||||
],
|
||||
assertionMethod: [did + "#" + encnumbasis],
|
||||
authentication: [did + "#" + encnumbasis],
|
||||
capabilityDelegation: [did + "#" + encnumbasis],
|
||||
capabilityInvocation: [did + "#" + encnumbasis],
|
||||
id: did,
|
||||
keyAgreement: undefined,
|
||||
service: undefined,
|
||||
verificationMethod: [
|
||||
{
|
||||
controller: did,
|
||||
id: did + "#" + encnumbasis,
|
||||
publicKeyMultibase: multibase,
|
||||
type: "EcdsaSecp256k1VerificationKey2019",
|
||||
},
|
||||
],
|
||||
};
|
||||
return {
|
||||
didDocument,
|
||||
didDocumentMetadata: {},
|
||||
didResolutionMetadata: { contentType: "application/did+ld+json" },
|
||||
};
|
||||
}
|
||||
|
||||
// convert COSE public key to PEM format
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
function COSEtoPEM(cose: Buffer) {
|
||||
// const alg = cose.get(3); // Algorithm
|
||||
const x = cose[-2]; // x-coordinate
|
||||
const y = cose[-3]; // y-coordinate
|
||||
|
||||
// Ensure the coordinates are in the correct format
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-expect-error because it complains about the type of x and y
|
||||
const pubKeyBuffer = Buffer.concat([Buffer.from([0x04]), x, y]);
|
||||
|
||||
// Convert to PEM format
|
||||
const pem = `-----BEGIN PUBLIC KEY-----
|
||||
${pubKeyBuffer.toString("base64")}
|
||||
-----END PUBLIC KEY-----`;
|
||||
|
||||
return pem;
|
||||
}
|
||||
|
||||
// tried the base64url library but got an error using their Buffer
|
||||
export function base64urlDecodeString(input: string) {
|
||||
return atob(input.replace(/-/g, "+").replace(/_/g, "/"));
|
||||
}
|
||||
|
||||
// tried the base64url library but got an error using their Buffer
|
||||
export function base64urlEncodeString(input: string) {
|
||||
return btoa(input).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
function base64urlDecodeArrayBuffer(input: string) {
|
||||
input = input.replace(/-/g, "+").replace(/_/g, "/");
|
||||
const pad = input.length % 4 === 0 ? "" : "====".slice(input.length % 4);
|
||||
const str = atob(input + pad);
|
||||
const bytes = new Uint8Array(str.length);
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
bytes[i] = str.charCodeAt(i);
|
||||
}
|
||||
return bytes.buffer;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
function base64urlEncodeArrayBuffer(buffer: ArrayBuffer) {
|
||||
const str = String.fromCharCode(...new Uint8Array(buffer));
|
||||
return base64urlEncodeString(str);
|
||||
}
|
||||
|
||||
// from @simplewebauthn/browser
|
||||
function arrayBufferToBase64URLString(buffer: ArrayBuffer) {
|
||||
const bytes = new Uint8Array(buffer);
|
||||
@@ -433,3 +523,28 @@ function base64URLStringToArrayBuffer(base64URLString: string) {
|
||||
}
|
||||
return buffer;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
async function pemToCryptoKey(pem: string) {
|
||||
const binaryDerString = atob(
|
||||
pem
|
||||
.split("\n")
|
||||
.filter((x) => !x.includes("-----"))
|
||||
.join(""),
|
||||
);
|
||||
const binaryDer = new Uint8Array(binaryDerString.length);
|
||||
for (let i = 0; i < binaryDerString.length; i++) {
|
||||
binaryDer[i] = binaryDerString.charCodeAt(i);
|
||||
}
|
||||
// console.log("binaryDer", binaryDer.buffer);
|
||||
return await window.crypto.subtle.importKey(
|
||||
"spki",
|
||||
binaryDer.buffer,
|
||||
{
|
||||
name: "RSASSA-PKCS1-v1_5",
|
||||
hash: "SHA-256",
|
||||
},
|
||||
true,
|
||||
["verify"],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -26,42 +26,29 @@ import {
|
||||
DEFAULT_IMAGE_API_SERVER,
|
||||
NotificationIface,
|
||||
APP_SERVER,
|
||||
USE_DEXIE_DB,
|
||||
} from "../constants/app";
|
||||
import { Contact } from "../db/tables/contacts";
|
||||
import { accessToken, deriveAddress, nextDerivationPath } from "../libs/crypto";
|
||||
import { NonsensitiveDexie } from "../db/index";
|
||||
import { logConsoleAndDb } from "../db/databaseUtil";
|
||||
import { logConsoleAndDb, NonsensitiveDexie } from "../db/index";
|
||||
|
||||
import {
|
||||
retrieveAccountMetadata,
|
||||
retrieveFullyDecryptedAccount,
|
||||
getPasskeyExpirationSeconds,
|
||||
} from "../libs/util";
|
||||
import { createEndorserJwtForKey } from "../libs/crypto/vc";
|
||||
import {
|
||||
GiveActionClaim,
|
||||
JoinActionClaim,
|
||||
OfferClaim,
|
||||
PlanActionClaim,
|
||||
RegisterActionClaim,
|
||||
TenureClaim,
|
||||
} from "../interfaces/claims";
|
||||
import { createEndorserJwtForKey, KeyMeta } from "../libs/crypto/vc";
|
||||
|
||||
import {
|
||||
GenericCredWrapper,
|
||||
GiveVerifiableCredential,
|
||||
OfferVerifiableCredential,
|
||||
RegisterVerifiableCredential,
|
||||
GenericVerifiableCredential,
|
||||
AxiosErrorResponse,
|
||||
GenericCredWrapper,
|
||||
PlanSummaryRecord,
|
||||
UserInfo,
|
||||
CreateAndSubmitClaimResult,
|
||||
ClaimObject,
|
||||
VerifiableCredentialClaim,
|
||||
QuantitativeValue,
|
||||
KeyMetaWithPrivate,
|
||||
KeyMetaMaybeWithPrivate,
|
||||
} from "../interfaces/common";
|
||||
import { PlanSummaryRecord } from "../interfaces/records";
|
||||
} from "../interfaces";
|
||||
import { logger } from "../utils/logger";
|
||||
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
|
||||
|
||||
/**
|
||||
* Standard context for schema.org data
|
||||
@@ -113,10 +100,7 @@ export const ENDORSER_CH_HANDLE_PREFIX = "https://endorser.ch/entity/";
|
||||
|
||||
export const BLANK_GENERIC_SERVER_RECORD: GenericCredWrapper<GenericVerifiableCredential> =
|
||||
{
|
||||
claim: {
|
||||
"@context": SCHEMA_ORG_CONTEXT,
|
||||
"@type": "",
|
||||
},
|
||||
claim: { "@type": "" },
|
||||
handleId: "",
|
||||
id: "",
|
||||
issuedAt: "",
|
||||
@@ -196,21 +180,37 @@ export function isEmptyOrHiddenDid(did?: string): boolean {
|
||||
* };
|
||||
* testRecursivelyOnStrings(isHiddenDid, obj); // Returns: true
|
||||
*/
|
||||
const testRecursivelyOnStrings = (
|
||||
function testRecursivelyOnStrings(
|
||||
func: (arg0: unknown) => boolean,
|
||||
input: unknown,
|
||||
test: (s: string) => boolean,
|
||||
): boolean => {
|
||||
if (typeof input === "string") {
|
||||
return test(input);
|
||||
} else if (Array.isArray(input)) {
|
||||
return input.some((item) => testRecursivelyOnStrings(item, test));
|
||||
} else if (input && typeof input === "object") {
|
||||
return Object.values(input as Record<string, unknown>).some((value) =>
|
||||
testRecursivelyOnStrings(value, test),
|
||||
);
|
||||
): boolean {
|
||||
// Test direct string values
|
||||
if (Object.prototype.toString.call(input) === "[object String]") {
|
||||
return func(input);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
// Recursively test objects and arrays
|
||||
else if (input instanceof Object) {
|
||||
if (!Array.isArray(input)) {
|
||||
// Handle plain objects
|
||||
for (const key in input) {
|
||||
if (testRecursivelyOnStrings(func, input[key])) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Handle arrays
|
||||
for (const value of input) {
|
||||
if (testRecursivelyOnStrings(func, value)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
} else {
|
||||
// Non-string, non-object values can't contain strings
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export function containsHiddenDid(obj: any) {
|
||||
@@ -551,11 +551,7 @@ export async function setPlanInCache(
|
||||
* @returns {string|undefined} User-friendly message or undefined if none found
|
||||
*/
|
||||
export function serverMessageForUser(error: unknown): string | undefined {
|
||||
if (error && typeof error === "object" && "response" in error) {
|
||||
const err = error as AxiosErrorResponse;
|
||||
return err.response?.data?.error?.message;
|
||||
}
|
||||
return undefined;
|
||||
return error?.response?.data?.error?.message;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -577,27 +573,18 @@ export function errorStringForLog(error: unknown) {
|
||||
// --- property '_value' closes the circle
|
||||
}
|
||||
let fullError = "" + error + " - JSON: " + stringifiedError;
|
||||
|
||||
if (error && typeof error === "object" && "response" in error) {
|
||||
const err = error as AxiosErrorResponse;
|
||||
const errorResponseText = JSON.stringify(err.response);
|
||||
// for some reason, error.response is not included in stringify result (eg. for 400 errors on invite redemptions)
|
||||
if (!R.empty(errorResponseText) && !fullError.includes(errorResponseText)) {
|
||||
// add error.response stuff
|
||||
if (
|
||||
err.response?.config &&
|
||||
err.config &&
|
||||
R.equals(err.config, err.response.config)
|
||||
) {
|
||||
// but exclude "config" because it's already in there
|
||||
const newErrorResponseText = JSON.stringify(
|
||||
R.omit(["config"] as never[], err.response),
|
||||
);
|
||||
fullError +=
|
||||
" - .response w/o same config JSON: " + newErrorResponseText;
|
||||
} else {
|
||||
fullError += " - .response JSON: " + errorResponseText;
|
||||
}
|
||||
const errorResponseText = JSON.stringify(error.response);
|
||||
// for some reason, error.response is not included in stringify result (eg. for 400 errors on invite redemptions)
|
||||
if (!R.empty(errorResponseText) && !fullError.includes(errorResponseText)) {
|
||||
// add error.response stuff
|
||||
if (R.equals(error?.config, error?.response?.config)) {
|
||||
// but exclude "config" because it's already in there
|
||||
const newErrorResponseText = JSON.stringify(
|
||||
R.omit(["config"] as never[], error.response),
|
||||
);
|
||||
fullError += " - .response w/o same config JSON: " + newErrorResponseText;
|
||||
} else {
|
||||
fullError += " - .response JSON: " + errorResponseText;
|
||||
}
|
||||
}
|
||||
return fullError;
|
||||
@@ -655,7 +642,7 @@ export async function getNewOffersToUserProjects(
|
||||
* @param lastClaimId supplied when editing a previous claim
|
||||
*/
|
||||
export function hydrateGive(
|
||||
vcClaimOrig?: GiveActionClaim,
|
||||
vcClaimOrig?: GiveVerifiableCredential,
|
||||
fromDid?: string,
|
||||
toDid?: string,
|
||||
description?: string,
|
||||
@@ -663,12 +650,14 @@ export function hydrateGive(
|
||||
unitCode?: string,
|
||||
fulfillsProjectHandleId?: string,
|
||||
fulfillsOfferHandleId?: string,
|
||||
isTrade: boolean = false,
|
||||
isTrade: boolean = false, // remove, because this app is all for gifting
|
||||
imageUrl?: string,
|
||||
providerPlanHandleId?: string,
|
||||
lastClaimId?: string,
|
||||
): GiveActionClaim {
|
||||
const vcClaim: GiveActionClaim = vcClaimOrig
|
||||
): GiveVerifiableCredential {
|
||||
// Remember: replace values or erase if it's null
|
||||
|
||||
const vcClaim: GiveVerifiableCredential = vcClaimOrig
|
||||
? R.clone(vcClaimOrig)
|
||||
: {
|
||||
"@context": SCHEMA_ORG_CONTEXT,
|
||||
@@ -676,72 +665,55 @@ export function hydrateGive(
|
||||
};
|
||||
|
||||
if (lastClaimId) {
|
||||
// this is an edit
|
||||
vcClaim.lastClaimId = lastClaimId;
|
||||
delete vcClaim.identifier;
|
||||
}
|
||||
|
||||
if (fromDid) {
|
||||
vcClaim.agent = { identifier: fromDid };
|
||||
}
|
||||
if (toDid) {
|
||||
vcClaim.recipient = { identifier: toDid };
|
||||
}
|
||||
vcClaim.agent = fromDid ? { identifier: fromDid } : undefined;
|
||||
vcClaim.recipient = toDid ? { identifier: toDid } : undefined;
|
||||
vcClaim.description = description || undefined;
|
||||
vcClaim.object =
|
||||
amount && !isNaN(amount)
|
||||
? { amountOfThisGood: amount, unitCode: unitCode || "HUR" }
|
||||
: undefined;
|
||||
|
||||
if (amount && !isNaN(amount)) {
|
||||
const quantitativeValue: QuantitativeValue = {
|
||||
"@type": "QuantitativeValue",
|
||||
amountOfThisGood: amount,
|
||||
unitCode: unitCode || "HUR",
|
||||
};
|
||||
vcClaim.object = quantitativeValue;
|
||||
}
|
||||
|
||||
// Initialize fulfills array if not present
|
||||
// ensure fulfills is an array
|
||||
if (!Array.isArray(vcClaim.fulfills)) {
|
||||
vcClaim.fulfills = vcClaim.fulfills ? [vcClaim.fulfills] : [];
|
||||
}
|
||||
|
||||
// Filter and add fulfills elements
|
||||
// ... and replace or add each element, ending with Trade or Donate
|
||||
// I realize the following doesn't change any elements that are not PlanAction or Offer or Trade/Action.
|
||||
vcClaim.fulfills = vcClaim.fulfills.filter(
|
||||
(elem: { "@type": string }) => elem["@type"] !== "PlanAction",
|
||||
(elem) => elem["@type"] !== "PlanAction",
|
||||
);
|
||||
|
||||
if (fulfillsProjectHandleId) {
|
||||
vcClaim.fulfills.push({
|
||||
"@type": "PlanAction",
|
||||
identifier: fulfillsProjectHandleId,
|
||||
});
|
||||
}
|
||||
|
||||
vcClaim.fulfills = vcClaim.fulfills.filter(
|
||||
(elem: { "@type": string }) => elem["@type"] !== "Offer",
|
||||
(elem) => elem["@type"] !== "Offer",
|
||||
);
|
||||
|
||||
if (fulfillsOfferHandleId) {
|
||||
vcClaim.fulfills.push({
|
||||
"@type": "Offer",
|
||||
identifier: fulfillsOfferHandleId,
|
||||
});
|
||||
}
|
||||
|
||||
// do Trade/Donate last because current endorser.ch only looks at the first for plans & offers
|
||||
vcClaim.fulfills = vcClaim.fulfills.filter(
|
||||
(elem: { "@type": string }) =>
|
||||
(elem) =>
|
||||
elem["@type"] !== "DonateAction" && elem["@type"] !== "TradeAction",
|
||||
);
|
||||
|
||||
vcClaim.fulfills.push({
|
||||
"@type": isTrade ? "TradeAction" : "DonateAction",
|
||||
});
|
||||
vcClaim.fulfills.push({ "@type": isTrade ? "TradeAction" : "DonateAction" });
|
||||
|
||||
vcClaim.image = imageUrl || undefined;
|
||||
|
||||
if (providerPlanHandleId) {
|
||||
vcClaim.provider = {
|
||||
"@type": "PlanAction",
|
||||
identifier: providerPlanHandleId,
|
||||
};
|
||||
}
|
||||
vcClaim.provider = providerPlanHandleId
|
||||
? { "@type": "PlanAction", identifier: providerPlanHandleId }
|
||||
: undefined;
|
||||
|
||||
return vcClaim;
|
||||
}
|
||||
@@ -802,7 +774,7 @@ export async function createAndSubmitGive(
|
||||
export async function editAndSubmitGive(
|
||||
axios: Axios,
|
||||
apiServer: string,
|
||||
fullClaim: GenericCredWrapper<GiveActionClaim>,
|
||||
fullClaim: GenericCredWrapper<GiveVerifiableCredential>,
|
||||
issuerDid: string,
|
||||
fromDid?: string,
|
||||
toDid?: string,
|
||||
@@ -843,7 +815,7 @@ export async function editAndSubmitGive(
|
||||
* @param lastClaimId supplied when editing a previous claim
|
||||
*/
|
||||
export function hydrateOffer(
|
||||
vcClaimOrig?: OfferClaim,
|
||||
vcClaimOrig?: OfferVerifiableCredential,
|
||||
fromDid?: string,
|
||||
toDid?: string,
|
||||
itemDescription?: string,
|
||||
@@ -853,8 +825,10 @@ export function hydrateOffer(
|
||||
fulfillsProjectHandleId?: string,
|
||||
validThrough?: string,
|
||||
lastClaimId?: string,
|
||||
): OfferClaim {
|
||||
const vcClaim: OfferClaim = vcClaimOrig
|
||||
): OfferVerifiableCredential {
|
||||
// Remember: replace values or erase if it's null
|
||||
|
||||
const vcClaim: OfferVerifiableCredential = vcClaimOrig
|
||||
? R.clone(vcClaimOrig)
|
||||
: {
|
||||
"@context": SCHEMA_ORG_CONTEXT,
|
||||
@@ -867,20 +841,14 @@ export function hydrateOffer(
|
||||
delete vcClaim.identifier;
|
||||
}
|
||||
|
||||
if (fromDid) {
|
||||
vcClaim.offeredBy = { identifier: fromDid };
|
||||
}
|
||||
if (toDid) {
|
||||
vcClaim.recipient = { identifier: toDid };
|
||||
}
|
||||
vcClaim.offeredBy = fromDid ? { identifier: fromDid } : undefined;
|
||||
vcClaim.recipient = toDid ? { identifier: toDid } : undefined;
|
||||
vcClaim.description = conditionDescription || undefined;
|
||||
|
||||
if (amount && !isNaN(amount)) {
|
||||
vcClaim.includesObject = {
|
||||
amountOfThisGood: amount,
|
||||
unitCode: unitCode || "HUR",
|
||||
};
|
||||
}
|
||||
vcClaim.includesObject =
|
||||
amount && !isNaN(amount)
|
||||
? { amountOfThisGood: amount, unitCode: unitCode || "HUR" }
|
||||
: undefined;
|
||||
|
||||
if (itemDescription || fulfillsProjectHandleId) {
|
||||
vcClaim.itemOffered = vcClaim.itemOffered || {};
|
||||
@@ -892,7 +860,6 @@ export function hydrateOffer(
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
vcClaim.validThrough = validThrough || undefined;
|
||||
|
||||
return vcClaim;
|
||||
@@ -932,7 +899,7 @@ export async function createAndSubmitOffer(
|
||||
undefined,
|
||||
);
|
||||
return createAndSubmitClaim(
|
||||
vcClaim as OfferClaim,
|
||||
vcClaim as OfferVerifiableCredential,
|
||||
issuerDid,
|
||||
apiServer,
|
||||
axios,
|
||||
@@ -942,7 +909,7 @@ export async function createAndSubmitOffer(
|
||||
export async function editAndSubmitOffer(
|
||||
axios: Axios,
|
||||
apiServer: string,
|
||||
fullClaim: GenericCredWrapper<OfferClaim>,
|
||||
fullClaim: GenericCredWrapper<OfferVerifiableCredential>,
|
||||
issuerDid: string,
|
||||
itemDescription: string,
|
||||
amount?: number,
|
||||
@@ -965,7 +932,7 @@ export async function editAndSubmitOffer(
|
||||
fullClaim.id,
|
||||
);
|
||||
return createAndSubmitClaim(
|
||||
vcClaim as OfferClaim,
|
||||
vcClaim as OfferVerifiableCredential,
|
||||
issuerDid,
|
||||
apiServer,
|
||||
axios,
|
||||
@@ -1001,12 +968,11 @@ export async function createAndSubmitClaim(
|
||||
axios: Axios,
|
||||
): Promise<CreateAndSubmitClaimResult> {
|
||||
try {
|
||||
const vcPayload: { vc: VerifiableCredentialClaim } = {
|
||||
const vcPayload = {
|
||||
vc: {
|
||||
"@context": "https://www.w3.org/2018/credentials/v1",
|
||||
"@type": "VerifiableCredential",
|
||||
"@context": ["https://www.w3.org/2018/credentials/v1"],
|
||||
type: ["VerifiableCredential"],
|
||||
credentialSubject: vcClaim as unknown as ClaimObject,
|
||||
credentialSubject: vcClaim,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1022,25 +988,26 @@ export async function createAndSubmitClaim(
|
||||
},
|
||||
});
|
||||
|
||||
return { success: true, handleId: response.data?.handleId };
|
||||
} catch (error: unknown) {
|
||||
return { type: "success", response };
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (error: any) {
|
||||
logger.error("Error submitting claim:", error);
|
||||
const errorMessage: string =
|
||||
serverMessageForUser(error) ||
|
||||
(error && typeof error === "object" && "message" in error
|
||||
? String(error.message)
|
||||
: undefined) ||
|
||||
error.message ||
|
||||
"Got some error submitting the claim. Check your permissions, network, and error logs.";
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: errorMessage,
|
||||
type: "error",
|
||||
error: {
|
||||
error: errorMessage,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function generateEndorserJwtUrlForAccount(
|
||||
account: KeyMetaMaybeWithPrivate,
|
||||
account: KeyMeta,
|
||||
isRegistered: boolean,
|
||||
givenName: string,
|
||||
profileImageUrl: string,
|
||||
@@ -1064,9 +1031,12 @@ export async function generateEndorserJwtUrlForAccount(
|
||||
}
|
||||
|
||||
// Add the next key -- not recommended for the QR code for such a high resolution
|
||||
if (isContact && account.derivationPath && account.mnemonic) {
|
||||
const newDerivPath = nextDerivationPath(account.derivationPath);
|
||||
const nextPublicHex = deriveAddress(account.mnemonic, newDerivPath)[2];
|
||||
if (isContact && account?.mnemonic && account?.derivationPath) {
|
||||
const newDerivPath = nextDerivationPath(account.derivationPath as string);
|
||||
const nextPublicHex = deriveAddress(
|
||||
account.mnemonic as string,
|
||||
newDerivPath,
|
||||
)[2];
|
||||
const nextPublicEncKey = Buffer.from(nextPublicHex, "hex");
|
||||
const nextPublicEncKeyHash = sha256(nextPublicEncKey);
|
||||
const nextPublicEncKeyHashBase64 =
|
||||
@@ -1086,11 +1056,7 @@ export async function createEndorserJwtForDid(
|
||||
expiresIn?: number,
|
||||
) {
|
||||
const account = await retrieveFullyDecryptedAccount(issuerDid);
|
||||
return createEndorserJwtForKey(
|
||||
account as KeyMetaWithPrivate,
|
||||
payload,
|
||||
expiresIn,
|
||||
);
|
||||
return createEndorserJwtForKey(account as KeyMeta, payload, expiresIn);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1138,21 +1104,21 @@ export const capitalizeAndInsertSpacesBeforeCaps = (text: string) => {
|
||||
|
||||
similar code is also contained in endorser-mobile
|
||||
**/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const claimSummary = (
|
||||
claim:
|
||||
| GenericVerifiableCredential
|
||||
| GenericCredWrapper<GenericVerifiableCredential>,
|
||||
claim: GenericCredWrapper<GenericVerifiableCredential>,
|
||||
) => {
|
||||
if (!claim) {
|
||||
// to differentiate from "something" above
|
||||
return "something";
|
||||
}
|
||||
let specificClaim: GenericVerifiableCredential;
|
||||
if ("claim" in claim) {
|
||||
// It's a GenericCredWrapper
|
||||
specificClaim = claim.claim as GenericVerifiableCredential;
|
||||
} else {
|
||||
// It's already a GenericVerifiableCredential
|
||||
specificClaim = claim;
|
||||
let specificClaim:
|
||||
| GenericVerifiableCredential
|
||||
| GenericCredWrapper<GenericVerifiableCredential> = claim;
|
||||
if (claim.claim) {
|
||||
// probably a Verified Credential
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
specificClaim = claim.claim;
|
||||
}
|
||||
if (Array.isArray(specificClaim)) {
|
||||
if (specificClaim.length === 1) {
|
||||
@@ -1187,112 +1153,88 @@ export const claimSpecialDescription = (
|
||||
identifiers: Array<string>,
|
||||
contacts: Array<Contact>,
|
||||
) => {
|
||||
let claim:
|
||||
| GenericVerifiableCredential
|
||||
| GenericCredWrapper<GenericVerifiableCredential> = record.claim;
|
||||
if ("claim" in claim) {
|
||||
// it's a nested GenericCredWrapper
|
||||
claim = claim.claim as GenericVerifiableCredential;
|
||||
let claim = record.claim;
|
||||
if (claim.claim) {
|
||||
// it's probably a Verified Credential
|
||||
claim = claim.claim;
|
||||
}
|
||||
|
||||
const issuer = didInfo(record.issuer, activeDid, identifiers, contacts);
|
||||
const type = claim["@type"] || "UnknownType";
|
||||
|
||||
if (type === "AgreeAction") {
|
||||
return (
|
||||
issuer +
|
||||
" agreed with " +
|
||||
claimSummary(claim.object as GenericVerifiableCredential)
|
||||
);
|
||||
return issuer + " agreed with " + claimSummary(claim.object);
|
||||
} else if (isAccept(claim)) {
|
||||
return (
|
||||
issuer +
|
||||
" accepted " +
|
||||
claimSummary(claim.object as GenericVerifiableCredential)
|
||||
);
|
||||
return issuer + " accepted " + claimSummary(claim.object);
|
||||
} else if (type === "GiveAction") {
|
||||
const giveClaim = claim as GiveActionClaim;
|
||||
// @ts-expect-error because .did may be found in legacy data, before March 2023
|
||||
const legacyGiverDid = giveClaim.agent?.did;
|
||||
const giver = giveClaim.agent?.identifier || legacyGiverDid;
|
||||
// agent.did is for legacy data, before March 2023
|
||||
const giver = claim.agent?.identifier || claim.agent?.did;
|
||||
const giverInfo = didInfo(giver, activeDid, identifiers, contacts);
|
||||
let gaveAmount = giveClaim.object?.amountOfThisGood
|
||||
? displayAmount(
|
||||
giveClaim.object.unitCode as string,
|
||||
giveClaim.object.amountOfThisGood as number,
|
||||
)
|
||||
let gaveAmount = claim.object?.amountOfThisGood
|
||||
? displayAmount(claim.object.unitCode, claim.object.amountOfThisGood)
|
||||
: "";
|
||||
if (giveClaim.description) {
|
||||
if (claim.description) {
|
||||
if (gaveAmount) {
|
||||
gaveAmount = gaveAmount + ", and also: ";
|
||||
}
|
||||
gaveAmount = gaveAmount + giveClaim.description;
|
||||
gaveAmount = gaveAmount + claim.description;
|
||||
}
|
||||
if (!gaveAmount) {
|
||||
gaveAmount = "something not described";
|
||||
}
|
||||
// @ts-expect-error because .did may be found in legacy data, before March 2023
|
||||
const legacyRecipDid = giveClaim.recipient?.did;
|
||||
const gaveRecipientId = giveClaim.recipient?.identifier || legacyRecipDid;
|
||||
// recipient.did is for legacy data, before March 2023
|
||||
const gaveRecipientId = claim.recipient?.identifier || claim.recipient?.did;
|
||||
const gaveRecipientInfo = gaveRecipientId
|
||||
? " to " + didInfo(gaveRecipientId, activeDid, identifiers, contacts)
|
||||
: "";
|
||||
return giverInfo + " gave" + gaveRecipientInfo + ": " + gaveAmount;
|
||||
} else if (type === "JoinAction") {
|
||||
const joinClaim = claim as JoinActionClaim;
|
||||
// @ts-expect-error because .did may be found in legacy data, before March 2023
|
||||
const legacyDid = joinClaim.agent?.did;
|
||||
const agent = joinClaim.agent?.identifier || legacyDid;
|
||||
// agent.did is for legacy data, before March 2023
|
||||
const agent = claim.agent?.identifier || claim.agent?.did;
|
||||
const contactInfo = didInfo(agent, activeDid, identifiers, contacts);
|
||||
|
||||
let eventOrganizer =
|
||||
joinClaim.event &&
|
||||
joinClaim.event.organizer &&
|
||||
joinClaim.event.organizer.name;
|
||||
claim.event && claim.event.organizer && claim.event.organizer.name;
|
||||
eventOrganizer = eventOrganizer || "";
|
||||
let eventName = joinClaim.event && joinClaim.event.name;
|
||||
let eventName = claim.event && claim.event.name;
|
||||
eventName = eventName ? " " + eventName : "";
|
||||
let fullEvent = eventOrganizer + eventName;
|
||||
fullEvent = fullEvent ? " attended the " + fullEvent : "";
|
||||
|
||||
let eventDate = joinClaim.event && joinClaim.event.startTime;
|
||||
let eventDate = claim.event && claim.event.startTime;
|
||||
eventDate = eventDate ? " at " + eventDate : "";
|
||||
return contactInfo + fullEvent + eventDate;
|
||||
} else if (isOffer(claim)) {
|
||||
const offerClaim = claim as OfferClaim;
|
||||
const offerer = offerClaim.offeredBy?.identifier;
|
||||
const offerer = claim.offeredBy?.identifier;
|
||||
const contactInfo = didInfo(offerer, activeDid, identifiers, contacts);
|
||||
let offering = "";
|
||||
if (offerClaim.includesObject) {
|
||||
if (claim.includesObject) {
|
||||
offering +=
|
||||
" " +
|
||||
displayAmount(
|
||||
offerClaim.includesObject.unitCode,
|
||||
offerClaim.includesObject.amountOfThisGood,
|
||||
claim.includesObject.unitCode,
|
||||
claim.includesObject.amountOfThisGood,
|
||||
);
|
||||
}
|
||||
if (offerClaim.itemOffered?.description) {
|
||||
offering += ", saying: " + offerClaim.itemOffered?.description;
|
||||
if (claim.itemOffered?.description) {
|
||||
offering += ", saying: " + claim.itemOffered?.description;
|
||||
}
|
||||
// @ts-expect-error because .did may be found in legacy data, before March 2023
|
||||
const legacyDid = offerClaim.recipient?.did;
|
||||
const offerRecipientId = offerClaim.recipient?.identifier || legacyDid;
|
||||
// recipient.did is for legacy data, before March 2023
|
||||
const offerRecipientId =
|
||||
claim.recipient?.identifier || claim.recipient?.did;
|
||||
const offerRecipientInfo = offerRecipientId
|
||||
? " to " + didInfo(offerRecipientId, activeDid, identifiers, contacts)
|
||||
: "";
|
||||
return contactInfo + " offered" + offering + offerRecipientInfo;
|
||||
} else if (type === "PlanAction") {
|
||||
const planClaim = claim as PlanActionClaim;
|
||||
const claimer = planClaim.agent?.identifier || record.issuer;
|
||||
const claimer = claim.agent?.identifier || record.issuer;
|
||||
const claimerInfo = didInfo(claimer, activeDid, identifiers, contacts);
|
||||
return claimerInfo + " announced a project: " + planClaim.name;
|
||||
return claimerInfo + " announced a project: " + claim.name;
|
||||
} else if (type === "Tenure") {
|
||||
const tenureClaim = claim as TenureClaim;
|
||||
// @ts-expect-error because .did may be found in legacy data, before March 2023
|
||||
const legacyDid = tenureClaim.party?.did;
|
||||
const claimer = tenureClaim.party?.identifier || legacyDid;
|
||||
// party.did is for legacy data, before March 2023
|
||||
const claimer = claim.party?.identifier || claim.party?.did;
|
||||
const contactInfo = didInfo(claimer, activeDid, identifiers, contacts);
|
||||
const polygon = tenureClaim.spatialUnit?.geo?.polygon || "";
|
||||
const polygon = claim.spatialUnit?.geo?.polygon || "";
|
||||
return (
|
||||
contactInfo +
|
||||
" possesses [" +
|
||||
@@ -1300,7 +1242,11 @@ export const claimSpecialDescription = (
|
||||
"...]"
|
||||
);
|
||||
} else {
|
||||
return issuer + " declared " + claimSummary(claim);
|
||||
return (
|
||||
issuer +
|
||||
" declared " +
|
||||
claimSummary(claim as GenericCredWrapper<GenericVerifiableCredential>)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1332,7 +1278,7 @@ export async function createEndorserJwtVcFromClaim(
|
||||
// Make a payload for the claim
|
||||
const vcPayload = {
|
||||
vc: {
|
||||
"@context": "https://www.w3.org/2018/credentials/v1",
|
||||
"@context": ["https://www.w3.org/2018/credentials/v1"],
|
||||
type: ["VerifiableCredential"],
|
||||
credentialSubject: claim,
|
||||
},
|
||||
@@ -1340,42 +1286,32 @@ export async function createEndorserJwtVcFromClaim(
|
||||
return createEndorserJwtForDid(issuerDid, vcPayload);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a JWT for a RegisterAction claim.
|
||||
*
|
||||
* @param activeDid - The DID of the user creating the invite
|
||||
* @param contact - The contact to register, with a 'did' field (all optional for invites)
|
||||
* @param identifier - The identifier for the invite, usually random
|
||||
* @param expiresIn - The number of seconds until the invite expires
|
||||
* @returns The JWT for the RegisterAction claim
|
||||
*/
|
||||
export async function createInviteJwt(
|
||||
activeDid: string,
|
||||
contact?: Contact,
|
||||
identifier?: string,
|
||||
expiresIn?: number, // in seconds
|
||||
inviteId?: string,
|
||||
expiresIn?: number,
|
||||
): Promise<string> {
|
||||
const vcClaim: RegisterActionClaim = {
|
||||
const vcClaim: RegisterVerifiableCredential = {
|
||||
"@context": SCHEMA_ORG_CONTEXT,
|
||||
"@type": "RegisterAction",
|
||||
agent: { identifier: activeDid },
|
||||
object: SERVICE_ID,
|
||||
identifier: identifier,
|
||||
};
|
||||
if (contact?.did) {
|
||||
if (contact) {
|
||||
vcClaim.participant = { identifier: contact.did };
|
||||
}
|
||||
|
||||
if (inviteId) {
|
||||
vcClaim.identifier = inviteId;
|
||||
}
|
||||
// Make a payload for the claim
|
||||
const vcPayload: { vc: VerifiableCredentialClaim } = {
|
||||
const vcPayload = {
|
||||
vc: {
|
||||
"@context": "https://www.w3.org/2018/credentials/v1",
|
||||
"@type": "VerifiableCredential",
|
||||
"@context": ["https://www.w3.org/2018/credentials/v1"],
|
||||
type: ["VerifiableCredential"],
|
||||
credentialSubject: vcClaim as unknown as ClaimObject,
|
||||
credentialSubject: vcClaim,
|
||||
},
|
||||
};
|
||||
|
||||
// Create a signature using private key of identity
|
||||
const vcJwt = await createEndorserJwtForDid(activeDid, vcPayload, expiresIn);
|
||||
return vcJwt;
|
||||
@@ -1387,44 +1323,21 @@ export async function register(
|
||||
axios: Axios,
|
||||
contact: Contact,
|
||||
): Promise<{ success?: boolean; error?: string }> {
|
||||
try {
|
||||
const vcJwt = await createInviteJwt(activeDid, contact);
|
||||
const url = apiServer + "/api/v2/claim";
|
||||
const resp = await axios.post<{
|
||||
success?: {
|
||||
handleId?: string;
|
||||
embeddedRecordError?: string;
|
||||
};
|
||||
error?: string;
|
||||
message?: string;
|
||||
}>(url, { jwtEncoded: vcJwt });
|
||||
const vcJwt = await createInviteJwt(activeDid, contact);
|
||||
|
||||
if (resp.data?.success?.handleId) {
|
||||
return { success: true };
|
||||
} else if (resp.data?.success?.embeddedRecordError) {
|
||||
let message =
|
||||
"There was some problem with the registration and so it may not be complete.";
|
||||
if (typeof resp.data.success.embeddedRecordError === "string") {
|
||||
message += " " + resp.data.success.embeddedRecordError;
|
||||
}
|
||||
return { error: message };
|
||||
} else {
|
||||
logger.error("Registration error:", JSON.stringify(resp.data));
|
||||
return { error: "Got a server error when registering." };
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
if (error && typeof error === "object") {
|
||||
const err = error as AxiosErrorResponse;
|
||||
const errorMessage =
|
||||
err.message ||
|
||||
(err.response?.data &&
|
||||
typeof err.response.data === "object" &&
|
||||
"message" in err.response.data
|
||||
? (err.response.data as { message: string }).message
|
||||
: undefined);
|
||||
logger.error("Registration error:", errorMessage || JSON.stringify(err));
|
||||
return { error: errorMessage || "Got a server error when registering." };
|
||||
const url = apiServer + "/api/v2/claim";
|
||||
const resp = await axios.post(url, { jwtEncoded: vcJwt });
|
||||
if (resp.data?.success?.handleId) {
|
||||
return { success: true };
|
||||
} else if (resp.data?.success?.embeddedRecordError) {
|
||||
let message =
|
||||
"There was some problem with the registration and so it may not be complete.";
|
||||
if (typeof resp.data.success.embeddedRecordError == "string") {
|
||||
message += " " + resp.data.success.embeddedRecordError;
|
||||
}
|
||||
return { error: message };
|
||||
} else {
|
||||
logger.error(resp);
|
||||
return { error: "Got a server error when registering." };
|
||||
}
|
||||
}
|
||||
@@ -1450,14 +1363,7 @@ export async function setVisibilityUtil(
|
||||
if (resp.status === 200) {
|
||||
const success = resp.data.success;
|
||||
if (success) {
|
||||
const platformService = PlatformServiceFactory.getInstance();
|
||||
await platformService.dbExec(
|
||||
"UPDATE contacts SET seesMe = ? WHERE did = ?",
|
||||
[visibility, contact.did],
|
||||
);
|
||||
if (USE_DEXIE_DB) {
|
||||
db.contacts.update(contact.did, { seesMe: visibility });
|
||||
}
|
||||
db.contacts.update(contact.did, { seesMe: visibility });
|
||||
}
|
||||
return { success };
|
||||
} else {
|
||||
|
||||
377
src/libs/util.ts
377
src/libs/util.ts
@@ -5,43 +5,25 @@ import { Buffer } from "buffer";
|
||||
import * as R from "ramda";
|
||||
import { useClipboard } from "@vueuse/core";
|
||||
|
||||
import {
|
||||
DEFAULT_PUSH_SERVER,
|
||||
NotificationIface,
|
||||
USE_DEXIE_DB,
|
||||
} from "../constants/app";
|
||||
import {
|
||||
accountsDBPromise,
|
||||
retrieveSettingsForActiveAccount,
|
||||
updateAccountSettings,
|
||||
updateDefaultSettings,
|
||||
} from "../db/index";
|
||||
import { Account, AccountEncrypted } from "../db/tables/accounts";
|
||||
import { DEFAULT_PUSH_SERVER, NotificationIface } from "../constants/app";
|
||||
import { retrieveSettingsForActiveAccount } from "../db/index";
|
||||
import { Account } from "../db/tables/accounts";
|
||||
import { Contact } from "../db/tables/contacts";
|
||||
import * as databaseUtil from "../db/databaseUtil";
|
||||
import { DEFAULT_PASSKEY_EXPIRATION_MINUTES } from "../db/tables/settings";
|
||||
import {
|
||||
arrayBufferToBase64,
|
||||
base64ToArrayBuffer,
|
||||
deriveAddress,
|
||||
generateSeed,
|
||||
newIdentifier,
|
||||
simpleDecrypt,
|
||||
simpleEncrypt,
|
||||
} from "../libs/crypto";
|
||||
import { deriveAddress, generateSeed, newIdentifier } from "../libs/crypto";
|
||||
import * as serverUtil from "../libs/endorserServer";
|
||||
import { containsHiddenDid } from "../libs/endorserServer";
|
||||
import {
|
||||
GenericCredWrapper,
|
||||
GenericVerifiableCredential,
|
||||
KeyMetaWithPrivate,
|
||||
} from "../interfaces/common";
|
||||
import { GiveSummaryRecord } from "../interfaces/records";
|
||||
import { OfferClaim } from "../interfaces/claims";
|
||||
GiveSummaryRecord,
|
||||
OfferVerifiableCredential,
|
||||
} from "../interfaces";
|
||||
import { containsHiddenDid } from "../libs/endorserServer";
|
||||
import { KeyMeta } from "../libs/crypto/vc";
|
||||
import { createPeerDid } from "../libs/crypto/vc/didPeer";
|
||||
import { registerCredential } from "../libs/crypto/vc/passkeyDidPeer";
|
||||
import { logger } from "../utils/logger";
|
||||
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
|
||||
import type { PlatformService } from "../services/PlatformService";
|
||||
|
||||
export interface GiverReceiverInputInfo {
|
||||
did?: string;
|
||||
@@ -378,19 +360,16 @@ export function base64ToBlob(base64DataUrl: string, sliceSize = 512) {
|
||||
* @param veriClaim is expected to have fields: claim and issuer
|
||||
*/
|
||||
export function offerGiverDid(
|
||||
veriClaim: GenericCredWrapper<OfferClaim>,
|
||||
veriClaim: GenericCredWrapper<OfferVerifiableCredential>,
|
||||
): string | undefined {
|
||||
const innerClaim = veriClaim.claim as OfferClaim;
|
||||
let giver: string | undefined = undefined;
|
||||
|
||||
giver = innerClaim.offeredBy?.identifier;
|
||||
if (giver && !serverUtil.isHiddenDid(giver)) {
|
||||
return giver;
|
||||
}
|
||||
|
||||
giver = veriClaim.issuer;
|
||||
if (giver && !serverUtil.isHiddenDid(giver)) {
|
||||
return giver;
|
||||
let giver;
|
||||
if (
|
||||
veriClaim.claim.offeredBy?.identifier &&
|
||||
!serverUtil.isHiddenDid(veriClaim.claim.offeredBy.identifier as string)
|
||||
) {
|
||||
giver = veriClaim.claim.offeredBy.identifier;
|
||||
} else if (veriClaim.issuer && !serverUtil.isHiddenDid(veriClaim.issuer)) {
|
||||
giver = veriClaim.issuer;
|
||||
}
|
||||
return giver;
|
||||
}
|
||||
@@ -404,7 +383,7 @@ export const canFulfillOffer = (
|
||||
) => {
|
||||
return (
|
||||
veriClaim.claimType === "Offer" &&
|
||||
!!offerGiverDid(veriClaim as GenericCredWrapper<OfferClaim>)
|
||||
!!offerGiverDid(veriClaim as GenericCredWrapper<OfferVerifiableCredential>)
|
||||
);
|
||||
};
|
||||
|
||||
@@ -474,258 +453,92 @@ export function findAllVisibleToDids(
|
||||
*
|
||||
**/
|
||||
|
||||
export type AccountKeyInfo = Account & KeyMetaWithPrivate;
|
||||
export interface AccountKeyInfo extends Account, KeyMeta {}
|
||||
|
||||
export const retrieveAccountCount = async (): Promise<number> => {
|
||||
let result = 0;
|
||||
const platformService = PlatformServiceFactory.getInstance();
|
||||
const dbResult = await platformService.dbQuery(
|
||||
`SELECT COUNT(*) FROM accounts`,
|
||||
);
|
||||
if (dbResult?.values?.[0]?.[0]) {
|
||||
result = dbResult.values[0][0] as number;
|
||||
}
|
||||
|
||||
if (USE_DEXIE_DB) {
|
||||
// one of the few times we use accountsDBPromise directly; try to avoid more usage
|
||||
const accountsDB = await accountsDBPromise;
|
||||
result = await accountsDB.accounts.count();
|
||||
}
|
||||
return result;
|
||||
export const retrieveAccountCount = async (
|
||||
platform: PlatformService,
|
||||
): Promise<number> => {
|
||||
const accounts = await platform.getAccounts();
|
||||
return accounts.length;
|
||||
};
|
||||
|
||||
export const retrieveAccountDids = async (): Promise<string[]> => {
|
||||
const platformService = PlatformServiceFactory.getInstance();
|
||||
const dbAccounts = await platformService.dbQuery(`SELECT did FROM accounts`);
|
||||
let allDids =
|
||||
databaseUtil
|
||||
.mapQueryResultToValues(dbAccounts)
|
||||
?.map((row) => row[0] as string) || [];
|
||||
if (USE_DEXIE_DB) {
|
||||
// this is the old way
|
||||
// one of the few times we use accountsDBPromise directly; try to avoid more usage
|
||||
const accountsDB = await accountsDBPromise;
|
||||
const allAccounts = await accountsDB.accounts.toArray();
|
||||
allDids = allAccounts.map((acc) => acc.did);
|
||||
}
|
||||
return allDids;
|
||||
export const retrieveAccountDids = async (
|
||||
platform: PlatformService,
|
||||
): Promise<string[]> => {
|
||||
const accounts = await platform.getAccounts();
|
||||
return accounts.map((acc: Account) => acc.did);
|
||||
};
|
||||
|
||||
/**
|
||||
* This is provided and recommended when the full key is not necessary so that
|
||||
* future work could separate this info from the sensitive key material.
|
||||
*
|
||||
* If you need the private key data, use retrieveFullyDecryptedAccount instead.
|
||||
*/
|
||||
export const retrieveAccountMetadata = async (
|
||||
platform: PlatformService,
|
||||
activeDid: string,
|
||||
): Promise<Account | undefined> => {
|
||||
let result: Account | undefined = undefined;
|
||||
const platformService = PlatformServiceFactory.getInstance();
|
||||
const dbAccount = await platformService.dbQuery(
|
||||
`SELECT * FROM accounts WHERE did = ?`,
|
||||
[activeDid],
|
||||
);
|
||||
const account = databaseUtil.mapQueryResultToValues(dbAccount)[0] as Account;
|
||||
): Promise<AccountKeyInfo | undefined> => {
|
||||
const account = await platform.getAccount(activeDid);
|
||||
if (account) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { identity, mnemonic, ...metadata } = account;
|
||||
result = metadata;
|
||||
} else {
|
||||
result = undefined;
|
||||
return metadata;
|
||||
}
|
||||
if (USE_DEXIE_DB) {
|
||||
// one of the few times we use accountsDBPromise directly; try to avoid more usage
|
||||
const accountsDB = await accountsDBPromise;
|
||||
const account = (await accountsDB.accounts
|
||||
.where("did")
|
||||
.equals(activeDid)
|
||||
.first()) as Account;
|
||||
if (account) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { identity, mnemonic, ...metadata } = account;
|
||||
result = metadata;
|
||||
} else {
|
||||
result = undefined;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
return undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* This contains sensitive data. If possible, use retrieveAccountMetadata instead.
|
||||
*
|
||||
* @param activeDid
|
||||
* @returns account info with private key data decrypted
|
||||
*/
|
||||
export const retrieveFullyDecryptedAccount = async (
|
||||
activeDid: string,
|
||||
): Promise<Account | undefined> => {
|
||||
let result: Account | undefined = undefined;
|
||||
const platformService = PlatformServiceFactory.getInstance();
|
||||
const dbSecrets = await platformService.dbQuery(
|
||||
`SELECT secretBase64 from secret`,
|
||||
);
|
||||
if (
|
||||
!dbSecrets ||
|
||||
dbSecrets.values.length === 0 ||
|
||||
dbSecrets.values[0].length === 0
|
||||
) {
|
||||
throw new Error(
|
||||
"No secret found. We recommend you clear your data and start over.",
|
||||
);
|
||||
}
|
||||
const secretBase64 = dbSecrets.values[0][0] as string;
|
||||
const secret = base64ToArrayBuffer(secretBase64);
|
||||
const dbAccount = await platformService.dbQuery(
|
||||
`SELECT * FROM accounts WHERE did = ?`,
|
||||
[activeDid],
|
||||
);
|
||||
if (
|
||||
!dbAccount ||
|
||||
dbAccount.values.length === 0 ||
|
||||
dbAccount.values[0].length === 0
|
||||
) {
|
||||
throw new Error("Account not found.");
|
||||
}
|
||||
const fullAccountData = databaseUtil.mapQueryResultToValues(
|
||||
dbAccount,
|
||||
)[0] as AccountEncrypted;
|
||||
const identityEncr = base64ToArrayBuffer(fullAccountData.identityEncrBase64);
|
||||
const mnemonicEncr = base64ToArrayBuffer(fullAccountData.mnemonicEncrBase64);
|
||||
fullAccountData.identity = await simpleDecrypt(identityEncr, secret);
|
||||
fullAccountData.mnemonic = await simpleDecrypt(mnemonicEncr, secret);
|
||||
result = fullAccountData;
|
||||
|
||||
if (USE_DEXIE_DB) {
|
||||
// one of the few times we use accountsDBPromise directly; try to avoid more usage
|
||||
const accountsDB = await accountsDBPromise;
|
||||
const account = (await accountsDB.accounts
|
||||
.where("did")
|
||||
.equals(activeDid)
|
||||
.first()) as Account;
|
||||
result = account;
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
export const retrieveAllAccountsMetadata = async (): Promise<Account[]> => {
|
||||
const platformService = PlatformServiceFactory.getInstance();
|
||||
const dbAccounts = await platformService.dbQuery(`SELECT * FROM accounts`);
|
||||
const accounts = databaseUtil.mapQueryResultToValues(dbAccounts) as Account[];
|
||||
let result = accounts.map((account) => {
|
||||
export const retrieveAllAccountsMetadata = async (
|
||||
platform: PlatformService,
|
||||
): Promise<Account[]> => {
|
||||
const accounts = await platform.getAccounts();
|
||||
return accounts.map((account: Account) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { identity, mnemonic, ...metadata } = account;
|
||||
return metadata as Account;
|
||||
return metadata;
|
||||
});
|
||||
if (USE_DEXIE_DB) {
|
||||
// one of the few times we use accountsDBPromise directly; try to avoid more usage
|
||||
const accountsDB = await accountsDBPromise;
|
||||
const array = await accountsDB.accounts.toArray();
|
||||
result = array.map((account) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { identity, mnemonic, ...metadata } = account;
|
||||
return metadata as Account;
|
||||
});
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* Saves a new identity to both SQL and Dexie databases
|
||||
*/
|
||||
export async function saveNewIdentity(
|
||||
identity: string,
|
||||
mnemonic: string,
|
||||
newId: { did: string; keys: Array<{ publicKeyHex: string }> },
|
||||
derivationPath: string,
|
||||
): Promise<void> {
|
||||
try {
|
||||
// add to the new sql db
|
||||
const platformService = PlatformServiceFactory.getInstance();
|
||||
const secrets = await platformService.dbQuery(
|
||||
`SELECT secretBase64 FROM secret`,
|
||||
);
|
||||
export const retrieveFullyDecryptedAccount = async (
|
||||
platform: PlatformService,
|
||||
activeDid: string,
|
||||
): Promise<AccountKeyInfo | undefined> => {
|
||||
return await platform.getAccount(activeDid);
|
||||
};
|
||||
|
||||
// If no secret exists, create one
|
||||
let secretBase64: string;
|
||||
if (!secrets?.values?.length || !secrets.values[0]?.length) {
|
||||
// Generate a new secret
|
||||
const randomBytes = crypto.getRandomValues(new Uint8Array(32));
|
||||
secretBase64 = arrayBufferToBase64(randomBytes);
|
||||
export const retrieveAllFullyDecryptedAccounts = async (
|
||||
platform: PlatformService,
|
||||
): Promise<Array<AccountKeyInfo>> => {
|
||||
return await platform.getAccounts();
|
||||
};
|
||||
|
||||
// Store the new secret
|
||||
await platformService.dbExec(
|
||||
`INSERT INTO secret (id, secretBase64) VALUES (1, ?)`,
|
||||
[secretBase64],
|
||||
);
|
||||
} else {
|
||||
secretBase64 = secrets.values[0][0] as string;
|
||||
}
|
||||
|
||||
const secret = base64ToArrayBuffer(secretBase64);
|
||||
const encryptedIdentity = await simpleEncrypt(identity, secret);
|
||||
const encryptedMnemonic = await simpleEncrypt(mnemonic, secret);
|
||||
const encryptedIdentityBase64 = arrayBufferToBase64(encryptedIdentity);
|
||||
const encryptedMnemonicBase64 = arrayBufferToBase64(encryptedMnemonic);
|
||||
|
||||
await platformService.dbExec(
|
||||
`INSERT INTO accounts (dateCreated, derivationPath, did, identityEncrBase64, mnemonicEncrBase64, publicKeyHex)
|
||||
VALUES (?, ?, ?, ?, ?, ?)`,
|
||||
[
|
||||
new Date().toISOString(),
|
||||
derivationPath,
|
||||
newId.did,
|
||||
encryptedIdentityBase64,
|
||||
encryptedMnemonicBase64,
|
||||
newId.keys[0].publicKeyHex,
|
||||
],
|
||||
);
|
||||
await databaseUtil.updateDefaultSettings({ activeDid: newId.did });
|
||||
|
||||
if (USE_DEXIE_DB) {
|
||||
// one of the few times we use accountsDBPromise directly; try to avoid more usage
|
||||
const accountsDB = await accountsDBPromise;
|
||||
await accountsDB.accounts.add({
|
||||
dateCreated: new Date().toISOString(),
|
||||
derivationPath: derivationPath,
|
||||
did: newId.did,
|
||||
identity: identity,
|
||||
mnemonic: mnemonic,
|
||||
publicKeyHex: newId.keys[0].publicKeyHex,
|
||||
});
|
||||
await updateDefaultSettings({ activeDid: newId.did });
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error("Failed to update default settings:", error);
|
||||
throw new Error(
|
||||
"Failed to set default settings. Please try again or restart the app.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a new identity, saves it to the database, and sets it as the active identity.
|
||||
* @return {Promise<string>} with the DID of the new identity
|
||||
*/
|
||||
export const generateSaveAndActivateIdentity = async (): Promise<string> => {
|
||||
export const generateSaveAndActivateIdentity = async (
|
||||
platform: PlatformService,
|
||||
): Promise<string> => {
|
||||
const mnemonic = generateSeed();
|
||||
// address is 0x... ETH address, without "did:eth:"
|
||||
const [address, privateHex, publicHex, derivationPath] =
|
||||
deriveAddress(mnemonic);
|
||||
|
||||
const newId = newIdentifier(address, publicHex, privateHex, derivationPath);
|
||||
const identity = JSON.stringify(newId);
|
||||
|
||||
await saveNewIdentity(identity, mnemonic, newId, derivationPath);
|
||||
await databaseUtil.updateAccountSettings(newId.did, { isRegistered: false });
|
||||
if (USE_DEXIE_DB) {
|
||||
await updateAccountSettings(newId.did, { isRegistered: false });
|
||||
try {
|
||||
await platform.addAccount({
|
||||
dateCreated: new Date().toISOString(),
|
||||
derivationPath: derivationPath,
|
||||
did: newId.did,
|
||||
identity: identity,
|
||||
mnemonic: mnemonic,
|
||||
publicKeyHex: newId.keys[0].publicKeyHex,
|
||||
});
|
||||
|
||||
await platform.updateMasterSettings({ activeDid: newId.did });
|
||||
await platform.updateAccountSettings(newId.did, { isRegistered: false });
|
||||
} catch (error) {
|
||||
logger.error("Failed to save new identity:", error);
|
||||
throw new Error(
|
||||
"Failed to save new identity. Please try again or restart the app.",
|
||||
);
|
||||
}
|
||||
return newId.did;
|
||||
};
|
||||
|
||||
export const registerAndSavePasskey = async (
|
||||
platform: PlatformService,
|
||||
keyName: string,
|
||||
): Promise<Account> => {
|
||||
const cred = await registerCredential(keyName);
|
||||
@@ -739,42 +552,25 @@ export const registerAndSavePasskey = async (
|
||||
passkeyCredIdHex,
|
||||
publicKeyHex: Buffer.from(publicKeyBytes).toString("hex"),
|
||||
};
|
||||
const insertStatement = databaseUtil.generateInsertStatement(
|
||||
account,
|
||||
"accounts",
|
||||
);
|
||||
await PlatformServiceFactory.getInstance().dbExec(
|
||||
insertStatement.sql,
|
||||
insertStatement.params,
|
||||
);
|
||||
if (USE_DEXIE_DB) {
|
||||
// one of the few times we use accountsDBPromise directly; try to avoid more usage
|
||||
const accountsDB = await accountsDBPromise;
|
||||
await accountsDB.accounts.add(account);
|
||||
}
|
||||
|
||||
await platform.addAccount(account);
|
||||
return account;
|
||||
};
|
||||
|
||||
export const registerSaveAndActivatePasskey = async (
|
||||
platform: PlatformService,
|
||||
keyName: string,
|
||||
): Promise<Account> => {
|
||||
const account = await registerAndSavePasskey(keyName);
|
||||
await databaseUtil.updateDefaultSettings({ activeDid: account.did });
|
||||
await databaseUtil.updateAccountSettings(account.did, {
|
||||
isRegistered: false,
|
||||
});
|
||||
if (USE_DEXIE_DB) {
|
||||
await updateDefaultSettings({ activeDid: account.did });
|
||||
await updateAccountSettings(account.did, { isRegistered: false });
|
||||
}
|
||||
const account = await registerAndSavePasskey(platform, keyName);
|
||||
await platform.updateMasterSettings({ activeDid: account.did });
|
||||
await platform.updateAccountSettings(account.did, { isRegistered: false });
|
||||
return account;
|
||||
};
|
||||
|
||||
export const getPasskeyExpirationSeconds = async (): Promise<number> => {
|
||||
let settings = await databaseUtil.retrieveSettingsForActiveAccount();
|
||||
if (USE_DEXIE_DB) {
|
||||
settings = await retrieveSettingsForActiveAccount();
|
||||
}
|
||||
export const getPasskeyExpirationSeconds = async (
|
||||
platform: PlatformService,
|
||||
): Promise<number> => {
|
||||
const settings = await platform.getActiveAccountSettings();
|
||||
return (
|
||||
(settings?.passkeyExpirationMinutes ?? DEFAULT_PASSKEY_EXPIRATION_MINUTES) *
|
||||
60
|
||||
@@ -790,10 +586,7 @@ export const sendTestThroughPushServer = async (
|
||||
subscriptionJSON: PushSubscriptionJSON,
|
||||
skipFilter: boolean,
|
||||
): Promise<AxiosResponse> => {
|
||||
let settings = await databaseUtil.retrieveSettingsForActiveAccount();
|
||||
if (USE_DEXIE_DB) {
|
||||
settings = await retrieveSettingsForActiveAccount();
|
||||
}
|
||||
const settings = await retrieveSettingsForActiveAccount();
|
||||
let pushUrl: string = DEFAULT_PUSH_SERVER as string;
|
||||
if (settings?.webPushServer) {
|
||||
pushUrl = settings.webPushServer;
|
||||
|
||||
@@ -34,7 +34,7 @@ import router from "./router";
|
||||
import { handleApiError } from "./services/api";
|
||||
import { AxiosError } from "axios";
|
||||
import { DeepLinkHandler } from "./services/deepLinks";
|
||||
import { logConsoleAndDb } from "./db/databaseUtil";
|
||||
import { logConsoleAndDb } from "./db";
|
||||
import { logger } from "./utils/logger";
|
||||
|
||||
logger.log("[Capacitor] Starting initialization");
|
||||
@@ -86,5 +86,19 @@ const handleDeepLink = async (data: { url: string }) => {
|
||||
App.addListener("appUrlOpen", handleDeepLink);
|
||||
|
||||
logger.log("[Capacitor] Mounting app");
|
||||
app.mount("#app");
|
||||
|
||||
// Initialize and mount the app
|
||||
initializeApp().then((app) => {
|
||||
app.mount("#app");
|
||||
}).catch((error) => {
|
||||
console.error("Failed to initialize app:", error);
|
||||
document.body.innerHTML = `
|
||||
<div style="color: red; padding: 20px; font-family: sans-serif;">
|
||||
<h1>Failed to initialize app</h1>
|
||||
<p>${error instanceof Error ? error.message : "Unknown error"}</p>
|
||||
<p>Please try restarting the app or contact support if the problem persists.</p>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
logger.log("[Capacitor] App mounted");
|
||||
|
||||
@@ -2,7 +2,6 @@ import { createPinia } from "pinia";
|
||||
import { App as VueApp, ComponentPublicInstance, createApp } from "vue";
|
||||
import App from "./App.vue";
|
||||
import router from "./router";
|
||||
// Use the browser version of axios for web builds
|
||||
import axios from "axios";
|
||||
import VueAxios from "vue-axios";
|
||||
import Notifications from "notiwind";
|
||||
@@ -10,12 +9,7 @@ import "./assets/styles/tailwind.css";
|
||||
import { FontAwesomeIcon } from "./libs/fontawesome";
|
||||
import Camera from "simple-vue-camera";
|
||||
import { logger } from "./utils/logger";
|
||||
|
||||
const platform = process.env.VITE_PLATFORM;
|
||||
const pwa_enabled = process.env.VITE_PWA_ENABLED === "true";
|
||||
|
||||
logger.log("Platform", { platform });
|
||||
logger.log("PWA enabled", { pwa_enabled });
|
||||
import { PlatformServiceFactory } from "./services/PlatformServiceFactory";
|
||||
|
||||
// Global Error Handler
|
||||
function setupGlobalErrorHandler(app: VueApp) {
|
||||
@@ -38,7 +32,7 @@ function setupGlobalErrorHandler(app: VueApp) {
|
||||
}
|
||||
|
||||
// Function to initialize the app
|
||||
export function initializeApp() {
|
||||
export async function initializeApp() {
|
||||
logger.log("[App Init] Starting app initialization");
|
||||
logger.log("[App Init] Platform:", process.env.VITE_PLATFORM);
|
||||
|
||||
@@ -61,6 +55,22 @@ export function initializeApp() {
|
||||
app.use(Notifications);
|
||||
logger.log("[App Init] Notifications initialized");
|
||||
|
||||
// Initialize platform service
|
||||
const platform = await PlatformServiceFactory.getInstance();
|
||||
app.config.globalProperties.$platform = platform;
|
||||
logger.log("[App Init] Platform service initialized");
|
||||
|
||||
// Initialize SQLite
|
||||
try {
|
||||
const sqlite = await platform.getSQLite();
|
||||
const config = { name: "TimeSafariDB", useWAL: true };
|
||||
await sqlite.initialize(config);
|
||||
logger.log("[App Init] SQLite database initialized");
|
||||
} catch (error) {
|
||||
logger.error("[App Init] Failed to initialize SQLite:", error);
|
||||
// Don't throw here - we want the app to start even if SQLite fails
|
||||
}
|
||||
|
||||
setupGlobalErrorHandler(app);
|
||||
logger.log("[App Init] App initialization complete");
|
||||
|
||||
|
||||
@@ -1,301 +1,15 @@
|
||||
import { initializeApp } from "./main.common";
|
||||
import { logger } from "./utils/logger";
|
||||
import { SQLiteQueryResult } from "./services/platforms/ElectronPlatformService";
|
||||
|
||||
const platform = process.env.VITE_PLATFORM;
|
||||
const pwa_enabled = process.env.VITE_PWA_ENABLED === "true";
|
||||
|
||||
logger.info("[Main Electron] Initializing app");
|
||||
logger.info("[Main Electron] Platform:", { platform });
|
||||
logger.info("[Main Electron] PWA enabled:", { pwa_enabled });
|
||||
|
||||
if (pwa_enabled) {
|
||||
logger.warn("[Main Electron] PWA is enabled, but not supported in electron");
|
||||
}
|
||||
|
||||
// Initialize app and SQLite
|
||||
const app = initializeApp();
|
||||
|
||||
// Create a promise that resolves when SQLite is ready
|
||||
const sqliteReady = new Promise<void>((resolve, reject) => {
|
||||
let retryCount = 0;
|
||||
let initializationTimeout: NodeJS.Timeout;
|
||||
|
||||
const attemptInitialization = () => {
|
||||
// Clear any existing timeout
|
||||
if (initializationTimeout) {
|
||||
clearTimeout(initializationTimeout);
|
||||
}
|
||||
|
||||
// Set timeout for this attempt
|
||||
initializationTimeout = setTimeout(() => {
|
||||
if (retryCount < 3) {
|
||||
// Use same retry count as ElectronPlatformService
|
||||
retryCount++;
|
||||
logger.warn(
|
||||
`[Main Electron] SQLite initialization attempt ${retryCount} timed out, retrying...`,
|
||||
);
|
||||
setTimeout(attemptInitialization, 1000); // Use same delay as ElectronPlatformService
|
||||
} else {
|
||||
logger.error(
|
||||
"[Main Electron] SQLite initialization failed after all retries",
|
||||
);
|
||||
reject(new Error("SQLite initialization timeout after all retries"));
|
||||
}
|
||||
}, 10000); // Use same timeout as ElectronPlatformService
|
||||
|
||||
// Wait for electron bridge to be available
|
||||
const checkElectronBridge = () => {
|
||||
if (!window.electron?.ipcRenderer) {
|
||||
// Check again in 100ms if bridge isn't ready
|
||||
setTimeout(checkElectronBridge, 100);
|
||||
return;
|
||||
}
|
||||
|
||||
// At this point we know ipcRenderer exists
|
||||
const ipcRenderer = window.electron.ipcRenderer;
|
||||
|
||||
logger.info("[Main Electron] [IPC:bridge] IPC renderer bridge available");
|
||||
|
||||
// Listen for SQLite ready signal
|
||||
logger.debug(
|
||||
"[Main Electron] [IPC:sqlite-ready] Registering listener for SQLite ready signal",
|
||||
);
|
||||
ipcRenderer.once("sqlite-ready", () => {
|
||||
clearTimeout(initializationTimeout);
|
||||
logger.info(
|
||||
"[Main Electron] [IPC:sqlite-ready] Received SQLite ready signal",
|
||||
);
|
||||
resolve();
|
||||
});
|
||||
|
||||
// Also listen for database errors
|
||||
logger.debug(
|
||||
"[Main Electron] [IPC:database-status] Registering listener for database status",
|
||||
);
|
||||
ipcRenderer.once("database-status", (...args: unknown[]) => {
|
||||
clearTimeout(initializationTimeout);
|
||||
const status = args[0] as { status: string; error?: string };
|
||||
if (status.status === "error") {
|
||||
logger.error(
|
||||
"[Main Electron] [IPC:database-status] Database error:",
|
||||
{
|
||||
error: status.error,
|
||||
channel: "database-status",
|
||||
},
|
||||
);
|
||||
reject(new Error(status.error || "Database initialization failed"));
|
||||
}
|
||||
});
|
||||
|
||||
// Check if SQLite is already available
|
||||
logger.debug(
|
||||
"[Main Electron] [IPC:sqlite-is-available] Checking SQLite availability",
|
||||
);
|
||||
ipcRenderer
|
||||
.invoke("sqlite-is-available")
|
||||
.then(async (result: unknown) => {
|
||||
const isAvailable = Boolean(result);
|
||||
if (isAvailable) {
|
||||
logger.info(
|
||||
"[Main Electron] [IPC:sqlite-is-available] SQLite is available",
|
||||
);
|
||||
|
||||
try {
|
||||
// First create a database connection
|
||||
logger.debug(
|
||||
"[Main Electron] [IPC:get-path] Requesting database path",
|
||||
);
|
||||
const dbPath = await ipcRenderer.invoke("get-path");
|
||||
logger.info(
|
||||
"[Main Electron] [IPC:get-path] Database path received:",
|
||||
{ dbPath },
|
||||
);
|
||||
|
||||
// Create the database connection
|
||||
logger.debug(
|
||||
"[Main Electron] [IPC:sqlite-create-connection] Creating database connection",
|
||||
);
|
||||
await ipcRenderer.invoke("sqlite-create-connection", {
|
||||
database: "timesafari",
|
||||
version: 1,
|
||||
});
|
||||
logger.info(
|
||||
"[Main Electron] [IPC:sqlite-create-connection] Database connection created",
|
||||
);
|
||||
|
||||
// Explicitly open the database
|
||||
logger.debug(
|
||||
"[Main Electron] [IPC:sqlite-open] Opening database",
|
||||
);
|
||||
await ipcRenderer.invoke("sqlite-open", {
|
||||
database: "timesafari",
|
||||
});
|
||||
logger.info(
|
||||
"[Main Electron] [IPC:sqlite-open] Database opened successfully",
|
||||
);
|
||||
|
||||
// Verify the database is open
|
||||
logger.debug(
|
||||
"[Main Electron] [IPC:sqlite-is-db-open] Verifying database is open",
|
||||
);
|
||||
const isOpen = await ipcRenderer.invoke("sqlite-is-db-open", {
|
||||
database: "timesafari",
|
||||
});
|
||||
logger.info(
|
||||
"[Main Electron] [IPC:sqlite-is-db-open] Database open status:",
|
||||
{ isOpen },
|
||||
);
|
||||
|
||||
if (!isOpen) {
|
||||
throw new Error("Database failed to open");
|
||||
}
|
||||
|
||||
// Now execute the test query
|
||||
logger.debug(
|
||||
"[Main Electron] [IPC:sqlite-query] Executing test query",
|
||||
);
|
||||
const testQuery = (await ipcRenderer.invoke("sqlite-query", {
|
||||
database: "timesafari",
|
||||
statement: "SELECT 1 as test;", // Safe test query
|
||||
})) as SQLiteQueryResult;
|
||||
logger.info(
|
||||
"[Main Electron] [IPC:sqlite-query] Test query successful:",
|
||||
{
|
||||
hasResults: Boolean(testQuery?.values),
|
||||
resultCount: testQuery?.values?.length,
|
||||
},
|
||||
);
|
||||
|
||||
// Signal that SQLite is ready - database stays open
|
||||
logger.debug(
|
||||
"[Main Electron] [IPC:sqlite-status] Sending SQLite ready status",
|
||||
);
|
||||
await ipcRenderer.invoke("sqlite-status", {
|
||||
status: "ready",
|
||||
database: "timesafari",
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
logger.info(
|
||||
"[Main Electron] SQLite ready status sent, database connection maintained",
|
||||
);
|
||||
|
||||
// Remove the close operations - database stays open for component use
|
||||
// Database will be closed during app shutdown
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
"[Main Electron] [IPC:*] SQLite test operation failed:",
|
||||
{
|
||||
error,
|
||||
lastOperation: "sqlite-test-query",
|
||||
database: "timesafari",
|
||||
},
|
||||
);
|
||||
|
||||
// Try to close everything if anything was opened
|
||||
try {
|
||||
logger.debug(
|
||||
"[Main Electron] [IPC:cleanup] Attempting database cleanup after error",
|
||||
);
|
||||
await ipcRenderer
|
||||
.invoke("sqlite-close", {
|
||||
database: "timesafari",
|
||||
})
|
||||
.catch((closeError) => {
|
||||
logger.warn(
|
||||
"[Main Electron] [IPC:sqlite-close] Failed to close database during cleanup:",
|
||||
closeError,
|
||||
);
|
||||
});
|
||||
|
||||
await ipcRenderer
|
||||
.invoke("sqlite-close-connection", {
|
||||
database: "timesafari",
|
||||
})
|
||||
.catch((closeError) => {
|
||||
logger.warn(
|
||||
"[Main Electron] [IPC:sqlite-close-connection] Failed to close connection during cleanup:",
|
||||
closeError,
|
||||
);
|
||||
});
|
||||
|
||||
logger.info(
|
||||
"[Main Electron] [IPC:cleanup] Database cleanup completed after error",
|
||||
);
|
||||
} catch (closeError) {
|
||||
logger.error(
|
||||
"[Main Electron] [IPC:cleanup] Failed to cleanup database:",
|
||||
{
|
||||
error: closeError,
|
||||
database: "timesafari",
|
||||
},
|
||||
);
|
||||
}
|
||||
// Don't reject here - we still want to wait for the ready signal
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((error: Error) => {
|
||||
logger.error(
|
||||
"[Main Electron] [IPC:sqlite-is-available] Failed to check SQLite availability:",
|
||||
{
|
||||
error,
|
||||
channel: "sqlite-is-available",
|
||||
},
|
||||
);
|
||||
// Don't reject here - wait for either ready signal or timeout
|
||||
});
|
||||
};
|
||||
|
||||
// Start checking for bridge
|
||||
checkElectronBridge();
|
||||
};
|
||||
|
||||
// Start first initialization attempt
|
||||
attemptInitialization();
|
||||
// Initialize and mount the app
|
||||
initializeApp().then((app) => {
|
||||
app.mount("#app");
|
||||
}).catch((error) => {
|
||||
console.error("Failed to initialize app:", error);
|
||||
document.body.innerHTML = `
|
||||
<div style="color: red; padding: 20px; font-family: sans-serif;">
|
||||
<h1>Failed to initialize app</h1>
|
||||
<p>${error instanceof Error ? error.message : "Unknown error"}</p>
|
||||
<p>Please try restarting the app or contact support if the problem persists.</p>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
// Wait for SQLite to be ready before initializing router and mounting app
|
||||
sqliteReady
|
||||
.then(async () => {
|
||||
logger.info("[Main Electron] SQLite ready, initializing router...");
|
||||
|
||||
// Initialize router after SQLite is ready
|
||||
const router = await import("./router").then((m) => m.default);
|
||||
app.use(router);
|
||||
logger.info("[Main Electron] Router initialized");
|
||||
|
||||
// Now mount the app
|
||||
logger.info("[Main Electron] Mounting app...");
|
||||
app.mount("#app");
|
||||
logger.info("[Main Electron] App mounted successfully");
|
||||
})
|
||||
.catch((error) => {
|
||||
logger.error(
|
||||
"[Main Electron] Failed to initialize SQLite:",
|
||||
error instanceof Error ? error.message : "Unknown error",
|
||||
);
|
||||
// Show error to user with retry option
|
||||
const errorDiv = document.createElement("div");
|
||||
errorDiv.style.cssText =
|
||||
"position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: #ffebee; color: #c62828; padding: 20px; border-radius: 4px; text-align: center; max-width: 80%; z-index: 9999;";
|
||||
errorDiv.innerHTML = `
|
||||
<h2>Failed to Initialize Application</h2>
|
||||
<p>There was an error initializing the database. This could be due to:</p>
|
||||
<ul style="text-align: left; margin: 10px 0;">
|
||||
<li>Database file is locked by another process</li>
|
||||
<li>Insufficient permissions to access the database</li>
|
||||
<li>Database file is corrupted</li>
|
||||
</ul>
|
||||
<p>Error details: ${error instanceof Error ? error.message : "Unknown error"}</p>
|
||||
<div style="margin-top: 15px;">
|
||||
<button onclick="window.location.reload()" style="margin: 0 5px; padding: 8px 16px; background: #c62828; color: white; border: none; border-radius: 4px; cursor: pointer;">
|
||||
Retry
|
||||
</button>
|
||||
<button onclick="window.electron.ipcRenderer.send('sqlite-status', { action: 'reset' })" style="margin: 0 5px; padding: 8px 16px; background: #f57c00; color: white; border: none; border-radius: 4px; cursor: pointer;">
|
||||
Reset Database
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(errorDiv);
|
||||
});
|
||||
|
||||
@@ -1,4 +1,15 @@
|
||||
import { initializeApp } from "./main.common";
|
||||
|
||||
const app = initializeApp();
|
||||
app.mount("#app");
|
||||
// Initialize and mount the app
|
||||
initializeApp().then((app) => {
|
||||
app.mount("#app");
|
||||
}).catch((error) => {
|
||||
console.error("Failed to initialize app:", error);
|
||||
document.body.innerHTML = `
|
||||
<div style="color: red; padding: 20px; font-family: sans-serif;">
|
||||
<h1>Failed to initialize app</h1>
|
||||
<p>${error instanceof Error ? error.message : "Unknown error"}</p>
|
||||
<p>Please try restarting the app or contact support if the problem persists.</p>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
@@ -1,17 +1,6 @@
|
||||
import { initBackend } from "absurd-sql/dist/indexeddb-main-thread";
|
||||
import { initializeApp } from "./main.common";
|
||||
import { logger } from "./utils/logger";
|
||||
|
||||
const platform = process.env.VITE_PLATFORM;
|
||||
const pwa_enabled = process.env.VITE_PWA_ENABLED === "true";
|
||||
|
||||
logger.error("[Web] PWA enabled", { pwa_enabled });
|
||||
logger.error("[Web] Platform", { platform });
|
||||
|
||||
// Only import service worker for web builds
|
||||
if (platform !== "electron" && pwa_enabled) {
|
||||
import("./registerServiceWorker"); // Web PWA support
|
||||
}
|
||||
import "./registerServiceWorker"; // Web PWA support
|
||||
|
||||
const app = initializeApp();
|
||||
|
||||
@@ -28,10 +17,18 @@ function sqlInit() {
|
||||
// workers through the main thread
|
||||
initBackend(worker);
|
||||
}
|
||||
if (platform === "web" || platform === "development") {
|
||||
sqlInit();
|
||||
} else {
|
||||
logger.info("[Web] SQL not initialized for platform", { platform });
|
||||
}
|
||||
sqlInit();
|
||||
|
||||
app.mount("#app");
|
||||
// Initialize and mount the app
|
||||
initializeApp().then((app) => {
|
||||
app.mount("#app");
|
||||
}).catch((error) => {
|
||||
console.error("Failed to initialize app:", error);
|
||||
document.body.innerHTML = `
|
||||
<div style="color: red; padding: 20px; font-family: sans-serif;">
|
||||
<h1>Failed to initialize app</h1>
|
||||
<p>${error instanceof Error ? error.message : "Unknown error"}</p>
|
||||
<p>Please try refreshing the page or contact support if the problem persists.</p>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import databaseService from "./services/AbsurdSqlDatabaseService";
|
||||
import databaseService from "./services/database";
|
||||
|
||||
async function run() {
|
||||
await databaseService.initialize();
|
||||
|
||||
@@ -2,18 +2,8 @@
|
||||
|
||||
import { register } from "register-service-worker";
|
||||
|
||||
// Check if we're in an Electron environment
|
||||
const isElectron =
|
||||
process.env.VITE_PLATFORM === "electron" ||
|
||||
process.env.VITE_DISABLE_PWA === "true" ||
|
||||
window.navigator.userAgent.toLowerCase().includes("electron");
|
||||
|
||||
// Only register service worker if:
|
||||
// 1. Not in Electron
|
||||
// 2. PWA is explicitly enabled
|
||||
// 3. In production mode
|
||||
// Only register service worker if explicitly enabled and in production
|
||||
if (
|
||||
!isElectron &&
|
||||
process.env.VITE_PWA_ENABLED === "true" &&
|
||||
process.env.NODE_ENV === "production"
|
||||
) {
|
||||
@@ -44,12 +34,6 @@ if (
|
||||
});
|
||||
} else {
|
||||
console.log(
|
||||
`Service worker registration skipped - ${
|
||||
isElectron
|
||||
? "running in Electron"
|
||||
: process.env.VITE_PWA_ENABLED !== "true"
|
||||
? "PWA not enabled"
|
||||
: "not in production mode"
|
||||
}`,
|
||||
"Service worker registration skipped - not enabled or not in production",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,11 +2,35 @@ import {
|
||||
createRouter,
|
||||
createWebHistory,
|
||||
createMemoryHistory,
|
||||
NavigationGuardNext,
|
||||
RouteLocationNormalized,
|
||||
RouteRecordRaw,
|
||||
} from "vue-router";
|
||||
import { accountsDBPromise } from "../db/index";
|
||||
import { logger } from "../utils/logger";
|
||||
|
||||
/**
|
||||
*
|
||||
* @param to :RouteLocationNormalized
|
||||
* @param from :RouteLocationNormalized
|
||||
* @param next :NavigationGuardNext
|
||||
*/
|
||||
const enterOrStart = async (
|
||||
to: RouteLocationNormalized,
|
||||
from: RouteLocationNormalized,
|
||||
next: NavigationGuardNext,
|
||||
) => {
|
||||
// one of the few times we use accountsDBPromise directly; try to avoid more usage
|
||||
const accountsDB = await accountsDBPromise;
|
||||
const num_accounts = await accountsDB.accounts.count();
|
||||
|
||||
if (num_accounts > 0) {
|
||||
next();
|
||||
} else {
|
||||
next({ name: "start" });
|
||||
}
|
||||
};
|
||||
|
||||
const routes: Array<RouteRecordRaw> = [
|
||||
{
|
||||
path: "/account",
|
||||
@@ -192,6 +216,7 @@ const routes: Array<RouteRecordRaw> = [
|
||||
path: "/projects",
|
||||
name: "projects",
|
||||
component: () => import("../views/ProjectsView.vue"),
|
||||
beforeEnter: enterOrStart,
|
||||
},
|
||||
{
|
||||
path: "/quick-action-bvc",
|
||||
@@ -277,31 +302,18 @@ const initialPath = isElectron
|
||||
? window.location.pathname.split("/dist-electron/www/")[1] || "/"
|
||||
: window.location.pathname;
|
||||
|
||||
logger.info("[Router] Initializing router", { isElectron, initialPath });
|
||||
|
||||
const history = isElectron
|
||||
? createMemoryHistory() // Memory history for Electron
|
||||
: createWebHistory("/"); // Add base path for web apps
|
||||
|
||||
/** @type {*} */
|
||||
const router = createRouter({
|
||||
history,
|
||||
routes,
|
||||
});
|
||||
|
||||
// Set initial route
|
||||
router.beforeEach((to, from, next) => {
|
||||
logger.info("[Router] Navigation", { to: to.path, from: from.path });
|
||||
next();
|
||||
});
|
||||
|
||||
// Replace initial URL to start at `/` if necessary
|
||||
if (initialPath === "/" || !initialPath) {
|
||||
logger.info("[Router] Setting initial route to /");
|
||||
router.replace("/");
|
||||
} else {
|
||||
logger.info("[Router] Setting initial route to", initialPath);
|
||||
router.replace(initialPath);
|
||||
}
|
||||
router.replace(initialPath || "/");
|
||||
|
||||
const errorHandler = (
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
|
||||
370
src/services/ElectronPlatformService.ts
Normal file
370
src/services/ElectronPlatformService.ts
Normal file
@@ -0,0 +1,370 @@
|
||||
import {
|
||||
PlatformService,
|
||||
PlatformCapabilities,
|
||||
SQLiteOperations,
|
||||
SQLiteConfig,
|
||||
PreparedStatement,
|
||||
SQLiteResult,
|
||||
ImageResult,
|
||||
} from "./PlatformService";
|
||||
import { BaseSQLiteService } from "./sqlite/BaseSQLiteService";
|
||||
import { app } from "electron";
|
||||
import { dialog } from "electron";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import sqlite3 from "sqlite3";
|
||||
import { open, Database } from "sqlite";
|
||||
import { logger } from "../utils/logger";
|
||||
import { Settings } from "../db/tables/settings";
|
||||
import { Account } from "../db/tables/accounts";
|
||||
import { Contact } from "../db/tables/contacts";
|
||||
import { db } from "../db";
|
||||
import { MASTER_SETTINGS_KEY } from "../db/tables/settings";
|
||||
import { accountsDBPromise } from "../db";
|
||||
import { accessToken } from "../libs/crypto";
|
||||
import { getPlanFromCache as getPlanFromCacheImpl } from "../libs/endorserServer";
|
||||
import { PlanSummaryRecord } from "../interfaces/records";
|
||||
import { Axios } from "axios";
|
||||
|
||||
interface SQLiteDatabase extends Database {
|
||||
changes: number;
|
||||
}
|
||||
|
||||
// Create Promise-based versions of fs functions
|
||||
const readFileAsync = (filePath: string, encoding: BufferEncoding): Promise<string> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
fs.readFile(filePath, { encoding }, (err: NodeJS.ErrnoException | null, data: string) => {
|
||||
if (err) reject(err);
|
||||
else resolve(data);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const readFileBufferAsync = (filePath: string): Promise<Buffer> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
fs.readFile(filePath, (err: NodeJS.ErrnoException | null, data: Buffer) => {
|
||||
if (err) reject(err);
|
||||
else resolve(data);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const writeFileAsync = (filePath: string, data: string, encoding: BufferEncoding): Promise<void> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
fs.writeFile(filePath, data, { encoding }, (err: NodeJS.ErrnoException | null) => {
|
||||
if (err) reject(err);
|
||||
else resolve();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const unlinkAsync = (filePath: string): Promise<void> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
fs.unlink(filePath, (err: NodeJS.ErrnoException | null) => {
|
||||
if (err) reject(err);
|
||||
else resolve();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const readdirAsync = (dirPath: string): Promise<string[]> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
fs.readdir(dirPath, (err: NodeJS.ErrnoException | null, files: string[]) => {
|
||||
if (err) reject(err);
|
||||
else resolve(files);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const statAsync = (filePath: string): Promise<fs.Stats> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
fs.stat(filePath, (err: NodeJS.ErrnoException | null, stats: fs.Stats) => {
|
||||
if (err) reject(err);
|
||||
else resolve(stats);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* SQLite implementation for Electron using native sqlite3
|
||||
*/
|
||||
class ElectronSQLiteService extends BaseSQLiteService {
|
||||
private db: SQLiteDatabase | null = null;
|
||||
private config: SQLiteConfig | null = null;
|
||||
|
||||
async initialize(config: SQLiteConfig): Promise<void> {
|
||||
if (this.initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.config = config;
|
||||
const dbPath = path.join(app.getPath("userData"), `${config.name}.db`);
|
||||
|
||||
this.db = await open({
|
||||
filename: dbPath,
|
||||
driver: sqlite3.Database,
|
||||
});
|
||||
|
||||
// Configure database settings
|
||||
if (config.useWAL) {
|
||||
await this.execute("PRAGMA journal_mode = WAL");
|
||||
this.stats.walMode = true;
|
||||
}
|
||||
|
||||
// Set other pragmas for performance
|
||||
await this.execute("PRAGMA synchronous = NORMAL");
|
||||
await this.execute("PRAGMA temp_store = MEMORY");
|
||||
await this.execute("PRAGMA cache_size = -2000"); // Use 2MB of cache
|
||||
|
||||
this.initialized = true;
|
||||
await this.updateStats();
|
||||
} catch (error) {
|
||||
logger.error("Failed to initialize Electron SQLite:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async close(): Promise<void> {
|
||||
if (!this.initialized || !this.db) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.db.close();
|
||||
this.db = null;
|
||||
this.initialized = false;
|
||||
} catch (error) {
|
||||
logger.error("Failed to close Electron SQLite connection:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
protected async _executeQuery<T>(
|
||||
sql: string,
|
||||
params: unknown[] = [],
|
||||
operation: "query" | "execute" = "query",
|
||||
): Promise<SQLiteResult<T>> {
|
||||
if (!this.db) {
|
||||
throw new Error("Database not initialized");
|
||||
}
|
||||
|
||||
try {
|
||||
if (operation === "query") {
|
||||
const rows = await this.db.all<T[]>(sql, params);
|
||||
const result = await this.db.run("SELECT last_insert_rowid() as id");
|
||||
return {
|
||||
rows,
|
||||
rowsAffected: this.db.changes,
|
||||
lastInsertId: result.lastID,
|
||||
executionTime: 0, // Will be set by base class
|
||||
};
|
||||
} else {
|
||||
const result = await this.db.run(sql, params);
|
||||
return {
|
||||
rows: [],
|
||||
rowsAffected: this.db.changes,
|
||||
lastInsertId: result.lastID,
|
||||
executionTime: 0, // Will be set by base class
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error("Electron SQLite query failed:", {
|
||||
sql,
|
||||
params,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
protected async _beginTransaction(): Promise<void> {
|
||||
if (!this.db) {
|
||||
throw new Error("Database not initialized");
|
||||
}
|
||||
await this.db.run("BEGIN TRANSACTION");
|
||||
}
|
||||
|
||||
protected async _commitTransaction(): Promise<void> {
|
||||
if (!this.db) {
|
||||
throw new Error("Database not initialized");
|
||||
}
|
||||
await this.db.run("COMMIT");
|
||||
}
|
||||
|
||||
protected async _rollbackTransaction(): Promise<void> {
|
||||
if (!this.db) {
|
||||
throw new Error("Database not initialized");
|
||||
}
|
||||
await this.db.run("ROLLBACK");
|
||||
}
|
||||
|
||||
protected async _prepareStatement<T>(
|
||||
sql: string,
|
||||
): Promise<PreparedStatement<T>> {
|
||||
if (!this.db) {
|
||||
throw new Error("Database not initialized");
|
||||
}
|
||||
|
||||
const stmt = await this.db.prepare(sql);
|
||||
return {
|
||||
execute: async (params: unknown[] = []) => {
|
||||
if (!this.db) {
|
||||
throw new Error("Database not initialized");
|
||||
}
|
||||
|
||||
const rows = await stmt.all<T>(params);
|
||||
return {
|
||||
rows,
|
||||
rowsAffected: this.db.changes,
|
||||
lastInsertId: (await this.db.run("SELECT last_insert_rowid() as id"))
|
||||
.lastID,
|
||||
executionTime: 0, // Will be set by base class
|
||||
};
|
||||
},
|
||||
finalize: async () => {
|
||||
await stmt.finalize();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
protected async _finalizeStatement(_sql: string): Promise<void> {
|
||||
// Statements are finalized when the PreparedStatement is finalized
|
||||
}
|
||||
|
||||
async getDatabaseSize(): Promise<number> {
|
||||
if (!this.db || !this.config) {
|
||||
throw new Error("Database not initialized");
|
||||
}
|
||||
|
||||
try {
|
||||
const dbPath = path.join(app.getPath("userData"), `${this.config.name}.db`);
|
||||
const stats = await statAsync(dbPath);
|
||||
return stats.size;
|
||||
} catch (error) {
|
||||
logger.error("Failed to get database size:", error);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Only import Electron-specific code in Electron environment
|
||||
let ElectronPlatformServiceImpl: typeof import("./platforms/ElectronPlatformService").ElectronPlatformService;
|
||||
|
||||
async function initializeElectronPlatformService() {
|
||||
if (process.env.ELECTRON) {
|
||||
// Dynamic import for Electron environment
|
||||
const { ElectronPlatformService } = await import("./platforms/ElectronPlatformService");
|
||||
ElectronPlatformServiceImpl = ElectronPlatformService;
|
||||
} else {
|
||||
// Stub implementation for non-Electron environments
|
||||
class StubElectronPlatformService implements PlatformService {
|
||||
#sqliteService: SQLiteOperations | null = null;
|
||||
|
||||
getCapabilities(): PlatformCapabilities {
|
||||
throw new Error("Electron platform service is not available in this environment");
|
||||
}
|
||||
|
||||
async getSQLite(): Promise<SQLiteOperations> {
|
||||
throw new Error("Electron platform service is not available in this environment");
|
||||
}
|
||||
|
||||
async readFile(path: string): Promise<string> {
|
||||
throw new Error("Electron platform service is not available in this environment");
|
||||
}
|
||||
|
||||
async writeFile(path: string, content: string): Promise<void> {
|
||||
throw new Error("Electron platform service is not available in this environment");
|
||||
}
|
||||
|
||||
async deleteFile(path: string): Promise<void> {
|
||||
throw new Error("Electron platform service is not available in this environment");
|
||||
}
|
||||
|
||||
async listFiles(directory: string): Promise<string[]> {
|
||||
throw new Error("Electron platform service is not available in this environment");
|
||||
}
|
||||
|
||||
async takePicture(): Promise<ImageResult> {
|
||||
throw new Error("Electron platform service is not available in this environment");
|
||||
}
|
||||
|
||||
async pickImage(): Promise<ImageResult> {
|
||||
throw new Error("Electron platform service is not available in this environment");
|
||||
}
|
||||
|
||||
async handleDeepLink(url: string): Promise<void> {
|
||||
throw new Error("Electron platform service is not available in this environment");
|
||||
}
|
||||
|
||||
async getAccounts(): Promise<Account[]> {
|
||||
throw new Error("Electron platform service is not available in this environment");
|
||||
}
|
||||
|
||||
async getAccount(did: string): Promise<Account | undefined> {
|
||||
throw new Error("Electron platform service is not available in this environment");
|
||||
}
|
||||
|
||||
async addAccount(account: Account): Promise<void> {
|
||||
throw new Error("Electron platform service is not available in this environment");
|
||||
}
|
||||
|
||||
async getContacts(): Promise<Contact[]> {
|
||||
throw new Error("Electron platform service is not available in this environment");
|
||||
}
|
||||
|
||||
async getAllContacts(): Promise<Contact[]> {
|
||||
throw new Error("Electron platform service is not available in this environment");
|
||||
}
|
||||
|
||||
async updateMasterSettings(settingsChanges: Partial<Settings>): Promise<void> {
|
||||
throw new Error("Electron platform service is not available in this environment");
|
||||
}
|
||||
|
||||
async getActiveAccountSettings(): Promise<Settings> {
|
||||
throw new Error("Electron platform service is not available in this environment");
|
||||
}
|
||||
|
||||
async updateAccountSettings(accountDid: string, settingsChanges: Partial<Settings>): Promise<void> {
|
||||
throw new Error("Electron platform service is not available in this environment");
|
||||
}
|
||||
|
||||
async getHeaders(did?: string): Promise<Record<string, string>> {
|
||||
throw new Error("Electron platform service is not available in this environment");
|
||||
}
|
||||
|
||||
async getPlanFromCache(
|
||||
handleId: string | undefined,
|
||||
axios: Axios,
|
||||
apiServer: string,
|
||||
requesterDid?: string,
|
||||
): Promise<PlanSummaryRecord | undefined> {
|
||||
throw new Error("Electron platform service is not available in this environment");
|
||||
}
|
||||
|
||||
isCapacitor(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
isElectron(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
isPyWebView(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
isWeb(): boolean {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
ElectronPlatformServiceImpl = StubElectronPlatformService;
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize the service
|
||||
initializeElectronPlatformService().catch(error => {
|
||||
logger.error("Failed to initialize Electron platform service:", error);
|
||||
});
|
||||
|
||||
export class ElectronPlatformService extends ElectronPlatformServiceImpl {}
|
||||
@@ -1,12 +1,8 @@
|
||||
import { QueryExecResult } from "@/interfaces/database";
|
||||
|
||||
/**
|
||||
* Query execution result interface
|
||||
*/
|
||||
export interface QueryExecResult<T = unknown> {
|
||||
columns: string[];
|
||||
values: T[];
|
||||
}
|
||||
import { Settings } from "../db/tables/settings";
|
||||
import { Account } from "../db/tables/accounts";
|
||||
import { Contact } from "../db/tables/contacts";
|
||||
import { Axios } from "axios";
|
||||
import { PlanSummaryRecord } from "../interfaces/records";
|
||||
|
||||
/**
|
||||
* Represents the result of an image capture or selection operation.
|
||||
@@ -36,6 +32,154 @@ export interface PlatformCapabilities {
|
||||
hasFileDownload: boolean;
|
||||
/** Whether the platform requires special file handling instructions */
|
||||
needsFileHandlingInstructions: boolean;
|
||||
/** SQLite capabilities of the platform */
|
||||
sqlite: {
|
||||
/** Whether SQLite is supported on this platform */
|
||||
supported: boolean;
|
||||
/** Whether SQLite runs in a Web Worker (browser) */
|
||||
runsInWorker: boolean;
|
||||
/** Whether the platform supports SharedArrayBuffer (required for optimal performance) */
|
||||
hasSharedArrayBuffer: boolean;
|
||||
/** Whether the platform supports WAL mode */
|
||||
supportsWAL: boolean;
|
||||
/** Maximum database size in bytes (if known) */
|
||||
maxSize?: number;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* SQLite configuration options
|
||||
*/
|
||||
export interface SQLiteConfig {
|
||||
/** Database name */
|
||||
name: string;
|
||||
/** Whether to use WAL mode (if supported) */
|
||||
useWAL?: boolean;
|
||||
/** Whether to use memory-mapped I/O (if supported) */
|
||||
useMMap?: boolean;
|
||||
/** Size of memory map in bytes (if using mmap) */
|
||||
mmapSize?: number;
|
||||
/** Whether to use prepared statements cache */
|
||||
usePreparedStatements?: boolean;
|
||||
/** Maximum number of prepared statements to cache */
|
||||
maxPreparedStatements?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a SQLite query result with typed rows
|
||||
*/
|
||||
export interface SQLiteResult<T> {
|
||||
/** The rows returned by the query */
|
||||
rows: T[];
|
||||
/** The number of rows affected by the query */
|
||||
rowsAffected: number;
|
||||
/** The last inserted row ID (if applicable) */
|
||||
lastInsertId?: number;
|
||||
/** Execution time in milliseconds */
|
||||
executionTime: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* SQLite operations interface for platform-agnostic database access
|
||||
*/
|
||||
export interface SQLiteOperations {
|
||||
/**
|
||||
* Initializes the SQLite database with the given configuration
|
||||
* @param config - SQLite configuration options
|
||||
* @returns Promise resolving when initialization is complete
|
||||
*/
|
||||
initialize(config: SQLiteConfig): Promise<void>;
|
||||
|
||||
/**
|
||||
* Executes a SQL query and returns typed results
|
||||
* @param sql - The SQL query to execute
|
||||
* @param params - Optional parameters for the query
|
||||
* @returns Promise resolving to the query results
|
||||
*/
|
||||
query<T>(sql: string, params?: unknown[]): Promise<SQLiteResult<T>>;
|
||||
|
||||
/**
|
||||
* Executes a SQL query that modifies data (INSERT, UPDATE, DELETE)
|
||||
* @param sql - The SQL query to execute
|
||||
* @param params - Optional parameters for the query
|
||||
* @returns Promise resolving to the number of rows affected
|
||||
*/
|
||||
execute(sql: string, params?: unknown[]): Promise<number>;
|
||||
|
||||
/**
|
||||
* Executes multiple SQL statements in a transaction
|
||||
* @param statements - Array of SQL statements to execute
|
||||
* @returns Promise resolving when the transaction is complete
|
||||
*/
|
||||
transaction(statements: { sql: string; params?: unknown[] }[]): Promise<void>;
|
||||
|
||||
/**
|
||||
* Gets the maximum value of a column for matching rows
|
||||
* @param table - The table to query
|
||||
* @param column - The column to find the maximum value of
|
||||
* @param where - Optional WHERE clause conditions
|
||||
* @param params - Optional parameters for the WHERE clause
|
||||
* @returns Promise resolving to the maximum value
|
||||
*/
|
||||
getMaxValue<T>(
|
||||
table: string,
|
||||
column: string,
|
||||
where?: string,
|
||||
params?: unknown[],
|
||||
): Promise<T | null>;
|
||||
|
||||
/**
|
||||
* Prepares a SQL statement for repeated execution
|
||||
* @param sql - The SQL statement to prepare
|
||||
* @returns A prepared statement that can be executed multiple times
|
||||
*/
|
||||
prepare<T>(sql: string): Promise<PreparedStatement<T>>;
|
||||
|
||||
/**
|
||||
* Gets the current database size in bytes
|
||||
* @returns Promise resolving to the database size
|
||||
*/
|
||||
getDatabaseSize(): Promise<number>;
|
||||
|
||||
/**
|
||||
* Gets the current database statistics
|
||||
* @returns Promise resolving to database statistics
|
||||
*/
|
||||
getStats(): Promise<SQLiteStats>;
|
||||
|
||||
/**
|
||||
* Closes the database connection
|
||||
* @returns Promise resolving when the connection is closed
|
||||
*/
|
||||
close(): Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a prepared SQL statement
|
||||
*/
|
||||
export interface PreparedStatement<T> {
|
||||
/** Executes the prepared statement with the given parameters */
|
||||
execute(params?: unknown[]): Promise<SQLiteResult<T>>;
|
||||
/** Frees the prepared statement */
|
||||
finalize(): Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Database statistics
|
||||
*/
|
||||
export interface SQLiteStats {
|
||||
/** Total number of queries executed */
|
||||
totalQueries: number;
|
||||
/** Average query execution time in milliseconds */
|
||||
avgExecutionTime: number;
|
||||
/** Number of prepared statements in cache */
|
||||
preparedStatements: number;
|
||||
/** Current database size in bytes */
|
||||
databaseSize: number;
|
||||
/** Whether WAL mode is active */
|
||||
walMode: boolean;
|
||||
/** Whether memory mapping is active */
|
||||
mmapActive: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -69,11 +213,12 @@ export interface PlatformService {
|
||||
|
||||
/**
|
||||
* Writes content to a file at the specified path and shares it.
|
||||
* Optional method - not all platforms need to implement this.
|
||||
* @param fileName - The filename of the file to write
|
||||
* @param content - The content to write to the file
|
||||
* @returns Promise that resolves when the write is complete
|
||||
*/
|
||||
writeAndShareFile(fileName: string, content: string): Promise<void>;
|
||||
writeAndShareFile?(fileName: string, content: string): Promise<void>;
|
||||
|
||||
/**
|
||||
* Deletes a file at the specified path.
|
||||
@@ -110,24 +255,90 @@ export interface PlatformService {
|
||||
handleDeepLink(url: string): Promise<void>;
|
||||
|
||||
/**
|
||||
* Execute a database query and return the results
|
||||
* @param sql SQL query to execute
|
||||
* @param params Query parameters
|
||||
* @returns Query results with columns and values
|
||||
* Gets the SQLite operations interface for the platform.
|
||||
* For browsers, this will use absurd-sql with Web Worker support.
|
||||
* @returns Promise resolving to the SQLite operations interface
|
||||
*/
|
||||
dbQuery<T = unknown>(
|
||||
sql: string,
|
||||
params?: unknown[],
|
||||
): Promise<QueryExecResult<T>>;
|
||||
getSQLite(): Promise<SQLiteOperations>;
|
||||
|
||||
/**
|
||||
* Executes a create/update/delete on the database.
|
||||
* @param sql - The SQL statement to execute
|
||||
* @param params - The parameters to pass to the statement
|
||||
* @returns Promise resolving to the result of the statement
|
||||
* Gets the headers for HTTP requests, including authorization if needed
|
||||
* @param did - Optional DID to include in authorization
|
||||
* @returns Promise resolving to headers object
|
||||
*/
|
||||
dbExec(
|
||||
sql: string,
|
||||
params?: unknown[],
|
||||
): Promise<{ changes: number; lastId?: number }>;
|
||||
getHeaders(did?: string): Promise<Record<string, string>>;
|
||||
|
||||
// Account Management
|
||||
/**
|
||||
* Gets all accounts in the database
|
||||
* @returns Promise resolving to array of accounts
|
||||
*/
|
||||
getAccounts(): Promise<Account[]>;
|
||||
|
||||
/**
|
||||
* Gets a specific account by DID
|
||||
* @param did - The DID of the account to retrieve
|
||||
* @returns Promise resolving to the account or undefined if not found
|
||||
*/
|
||||
getAccount(did: string): Promise<Account | undefined>;
|
||||
|
||||
/**
|
||||
* Adds a new account to the database
|
||||
* @param account - The account to add
|
||||
* @returns Promise resolving when the account is added
|
||||
*/
|
||||
addAccount(account: Account): Promise<void>;
|
||||
|
||||
// Settings Management
|
||||
/**
|
||||
* Updates the master settings with the provided changes
|
||||
* @param settingsChanges - The settings to update
|
||||
* @returns Promise resolving when the update is complete
|
||||
*/
|
||||
updateMasterSettings(settingsChanges: Partial<Settings>): Promise<void>;
|
||||
|
||||
/**
|
||||
* Gets the settings for the active account
|
||||
* @returns Promise resolving to the active account settings
|
||||
*/
|
||||
getActiveAccountSettings(): Promise<Settings>;
|
||||
|
||||
/**
|
||||
* Updates settings for a specific account
|
||||
* @param accountDid - The DID of the account to update settings for
|
||||
* @param settingsChanges - The settings to update
|
||||
* @returns Promise resolving when the update is complete
|
||||
*/
|
||||
updateAccountSettings(
|
||||
accountDid: string,
|
||||
settingsChanges: Partial<Settings>,
|
||||
): Promise<void>;
|
||||
|
||||
// Contact Management
|
||||
/**
|
||||
* Gets all contacts from the database
|
||||
* @returns Promise resolving to array of contacts
|
||||
*/
|
||||
getContacts(): Promise<Contact[]>;
|
||||
|
||||
/**
|
||||
* Gets all contacts from the database (alias for getContacts)
|
||||
* @returns Promise resolving to array of contacts
|
||||
*/
|
||||
getAllContacts(): Promise<Contact[]>;
|
||||
|
||||
/**
|
||||
* Retrieves plan data from cache or server
|
||||
* @param handleId - Plan handle ID
|
||||
* @param axios - Axios instance for making HTTP requests
|
||||
* @param apiServer - API server URL
|
||||
* @param requesterDid - Optional requester DID for private info
|
||||
* @returns Promise resolving to plan data or undefined if not found
|
||||
*/
|
||||
getPlanFromCache(
|
||||
handleId: string | undefined,
|
||||
axios: Axios,
|
||||
apiServer: string,
|
||||
requesterDid?: string,
|
||||
): Promise<PlanSummaryRecord | undefined>;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import { PlatformService } from "./PlatformService";
|
||||
import { WebPlatformService } from "./platforms/WebPlatformService";
|
||||
import { CapacitorPlatformService } from "./platforms/CapacitorPlatformService";
|
||||
import { ElectronPlatformService } from "./platforms/ElectronPlatformService";
|
||||
import { PyWebViewPlatformService } from "./platforms/PyWebViewPlatformService";
|
||||
|
||||
/**
|
||||
* Factory class for creating platform-specific service implementations.
|
||||
@@ -17,7 +14,7 @@ import { PyWebViewPlatformService } from "./platforms/PyWebViewPlatformService";
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const platformService = PlatformServiceFactory.getInstance();
|
||||
* const platformService = await PlatformServiceFactory.getInstance();
|
||||
* await platformService.takePicture();
|
||||
* ```
|
||||
*/
|
||||
@@ -28,31 +25,48 @@ export class PlatformServiceFactory {
|
||||
* Gets or creates the singleton instance of PlatformService.
|
||||
* Creates the appropriate platform-specific implementation based on environment.
|
||||
*
|
||||
* @returns {PlatformService} The singleton instance of PlatformService
|
||||
* @returns {Promise<PlatformService>} Promise resolving to the singleton instance of PlatformService
|
||||
*/
|
||||
public static getInstance(): PlatformService {
|
||||
public static async getInstance(): Promise<PlatformService> {
|
||||
if (PlatformServiceFactory.instance) {
|
||||
return PlatformServiceFactory.instance;
|
||||
}
|
||||
|
||||
const platform = process.env.VITE_PLATFORM || "web";
|
||||
|
||||
switch (platform) {
|
||||
case "capacitor":
|
||||
PlatformServiceFactory.instance = new CapacitorPlatformService();
|
||||
break;
|
||||
case "electron":
|
||||
PlatformServiceFactory.instance = new ElectronPlatformService();
|
||||
break;
|
||||
case "pywebview":
|
||||
PlatformServiceFactory.instance = new PyWebViewPlatformService();
|
||||
break;
|
||||
case "web":
|
||||
default:
|
||||
PlatformServiceFactory.instance = new WebPlatformService();
|
||||
break;
|
||||
}
|
||||
try {
|
||||
switch (platform) {
|
||||
case "capacitor": {
|
||||
const { CapacitorPlatformService } = await import("./platforms/CapacitorPlatformService");
|
||||
PlatformServiceFactory.instance = new CapacitorPlatformService();
|
||||
break;
|
||||
}
|
||||
case "electron": {
|
||||
const { ElectronPlatformService } = await import("./ElectronPlatformService");
|
||||
PlatformServiceFactory.instance = new ElectronPlatformService();
|
||||
break;
|
||||
}
|
||||
case "pywebview": {
|
||||
const { PyWebViewPlatformService } = await import("./platforms/PyWebViewPlatformService");
|
||||
PlatformServiceFactory.instance = new PyWebViewPlatformService();
|
||||
break;
|
||||
}
|
||||
case "web":
|
||||
default:
|
||||
PlatformServiceFactory.instance = new WebPlatformService();
|
||||
break;
|
||||
}
|
||||
|
||||
return PlatformServiceFactory.instance;
|
||||
if (!PlatformServiceFactory.instance) {
|
||||
throw new Error(`Failed to initialize platform service for ${platform}`);
|
||||
}
|
||||
|
||||
return PlatformServiceFactory.instance;
|
||||
} catch (error) {
|
||||
console.error(`Failed to initialize ${platform} platform service:`, error);
|
||||
// Fallback to web platform if initialization fails
|
||||
PlatformServiceFactory.instance = new WebPlatformService();
|
||||
return PlatformServiceFactory.instance;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,13 +4,7 @@ import {
|
||||
StartScanOptions,
|
||||
LensFacing,
|
||||
} from "@capacitor-mlkit/barcode-scanning";
|
||||
import {
|
||||
QRScannerService,
|
||||
ScanListener,
|
||||
QRScannerOptions,
|
||||
CameraStateListener,
|
||||
CameraState,
|
||||
} from "./types";
|
||||
import { QRScannerService, ScanListener, QRScannerOptions } from "./types";
|
||||
import { logger } from "@/utils/logger";
|
||||
|
||||
export class CapacitorQRScanner implements QRScannerService {
|
||||
@@ -18,9 +12,6 @@ export class CapacitorQRScanner implements QRScannerService {
|
||||
private isScanning = false;
|
||||
private listenerHandles: Array<() => Promise<void>> = [];
|
||||
private cleanupPromise: Promise<void> | null = null;
|
||||
private cameraStateListeners: Set<CameraStateListener> = new Set();
|
||||
private currentState: CameraState = "off";
|
||||
private currentStateMessage?: string;
|
||||
|
||||
async checkPermissions(): Promise<boolean> {
|
||||
try {
|
||||
@@ -88,11 +79,8 @@ export class CapacitorQRScanner implements QRScannerService {
|
||||
}
|
||||
|
||||
try {
|
||||
this.updateCameraState("initializing", "Starting camera...");
|
||||
|
||||
// Ensure we have permissions before starting
|
||||
if (!(await this.checkPermissions())) {
|
||||
this.updateCameraState("permission_denied", "Camera permission denied");
|
||||
logger.debug("Requesting camera permissions");
|
||||
const granted = await this.requestPermissions();
|
||||
if (!granted) {
|
||||
@@ -102,16 +90,11 @@ export class CapacitorQRScanner implements QRScannerService {
|
||||
|
||||
// Check if scanning is supported
|
||||
if (!(await this.isSupported())) {
|
||||
this.updateCameraState(
|
||||
"error",
|
||||
"QR scanning not supported on this device",
|
||||
);
|
||||
throw new Error("QR scanning not supported on this device");
|
||||
}
|
||||
|
||||
logger.info("Starting MLKit scanner");
|
||||
this.isScanning = true;
|
||||
this.updateCameraState("active", "Camera is active");
|
||||
|
||||
const scanOptions: StartScanOptions = {
|
||||
formats: [BarcodeFormat.QrCode],
|
||||
@@ -143,7 +126,6 @@ export class CapacitorQRScanner implements QRScannerService {
|
||||
stack: wrappedError.stack,
|
||||
});
|
||||
this.isScanning = false;
|
||||
this.updateCameraState("error", wrappedError.message);
|
||||
await this.cleanup();
|
||||
this.scanListener?.onError?.(wrappedError);
|
||||
throw wrappedError;
|
||||
@@ -158,7 +140,6 @@ export class CapacitorQRScanner implements QRScannerService {
|
||||
|
||||
try {
|
||||
logger.debug("Stopping QR scanner");
|
||||
this.updateCameraState("off", "Camera stopped");
|
||||
await BarcodeScanner.stopScan();
|
||||
logger.info("QR scanner stopped successfully");
|
||||
} catch (error) {
|
||||
@@ -168,7 +149,6 @@ export class CapacitorQRScanner implements QRScannerService {
|
||||
error: wrappedError.message,
|
||||
stack: wrappedError.stack,
|
||||
});
|
||||
this.updateCameraState("error", wrappedError.message);
|
||||
this.scanListener?.onError?.(wrappedError);
|
||||
throw wrappedError;
|
||||
} finally {
|
||||
@@ -227,23 +207,4 @@ export class CapacitorQRScanner implements QRScannerService {
|
||||
// No-op for native scanner
|
||||
callback(null);
|
||||
}
|
||||
|
||||
addCameraStateListener(listener: CameraStateListener): void {
|
||||
this.cameraStateListeners.add(listener);
|
||||
// Immediately notify the new listener of current state
|
||||
listener.onStateChange(this.currentState, this.currentStateMessage);
|
||||
}
|
||||
|
||||
removeCameraStateListener(listener: CameraStateListener): void {
|
||||
this.cameraStateListeners.delete(listener);
|
||||
}
|
||||
|
||||
private updateCameraState(state: CameraState, message?: string): void {
|
||||
this.currentState = state;
|
||||
this.currentStateMessage = message;
|
||||
// Notify all listeners of state change
|
||||
for (const listener of this.cameraStateListeners) {
|
||||
listener.onStateChange(state, message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,16 +30,14 @@ export class WebInlineQRScanner implements QRScannerService {
|
||||
private cameraStateListeners: Set<CameraStateListener> = new Set();
|
||||
private currentState: CameraState = "off";
|
||||
private currentStateMessage?: string;
|
||||
private options: QRScannerOptions;
|
||||
|
||||
constructor(options?: QRScannerOptions) {
|
||||
constructor(private options?: QRScannerOptions) {
|
||||
// Generate a short random ID for this scanner instance
|
||||
this.id = Math.random().toString(36).substring(2, 8).toUpperCase();
|
||||
this.options = options ?? {};
|
||||
logger.error(
|
||||
`[WebInlineQRScanner:${this.id}] Initializing scanner with options:`,
|
||||
{
|
||||
...this.options,
|
||||
...options,
|
||||
buildId: BUILD_ID,
|
||||
targetFps: this.TARGET_FPS,
|
||||
},
|
||||
@@ -496,34 +494,26 @@ export class WebInlineQRScanner implements QRScannerService {
|
||||
}
|
||||
}
|
||||
|
||||
async startScan(options?: QRScannerOptions): Promise<void> {
|
||||
async startScan(): Promise<void> {
|
||||
if (this.isScanning) {
|
||||
logger.error(`[WebInlineQRScanner:${this.id}] Scanner already running`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Update options if provided
|
||||
if (options) {
|
||||
this.options = { ...this.options, ...options };
|
||||
}
|
||||
|
||||
try {
|
||||
this.isScanning = true;
|
||||
this.scanAttempts = 0;
|
||||
this.lastScanTime = Date.now();
|
||||
this.updateCameraState("initializing", "Starting camera...");
|
||||
logger.error(
|
||||
`[WebInlineQRScanner:${this.id}] Starting scan with options:`,
|
||||
this.options,
|
||||
);
|
||||
logger.error(`[WebInlineQRScanner:${this.id}] Starting scan`);
|
||||
|
||||
// Get camera stream with options
|
||||
// Get camera stream
|
||||
logger.error(
|
||||
`[WebInlineQRScanner:${this.id}] Requesting camera stream...`,
|
||||
);
|
||||
this.stream = await navigator.mediaDevices.getUserMedia({
|
||||
video: {
|
||||
facingMode: this.options.camera === "front" ? "user" : "environment",
|
||||
facingMode: "environment",
|
||||
width: { ideal: 1280 },
|
||||
height: { ideal: 720 },
|
||||
},
|
||||
@@ -537,18 +527,11 @@ export class WebInlineQRScanner implements QRScannerService {
|
||||
label: t.label,
|
||||
readyState: t.readyState,
|
||||
})),
|
||||
options: this.options,
|
||||
});
|
||||
|
||||
// Set up video element
|
||||
if (this.video) {
|
||||
this.video.srcObject = this.stream;
|
||||
// Only show preview if showPreview is true
|
||||
if (this.options.showPreview) {
|
||||
this.video.style.display = "block";
|
||||
} else {
|
||||
this.video.style.display = "none";
|
||||
}
|
||||
await this.video.play();
|
||||
logger.error(
|
||||
`[WebInlineQRScanner:${this.id}] Video element started playing`,
|
||||
|
||||
@@ -1,20 +1,17 @@
|
||||
// Add type declarations for external modules
|
||||
declare module "@jlongster/sql.js";
|
||||
declare module "absurd-sql";
|
||||
declare module "absurd-sql/dist/indexeddb-backend";
|
||||
|
||||
import initSqlJs from "@jlongster/sql.js";
|
||||
import { SQLiteFS } from "absurd-sql";
|
||||
import IndexedDBBackend from "absurd-sql/dist/indexeddb-backend";
|
||||
|
||||
import { runMigrations } from "../db-sql/migration";
|
||||
import type { DatabaseService, QueryExecResult } from "../interfaces/database";
|
||||
import type { QueryExecResult } from "../interfaces/database";
|
||||
import { logger } from "@/utils/logger";
|
||||
|
||||
interface QueuedOperation {
|
||||
type: "run" | "query" | "getOneRow" | "getAll";
|
||||
sql: string;
|
||||
params: unknown[];
|
||||
resolve: (value: unknown) => void;
|
||||
reject: (reason: unknown) => void;
|
||||
}
|
||||
|
||||
interface AbsurdSqlDatabase {
|
||||
interface SQLDatabase {
|
||||
exec: (sql: string, params?: unknown[]) => Promise<QueryExecResult[]>;
|
||||
run: (
|
||||
sql: string,
|
||||
@@ -22,24 +19,22 @@ interface AbsurdSqlDatabase {
|
||||
) => Promise<{ changes: number; lastId?: number }>;
|
||||
}
|
||||
|
||||
class AbsurdSqlDatabaseService implements DatabaseService {
|
||||
private static instance: AbsurdSqlDatabaseService | null = null;
|
||||
private db: AbsurdSqlDatabase | null;
|
||||
class DatabaseService {
|
||||
private static instance: DatabaseService | null = null;
|
||||
private db: SQLDatabase | null;
|
||||
private initialized: boolean;
|
||||
private initializationPromise: Promise<void> | null = null;
|
||||
private operationQueue: Array<QueuedOperation> = [];
|
||||
private isProcessingQueue: boolean = false;
|
||||
|
||||
private constructor() {
|
||||
this.db = null;
|
||||
this.initialized = false;
|
||||
}
|
||||
|
||||
static getInstance(): AbsurdSqlDatabaseService {
|
||||
if (!AbsurdSqlDatabaseService.instance) {
|
||||
AbsurdSqlDatabaseService.instance = new AbsurdSqlDatabaseService();
|
||||
static getInstance(): DatabaseService {
|
||||
if (!DatabaseService.instance) {
|
||||
DatabaseService.instance = new DatabaseService();
|
||||
}
|
||||
return AbsurdSqlDatabaseService.instance;
|
||||
return DatabaseService.instance;
|
||||
}
|
||||
|
||||
async initialize(): Promise<void> {
|
||||
@@ -58,7 +53,7 @@ class AbsurdSqlDatabaseService implements DatabaseService {
|
||||
try {
|
||||
await this.initializationPromise;
|
||||
} catch (error) {
|
||||
logger.error(`AbsurdSqlDatabaseService initialize method failed:`, error);
|
||||
logger.error(`DatabaseService initialize method failed:`, error);
|
||||
this.initializationPromise = null; // Reset on failure
|
||||
throw error;
|
||||
}
|
||||
@@ -84,7 +79,7 @@ class AbsurdSqlDatabaseService implements DatabaseService {
|
||||
SQL.FS.mkdir("/sql");
|
||||
SQL.FS.mount(sqlFS, {}, "/sql");
|
||||
|
||||
const path = "/sql/timesafari.absurd-sql";
|
||||
const path = "/sql/db.sqlite";
|
||||
if (typeof SharedArrayBuffer === "undefined") {
|
||||
const stream = SQL.FS.open(path, "a+");
|
||||
await stream.node.contents.readIfFallback();
|
||||
@@ -98,7 +93,6 @@ class AbsurdSqlDatabaseService implements DatabaseService {
|
||||
);
|
||||
}
|
||||
|
||||
// An error is thrown without this pragma: "File has invalid page size. (the first block of a new file must be written first)"
|
||||
await this.db.exec(`PRAGMA journal_mode=MEMORY;`);
|
||||
const sqlExec = this.db.exec.bind(this.db);
|
||||
|
||||
@@ -106,78 +100,6 @@ class AbsurdSqlDatabaseService implements DatabaseService {
|
||||
await runMigrations(sqlExec);
|
||||
|
||||
this.initialized = true;
|
||||
|
||||
// Start processing the queue after initialization
|
||||
this.processQueue();
|
||||
}
|
||||
|
||||
private async processQueue(): Promise<void> {
|
||||
if (this.isProcessingQueue || !this.initialized || !this.db) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isProcessingQueue = true;
|
||||
|
||||
while (this.operationQueue.length > 0) {
|
||||
const operation = this.operationQueue.shift();
|
||||
if (!operation) continue;
|
||||
|
||||
try {
|
||||
let queryResult: QueryExecResult[] = [];
|
||||
let result: unknown;
|
||||
switch (operation.type) {
|
||||
case "run":
|
||||
result = await this.db.run(operation.sql, operation.params);
|
||||
break;
|
||||
case "query":
|
||||
result = await this.db.exec(operation.sql, operation.params);
|
||||
break;
|
||||
case "getOneRow":
|
||||
queryResult = await this.db.exec(operation.sql, operation.params);
|
||||
result = queryResult[0]?.values[0];
|
||||
break;
|
||||
case "getAll":
|
||||
queryResult = await this.db.exec(operation.sql, operation.params);
|
||||
result = queryResult[0]?.values || [];
|
||||
break;
|
||||
}
|
||||
operation.resolve(result);
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
"Error while processing SQL queue:",
|
||||
error,
|
||||
" ... for sql:",
|
||||
operation.sql,
|
||||
" ... with params:",
|
||||
operation.params,
|
||||
);
|
||||
operation.reject(error);
|
||||
}
|
||||
}
|
||||
|
||||
this.isProcessingQueue = false;
|
||||
}
|
||||
|
||||
private async queueOperation<R>(
|
||||
type: QueuedOperation["type"],
|
||||
sql: string,
|
||||
params: unknown[] = [],
|
||||
): Promise<R> {
|
||||
return new Promise<R>((resolve, reject) => {
|
||||
const operation: QueuedOperation = {
|
||||
type,
|
||||
sql,
|
||||
params,
|
||||
resolve: (value: unknown) => resolve(value as R),
|
||||
reject,
|
||||
};
|
||||
this.operationQueue.push(operation);
|
||||
|
||||
// If we're already initialized, start processing the queue
|
||||
if (this.initialized && this.db) {
|
||||
this.processQueue();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async waitForInitialization(): Promise<void> {
|
||||
@@ -210,17 +132,13 @@ class AbsurdSqlDatabaseService implements DatabaseService {
|
||||
params: unknown[] = [],
|
||||
): Promise<{ changes: number; lastId?: number }> {
|
||||
await this.waitForInitialization();
|
||||
return this.queueOperation<{ changes: number; lastId?: number }>(
|
||||
"run",
|
||||
sql,
|
||||
params,
|
||||
);
|
||||
return this.db!.run(sql, params);
|
||||
}
|
||||
|
||||
// Note that the resulting array may be empty if there are no results from the query
|
||||
async query(sql: string, params: unknown[] = []): Promise<QueryExecResult[]> {
|
||||
await this.waitForInitialization();
|
||||
return this.queueOperation<QueryExecResult[]>("query", sql, params);
|
||||
return this.db!.exec(sql, params);
|
||||
}
|
||||
|
||||
async getOneRow(
|
||||
@@ -228,16 +146,18 @@ class AbsurdSqlDatabaseService implements DatabaseService {
|
||||
params: unknown[] = [],
|
||||
): Promise<unknown[] | undefined> {
|
||||
await this.waitForInitialization();
|
||||
return this.queueOperation<unknown[] | undefined>("getOneRow", sql, params);
|
||||
const result = await this.db!.exec(sql, params);
|
||||
return result[0]?.values[0];
|
||||
}
|
||||
|
||||
async getAll(sql: string, params: unknown[] = []): Promise<unknown[][]> {
|
||||
async all(sql: string, params: unknown[] = []): Promise<unknown[][]> {
|
||||
await this.waitForInitialization();
|
||||
return this.queueOperation<unknown[][]>("getAll", sql, params);
|
||||
const result = await this.db!.exec(sql, params);
|
||||
return result[0]?.values || [];
|
||||
}
|
||||
}
|
||||
|
||||
// Create a singleton instance
|
||||
const databaseService = AbsurdSqlDatabaseService.getInstance();
|
||||
const databaseService = DatabaseService.getInstance();
|
||||
|
||||
export default databaseService;
|
||||
@@ -1,132 +0,0 @@
|
||||
import { logger } from "../../utils/logger";
|
||||
import { SQLiteDBConnection } from "@capacitor-community/sqlite";
|
||||
|
||||
interface ConnectionState {
|
||||
connection: SQLiteDBConnection;
|
||||
lastUsed: number;
|
||||
inUse: boolean;
|
||||
}
|
||||
|
||||
export class DatabaseConnectionPool {
|
||||
private static instance: DatabaseConnectionPool | null = null;
|
||||
private connections: Map<string, ConnectionState> = new Map();
|
||||
private readonly MAX_CONNECTIONS = 1; // We only need one connection for SQLite
|
||||
private readonly MAX_IDLE_TIME = 5 * 60 * 1000; // 5 minutes
|
||||
private readonly CLEANUP_INTERVAL = 60 * 1000; // 1 minute
|
||||
private cleanupInterval: NodeJS.Timeout | null = null;
|
||||
|
||||
private constructor() {
|
||||
// Start cleanup interval
|
||||
this.cleanupInterval = setInterval(
|
||||
() => this.cleanup(),
|
||||
this.CLEANUP_INTERVAL,
|
||||
);
|
||||
}
|
||||
|
||||
public static getInstance(): DatabaseConnectionPool {
|
||||
if (!DatabaseConnectionPool.instance) {
|
||||
DatabaseConnectionPool.instance = new DatabaseConnectionPool();
|
||||
}
|
||||
return DatabaseConnectionPool.instance;
|
||||
}
|
||||
|
||||
public async getConnection(
|
||||
dbName: string,
|
||||
createConnection: () => Promise<SQLiteDBConnection>,
|
||||
): Promise<SQLiteDBConnection> {
|
||||
// Check if we have an existing connection
|
||||
const existing = this.connections.get(dbName);
|
||||
if (existing && !existing.inUse) {
|
||||
existing.inUse = true;
|
||||
existing.lastUsed = Date.now();
|
||||
logger.debug(
|
||||
`[ConnectionPool] Reusing existing connection for ${dbName}`,
|
||||
);
|
||||
return existing.connection;
|
||||
}
|
||||
|
||||
// If we have too many connections, wait for one to be released
|
||||
if (this.connections.size >= this.MAX_CONNECTIONS) {
|
||||
logger.debug(`[ConnectionPool] Waiting for connection to be released...`);
|
||||
await this.waitForConnection();
|
||||
}
|
||||
|
||||
// Create new connection
|
||||
try {
|
||||
const connection = await createConnection();
|
||||
this.connections.set(dbName, {
|
||||
connection,
|
||||
lastUsed: Date.now(),
|
||||
inUse: true,
|
||||
});
|
||||
logger.debug(`[ConnectionPool] Created new connection for ${dbName}`);
|
||||
return connection;
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`[ConnectionPool] Failed to create connection for ${dbName}:`,
|
||||
error,
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
public async releaseConnection(dbName: string): Promise<void> {
|
||||
const connection = this.connections.get(dbName);
|
||||
if (connection) {
|
||||
connection.inUse = false;
|
||||
connection.lastUsed = Date.now();
|
||||
logger.debug(`[ConnectionPool] Released connection for ${dbName}`);
|
||||
}
|
||||
}
|
||||
|
||||
private async waitForConnection(): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
const checkInterval = setInterval(() => {
|
||||
if (this.connections.size < this.MAX_CONNECTIONS) {
|
||||
clearInterval(checkInterval);
|
||||
resolve();
|
||||
}
|
||||
}, 100);
|
||||
});
|
||||
}
|
||||
|
||||
private async cleanup(): Promise<void> {
|
||||
const now = Date.now();
|
||||
for (const [dbName, state] of this.connections.entries()) {
|
||||
if (!state.inUse && now - state.lastUsed > this.MAX_IDLE_TIME) {
|
||||
try {
|
||||
await state.connection.close();
|
||||
this.connections.delete(dbName);
|
||||
logger.debug(
|
||||
`[ConnectionPool] Cleaned up idle connection for ${dbName}`,
|
||||
);
|
||||
} catch (error) {
|
||||
logger.warn(
|
||||
`[ConnectionPool] Error closing idle connection for ${dbName}:`,
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async closeAll(): Promise<void> {
|
||||
if (this.cleanupInterval) {
|
||||
clearInterval(this.cleanupInterval);
|
||||
this.cleanupInterval = null;
|
||||
}
|
||||
|
||||
for (const [dbName, state] of this.connections.entries()) {
|
||||
try {
|
||||
await state.connection.close();
|
||||
logger.debug(`[ConnectionPool] Closed connection for ${dbName}`);
|
||||
} catch (error) {
|
||||
logger.warn(
|
||||
`[ConnectionPool] Error closing connection for ${dbName}:`,
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
this.connections.clear();
|
||||
}
|
||||
}
|
||||
@@ -52,7 +52,7 @@ import {
|
||||
routeSchema,
|
||||
DeepLinkRoute,
|
||||
} from "../interfaces/deepLinks";
|
||||
import { logConsoleAndDb } from "../db/databaseUtil";
|
||||
import { logConsoleAndDb } from "../db";
|
||||
import type { DeepLinkError } from "../interfaces/deepLinks";
|
||||
|
||||
/**
|
||||
@@ -119,15 +119,6 @@ export class DeepLinkHandler {
|
||||
const [path, queryString] = parts[1].split("?");
|
||||
const [routePath, param] = path.split("/");
|
||||
|
||||
// Validate route exists before proceeding
|
||||
if (!this.ROUTE_MAP[routePath]) {
|
||||
throw {
|
||||
code: "INVALID_ROUTE",
|
||||
message: `Invalid route path: ${routePath}`,
|
||||
details: { routePath },
|
||||
};
|
||||
}
|
||||
|
||||
const query: Record<string, string> = {};
|
||||
if (queryString) {
|
||||
new URLSearchParams(queryString).forEach((value, key) => {
|
||||
@@ -137,9 +128,11 @@ export class DeepLinkHandler {
|
||||
|
||||
const params: Record<string, string> = {};
|
||||
if (param) {
|
||||
// Now we know routePath exists in ROUTE_MAP
|
||||
const routeConfig = this.ROUTE_MAP[routePath];
|
||||
params[routeConfig.paramKey ?? "id"] = param;
|
||||
if (this.ROUTE_MAP[routePath].paramKey) {
|
||||
params[this.ROUTE_MAP[routePath].paramKey] = param;
|
||||
} else {
|
||||
params["id"] = param;
|
||||
}
|
||||
}
|
||||
return { path: routePath, params, query };
|
||||
}
|
||||
|
||||
@@ -54,11 +54,16 @@ export class MigrationService {
|
||||
// Run pending migrations in order
|
||||
for (const migration of this.migrations) {
|
||||
if (!executedMigrations.has(migration.name)) {
|
||||
await sqlExec(migration.sql);
|
||||
await sqlExec("INSERT INTO migrations (name) VALUES (?)", [
|
||||
migration.name,
|
||||
]);
|
||||
logger.log(`Migration ${migration.name} executed successfully`);
|
||||
try {
|
||||
await sqlExec(migration.sql);
|
||||
await sqlExec("INSERT INTO migrations (name) VALUES (?)", [
|
||||
migration.name,
|
||||
]);
|
||||
logger.log(`Migration ${migration.name} executed successfully`);
|
||||
} catch (error) {
|
||||
logger.error(`Error executing migration ${migration.name}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,20 +6,11 @@ import {
|
||||
import { Filesystem, Directory, Encoding } from "@capacitor/filesystem";
|
||||
import { Camera, CameraResultType, CameraSource } from "@capacitor/camera";
|
||||
import { Share } from "@capacitor/share";
|
||||
import {
|
||||
SQLiteConnection,
|
||||
SQLiteDBConnection,
|
||||
CapacitorSQLite,
|
||||
Changes,
|
||||
} from "@capacitor-community/sqlite";
|
||||
import { logger } from "../../utils/logger";
|
||||
import { QueryExecResult, SqlValue } from "@/interfaces/database";
|
||||
import { DEFAULT_ENDORSER_API_SERVER } from "@/constants/app";
|
||||
|
||||
interface Migration {
|
||||
name: string;
|
||||
sql: string;
|
||||
}
|
||||
import { Account } from "../../db/tables/accounts";
|
||||
import { Settings } from "../../db/tables/settings";
|
||||
import { db } from "../../db";
|
||||
import { Contact } from "../../db/tables/contacts";
|
||||
|
||||
/**
|
||||
* Platform service implementation for Capacitor (mobile) platform.
|
||||
@@ -27,168 +18,8 @@ interface Migration {
|
||||
* - File system operations
|
||||
* - Camera and image picker
|
||||
* - Platform-specific features
|
||||
* - SQLite database operations
|
||||
*/
|
||||
export class CapacitorPlatformService implements PlatformService {
|
||||
private sqlite: SQLiteConnection;
|
||||
private db: SQLiteDBConnection | null = null;
|
||||
private dbName = "timesafari.db";
|
||||
private initialized = false;
|
||||
|
||||
constructor() {
|
||||
this.sqlite = new SQLiteConnection(CapacitorSQLite);
|
||||
}
|
||||
|
||||
private async initializeDatabase(): Promise<void> {
|
||||
if (this.initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Create/Open database
|
||||
this.db = await this.sqlite.createConnection(
|
||||
this.dbName,
|
||||
false,
|
||||
"no-encryption",
|
||||
1,
|
||||
false,
|
||||
);
|
||||
|
||||
await this.db.open();
|
||||
|
||||
// Set journal mode to WAL for better performance
|
||||
await this.db.execute("PRAGMA journal_mode=WAL;");
|
||||
|
||||
// Run migrations
|
||||
await this.runMigrations();
|
||||
|
||||
this.initialized = true;
|
||||
logger.log("SQLite database initialized successfully");
|
||||
} catch (error) {
|
||||
logger.error("Error initializing SQLite database:", error);
|
||||
throw new Error("Failed to initialize database");
|
||||
}
|
||||
}
|
||||
|
||||
private async runMigrations(): Promise<void> {
|
||||
if (!this.db) {
|
||||
throw new Error("Database not initialized");
|
||||
}
|
||||
|
||||
// Create migrations table if it doesn't exist
|
||||
await this.db.execute(`
|
||||
CREATE TABLE IF NOT EXISTS migrations (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
executed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
`);
|
||||
|
||||
// Get list of executed migrations
|
||||
const result = await this.db.query("SELECT name FROM migrations;");
|
||||
const executedMigrations = new Set(
|
||||
result.values?.map((row) => row[0]) || [],
|
||||
);
|
||||
|
||||
// Run pending migrations in order
|
||||
const migrations: Migration[] = [
|
||||
{
|
||||
name: "001_initial",
|
||||
sql: `
|
||||
CREATE TABLE IF NOT EXISTS accounts (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
dateCreated TEXT NOT NULL,
|
||||
derivationPath TEXT,
|
||||
did TEXT NOT NULL,
|
||||
identityEncrBase64 TEXT,
|
||||
mnemonicEncrBase64 TEXT,
|
||||
passkeyCredIdHex TEXT,
|
||||
publicKeyHex TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_accounts_did ON accounts(did);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS secret (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
secretBase64 TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS settings (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
accountDid TEXT,
|
||||
activeDid TEXT,
|
||||
apiServer TEXT,
|
||||
filterFeedByNearby BOOLEAN,
|
||||
filterFeedByVisible BOOLEAN,
|
||||
finishedOnboarding BOOLEAN,
|
||||
firstName TEXT,
|
||||
hideRegisterPromptOnNewContact BOOLEAN,
|
||||
isRegistered BOOLEAN,
|
||||
lastName TEXT,
|
||||
lastAckedOfferToUserJwtId TEXT,
|
||||
lastAckedOfferToUserProjectsJwtId TEXT,
|
||||
lastNotifiedClaimId TEXT,
|
||||
lastViewedClaimId TEXT,
|
||||
notifyingNewActivityTime TEXT,
|
||||
notifyingReminderMessage TEXT,
|
||||
notifyingReminderTime TEXT,
|
||||
partnerApiServer TEXT,
|
||||
passkeyExpirationMinutes INTEGER,
|
||||
profileImageUrl TEXT,
|
||||
searchBoxes TEXT,
|
||||
showContactGivesInline BOOLEAN,
|
||||
showGeneralAdvanced BOOLEAN,
|
||||
showShortcutBvc BOOLEAN,
|
||||
vapid TEXT,
|
||||
warnIfProdServer BOOLEAN,
|
||||
warnIfTestServer BOOLEAN,
|
||||
webPushServer TEXT
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_settings_accountDid ON settings(accountDid);
|
||||
|
||||
INSERT INTO settings (id, apiServer) VALUES (1, '${DEFAULT_ENDORSER_API_SERVER}');
|
||||
|
||||
CREATE TABLE IF NOT EXISTS contacts (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
did TEXT NOT NULL,
|
||||
name TEXT,
|
||||
contactMethods TEXT,
|
||||
nextPubKeyHashB64 TEXT,
|
||||
notes TEXT,
|
||||
profileImageUrl TEXT,
|
||||
publicKeyBase64 TEXT,
|
||||
seesMe BOOLEAN,
|
||||
registered BOOLEAN
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_contacts_did ON contacts(did);
|
||||
CREATE INDEX IF NOT EXISTS idx_contacts_name ON contacts(name);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS logs (
|
||||
date TEXT PRIMARY KEY,
|
||||
message TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS temp (
|
||||
id TEXT PRIMARY KEY,
|
||||
blobB64 TEXT
|
||||
);
|
||||
`,
|
||||
},
|
||||
];
|
||||
|
||||
for (const migration of migrations) {
|
||||
if (!executedMigrations.has(migration.name)) {
|
||||
await this.db.execute(migration.sql);
|
||||
await this.db.run("INSERT INTO migrations (name) VALUES (?)", [
|
||||
migration.name,
|
||||
]);
|
||||
logger.log(`Migration ${migration.name} executed successfully`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the capabilities of the Capacitor platform
|
||||
* @returns Platform capabilities object
|
||||
@@ -358,9 +189,6 @@ export class CapacitorPlatformService implements PlatformService {
|
||||
*/
|
||||
async writeFile(fileName: string, content: string): Promise<void> {
|
||||
try {
|
||||
// Check storage permissions before proceeding
|
||||
await this.checkStoragePermissions();
|
||||
|
||||
const logData = {
|
||||
targetFileName: fileName,
|
||||
contentLength: content.length,
|
||||
@@ -502,9 +330,6 @@ export class CapacitorPlatformService implements PlatformService {
|
||||
logger.log("[CapacitorPlatformService]", JSON.stringify(logData, null, 2));
|
||||
|
||||
try {
|
||||
// Check storage permissions before proceeding
|
||||
await this.checkStoragePermissions();
|
||||
|
||||
const { uri } = await Filesystem.writeFile({
|
||||
path: fileName,
|
||||
data: content,
|
||||
@@ -656,54 +481,43 @@ export class CapacitorPlatformService implements PlatformService {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
/**
|
||||
* @see PlatformService.dbQuery
|
||||
*/
|
||||
async dbQuery(sql: string, params?: unknown[]): Promise<QueryExecResult> {
|
||||
await this.initializeDatabase();
|
||||
if (!this.db) {
|
||||
throw new Error("Database not initialized");
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await this.db.query(sql, params || []);
|
||||
const values = result.values || [];
|
||||
return {
|
||||
columns: [], // SQLite plugin doesn't provide column names in query result
|
||||
values: values as SqlValue[][],
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error("Error executing query:", error);
|
||||
throw new Error(
|
||||
`Database query failed: ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
}
|
||||
// Account Management
|
||||
async getAccounts(): Promise<Account[]> {
|
||||
return await db.accounts.toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* @see PlatformService.dbExec
|
||||
*/
|
||||
async dbExec(
|
||||
sql: string,
|
||||
params?: unknown[],
|
||||
): Promise<{ changes: number; lastId?: number }> {
|
||||
await this.initializeDatabase();
|
||||
if (!this.db) {
|
||||
throw new Error("Database not initialized");
|
||||
}
|
||||
async getAccount(did: string): Promise<Account | undefined> {
|
||||
return await db.accounts.where("did").equals(did).first();
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await this.db.run(sql, params || []);
|
||||
const changes = result.changes as Changes;
|
||||
return {
|
||||
changes: changes?.changes || 0,
|
||||
lastId: changes?.lastId,
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error("Error executing statement:", error);
|
||||
throw new Error(
|
||||
`Database execution failed: ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
}
|
||||
async addAccount(account: Account): Promise<void> {
|
||||
await db.accounts.add(account);
|
||||
}
|
||||
|
||||
// Settings Management
|
||||
async updateMasterSettings(
|
||||
settingsChanges: Partial<Settings>,
|
||||
): Promise<void> {
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
|
||||
async getActiveAccountSettings(): Promise<Settings> {
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
|
||||
async updateAccountSettings(
|
||||
accountDid: string,
|
||||
settingsChanges: Partial<Settings>,
|
||||
): Promise<void> {
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
|
||||
// Contact Management
|
||||
async getContacts(): Promise<Contact[]> {
|
||||
return await db.contacts.toArray();
|
||||
}
|
||||
|
||||
async getAllContacts(): Promise<Contact[]> {
|
||||
return await this.getContacts();
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user