feat(crypto): explicit device-approval flow + dev userData isolation

Disable the previous auto-share of conversation keys to newly-registered
devices: a stolen password / new device registered by an attacker no
longer automatically grants history access. Backup-Restore (which
restores the old device-id) still opens existing wraps as before.

Phase 1 of the approval replacement:
- New `lib/deviceApproval.ts`: realtime listener for `devices` INSERT,
  surfaces a pending list, persists approve/deny decisions in
  `chatapp.approvedDeviceIds` / `chatapp.dismissedDeviceIds`. Filters the
  initial fetch by created_at > own-device's created_at so a freshly
  installed client doesn't try to "approve" pre-existing devices.
- New `components/DeviceApprovalBanner.tsx`: bottom-right Discord-style
  banner per pending request with Genehmigen / Ablehnen actions; reuses
  `wrapForOneDevice` from conversationKeySync to fan out conv-keys.
- AppShell mounts both the listener and the banner.

Plus dev userData isolation in main.ts: when running unpackaged, append
`-Dev` to the userData path so `pnpm dev` runs side-by-side with the
installed packaged build instead of colliding on the single-instance
lock. Window title also distinguished as "ChatApp (Dev)".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
byGalax
2026-05-07 17:03:04 +02:00
parent 950ef5b706
commit 9b764053c4
5 changed files with 531 additions and 43 deletions
+11 -1
View File
@@ -37,6 +37,16 @@ const __dirnameSafe = path.dirname(__filenameSafe);
const DEV_URL = 'http://localhost:1420';
const WINDOW_STATE_FILE = 'window-state.json';
// Run dev side-by-side with the installed packaged build by isolating the
// renderer profile / secret-store / SQLite / IndexedDB / localStorage in
// a separate userData dir. Without this both share `%APPDATA%\ChatApp`,
// the single-instance lock fires, and `pnpm dev` exits immediately while
// the installed prod app holds the lock. Must run BEFORE the lock check
// below + before any other module reads `app.getPath('userData')`.
if (!app.isPackaged) {
app.setPath('userData', app.getPath('userData') + '-Dev');
}
let mainWindow: BrowserWindow | null = null;
function resolvePreloadPath(): string {
@@ -64,7 +74,7 @@ async function createWindow(): Promise<BrowserWindow> {
const state = await loadState(WINDOW_STATE_FILE);
const win = new BrowserWindow({
title: 'ChatApp',
title: app.isPackaged ? 'ChatApp' : 'ChatApp (Dev)',
width: state.width,
height: state.height,
...(state.x !== undefined ? { x: state.x } : {}),