From 5170f10b641004ab30ca6f6d46d6903aa06f9371 Mon Sep 17 00:00:00 2001 From: roberto Date: Tue, 3 Mar 2026 23:14:57 +0100 Subject: [PATCH] refactor: remove keytar dependency and simplify token storage fallback mechanism --- .claude/CLAUDE.md | 9 +++----- forge.config.ts | 39 ++++++++++++++++++++--------------- package-lock.json | 18 ---------------- package.json | 1 - src/main/ai/token.ts | 49 +++----------------------------------------- vite.main.config.mts | 1 - 6 files changed, 28 insertions(+), 89 deletions(-) diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index aea9e9d..8874721 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -82,12 +82,9 @@ Tool-calling strategy differs by provider: OpenAI/Anthropic use LangChain `bindT **Provider factory** (`llm.ts`): `gpt-4o-mini` (OpenAI), `claude-sonnet-4-20250514` (Anthropic), or ChatCopilot wrapper — all with `temperature: 0.3` and streaming enabled. -**Token storage** (`token.ts`) — three-tier fallback: -1. keytar (OS keychain) — preferred, encrypted per-user -2. electron-store + `safeStorage` — encrypted at rest -3. Plain electron-store — WSL fallback - -Keytar service name is `'adiuva'`. Once keytar fails, `keytarFailed` flag skips it for the session. +**Token storage** (`token.ts`) — two-tier fallback: +1. electron-store + `safeStorage` — encrypted at rest (preferred) +2. Plain electron-store — last resort (e.g. WSL with no keyring) **AI approval pattern**: Tasks and checkpoints have `isAiSuggested` (bool) and `isApproved` (bool) columns. AI-suggested items appear in the UI pending user approval before being treated as real records. diff --git a/forge.config.ts b/forge.config.ts index edb499e..76c2ae7 100644 --- a/forge.config.ts +++ b/forge.config.ts @@ -15,7 +15,6 @@ import { execSync } from 'node:child_process'; // Keep this list in sync with the Vite external array. const externalPackages = [ 'better-sqlite3', - 'keytar', '@github/copilot-sdk', '@langchain/core', '@langchain/langgraph', @@ -69,8 +68,9 @@ const config: ForgeConfig = { env: { ...process.env, npm_config_nodedir: '' }, }); - // When cross-compiling, native addons (better-sqlite3, keytar) are built - // for the build machine (Linux). Re-download prebuilts for the target. + // When cross-compiling, native addons (better-sqlite3) are built + // for the build machine (Linux). Use @electron/rebuild to get the + // correct binaries for the target platform. const targetKey = `${platform}-${arch}`; const buildKey = `${process.platform}-${process.arch}`; if (targetKey !== buildKey) { @@ -79,22 +79,27 @@ const config: ForgeConfig = { fs.readFileSync(path.resolve(__dirname, 'node_modules', 'electron', 'package.json'), 'utf-8'), ).version; - // Use prebuild-install to fetch correct native binaries - const prebuildModules = ['better-sqlite3', 'keytar']; - for (const mod of prebuildModules) { - const modDir = path.join(buildPath, 'node_modules', mod); - if (fs.existsSync(modDir)) { - console.log(`[forge] Downloading ${mod} prebuilt for ${targetKey} (Electron ${electronVersion})...`); - try { - execSync( - `npx --yes prebuild-install -r electron -t ${electronVersion} --platform ${platform} --arch ${arch} --verbose`, - { cwd: modDir, stdio: 'inherit' }, - ); - } catch (e) { - console.warn(`[forge] prebuild-install failed for ${mod}, trying node-gyp rebuild...`); - } + // Clean stale build artifacts before rebuilding — a leftover + // build/Release/*.node for the host platform must not survive. + const nativeModules = ['better-sqlite3']; + for (const mod of nativeModules) { + const buildRelease = path.join(buildPath, 'node_modules', mod, 'build', 'Release'); + if (fs.existsSync(buildRelease)) { + fs.rmSync(buildRelease, { recursive: true, force: true }); + console.log(`[forge] Cleaned stale build/Release for ${mod}`); } } + + // Rebuild native modules for target platform using @electron/rebuild. + // This is fatal — a Linux .node in a Windows package is always broken. + console.log(`[forge] Rebuilding native modules for ${targetKey} (Electron ${electronVersion})...`); + execSync( + `npx --yes @electron/rebuild --platform ${platform} --arch ${arch} ` + + `--module-dir "${path.join(buildPath, 'node_modules')}" ` + + `--electron-version ${electronVersion} ` + + `--only better-sqlite3`, + { cwd: buildPath, stdio: 'inherit' }, + ); } // vectordb uses platform-specific optional deps (@lancedb/vectordb---*). diff --git a/package-lock.json b/package-lock.json index 966c182..5e421c6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,7 +32,6 @@ "electron-squirrel-startup": "^1.0.1", "electron-store": "^8.2.0", "framer-motion": "^12.34.2", - "keytar": "^7.9.0", "lucide-react": "^0.575.0", "radix-ui": "^1.4.3", "react": "^19.2.4", @@ -16007,17 +16006,6 @@ "node": ">= 12" } }, - "node_modules/keytar": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/keytar/-/keytar-7.9.0.tgz", - "integrity": "sha512-VPD8mtVtm5JNtA2AErl6Chp06JBfy7diFQ7TQQhdpWOl6MrCRB+eRbvAZUsbGQS9kiMq0coJsy0W0vHpDCkWsQ==", - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "node-addon-api": "^4.3.0", - "prebuild-install": "^7.0.1" - } - }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -18209,12 +18197,6 @@ "node": ">=10" } }, - "node_modules/node-addon-api": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.3.0.tgz", - "integrity": "sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==", - "license": "MIT" - }, "node_modules/node-api-version": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/node-api-version/-/node-api-version-0.2.1.tgz", diff --git a/package.json b/package.json index c4bdad2..214267f 100644 --- a/package.json +++ b/package.json @@ -71,7 +71,6 @@ "electron-squirrel-startup": "^1.0.1", "electron-store": "^8.2.0", "framer-motion": "^12.34.2", - "keytar": "^7.9.0", "lucide-react": "^0.575.0", "radix-ui": "^1.4.3", "react": "^19.2.4", diff --git a/src/main/ai/token.ts b/src/main/ai/token.ts index d26e8b9..19ab01c 100644 --- a/src/main/ai/token.ts +++ b/src/main/ai/token.ts @@ -2,27 +2,11 @@ import { safeStorage } from 'electron'; import { getStore } from '../store'; /** - * Token storage with three-tier fallback: - * 1. OS keychain via keytar (best — encrypted, per-user) - * 2. Electron safeStorage + electron-store (encrypted at rest) - * 3. Plain electron-store (last resort — e.g. WSL with no keyring) + * Token storage with two-tier fallback: + * 1. Electron safeStorage + electron-store (encrypted at rest) + * 2. Plain electron-store (last resort — e.g. WSL with no keyring) */ -let keytar: typeof import('keytar') | null = null; -let keytarFailed = false; - -try { - // eslint-disable-next-line @typescript-eslint/no-require-imports - keytar = require('keytar') as typeof import('keytar'); -} catch { - keytarFailed = true; - console.log('[Token] keytar native module unavailable'); -} - -function useKeytar(): boolean { - return keytar !== null && !keytarFailed; -} - function canUseSafeStorage(): boolean { try { return safeStorage.isEncryptionAvailable(); @@ -31,8 +15,6 @@ function canUseSafeStorage(): boolean { } } -const SERVICE_NAME = 'adiuva'; - // --- electron-store helpers (with optional safeStorage encryption) --- function readFromStore(providerName: string): string | null { @@ -74,41 +56,16 @@ function removeFromStore(providerName: string): void { /** Read a stored token for the given provider. */ export async function getToken(providerName: string): Promise { - if (useKeytar()) { - try { - return await keytar!.getPassword(SERVICE_NAME, providerName); - } catch (err) { - console.log('[Token] keytar runtime error, falling back:', (err as Error).message); - keytarFailed = true; - } - } return readFromStore(providerName); } /** Store a token for the given provider. */ export async function setToken(providerName: string, token: string): Promise { - if (useKeytar()) { - try { - await keytar!.setPassword(SERVICE_NAME, providerName, token); - return; - } catch (err) { - console.log('[Token] keytar runtime error, falling back:', (err as Error).message); - keytarFailed = true; - } - } writeToStore(providerName, token); } /** Delete a stored token for the given provider. */ async function deleteToken(providerName: string): Promise { - if (useKeytar()) { - try { - return await keytar!.deletePassword(SERVICE_NAME, providerName); - } catch (err) { - console.log('[Token] keytar runtime error, falling back:', (err as Error).message); - keytarFailed = true; - } - } removeFromStore(providerName); return true; } diff --git a/vite.main.config.mts b/vite.main.config.mts index c8ffe97..56a4813 100644 --- a/vite.main.config.mts +++ b/vite.main.config.mts @@ -7,7 +7,6 @@ export default defineConfig({ // Externalize native Node modules — they're rebuilt by electron-forge external: [ 'better-sqlite3', - 'keytar', '@github/copilot-sdk', '@github/copilot', // LangChain — externalize to avoid bundling Node.js-specific code