feat(security): 前端状态迁移至无明文密钥存储

This commit is contained in:
tianqijiuyun-latiao
2026-04-03 08:06:43 +08:00
parent 263db6bf30
commit c842201bf4
9 changed files with 460 additions and 62 deletions

View File

@@ -17,6 +17,8 @@ import { blurToFilter, normalizeBlurForPlatform, normalizeOpacityForPlatform, is
import { getMacNativeTitlebarPaddingLeft, getMacNativeTitlebarPaddingRight, shouldHandleMacNativeFullscreenShortcut, shouldSuppressMacNativeEscapeExit } from './utils/macWindow';
import { buildOverlayWorkbenchTheme } from './utils/overlayWorkbenchTheme';
import { getConnectionWorkbenchState } from './utils/startupReadiness';
import { createGlobalProxyDraft, toSaveGlobalProxyInput } from './utils/globalProxyDraft';
import { LEGACY_PERSIST_KEY, readLegacyPersistedSecrets, stripLegacyPersistedSecrets } from './utils/legacyConnectionStorage';
import {
SHORTCUT_ACTION_META,
SHORTCUT_ACTION_ORDER,
@@ -35,7 +37,7 @@ import {
resolveAIEdgeHandleDockStyle,
resolveAIEdgeHandleStyle,
} from './utils/aiEntryLayout';
import { ConfigureGlobalProxy, SetMacNativeWindowControls, SetWindowTranslucency } from '../wailsjs/go/app/App';
import { SetMacNativeWindowControls, SetWindowTranslucency } from '../wailsjs/go/app/App';
import './App.css';
const { Sider, Content } = Layout;
@@ -76,6 +78,8 @@ function App() {
const setStartupFullscreen = useStore(state => state.setStartupFullscreen);
const globalProxy = useStore(state => state.globalProxy);
const setGlobalProxy = useStore(state => state.setGlobalProxy);
const replaceConnections = useStore(state => state.replaceConnections);
const replaceGlobalProxy = useStore(state => state.replaceGlobalProxy);
const shortcutOptions = useStore(state => state.shortcutOptions);
const updateShortcut = useStore(state => state.updateShortcut);
const resetShortcutOptions = useStore(state => state.resetShortcutOptions);
@@ -100,14 +104,14 @@ function App() {
const [runtimePlatform, setRuntimePlatform] = useState('');
const [isLinuxRuntime, setIsLinuxRuntime] = useState(false);
const [isStoreHydrated, setIsStoreHydrated] = useState(() => useStore.persist.hasHydrated());
const [hasAppliedInitialGlobalProxy, setHasAppliedInitialGlobalProxy] = useState(false);
const [hasLoadedSecureConfig, setHasLoadedSecureConfig] = useState(false);
const sidebarWidth = useStore(state => state.sidebarWidth);
const setSidebarWidth = useStore(state => state.setSidebarWidth);
const aiPanelVisible = useStore(state => state.aiPanelVisible);
const toggleAIPanel = useStore(state => state.toggleAIPanel);
const setAIPanelVisible = useStore(state => state.setAIPanelVisible);
const globalProxyInvalidHintShownRef = React.useRef(false);
const connectionWorkbenchState = getConnectionWorkbenchState(isStoreHydrated, hasAppliedInitialGlobalProxy);
const connectionWorkbenchState = getConnectionWorkbenchState(isStoreHydrated, hasLoadedSecureConfig);
// 同步 macOS 窗口透明度opacity=1.0 且 blur=0 时关闭 NSVisualEffectView
// 避免 GPU 持续计算窗口背后的模糊合成
@@ -167,6 +171,90 @@ function App() {
return;
}
let cancelled = false;
const loadSecureConfig = async () => {
const backendApp = (window as any).go?.app?.App;
const persistedPayload = typeof window !== 'undefined'
? window.localStorage.getItem(LEGACY_PERSIST_KEY)
: null;
const legacy = readLegacyPersistedSecrets(persistedPayload);
let importedLegacyConnections = false;
let importedLegacyGlobalProxy = false;
if (legacy.connections.length > 0) {
if (typeof backendApp?.ImportLegacyConnections === 'function') {
try {
await backendApp.ImportLegacyConnections(
legacy.connections.map(({ id, name, config }) => ({ id, name, config }))
);
importedLegacyConnections = true;
} catch (err) {
console.warn('Failed to import legacy saved connections', err);
}
} else {
replaceConnections(legacy.connections);
}
}
if (legacy.globalProxy) {
if (typeof backendApp?.ImportLegacyGlobalProxy === 'function') {
try {
await backendApp.ImportLegacyGlobalProxy(toSaveGlobalProxyInput(legacy.globalProxy));
importedLegacyGlobalProxy = true;
} catch (err) {
console.warn('Failed to import legacy global proxy', err);
}
} else {
replaceGlobalProxy(createGlobalProxyDraft(legacy.globalProxy));
}
}
if ((importedLegacyConnections || importedLegacyGlobalProxy) && persistedPayload && typeof window !== 'undefined') {
const sanitizedPayload = stripLegacyPersistedSecrets(persistedPayload);
if (sanitizedPayload && sanitizedPayload !== persistedPayload) {
window.localStorage.setItem(LEGACY_PERSIST_KEY, sanitizedPayload);
}
}
if (typeof backendApp?.GetSavedConnections === 'function') {
try {
const savedConnections = await backendApp.GetSavedConnections();
if (!cancelled && Array.isArray(savedConnections)) {
replaceConnections(savedConnections);
}
} catch (err) {
console.warn('Failed to load saved connections from backend', err);
}
}
if (typeof backendApp?.GetGlobalProxyConfig === 'function') {
try {
const proxyResult = await backendApp.GetGlobalProxyConfig();
if (!cancelled && proxyResult?.success && proxyResult.data) {
replaceGlobalProxy(createGlobalProxyDraft(proxyResult.data));
}
} catch (err) {
console.warn('Failed to load global proxy from backend', err);
}
}
if (!cancelled) {
setHasLoadedSecureConfig(true);
}
};
void loadSecureConfig();
return () => {
cancelled = true;
};
}, [isStoreHydrated, replaceConnections, replaceGlobalProxy]);
useEffect(() => {
if (!isStoreHydrated || !hasLoadedSecureConfig) {
return;
}
const host = String(globalProxy.host || '').trim();
const port = Number(globalProxy.port);
const portValid = Number.isFinite(port) && port > 0 && port <= 65535;
@@ -180,57 +268,44 @@ function App() {
});
globalProxyInvalidHintShownRef.current = true;
}
} else {
globalProxyInvalidHintShownRef.current = false;
void message.destroy('global-proxy-invalid');
return;
}
const enabledForBackend = globalProxy.enabled && !invalidWhenEnabled;
let cancelled = false;
try {
ConfigureGlobalProxy(enabledForBackend, {
type: globalProxy.type,
host,
port: portValid ? port : (globalProxy.type === 'http' ? 8080 : 1080),
user: String(globalProxy.user || '').trim(),
password: globalProxy.password || '',
})
.then((res) => {
if (cancelled || res?.success) {
return;
}
void message.error({
content: '全局代理配置失败: ' + (res?.message || '未知错误'),
key: 'global-proxy-sync-error',
});
})
.catch((err) => {
if (cancelled) {
return;
}
const errMsg = err instanceof Error ? err.message : String(err || '未知错误');
void message.error({
content: '全局代理配置失败: ' + errMsg,
key: 'global-proxy-sync-error',
});
})
.finally(() => {
if (!cancelled) {
setHasAppliedInitialGlobalProxy(true);
}
});
} catch (e) {
if (!cancelled) {
setHasAppliedInitialGlobalProxy(true);
}
console.warn("Wails API: ConfigureGlobalProxy unavailable", e);
globalProxyInvalidHintShownRef.current = false;
void message.destroy('global-proxy-invalid');
const backendApp = (window as any).go?.app?.App;
if (typeof backendApp?.SaveGlobalProxy !== 'function') {
return;
}
let cancelled = false;
Promise.resolve(
backendApp.SaveGlobalProxy(
toSaveGlobalProxyInput({
...globalProxy,
host,
port: portValid ? port : (globalProxy.type === 'http' ? 8080 : 1080),
})
)
)
.catch((err) => {
if (cancelled) {
return;
}
const errMsg = err instanceof Error ? err.message : String(err || '未知错误');
void message.error({
content: '全局代理配置失败: ' + errMsg,
key: 'global-proxy-sync-error',
});
});
return () => {
cancelled = true;
};
}, [
isStoreHydrated,
hasLoadedSecureConfig,
globalProxy.enabled,
globalProxy.type,
globalProxy.host,
@@ -2490,3 +2565,5 @@ function App() {
}
export default App;

View File

@@ -1,6 +1,6 @@
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
import { ConnectionConfig, ProxyConfig, SavedConnection, TabData, SavedQuery, ConnectionTag, AIChatMessage, AIContextItem } from './types';
import { ConnectionConfig, ProxyConfig, SavedConnection, TabData, SavedQuery, ConnectionTag, AIChatMessage, AIContextItem, GlobalProxyConfig } from './types';
import {
ShortcutAction,
ShortcutBinding,
@@ -9,6 +9,7 @@ import {
cloneShortcutOptions,
sanitizeShortcutOptions,
} from './utils/shortcuts';
import { toPersistedGlobalProxy } from './utils/globalProxyDraft';
const DEFAULT_APPEARANCE = { enabled: true, opacity: 1.0, blur: 0, useNativeMacWindowControls: false };
const DEFAULT_UI_SCALE = 1.0;
@@ -34,6 +35,7 @@ const DEFAULT_GLOBAL_PROXY: GlobalProxyConfig = {
port: 1080,
user: '',
password: '',
hasPassword: false,
};
const SUPPORTED_CONNECTION_TYPES = new Set([
'mysql',
@@ -246,6 +248,7 @@ const sanitizeConnectionConfig = (value: unknown): ConnectionConfig => {
const safeConfig: ConnectionConfig & Record<string, unknown> = {
...raw,
id: toTrimmedString(raw.id ?? raw.ID),
type,
host: toTrimmedString(raw.host, 'localhost') || 'localhost',
port: normalizePort(raw.port, defaultPort),
@@ -321,7 +324,16 @@ const sanitizeSavedConnection = (value: unknown, index: number): SavedConnection
return {
id,
name,
config,
config: { ...config, id: config.id || id },
secretRef: toTrimmedString(raw.secretRef) || undefined,
hasPrimaryPassword: raw.hasPrimaryPassword === true,
hasSSHPassword: raw.hasSSHPassword === true,
hasProxyPassword: raw.hasProxyPassword === true,
hasHttpTunnelPassword: raw.hasHttpTunnelPassword === true,
hasMySQLReplicaPassword: raw.hasMySQLReplicaPassword === true,
hasMongoReplicaPassword: raw.hasMongoReplicaPassword === true,
hasOpaqueURI: raw.hasOpaqueURI === true,
hasOpaqueDSN: raw.hasOpaqueDSN === true,
includeDatabases: includeDatabases.length > 0 ? includeDatabases : undefined,
includeRedisDatabases: includeRedisDatabases.length > 0 ? includeRedisDatabases : undefined,
};
@@ -393,10 +405,6 @@ export interface QueryOptions {
showColumnType: boolean;
}
export interface GlobalProxyConfig extends ProxyConfig {
enabled: boolean;
}
interface AppState {
connections: SavedConnection[];
connectionTags: ConnectionTag[];
@@ -440,6 +448,7 @@ interface AppState {
addConnection: (conn: SavedConnection) => void;
updateConnection: (conn: SavedConnection) => void;
removeConnection: (id: string) => void;
replaceConnections: (connections: SavedConnection[]) => void;
addConnectionTag: (tag: ConnectionTag) => void;
updateConnectionTag: (tag: ConnectionTag) => void;
@@ -468,6 +477,7 @@ interface AppState {
setFontSize: (size: number) => void;
setStartupFullscreen: (enabled: boolean) => void;
setGlobalProxy: (proxy: Partial<GlobalProxyConfig>) => void;
replaceGlobalProxy: (proxy: Partial<GlobalProxyConfig>) => void;
setSqlFormatOptions: (options: { keywordCase: 'upper' | 'lower' }) => void;
setQueryOptions: (options: Partial<QueryOptions>) => void;
updateShortcut: (action: ShortcutAction, binding: Partial<ShortcutBinding>) => void;
@@ -618,18 +628,24 @@ const sanitizeFontSize = (value: unknown): number => {
return normalizeIntegerInRange(value, DEFAULT_FONT_SIZE, MIN_FONT_SIZE, MAX_FONT_SIZE);
};
const sanitizeGlobalProxy = (value: unknown): GlobalProxyConfig => {
const sanitizeGlobalProxy = (
value: unknown,
options: { allowPassword?: boolean } = {}
): GlobalProxyConfig => {
const raw = (value && typeof value === 'object') ? value as Record<string, unknown> : {};
const typeRaw = toTrimmedString(raw.type, DEFAULT_GLOBAL_PROXY.type).toLowerCase();
const type: 'socks5' | 'http' = typeRaw === 'http' ? 'http' : 'socks5';
const fallbackPort = type === 'http' ? 8080 : 1080;
const password = toTrimmedString(raw.password);
return {
enabled: raw.enabled === true,
type,
host: toTrimmedString(raw.host),
port: normalizePort(raw.port, fallbackPort),
user: toTrimmedString(raw.user),
password: toTrimmedString(raw.password),
password: options.allowPassword === false ? '' : password,
hasPassword: raw.hasPassword === true || password !== '',
secretRef: toTrimmedString(raw.secretRef) || undefined,
};
};
@@ -782,6 +798,7 @@ export const useStore = create<AppState>()(
connectionIds: tag.connectionIds.filter(cid => cid !== id)
}))
})),
replaceConnections: (connections) => set({ connections: sanitizeConnections(connections) }),
addConnectionTag: (tag) => set((state) => ({ connectionTags: [...state.connectionTags, tag] })),
updateConnectionTag: (tag) => set((state) => ({
@@ -963,6 +980,7 @@ export const useStore = create<AppState>()(
setFontSize: (size) => set({ fontSize: sanitizeFontSize(size) }),
setStartupFullscreen: (enabled) => set({ startupFullscreen: !!enabled }),
setGlobalProxy: (proxy) => set((state) => ({ globalProxy: sanitizeGlobalProxy({ ...state.globalProxy, ...proxy }) })),
replaceGlobalProxy: (proxy) => set({ globalProxy: sanitizeGlobalProxy({ ...DEFAULT_GLOBAL_PROXY, ...proxy }) }),
setSqlFormatOptions: (options) => set({ sqlFormatOptions: options }),
setQueryOptions: (options) => set((state) => ({ queryOptions: { ...state.queryOptions, ...options } })),
updateShortcut: (action, binding) => set((state) => ({
@@ -1203,7 +1221,7 @@ export const useStore = create<AppState>()(
migrate: (persistedState: unknown, version: number) => {
const state = unwrapPersistedAppState(persistedState) as Partial<AppState>;
const nextState: Partial<AppState> = { ...state };
nextState.connections = sanitizeConnections(state.connections);
nextState.connections = [];
if (version < 5) {
nextState.connectionTags = sanitizeConnectionTags(state.connectionTags);
} else {
@@ -1215,7 +1233,7 @@ export const useStore = create<AppState>()(
nextState.uiScale = sanitizeUiScale(state.uiScale);
nextState.fontSize = sanitizeFontSize(state.fontSize);
nextState.startupFullscreen = sanitizeStartupFullscreen(state.startupFullscreen);
nextState.globalProxy = sanitizeGlobalProxy(state.globalProxy);
nextState.globalProxy = sanitizeGlobalProxy(state.globalProxy, { allowPassword: false });
nextState.sqlFormatOptions = sanitizeSqlFormatOptions(state.sqlFormatOptions);
nextState.queryOptions = sanitizeQueryOptions(state.queryOptions);
nextState.shortcutOptions = sanitizeShortcutOptions(state.shortcutOptions);
@@ -1242,7 +1260,7 @@ export const useStore = create<AppState>()(
return {
...currentState,
...state,
connections: sanitizeConnections(state.connections),
connections: currentState.connections,
connectionTags: sanitizeConnectionTags(state.connectionTags),
savedQueries: sanitizeSavedQueries(state.savedQueries),
theme: sanitizeTheme(state.theme),
@@ -1250,7 +1268,7 @@ export const useStore = create<AppState>()(
uiScale: sanitizeUiScale(state.uiScale),
fontSize: sanitizeFontSize(state.fontSize),
startupFullscreen: sanitizeStartupFullscreen(state.startupFullscreen),
globalProxy: sanitizeGlobalProxy(state.globalProxy),
globalProxy: sanitizeGlobalProxy(state.globalProxy, { allowPassword: false }),
tableSortPreference: sanitizeTableSortPreference(state.tableSortPreference),
tableColumnOrders: sanitizeTableColumnOrders(state.tableColumnOrders),
enableColumnOrderMemory: state.enableColumnOrderMemory !== false,
@@ -1271,7 +1289,6 @@ export const useStore = create<AppState>()(
};
},
partialize: (state) => ({
connections: state.connections,
connectionTags: state.connectionTags,
savedQueries: state.savedQueries,
theme: state.theme,
@@ -1279,7 +1296,7 @@ export const useStore = create<AppState>()(
uiScale: state.uiScale,
fontSize: state.fontSize,
startupFullscreen: state.startupFullscreen,
globalProxy: state.globalProxy,
globalProxy: toPersistedGlobalProxy(state.globalProxy),
sqlFormatOptions: state.sqlFormatOptions,
queryOptions: state.queryOptions,
shortcutOptions: state.shortcutOptions,
@@ -1298,3 +1315,5 @@ export const useStore = create<AppState>()(
}
)
);

View File

@@ -22,6 +22,7 @@ export interface HTTPTunnelConfig {
}
export interface ConnectionConfig {
id?: string;
type: string;
host: string;
port: number;
@@ -70,12 +71,27 @@ export interface SavedConnection {
id: string;
name: string;
config: ConnectionConfig;
secretRef?: string;
hasPrimaryPassword?: boolean;
hasSSHPassword?: boolean;
hasProxyPassword?: boolean;
hasHttpTunnelPassword?: boolean;
hasMySQLReplicaPassword?: boolean;
hasMongoReplicaPassword?: boolean;
hasOpaqueURI?: boolean;
hasOpaqueDSN?: boolean;
includeDatabases?: string[];
includeRedisDatabases?: number[]; // Redis databases to show (0-15)
iconType?: string; // 自定义图标类型(如 'mysql','postgres'),不填则取 config.type
iconColor?: string; // 自定义图标颜色(十六进制),不填则取类型默认色
}
export interface GlobalProxyConfig extends ProxyConfig {
enabled: boolean;
hasPassword?: boolean;
secretRef?: string;
}
export interface ConnectionTag {
id: string;
name: string;
@@ -243,3 +259,5 @@ export interface AISafetyResult {
requiresConfirm: boolean;
warningMessage?: string;
}

View File

@@ -0,0 +1,35 @@
import { describe, expect, it } from 'vitest';
import { createGlobalProxyDraft, toPersistedGlobalProxy } from './globalProxyDraft';
describe('global proxy draft', () => {
it('hydrates a secretless draft from backend metadata while keeping password input blank', () => {
const draft = createGlobalProxyDraft({
enabled: true,
type: 'http',
host: '127.0.0.1',
port: 8080,
user: 'ops',
hasPassword: true,
password: 'should-be-ignored',
});
expect(draft.password).toBe('');
expect(draft.hasPassword).toBe(true);
});
it('drops password from persisted metadata but preserves hasPassword', () => {
const persisted = toPersistedGlobalProxy({
enabled: true,
type: 'http',
host: '127.0.0.1',
port: 8080,
user: 'ops',
password: 'proxy-secret',
hasPassword: true,
});
expect('password' in persisted).toBe(false);
expect(persisted.hasPassword).toBe(true);
});
});

View File

@@ -0,0 +1,62 @@
import { GlobalProxyConfig } from '../types';
const toTrimmedString = (value: unknown): string => {
if (typeof value === 'string') {
return value.trim();
}
if (typeof value === 'number' || typeof value === 'boolean') {
return String(value).trim();
}
return '';
};
const normalizeProxyType = (value: unknown): 'socks5' | 'http' => {
return toTrimmedString(value).toLowerCase() === 'http' ? 'http' : 'socks5';
};
const normalizePort = (value: unknown, fallbackPort: number): number => {
const parsed = Number(value);
if (!Number.isFinite(parsed)) {
return fallbackPort;
}
const port = Math.trunc(parsed);
if (port <= 0 || port > 65535) {
return fallbackPort;
}
return port;
};
export function createGlobalProxyDraft(value: Partial<GlobalProxyConfig> = {}): GlobalProxyConfig {
const type = normalizeProxyType(value.type);
return {
enabled: value.enabled === true,
type,
host: toTrimmedString(value.host),
port: normalizePort(value.port, type === 'http' ? 8080 : 1080),
user: toTrimmedString(value.user),
password: '',
hasPassword: value.hasPassword === true,
secretRef: toTrimmedString(value.secretRef) || undefined,
};
}
export function toPersistedGlobalProxy(value: Partial<GlobalProxyConfig> = {}): Omit<GlobalProxyConfig, 'password'> {
const draft = createGlobalProxyDraft(value);
return {
enabled: draft.enabled,
type: draft.type,
host: draft.host,
port: draft.port,
user: draft.user,
hasPassword: draft.hasPassword,
secretRef: draft.secretRef,
};
}
export function toSaveGlobalProxyInput(value: Partial<GlobalProxyConfig> = {}): GlobalProxyConfig {
const draft = createGlobalProxyDraft(value);
return {
...draft,
password: typeof value.password === 'string' ? value.password : '',
};
}

View File

@@ -0,0 +1,75 @@
import { describe, expect, it } from 'vitest';
import { readLegacyPersistedSecrets, stripLegacyPersistedSecrets } from './legacyConnectionStorage';
describe('legacy connection storage', () => {
it('extracts legacy saved connections and global proxy password from lite-db-storage', () => {
const payload = JSON.stringify({
state: {
connections: [
{
id: 'conn-1',
name: 'Primary',
config: {
id: 'conn-1',
type: 'postgres',
host: 'db.local',
port: 5432,
user: 'postgres',
password: 'secret',
},
},
],
globalProxy: {
enabled: true,
type: 'http',
host: '127.0.0.1',
port: 8080,
user: 'ops',
password: 'proxy-secret',
},
},
});
const result = readLegacyPersistedSecrets(payload);
expect(result.connections).toHaveLength(1);
expect(result.connections[0]?.config.password).toBe('secret');
expect(result.globalProxy?.password).toBe('proxy-secret');
});
it('strips persisted connection secrets but keeps secretless proxy metadata', () => {
const payload = JSON.stringify({
state: {
connections: [
{
id: 'conn-1',
name: 'Primary',
config: {
id: 'conn-1',
type: 'postgres',
host: 'db.local',
port: 5432,
user: 'postgres',
password: 'secret',
},
},
],
globalProxy: {
enabled: true,
type: 'http',
host: '127.0.0.1',
port: 8080,
user: 'ops',
password: 'proxy-secret',
},
},
});
const sanitized = stripLegacyPersistedSecrets(payload);
const parsed = JSON.parse(sanitized);
expect(parsed.state.connections).toEqual([]);
expect(parsed.state.globalProxy.password).toBeUndefined();
expect(parsed.state.globalProxy.hasPassword).toBe(true);
});
});

View File

@@ -0,0 +1,110 @@
import { GlobalProxyConfig, SavedConnection } from '../types';
export const LEGACY_PERSIST_KEY = 'lite-db-storage';
const toTrimmedString = (value: unknown): string => {
if (typeof value === 'string') {
return value.trim();
}
if (typeof value === 'number' || typeof value === 'boolean') {
return String(value).trim();
}
return '';
};
const normalizeProxyType = (value: unknown): 'socks5' | 'http' => {
return toTrimmedString(value).toLowerCase() === 'http' ? 'http' : 'socks5';
};
const normalizePort = (value: unknown, fallbackPort: number): number => {
const parsed = Number(value);
if (!Number.isFinite(parsed)) {
return fallbackPort;
}
const port = Math.trunc(parsed);
if (port <= 0 || port > 65535) {
return fallbackPort;
}
return port;
};
const parsePersistedEnvelope = (payload: string | null | undefined): Record<string, unknown> => {
if (!payload || typeof payload !== 'string') {
return {};
}
try {
const parsed = JSON.parse(payload) as Record<string, unknown>;
if (parsed.state && typeof parsed.state === 'object') {
return parsed.state as Record<string, unknown>;
}
return parsed;
} catch {
return {};
}
};
export function readLegacyPersistedSecrets(payload: string | null | undefined): {
connections: SavedConnection[];
globalProxy: GlobalProxyConfig | null;
} {
const state = parsePersistedEnvelope(payload);
const connections = Array.isArray(state.connections)
? state.connections.filter((item): item is SavedConnection => !!item && typeof item === 'object')
: [];
const proxyRaw = state.globalProxy && typeof state.globalProxy === 'object'
? state.globalProxy as Record<string, unknown>
: null;
if (!proxyRaw) {
return { connections, globalProxy: null };
}
const type = normalizeProxyType(proxyRaw.type);
const password = toTrimmedString(proxyRaw.password);
const globalProxy: GlobalProxyConfig = {
enabled: proxyRaw.enabled === true,
type,
host: toTrimmedString(proxyRaw.host),
port: normalizePort(proxyRaw.port, type === 'http' ? 8080 : 1080),
user: toTrimmedString(proxyRaw.user),
password,
hasPassword: proxyRaw.hasPassword === true || password !== '',
secretRef: toTrimmedString(proxyRaw.secretRef) || undefined,
};
const hasMeaningfulProxyState = globalProxy.enabled || globalProxy.host !== '' || globalProxy.user !== '' || globalProxy.password !== '' || globalProxy.hasPassword === true;
return {
connections,
globalProxy: hasMeaningfulProxyState ? globalProxy : null,
};
}
export function stripLegacyPersistedSecrets(payload: string | null | undefined): string {
if (!payload || typeof payload !== 'string') {
return '';
}
let parsed: Record<string, unknown>;
try {
parsed = JSON.parse(payload) as Record<string, unknown>;
} catch {
return payload;
}
const state = parsed.state && typeof parsed.state === 'object'
? parsed.state as Record<string, unknown>
: parsed;
state.connections = [];
if (state.globalProxy && typeof state.globalProxy === 'object') {
const proxy = { ...(state.globalProxy as Record<string, unknown>) };
const password = toTrimmedString(proxy.password);
delete proxy.password;
if (password !== '') {
proxy.hasPassword = true;
}
state.globalProxy = proxy;
}
return JSON.stringify(parsed);
}

View File

@@ -10,10 +10,10 @@ describe('startup readiness helpers', () => {
});
});
it('keeps sidebar blocked until initial global proxy sync finishes', () => {
it('keeps sidebar blocked until secure config bootstrap finishes', () => {
expect(getConnectionWorkbenchState(true, false)).toEqual({
ready: false,
message: '正在同步全局代理配置...',
message: '正在加载安全配置...',
});
});
@@ -24,3 +24,4 @@ describe('startup readiness helpers', () => {
});
});
});

View File

@@ -16,7 +16,7 @@ export function getConnectionWorkbenchState(
if (!hasAppliedInitialGlobalProxy) {
return {
ready: false,
message: '正在同步全局代理配置...',
message: '正在加载安全配置...',
};
}
return {
@@ -24,3 +24,4 @@ export function getConnectionWorkbenchState(
message: '',
};
}