refactor: remove keytar dependency and simplify token storage fallback mechanism
Some checks failed
Release Electron App / release-desktop (push) Failing after 8m13s
Some checks failed
Release Electron App / release-desktop (push) Failing after 8m13s
This commit is contained in:
@@ -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.
|
||||
|
||||
|
||||
@@ -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-<platform>-<arch>-*).
|
||||
|
||||
18
package-lock.json
generated
18
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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<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);
|
||||
}
|
||||
|
||||
/** Store a token for the given provider. */
|
||||
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);
|
||||
}
|
||||
|
||||
/** Delete a stored token for the given provider. */
|
||||
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);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user