115 lines
3.3 KiB
TypeScript
115 lines
3.3 KiB
TypeScript
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)
|
|
*/
|
|
|
|
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();
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
const SERVICE_NAME = 'adiuva';
|
|
|
|
// --- electron-store helpers (with optional safeStorage encryption) ---
|
|
|
|
function readFromStore(providerName: string): string | null {
|
|
const tokens = getStore().get('encryptedTokens');
|
|
const stored = tokens[providerName];
|
|
if (!stored) return null;
|
|
|
|
if (canUseSafeStorage()) {
|
|
try {
|
|
return safeStorage.decryptString(Buffer.from(stored, 'base64'));
|
|
} catch {
|
|
// Stored value might be plaintext from a previous fallback
|
|
return stored;
|
|
}
|
|
}
|
|
// No encryption available — value is stored as plaintext
|
|
return stored;
|
|
}
|
|
|
|
function writeToStore(providerName: string, token: string): void {
|
|
let value: string;
|
|
if (canUseSafeStorage()) {
|
|
value = safeStorage.encryptString(token).toString('base64');
|
|
} else {
|
|
// Last resort: store plaintext (WSL with no keyring)
|
|
value = token;
|
|
}
|
|
const tokens = getStore().get('encryptedTokens');
|
|
getStore().set('encryptedTokens', { ...tokens, [providerName]: value });
|
|
}
|
|
|
|
function removeFromStore(providerName: string): void {
|
|
const tokens = getStore().get('encryptedTokens');
|
|
const { [providerName]: _, ...rest } = tokens;
|
|
getStore().set('encryptedTokens', rest);
|
|
}
|
|
|
|
// --- public API ---
|
|
|
|
/** 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;
|
|
}
|