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.
|
**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.
|
||||||
|
|
||||||
|
|||||||
@@ -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
18
package-lock.json
generated
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user