mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-10-26 02:04:37 +08:00
feat(security): 新增桌面端应用锁
新增 4 位 PIN、手动锁定、启动锁定和系统闲置锁定。 新增主进程与 preload 能力、桌面端设置面板、全局锁屏覆盖层及四语言界面。 Fixes #190 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
1 parent
32bd268af4
commit
84b371e73d
17 files changed
+1115
-6
No files matched your search
@@ -10,6 +10,7 @@ import { initAnalytics } from './analytics'
|
||||
import { logger } from './logger'
|
||||
import { prepareDesktopRuntime } from './app/bootstrap'
|
||||
import { createMainWindow, markAppQuitting } from './window/main-window'
|
||||
import { initLockManager, cleanupLockManager } from './security/lock-manager'
|
||||
|
||||
class MainProcess {
|
||||
mainWindow: BrowserWindow | null
|
||||
@@ -104,6 +105,11 @@ class MainProcess {
|
||||
console.log('[Main] Registering IPC handlers...')
|
||||
mainIpcMain(this.mainWindow)
|
||||
console.log('[Main] IPC handlers registered')
|
||||
|
||||
// 初始化应用锁(在 IPC 注册之后)
|
||||
console.log('[Main] Initializing app lock...')
|
||||
initLockManager(this.mainWindow)
|
||||
console.log('[Main] App lock initialized')
|
||||
}
|
||||
|
||||
app.on('browser-window-created', (_, window) => {
|
||||
@@ -144,6 +150,7 @@ class MainProcess {
|
||||
app.on('will-quit', () => {
|
||||
stopInternalServer().catch(() => {})
|
||||
cleanup()
|
||||
cleanupLockManager()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import { registerNetworkHandlers } from './network'
|
||||
import { registerAnalyticsHandlers } from '../analytics'
|
||||
import { registerApiHandlers, initApiServer, cleanupApiServer } from './api'
|
||||
import { registerDemoHandlers } from './demo'
|
||||
import { registerSecurityHandlers } from './security'
|
||||
import { cleanupArchiveImportSources } from '../import/archive-source-runtime'
|
||||
// 导入 Worker 模块(用于异步分析查询和流式导入)
|
||||
import * as worker from '../worker/workerManager'
|
||||
@@ -46,6 +47,7 @@ const mainIpcMain = (win: BrowserWindow) => {
|
||||
registerAnalyticsHandlers()
|
||||
registerApiHandlers(context)
|
||||
registerDemoHandlers(context)
|
||||
registerSecurityHandlers(context)
|
||||
|
||||
// 启动 ChatLab API 服务(异步,不阻塞 IPC 注册)
|
||||
initApiServer(context).catch((err) => {
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { ipcMain } from 'electron'
|
||||
import type { IpcContext } from './types'
|
||||
import {
|
||||
changePassword,
|
||||
getLockConfig,
|
||||
getLockState,
|
||||
lockApp,
|
||||
resetAppLockPassword,
|
||||
setPassword,
|
||||
unlockApp,
|
||||
updateLockConfig,
|
||||
} from '../security/lock-manager'
|
||||
|
||||
export function registerSecurityHandlers(_ctx: IpcContext): void {
|
||||
ipcMain.handle('app-lock:getConfig', getLockConfig)
|
||||
ipcMain.handle('app-lock:getState', getLockState)
|
||||
ipcMain.handle('app-lock:lock', lockApp)
|
||||
ipcMain.handle('app-lock:unlock', (_event, password: unknown) => unlockApp(password))
|
||||
ipcMain.handle('app-lock:setPassword', (_event, password: unknown) => setPassword(password))
|
||||
ipcMain.handle('app-lock:changePassword', (_event, oldPassword: unknown, newPassword: unknown) =>
|
||||
changePassword(oldPassword, newPassword)
|
||||
)
|
||||
ipcMain.handle('app-lock:resetPassword', resetAppLockPassword)
|
||||
ipcMain.handle('app-lock:updateConfig', (_event, updates: unknown) => updateLockConfig(updates))
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import * as crypto from 'node:crypto'
|
||||
|
||||
const PBKDF2_ITERATIONS = 600000
|
||||
const SALT_LENGTH = 32
|
||||
const HASH_LENGTH = 64
|
||||
const HEX_PATTERN = /^[0-9a-f]+$/i
|
||||
|
||||
export interface PasswordHash {
|
||||
hash: string
|
||||
salt: string
|
||||
version: 1
|
||||
}
|
||||
|
||||
export function isPasswordHash(value: unknown): value is PasswordHash {
|
||||
if (!value || typeof value !== 'object') return false
|
||||
const record = value as Record<string, unknown>
|
||||
return (
|
||||
record.version === 1 &&
|
||||
typeof record.hash === 'string' &&
|
||||
record.hash.length === HASH_LENGTH * 2 &&
|
||||
HEX_PATTERN.test(record.hash) &&
|
||||
typeof record.salt === 'string' &&
|
||||
record.salt.length === SALT_LENGTH * 2 &&
|
||||
HEX_PATTERN.test(record.salt)
|
||||
)
|
||||
}
|
||||
|
||||
function derivePassword(password: string, salt: Buffer): Promise<Buffer> {
|
||||
return new Promise((resolve, reject) => {
|
||||
crypto.pbkdf2(password, salt, PBKDF2_ITERATIONS, HASH_LENGTH, 'sha512', (error, hash) => {
|
||||
if (error) reject(error)
|
||||
else resolve(hash)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export async function hashPassword(password: string): Promise<PasswordHash> {
|
||||
const salt = crypto.randomBytes(SALT_LENGTH)
|
||||
const hash = await derivePassword(password, salt)
|
||||
return {
|
||||
hash: hash.toString('hex'),
|
||||
salt: salt.toString('hex'),
|
||||
version: 1,
|
||||
}
|
||||
}
|
||||
|
||||
export async function verifyPasswordHash(password: string, stored: PasswordHash): Promise<boolean> {
|
||||
if (!isPasswordHash(stored)) return false
|
||||
const expected = Buffer.from(stored.hash, 'hex')
|
||||
const actual = await derivePassword(password, Buffer.from(stored.salt, 'hex'))
|
||||
return crypto.timingSafeEqual(actual, expected)
|
||||
}
|
||||
@@ -0,0 +1,336 @@
|
||||
import { powerMonitor, type BrowserWindow } from 'electron'
|
||||
import * as fs from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
import type {
|
||||
AppLockConfig,
|
||||
AppLockConfigUpdate,
|
||||
AppLockResult,
|
||||
AppLockState,
|
||||
AppLockUnlockResult,
|
||||
} from '../../shared/types'
|
||||
import { logger } from '../logger'
|
||||
import { getPathProvider } from '../paths/provider'
|
||||
import { hashPassword, isPasswordHash, verifyPasswordHash, type PasswordHash } from './crypto-utils'
|
||||
|
||||
interface LockSettings {
|
||||
idleTimeoutMinutes: number
|
||||
lockOnStartup: boolean
|
||||
}
|
||||
|
||||
const LOCK_CONFIG_FILE = 'app-lock.json'
|
||||
const LOCK_FLAG_FILE = '.app-lock-flag'
|
||||
const PIN_PATTERN = /^\d{4}$/
|
||||
const IDLE_CHECK_INTERVAL_MS = 10000
|
||||
const IDLE_TIMEOUT_OPTIONS = new Set([0, 1, 3, 5, 10, 15, 30, 60])
|
||||
const DEFAULT_LOCK_SETTINGS: LockSettings = {
|
||||
idleTimeoutMinutes: 0,
|
||||
lockOnStartup: false,
|
||||
}
|
||||
|
||||
let lockState: AppLockState = 'unlocked'
|
||||
let currentSettings: LockSettings = { ...DEFAULT_LOCK_SETTINGS }
|
||||
let storedPasswordHash: PasswordHash | null = null
|
||||
let idleTimer: ReturnType<typeof setInterval> | null = null
|
||||
let isTransitioning = false
|
||||
let mainWindowRef: BrowserWindow | null = null
|
||||
|
||||
function getSettingsDir(): string {
|
||||
return getPathProvider().getSettingsDir()
|
||||
}
|
||||
|
||||
function getLockConfigPath(): string {
|
||||
return join(getSettingsDir(), LOCK_CONFIG_FILE)
|
||||
}
|
||||
|
||||
function getLockFlagPath(): string {
|
||||
return join(getSettingsDir(), LOCK_FLAG_FILE)
|
||||
}
|
||||
|
||||
function isValidIdleTimeout(value: unknown): value is number {
|
||||
return typeof value === 'number' && IDLE_TIMEOUT_OPTIONS.has(value)
|
||||
}
|
||||
|
||||
function loadConfig(): boolean {
|
||||
currentSettings = { ...DEFAULT_LOCK_SETTINGS }
|
||||
storedPasswordHash = null
|
||||
|
||||
try {
|
||||
const configPath = getLockConfigPath()
|
||||
if (!fs.existsSync(configPath)) return false
|
||||
|
||||
const data = JSON.parse(fs.readFileSync(configPath, 'utf-8')) as unknown
|
||||
if (!data || typeof data !== 'object' || Array.isArray(data)) {
|
||||
logger.warn('App lock config ignored because it is not an object')
|
||||
return false
|
||||
}
|
||||
|
||||
const record = data as Record<string, unknown>
|
||||
|
||||
currentSettings = {
|
||||
idleTimeoutMinutes: isValidIdleTimeout(record.idleTimeoutMinutes)
|
||||
? record.idleTimeoutMinutes
|
||||
: DEFAULT_LOCK_SETTINGS.idleTimeoutMinutes,
|
||||
lockOnStartup:
|
||||
typeof record.lockOnStartup === 'boolean' ? record.lockOnStartup : DEFAULT_LOCK_SETTINGS.lockOnStartup,
|
||||
}
|
||||
storedPasswordHash = isPasswordHash(record.passwordHash) ? record.passwordHash : null
|
||||
return true
|
||||
} catch (error) {
|
||||
logger.error(`App lock config ignored because it is invalid: ${error instanceof Error ? error.message : error}`)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function saveConfig(settings: LockSettings, passwordHash: PasswordHash | null): boolean {
|
||||
const configPath = getLockConfigPath()
|
||||
|
||||
try {
|
||||
fs.mkdirSync(getSettingsDir(), { recursive: true })
|
||||
const record: Record<string, unknown> = { ...settings }
|
||||
if (passwordHash) record.passwordHash = passwordHash
|
||||
fs.writeFileSync(configPath, JSON.stringify(record, null, 2), 'utf-8')
|
||||
return true
|
||||
} catch (error) {
|
||||
logger.error(`Failed to save app lock config: ${error instanceof Error ? error.message : error}`)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function setLockFlag(): boolean {
|
||||
try {
|
||||
fs.writeFileSync(getLockFlagPath(), Date.now().toString(), 'utf-8')
|
||||
return true
|
||||
} catch (error) {
|
||||
logger.error(`Failed to write app lock flag: ${error instanceof Error ? error.message : error}`)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function clearLockFlag(): boolean {
|
||||
try {
|
||||
const flagPath = getLockFlagPath()
|
||||
if (fs.existsSync(flagPath)) fs.unlinkSync(flagPath)
|
||||
return true
|
||||
} catch (error) {
|
||||
logger.error(`Failed to clear app lock flag: ${error instanceof Error ? error.message : error}`)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function hasLockFlag(): boolean {
|
||||
try {
|
||||
return fs.existsSync(getLockFlagPath())
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function isEnabled(): boolean {
|
||||
return storedPasswordHash !== null
|
||||
}
|
||||
|
||||
function notifyLockState(): void {
|
||||
if (mainWindowRef && !mainWindowRef.isDestroyed()) {
|
||||
mainWindowRef.webContents.send('app-lock-state-changed', lockState === 'locked')
|
||||
}
|
||||
}
|
||||
|
||||
export function initLockManager(mainWindow: BrowserWindow): void {
|
||||
mainWindowRef = mainWindow
|
||||
lockState = 'unlocked'
|
||||
isTransitioning = false
|
||||
|
||||
const loaded = loadConfig()
|
||||
if (!loaded || !isEnabled()) clearLockFlag()
|
||||
const wasLocked = isEnabled() && hasLockFlag()
|
||||
|
||||
logger.info(`App lock initialized: enabled=${isEnabled()}, wasLocked=${wasLocked}`)
|
||||
|
||||
if (isEnabled() && (currentSettings.lockOnStartup || wasLocked)) {
|
||||
lockApp()
|
||||
} else if (isEnabled() && currentSettings.idleTimeoutMinutes > 0) {
|
||||
startIdleTimer()
|
||||
}
|
||||
}
|
||||
|
||||
export function lockApp(): AppLockResult {
|
||||
if (lockState === 'locked') return { success: true }
|
||||
if (isTransitioning) return { success: false, error: 'busy' }
|
||||
if (!isEnabled()) return { success: false, error: 'disabled' }
|
||||
|
||||
isTransitioning = true
|
||||
try {
|
||||
if (!setLockFlag()) return { success: false, error: 'save-failed' }
|
||||
lockState = 'locked'
|
||||
stopIdleTimer()
|
||||
logger.info('App locked')
|
||||
notifyLockState()
|
||||
return { success: true }
|
||||
} finally {
|
||||
isTransitioning = false
|
||||
}
|
||||
}
|
||||
|
||||
export async function unlockApp(password: unknown): Promise<AppLockUnlockResult> {
|
||||
if (lockState === 'unlocked') return { success: true }
|
||||
if (isTransitioning) return { success: false, error: 'busy' }
|
||||
if (!storedPasswordHash) return { success: false, error: 'password-not-set' }
|
||||
if (typeof password !== 'string' || !PIN_PATTERN.test(password)) {
|
||||
return { success: false, error: 'wrong-password', wrongPassword: true }
|
||||
}
|
||||
|
||||
isTransitioning = true
|
||||
try {
|
||||
if (!(await verifyPasswordHash(password, storedPasswordHash))) {
|
||||
return { success: false, error: 'wrong-password', wrongPassword: true }
|
||||
}
|
||||
|
||||
clearLockFlag()
|
||||
lockState = 'unlocked'
|
||||
logger.info('App unlocked')
|
||||
notifyLockState()
|
||||
if (currentSettings.idleTimeoutMinutes > 0) startIdleTimer()
|
||||
return { success: true }
|
||||
} finally {
|
||||
isTransitioning = false
|
||||
}
|
||||
}
|
||||
|
||||
export async function setPassword(newPassword: unknown): Promise<AppLockResult> {
|
||||
if (lockState === 'locked') return { success: false, error: 'locked' }
|
||||
if (isTransitioning) return { success: false, error: 'busy' }
|
||||
if (typeof newPassword !== 'string' || !PIN_PATTERN.test(newPassword)) {
|
||||
return { success: false, error: 'invalid-pin' }
|
||||
}
|
||||
if (storedPasswordHash) return { success: false, error: 'password-already-set' }
|
||||
|
||||
isTransitioning = true
|
||||
try {
|
||||
const passwordHash = await hashPassword(newPassword)
|
||||
if (!saveConfig(currentSettings, passwordHash)) return { success: false, error: 'save-failed' }
|
||||
storedPasswordHash = passwordHash
|
||||
if (currentSettings.idleTimeoutMinutes > 0) startIdleTimer()
|
||||
logger.info('App lock password initialized')
|
||||
return { success: true }
|
||||
} catch (error) {
|
||||
logger.error(`Failed to set app lock password: ${error instanceof Error ? error.message : error}`)
|
||||
return { success: false, error: 'internal-error' }
|
||||
} finally {
|
||||
isTransitioning = false
|
||||
}
|
||||
}
|
||||
|
||||
export async function changePassword(oldPassword: unknown, newPassword: unknown): Promise<AppLockResult> {
|
||||
if (lockState === 'locked') return { success: false, error: 'locked' }
|
||||
if (isTransitioning) return { success: false, error: 'busy' }
|
||||
if (!storedPasswordHash) return { success: false, error: 'password-not-set' }
|
||||
if (typeof oldPassword !== 'string' || typeof newPassword !== 'string' || !PIN_PATTERN.test(newPassword)) {
|
||||
return { success: false, error: 'invalid-pin' }
|
||||
}
|
||||
if (oldPassword === newPassword) return { success: false, error: 'same-password' }
|
||||
isTransitioning = true
|
||||
|
||||
try {
|
||||
if (!(await verifyPasswordHash(oldPassword, storedPasswordHash))) {
|
||||
return { success: false, error: 'wrong-password' }
|
||||
}
|
||||
const passwordHash = await hashPassword(newPassword)
|
||||
if (!saveConfig(currentSettings, passwordHash)) return { success: false, error: 'save-failed' }
|
||||
storedPasswordHash = passwordHash
|
||||
logger.info('App lock password changed')
|
||||
return { success: true }
|
||||
} catch (error) {
|
||||
logger.error(`Failed to change app lock password: ${error instanceof Error ? error.message : error}`)
|
||||
return { success: false, error: 'internal-error' }
|
||||
} finally {
|
||||
isTransitioning = false
|
||||
}
|
||||
}
|
||||
|
||||
export function resetAppLockPassword(): AppLockResult {
|
||||
if (lockState === 'locked') return { success: false, error: 'locked' }
|
||||
if (isTransitioning) return { success: false, error: 'busy' }
|
||||
if (!saveConfig(currentSettings, null)) return { success: false, error: 'save-failed' }
|
||||
|
||||
storedPasswordHash = null
|
||||
stopIdleTimer()
|
||||
clearLockFlag()
|
||||
logger.info('App lock disabled and password cleared')
|
||||
return { success: true }
|
||||
}
|
||||
|
||||
export function getLockConfig(): AppLockConfig {
|
||||
return {
|
||||
enabled: isEnabled(),
|
||||
...currentSettings,
|
||||
}
|
||||
}
|
||||
|
||||
function parseConfigUpdate(value: unknown): AppLockConfigUpdate | null {
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value)) return null
|
||||
const record = value as Record<string, unknown>
|
||||
if (Object.keys(record).some((key) => key !== 'idleTimeoutMinutes' && key !== 'lockOnStartup')) return null
|
||||
|
||||
const updates: AppLockConfigUpdate = {}
|
||||
if ('idleTimeoutMinutes' in record) {
|
||||
if (!isValidIdleTimeout(record.idleTimeoutMinutes)) return null
|
||||
updates.idleTimeoutMinutes = record.idleTimeoutMinutes
|
||||
}
|
||||
if ('lockOnStartup' in record) {
|
||||
if (typeof record.lockOnStartup !== 'boolean') return null
|
||||
updates.lockOnStartup = record.lockOnStartup
|
||||
}
|
||||
return updates
|
||||
}
|
||||
|
||||
export function updateLockConfig(value: unknown): AppLockResult & { config?: AppLockConfig } {
|
||||
if (isTransitioning) return { success: false, error: 'busy' }
|
||||
const updates = parseConfigUpdate(value)
|
||||
if (!updates) return { success: false, error: 'invalid-config' }
|
||||
|
||||
const nextSettings = { ...currentSettings, ...updates }
|
||||
if (!saveConfig(nextSettings, storedPasswordHash)) return { success: false, error: 'save-failed' }
|
||||
|
||||
currentSettings = nextSettings
|
||||
if (isEnabled() && currentSettings.idleTimeoutMinutes > 0) startIdleTimer()
|
||||
else stopIdleTimer()
|
||||
|
||||
logger.info(`App lock config updated: ${JSON.stringify(updates)}`)
|
||||
return { success: true, config: getLockConfig() }
|
||||
}
|
||||
|
||||
export function getLockState(): AppLockState {
|
||||
return lockState
|
||||
}
|
||||
|
||||
function checkSystemIdle(): void {
|
||||
if (!isEnabled() || lockState === 'locked' || currentSettings.idleTimeoutMinutes <= 0) return
|
||||
|
||||
try {
|
||||
if (powerMonitor.getSystemIdleTime() >= currentSettings.idleTimeoutMinutes * 60) {
|
||||
logger.info(`App idle timeout reached (${currentSettings.idleTimeoutMinutes} min)`)
|
||||
lockApp()
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`Failed to read system idle time: ${error instanceof Error ? error.message : error}`)
|
||||
stopIdleTimer()
|
||||
}
|
||||
}
|
||||
|
||||
function startIdleTimer(): void {
|
||||
stopIdleTimer()
|
||||
idleTimer = setInterval(checkSystemIdle, IDLE_CHECK_INTERVAL_MS)
|
||||
checkSystemIdle()
|
||||
}
|
||||
|
||||
function stopIdleTimer(): void {
|
||||
if (!idleTimer) return
|
||||
clearInterval(idleTimer)
|
||||
idleTimer = null
|
||||
}
|
||||
|
||||
export function cleanupLockManager(): void {
|
||||
stopIdleTimer()
|
||||
mainWindowRef = null
|
||||
logger.info('App lock manager cleaned up')
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { ipcRenderer } from 'electron'
|
||||
import type { SecurityApi } from '../../shared/types'
|
||||
|
||||
export const securityApi: SecurityApi = {
|
||||
getConfig: () => ipcRenderer.invoke('app-lock:getConfig'),
|
||||
getState: () => ipcRenderer.invoke('app-lock:getState'),
|
||||
lock: () => ipcRenderer.invoke('app-lock:lock'),
|
||||
unlock: (password) => ipcRenderer.invoke('app-lock:unlock', password),
|
||||
setPassword: (password) => ipcRenderer.invoke('app-lock:setPassword', password),
|
||||
changePassword: (oldPassword, newPassword) => ipcRenderer.invoke('app-lock:changePassword', oldPassword, newPassword),
|
||||
resetPassword: () => ipcRenderer.invoke('app-lock:resetPassword'),
|
||||
updateConfig: (updates) => ipcRenderer.invoke('app-lock:updateConfig', updates),
|
||||
onLockStateChanged: (callback) => {
|
||||
const handler = (_event: Electron.IpcRendererEvent, locked: boolean) => callback(locked)
|
||||
ipcRenderer.on('app-lock-state-changed', handler)
|
||||
return () => ipcRenderer.removeListener('app-lock-state-changed', handler)
|
||||
},
|
||||
}
|
||||
Vendored
+3
-1
@@ -1,6 +1,6 @@
|
||||
import { ElectronAPI } from '@electron-toolkit/preload'
|
||||
import type { ImportProgress, ExportProgress } from '../../../src/types/base'
|
||||
import type { TokenUsage, AgentRuntimeStatus, SerializedErrorInfo } from '../shared/types'
|
||||
import type { TokenUsage, AgentRuntimeStatus, SerializedErrorInfo, SecurityApi } from '../shared/types'
|
||||
import type { TimeFilter } from '@openchatlab/shared-types'
|
||||
|
||||
// 迁移相关类型
|
||||
@@ -299,6 +299,7 @@ declare global {
|
||||
networkApi: NetworkApi
|
||||
apiServerApi: ApiServerApi
|
||||
internalApi: InternalApi
|
||||
securityApi: SecurityApi
|
||||
}
|
||||
}
|
||||
|
||||
@@ -334,4 +335,5 @@ export {
|
||||
ApiServerStatus,
|
||||
InternalApi,
|
||||
InternalEndpoint,
|
||||
SecurityApi,
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import { aiApi } from './apis/ai'
|
||||
import { networkApi, cacheApi } from './apis/utils'
|
||||
import { apiServerApi } from './apis/api-server'
|
||||
import { internalApi } from './apis/internal-api'
|
||||
import { securityApi } from './apis/security'
|
||||
|
||||
// 为渲染进程提供统一的类型入口,避免 type-only import 指向无导出的运行时代码。
|
||||
export type { PreprocessConfig } from './apis/ai'
|
||||
@@ -41,6 +42,7 @@ if (process.contextIsolated) {
|
||||
contextBridge.exposeInMainWorld('networkApi', networkApi)
|
||||
contextBridge.exposeInMainWorld('apiServerApi', apiServerApi)
|
||||
contextBridge.exposeInMainWorld('internalApi', internalApi)
|
||||
contextBridge.exposeInMainWorld('securityApi', securityApi)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
@@ -61,4 +63,6 @@ if (process.contextIsolated) {
|
||||
window.apiServerApi = apiServerApi
|
||||
// @ts-ignore (define in dts)
|
||||
window.internalApi = internalApi
|
||||
// @ts-ignore (define in dts)
|
||||
window.securityApi = securityApi
|
||||
}
|
||||
@@ -41,3 +41,50 @@ export interface AgentRuntimeStatus {
|
||||
totalUsage: TokenUsage
|
||||
updatedAt: number
|
||||
}
|
||||
|
||||
export type AppLockState = 'unlocked' | 'locked'
|
||||
|
||||
export type AppLockErrorCode =
|
||||
| 'busy'
|
||||
| 'locked'
|
||||
| 'disabled'
|
||||
| 'invalid-pin'
|
||||
| 'password-already-set'
|
||||
| 'password-not-set'
|
||||
| 'same-password'
|
||||
| 'wrong-password'
|
||||
| 'save-failed'
|
||||
| 'invalid-config'
|
||||
| 'internal-error'
|
||||
|
||||
export interface AppLockConfig {
|
||||
enabled: boolean
|
||||
idleTimeoutMinutes: number
|
||||
lockOnStartup: boolean
|
||||
}
|
||||
|
||||
export interface AppLockConfigUpdate {
|
||||
idleTimeoutMinutes?: number
|
||||
lockOnStartup?: boolean
|
||||
}
|
||||
|
||||
export interface AppLockResult {
|
||||
success: boolean
|
||||
error?: AppLockErrorCode
|
||||
}
|
||||
|
||||
export interface AppLockUnlockResult extends AppLockResult {
|
||||
wrongPassword?: boolean
|
||||
}
|
||||
|
||||
export interface SecurityApi {
|
||||
getConfig: () => Promise<AppLockConfig>
|
||||
getState: () => Promise<AppLockState>
|
||||
lock: () => Promise<AppLockResult>
|
||||
unlock: (password: string) => Promise<AppLockUnlockResult>
|
||||
setPassword: (newPassword: string) => Promise<AppLockResult>
|
||||
changePassword: (oldPassword: string, newPassword: string) => Promise<AppLockResult>
|
||||
resetPassword: () => Promise<AppLockResult>
|
||||
updateConfig: (updates: AppLockConfigUpdate) => Promise<AppLockResult & { config?: AppLockConfig }>
|
||||
onLockStateChanged: (callback: (locked: boolean) => void) => () => void
|
||||
}
|
||||
Reference in new issue
Block a user