feat(desktop): port v0.11.4-v0.15.2 from Tauri to Electron + Discord-parity audio (v0.16.0)
Chronological port of every Tauri release commit (v0.11.4 -> v0.15.2)
into the Electron rebuild, plus Discord-style audio handling that goes
beyond the Tauri original.
Highlights:
- All 17 Tauri release commits ported (audio fixes, custom notification
sound, Discord-style chat UX, profile banner, changelog page,
Discord-parity call UX, screen-share echo + re-watch UX, audio-loop
fix).
- Native napi-rs audio-loopback addon with WASAPI process-loopback:
* EXCLUDE_TARGET_PROCESS_TREE for full-screen shares -> peers
never hear themselves echoed back through the capture.
* INCLUDE_TARGET_PROCESS_TREE for window shares -> only the
picked window's audio is captured, not the whole OS mixer
(Discord parity).
* HWND -> PID resolution via Win32 GetWindowThreadProcessId.
- Discord-style screen-source picker (thumbnail grid, screens vs
apps tabs, live-refreshing thumbnails).
- Hash routing fix for packaged builds (file:// can't resolve
BrowserRouter paths).
- Tauri sources removed (apps/desktop/src-tauri).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,143 @@
|
||||
// Per-user secure key/value store. Replaces Tauri's plugin-stronghold +
|
||||
// custom file vault with Electron's safeStorage (DPAPI on Windows,
|
||||
// Keychain on macOS, libsecret on Linux). Encryption is at the file
|
||||
// level — the whole entries map is a single encrypted blob — so there's
|
||||
// no per-set ciphertext rotation to track.
|
||||
//
|
||||
// Pre-encrypt JSON: {version:1, entries: { <key>: <utf8-string-value> }}.
|
||||
// When safeStorage is unavailable we degrade to a `.plaintext` JSON file
|
||||
// and flag `encrypted: false` back to the renderer so the renderer can
|
||||
// warn the user and avoid long-lived secrets.
|
||||
|
||||
import { app, ipcMain, safeStorage } from 'electron';
|
||||
import { createHash } from 'node:crypto';
|
||||
import { promises as fs } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
import {
|
||||
CHANNELS,
|
||||
type SecureStoreHandle,
|
||||
type SecureStoreOpenArgs,
|
||||
} from '../ipc-types';
|
||||
|
||||
interface HandleState {
|
||||
filePath: string;
|
||||
encrypted: boolean;
|
||||
entries: Map<string, string>;
|
||||
saveTimer: NodeJS.Timeout | null;
|
||||
}
|
||||
|
||||
const handles = new Map<string, HandleState>();
|
||||
|
||||
function hashUserId(userId: string): string {
|
||||
return createHash('sha256').update(userId).digest('hex').slice(0, 16);
|
||||
}
|
||||
|
||||
function filePathFor(userId: string, encrypted: boolean): string {
|
||||
const suffix = hashUserId(userId);
|
||||
const ext = encrypted ? 'bin' : 'plaintext';
|
||||
return path.join(app.getPath('userData'), `chatapp-secure-${suffix}.${ext}`);
|
||||
}
|
||||
|
||||
async function loadState(filePath: string, encrypted: boolean): Promise<Map<string, string>> {
|
||||
try {
|
||||
if (encrypted) {
|
||||
const buf = await fs.readFile(filePath);
|
||||
const json = safeStorage.decryptString(buf);
|
||||
const parsed = JSON.parse(json) as { version?: number; entries?: Record<string, string> };
|
||||
return new Map(Object.entries(parsed.entries ?? {}));
|
||||
} else {
|
||||
const raw = await fs.readFile(filePath, 'utf8');
|
||||
const parsed = JSON.parse(raw) as { version?: number; entries?: Record<string, string> };
|
||||
return new Map(Object.entries(parsed.entries ?? {}));
|
||||
}
|
||||
} catch {
|
||||
// Missing file or malformed contents — start fresh. The next write
|
||||
// will overwrite with a fresh blob.
|
||||
return new Map();
|
||||
}
|
||||
}
|
||||
|
||||
async function writeStateNow(state: HandleState): Promise<void> {
|
||||
const obj: Record<string, string> = {};
|
||||
for (const [k, v] of state.entries) obj[k] = v;
|
||||
const serialised = JSON.stringify({ version: 1, entries: obj });
|
||||
await fs.mkdir(path.dirname(state.filePath), { recursive: true });
|
||||
if (state.encrypted) {
|
||||
const buf = safeStorage.encryptString(serialised);
|
||||
await fs.writeFile(state.filePath, buf);
|
||||
} else {
|
||||
await fs.writeFile(state.filePath, serialised, 'utf8');
|
||||
}
|
||||
}
|
||||
|
||||
function scheduleSave(state: HandleState): void {
|
||||
if (state.saveTimer) clearTimeout(state.saveTimer);
|
||||
state.saveTimer = setTimeout(() => {
|
||||
state.saveTimer = null;
|
||||
void writeStateNow(state).catch((err: unknown) => {
|
||||
console.warn('[secure-store] persist failed', err);
|
||||
});
|
||||
}, 100);
|
||||
}
|
||||
|
||||
function requireState(handle: string): HandleState {
|
||||
const s = handles.get(handle);
|
||||
if (!s) throw new Error('secure-store: unknown handle');
|
||||
return s;
|
||||
}
|
||||
|
||||
export function register(): void {
|
||||
ipcMain.handle(
|
||||
CHANNELS.SECURE_STORE_OPEN,
|
||||
async (_evt, args: SecureStoreOpenArgs): Promise<SecureStoreHandle> => {
|
||||
const encrypted = safeStorage.isEncryptionAvailable();
|
||||
const filePath = filePathFor(args.userId, encrypted);
|
||||
const entries = await loadState(filePath, encrypted);
|
||||
const handle = hashUserId(args.userId);
|
||||
handles.set(handle, { filePath, encrypted, entries, saveTimer: null });
|
||||
return { handle, encrypted };
|
||||
},
|
||||
);
|
||||
|
||||
ipcMain.handle(
|
||||
CHANNELS.SECURE_STORE_GET,
|
||||
async (_evt, handle: string, key: string): Promise<string | null> => {
|
||||
const state = requireState(handle);
|
||||
return state.entries.get(key) ?? null;
|
||||
},
|
||||
);
|
||||
|
||||
ipcMain.handle(
|
||||
CHANNELS.SECURE_STORE_SET,
|
||||
async (_evt, handle: string, key: string, value: string): Promise<void> => {
|
||||
const state = requireState(handle);
|
||||
state.entries.set(key, value);
|
||||
scheduleSave(state);
|
||||
},
|
||||
);
|
||||
|
||||
ipcMain.handle(
|
||||
CHANNELS.SECURE_STORE_REMOVE,
|
||||
async (_evt, handle: string, key: string): Promise<void> => {
|
||||
const state = requireState(handle);
|
||||
state.entries.delete(key);
|
||||
scheduleSave(state);
|
||||
},
|
||||
);
|
||||
|
||||
ipcMain.handle(CHANNELS.SECURE_STORE_CLOSE, async (_evt, handle: string): Promise<void> => {
|
||||
const state = handles.get(handle);
|
||||
if (!state) return;
|
||||
if (state.saveTimer) {
|
||||
clearTimeout(state.saveTimer);
|
||||
state.saveTimer = null;
|
||||
try {
|
||||
await writeStateNow(state);
|
||||
} catch (err: unknown) {
|
||||
console.warn('[secure-store] close-flush failed', err);
|
||||
}
|
||||
}
|
||||
handles.delete(handle);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user