import { NIANCODE_AUTH_GATEWAY_URL } from '../../shared/auth-public'; import { logger } from '../utils/logger'; import type { WorksSquareSecureStorage } from './works-square-session'; const STORE_SCHEMA_VERSION = 1; export type RememberedPassword = Readonly<{ username: string; password: string; }>; export interface RememberedPasswordPersistence { load(): Promise; save(credentials: RememberedPassword | null): Promise; } export type RememberedPasswordState = Readonly<{ available: boolean; credentials: RememberedPassword | null; }>; let persistence: RememberedPasswordPersistence | null = null; let rememberedPassword: RememberedPassword | null = null; let initialization: Promise = Promise.resolve(); function normalizeCredentials(value: unknown): RememberedPassword | null { if (!value || typeof value !== 'object' || Array.isArray(value)) return null; const candidate = value as Record; if ( typeof candidate.username !== 'string' || !candidate.username.trim() || typeof candidate.password !== 'string' || !candidate.password ) { return null; } return { username: candidate.username.trim(), password: candidate.password, }; } async function createElectronPersistence( safeStorage: WorksSquareSecureStorage, ): Promise { const usesUnprotectedLinuxBackend = process.platform === 'linux' && safeStorage.getSelectedStorageBackend() === 'basic_text'; if (!safeStorage.isEncryptionAvailable() || usesUnprotectedLinuxBackend) { logger.warn('[remembered-password] OS credential encryption is unavailable; persistence disabled'); return null; } const { default: Store } = await import('electron-store'); type EncryptedPasswordRecord = { version: number; authBase: string; ciphertext: string; }; const storeOptions = { name: 'remembered-password', configFileMode: 0o600, } as const; let store: Store<{ record?: EncryptedPasswordRecord }>; try { store = new Store<{ record?: EncryptedPasswordRecord }>(storeOptions); } catch (error) { logger.warn('[remembered-password] Invalid credential store config; discarding it', error); store = new Store<{ record?: EncryptedPasswordRecord }>({ ...storeOptions, clearInvalidConfig: true, }); } return { async load() { const record = store.get('record'); if (!record) return null; if ( record.version !== STORE_SCHEMA_VERSION || record.authBase !== NIANCODE_AUTH_GATEWAY_URL ) { store.delete('record'); return null; } return JSON.parse( safeStorage.decryptString(Buffer.from(record.ciphertext, 'base64')), ) as unknown; }, async save(credentials) { if (!credentials) { store.delete('record'); return; } const ciphertext = safeStorage.encryptString(JSON.stringify(credentials)); store.set('record', { version: STORE_SCHEMA_VERSION, authBase: NIANCODE_AUTH_GATEWAY_URL, ciphertext: ciphertext.toString('base64'), }); }, }; } export function initializeRememberedPassword( options: { persistence?: RememberedPasswordPersistence; secureStorage?: WorksSquareSecureStorage | null; } = {}, ): Promise { initialization = (async () => { rememberedPassword = null; try { persistence = options.persistence ?? (options.secureStorage ? await createElectronPersistence(options.secureStorage) : null); } catch (error) { logger.warn('[remembered-password] Failed to initialize secure credential persistence', error); persistence = null; return; } if (!persistence) return; try { const loaded = await persistence.load(); const normalized = normalizeCredentials(loaded); if (loaded && !normalized) { await persistence.save(null); } rememberedPassword = normalized; } catch (error) { logger.warn('[remembered-password] Failed to restore credentials; discarding the record', error); try { await persistence.save(null); } catch (clearError) { logger.warn('[remembered-password] Failed to discard unreadable credentials', clearError); persistence = null; } } })(); return initialization; } export async function getRememberedPasswordState(): Promise { await initialization; return { available: persistence !== null, credentials: rememberedPassword ? { ...rememberedPassword } : null, }; } export async function updateRememberedPassword( credentials: RememberedPassword | null, ): Promise { await initialization; if (!persistence) return false; const normalized = credentials ? normalizeCredentials(credentials) : null; if (credentials && !normalized) return false; try { await persistence.save(normalized); rememberedPassword = normalized; return true; } catch (error) { logger.warn('[remembered-password] Failed to update secure credentials', error); return false; } } export function resetRememberedPasswordForTests(): void { persistence = null; rememberedPassword = null; initialization = Promise.resolve(); }