refactor: remove keytar dependency and simplify token storage fallback mechanism
Some checks failed
Release Electron App / release-desktop (push) Failing after 8m13s

This commit is contained in:
2026-03-03 23:14:57 +01:00
parent f5d6978e0b
commit 5170f10b64
6 changed files with 28 additions and 89 deletions

View File

@@ -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. **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: **Token storage** (`token.ts`) — two-tier fallback:
1. keytar (OS keychain) — preferred, encrypted per-user 1. electron-store + `safeStorage` — encrypted at rest (preferred)
2. electron-store + `safeStorage` — encrypted at rest 2. Plain electron-store — last resort (e.g. WSL with no keyring)
3. Plain electron-store — WSL fallback
Keytar service name is `'adiuva'`. Once keytar fails, `keytarFailed` flag skips it for the session.
**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. **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.

View File

@@ -15,7 +15,6 @@ import { execSync } from 'node:child_process';
// Keep this list in sync with the Vite external array. // Keep this list in sync with the Vite external array.
const externalPackages = [ const externalPackages = [
'better-sqlite3', 'better-sqlite3',
'keytar',
'@github/copilot-sdk', '@github/copilot-sdk',
'@langchain/core', '@langchain/core',
'@langchain/langgraph', '@langchain/langgraph',
@@ -69,8 +68,9 @@ const config: ForgeConfig = {
env: { ...process.env, npm_config_nodedir: '' }, env: { ...process.env, npm_config_nodedir: '' },
}); });
// When cross-compiling, native addons (better-sqlite3, keytar) are built // When cross-compiling, native addons (better-sqlite3) are built
// for the build machine (Linux). Re-download prebuilts for the target. // for the build machine (Linux). Use @electron/rebuild to get the
// correct binaries for the target platform.
const targetKey = `${platform}-${arch}`; const targetKey = `${platform}-${arch}`;
const buildKey = `${process.platform}-${process.arch}`; const buildKey = `${process.platform}-${process.arch}`;
if (targetKey !== buildKey) { if (targetKey !== buildKey) {
@@ -79,22 +79,27 @@ const config: ForgeConfig = {
fs.readFileSync(path.resolve(__dirname, 'node_modules', 'electron', 'package.json'), 'utf-8'), fs.readFileSync(path.resolve(__dirname, 'node_modules', 'electron', 'package.json'), 'utf-8'),
).version; ).version;
// Use prebuild-install to fetch correct native binaries // Clean stale build artifacts before rebuilding — a leftover
const prebuildModules = ['better-sqlite3', 'keytar']; // build/Release/*.node for the host platform must not survive.
for (const mod of prebuildModules) { const nativeModules = ['better-sqlite3'];
const modDir = path.join(buildPath, 'node_modules', mod); for (const mod of nativeModules) {
if (fs.existsSync(modDir)) { const buildRelease = path.join(buildPath, 'node_modules', mod, 'build', 'Release');
console.log(`[forge] Downloading ${mod} prebuilt for ${targetKey} (Electron ${electronVersion})...`); if (fs.existsSync(buildRelease)) {
try { 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( execSync(
`npx --yes prebuild-install -r electron -t ${electronVersion} --platform ${platform} --arch ${arch} --verbose`, `npx --yes @electron/rebuild --platform ${platform} --arch ${arch} ` +
{ cwd: modDir, stdio: 'inherit' }, `--module-dir "${path.join(buildPath, 'node_modules')}" ` +
`--electron-version ${electronVersion} ` +
`--only better-sqlite3`,
{ cwd: buildPath, stdio: 'inherit' },
); );
} catch (e) {
console.warn(`[forge] prebuild-install failed for ${mod}, trying node-gyp rebuild...`);
}
}
}
} }
// vectordb uses platform-specific optional deps (@lancedb/vectordb-<platform>-<arch>-*). // vectordb uses platform-specific optional deps (@lancedb/vectordb-<platform>-<arch>-*).

18
package-lock.json generated
View File

@@ -32,7 +32,6 @@
"electron-squirrel-startup": "^1.0.1", "electron-squirrel-startup": "^1.0.1",
"electron-store": "^8.2.0", "electron-store": "^8.2.0",
"framer-motion": "^12.34.2", "framer-motion": "^12.34.2",
"keytar": "^7.9.0",
"lucide-react": "^0.575.0", "lucide-react": "^0.575.0",
"radix-ui": "^1.4.3", "radix-ui": "^1.4.3",
"react": "^19.2.4", "react": "^19.2.4",
@@ -16007,17 +16006,6 @@
"node": ">= 12" "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": { "node_modules/keyv": {
"version": "4.5.4", "version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
@@ -18209,12 +18197,6 @@
"node": ">=10" "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": { "node_modules/node-api-version": {
"version": "0.2.1", "version": "0.2.1",
"resolved": "https://registry.npmjs.org/node-api-version/-/node-api-version-0.2.1.tgz", "resolved": "https://registry.npmjs.org/node-api-version/-/node-api-version-0.2.1.tgz",

View File

@@ -71,7 +71,6 @@
"electron-squirrel-startup": "^1.0.1", "electron-squirrel-startup": "^1.0.1",
"electron-store": "^8.2.0", "electron-store": "^8.2.0",
"framer-motion": "^12.34.2", "framer-motion": "^12.34.2",
"keytar": "^7.9.0",
"lucide-react": "^0.575.0", "lucide-react": "^0.575.0",
"radix-ui": "^1.4.3", "radix-ui": "^1.4.3",
"react": "^19.2.4", "react": "^19.2.4",

View File

@@ -2,27 +2,11 @@ import { safeStorage } from 'electron';
import { getStore } from '../store'; import { getStore } from '../store';
/** /**
* Token storage with three-tier fallback: * Token storage with two-tier fallback:
* 1. OS keychain via keytar (best — encrypted, per-user) * 1. Electron safeStorage + electron-store (encrypted at rest)
* 2. Electron safeStorage + electron-store (encrypted at rest) * 2. Plain electron-store (last resort — e.g. WSL with no keyring)
* 3. 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 { function canUseSafeStorage(): boolean {
try { try {
return safeStorage.isEncryptionAvailable(); return safeStorage.isEncryptionAvailable();
@@ -31,8 +15,6 @@ function canUseSafeStorage(): boolean {
} }
} }
const SERVICE_NAME = 'adiuva';
// --- electron-store helpers (with optional safeStorage encryption) --- // --- electron-store helpers (with optional safeStorage encryption) ---
function readFromStore(providerName: string): string | null { function readFromStore(providerName: string): string | null {
@@ -74,41 +56,16 @@ function removeFromStore(providerName: string): void {
/** Read a stored token for the given provider. */ /** Read a stored token for the given provider. */
export async function getToken(providerName: string): Promise<string | null> { export async function getToken(providerName: string): Promise<string | null> {
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); return readFromStore(providerName);
} }
/** Store a token for the given provider. */ /** Store a token for the given provider. */
export async function setToken(providerName: string, token: string): Promise<void> { export async function setToken(providerName: string, token: string): Promise<void> {
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); writeToStore(providerName, token);
} }
/** Delete a stored token for the given provider. */ /** Delete a stored token for the given provider. */
async function deleteToken(providerName: string): Promise<boolean> { async function deleteToken(providerName: string): Promise<boolean> {
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); removeFromStore(providerName);
return true; return true;
} }

View File

@@ -7,7 +7,6 @@ export default defineConfig({
// Externalize native Node modules — they're rebuilt by electron-forge // Externalize native Node modules — they're rebuilt by electron-forge
external: [ external: [
'better-sqlite3', 'better-sqlite3',
'keytar',
'@github/copilot-sdk', '@github/copilot-sdk',
'@github/copilot', '@github/copilot',
// LangChain — externalize to avoid bundling Node.js-specific code // LangChain — externalize to avoid bundling Node.js-specific code