From 5edb8e347f88864dfdcd28588871cd31a1aeba25 Mon Sep 17 00:00:00 2001 From: iven Date: Sat, 21 Mar 2026 16:38:04 +0800 Subject: [PATCH] docs(plan): add Phase 1 implementation plan Detailed plan for Security + Testing phase (2 weeks): - Task 1.1-1.2: Test framework setup (Vitest) - Task 2.1-2.2: Crypto utilities module - Task 3.1: Secure storage encryption enhancement - Task 4.1: WSS enforcement - Task 5.1: ChatStore unit tests - Task 6.1-6.4: Integration and verification Each task has bite-sized steps with exact commands and expected output. Co-Authored-By: Claude Opus 4.6 --- .../2026-03-21-phase1-security-testing.md | 1299 +++++++++++++++++ 1 file changed, 1299 insertions(+) create mode 100644 docs/superpowers/plans/2026-03-21-phase1-security-testing.md diff --git a/docs/superpowers/plans/2026-03-21-phase1-security-testing.md b/docs/superpowers/plans/2026-03-21-phase1-security-testing.md new file mode 100644 index 0000000..e25af91 --- /dev/null +++ b/docs/superpowers/plans/2026-03-21-phase1-security-testing.md @@ -0,0 +1,1299 @@ +# ZCLAW 架构优化 - Phase 1: 安全 + 测试 实施计划 + +> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** 建立安全基础和测试框架,为后续架构优化奠定基础 + +**Architecture:** 增强 `secure-storage.ts` 添加 AES-GCM 加密回退,增强 `gateway-client.ts` 强制 WSS,建立 Vitest 单元测试框架 + +**Tech Stack:** TypeScript, Vitest, Web Crypto API, AES-GCM + +**Spec Reference:** `docs/superpowers/specs/2026-03-21-architecture-optimization-design.md` + +**Duration:** 2 周 (8 人日) + +--- + +## File Structure + +### New Files +``` +desktop/ +├── src/ +│ └── lib/ +│ └── crypto-utils.ts # 加密工具函数 (新建) +├── tests/ +│ ├── setup.ts # 测试设置文件 (新建) +│ ├── lib/ +│ │ ├── secure-storage.test.ts # 安全存储测试 (新建) +│ │ ├── crypto-utils.test.ts # 加密工具测试 (新建) +│ │ └── gateway-security.test.ts # 网关安全测试 (新建) +│ └── store/ +│ └── chatStore.test.ts # Chat Store 测试 (新建) +└── vitest.config.ts # Vitest 配置 (新建) +``` + +### Modified Files +``` +desktop/ +├── src/ +│ └── lib/ +│ ├── secure-storage.ts # 增强加密回退 +│ └── gateway-client.ts # 强制 WSS +├── package.json # 添加依赖 +└── tests/ # 现有测试目录 +``` + +--- + +## Chunk 1: 测试框架建立 + +### Task 1.1: 安装测试依赖 + +**Files:** +- Modify: `desktop/package.json` + +- [ ] **Step 1: 安装 Vitest 和测试相关依赖** + +Run: +```bash +cd g:/ZClaw_openfang/desktop && pnpm add -D vitest@2.1.8 @testing-library/react@16.1.0 @testing-library/jest-dom@6.6.3 jsdom@25.0.1 +``` + +Expected: 依赖安装成功 + +- [ ] **Step 2: 验证安装** + +Run: +```bash +cd g:/ZClaw_openfang/desktop && pnpm vitest --version +``` + +Expected: 输出 `vitest/2.1.8` + +--- + +### Task 1.2: 创建 Vitest 配置 + +**Files:** +- Create: `desktop/vitest.config.ts` +- Create: `desktop/tests/setup.ts` + +- [ ] **Step 1: 创建 Vitest 配置文件** + +Create `desktop/vitest.config.ts`: + +```typescript +import { defineConfig } from 'vitest/config'; +import react from '@vitejs/plugin-react'; +import path from 'path'; + +export default defineConfig({ + plugins: [react()], + test: { + globals: true, + environment: 'jsdom', + setupFiles: ['./tests/setup.ts'], + include: ['tests/**/*.test.ts', 'tests/**/*.test.tsx'], + exclude: ['tests/e2e/**', 'node_modules/**'], + coverage: { + provider: 'v8', + reporter: ['text', 'json', 'html'], + exclude: [ + 'node_modules/**', + 'tests/**', + '**/*.d.ts', + '**/*.config.*', + 'src/main.tsx', + ], + thresholds: { + lines: 60, + functions: 60, + branches: 60, + statements: 60, + }, + }, + }, + resolve: { + alias: { + '@': path.resolve(__dirname, './src'), + }, + }, +}); +``` + +- [ ] **Step 2: 创建测试设置文件** + +Create `desktop/tests/setup.ts`: + +```typescript +import '@testing-library/jest-dom'; +import { vi } from 'vitest'; + +// Mock Tauri API +vi.mock('@tauri-apps/api/core', () => ({ + invoke: vi.fn(), +})); + +// Mock Tauri runtime check +vi.mock('../src/lib/tauri-gateway', () => ({ + isTauriRuntime: () => false, + getGatewayClient: vi.fn(), +})); + +// Mock localStorage +const localStorageMock = (() => { + let store: Record = {}; + return { + getItem: (key: string) => store[key] || null, + setItem: (key: string, value: string) => { + store[key] = value; + }, + removeItem: (key: string) => { + delete store[key]; + }, + clear: () => { + store = {}; + }, + }; +})(); + +Object.defineProperty(global, 'localStorage', { + value: localStorageMock, +}); + +// Mock crypto.subtle for tests +const cryptoMock = { + subtle: { + encrypt: vi.fn(), + decrypt: vi.fn(), + generateKey: vi.fn(), + deriveKey: vi.fn(), + importKey: vi.fn(), + exportKey: vi.fn(), + }, + getRandomValues: (array: Uint8Array) => { + for (let i = 0; i < array.length; i++) { + array[i] = Math.floor(Math.random() * 256); + } + return array; + }, +}; + +Object.defineProperty(global, 'crypto', { + value: cryptoMock, +}); +``` + +- [ ] **Step 3: 更新 package.json 测试脚本** + +Verify `desktop/package.json` scripts section includes: + +```json +{ + "scripts": { + "test": "vitest run", + "test:watch": "vitest", + "test:coverage": "vitest run --coverage" + } +} +``` + +- [ ] **Step 4: 运行测试验证配置** + +Run: +```bash +cd g:/ZClaw_openfang/desktop && pnpm test +``` + +Expected: 测试运行成功 (可能显示 0 tests,这是正常的) + +- [ ] **Step 5: 提交测试框架配置** + +```bash +cd g:/ZClaw_openfang && git add desktop/vitest.config.ts desktop/tests/setup.ts desktop/package.json +git commit -m "test: add Vitest configuration and setup + +- Add vitest.config.ts with coverage thresholds +- Add tests/setup.ts with mocks for Tauri and crypto +- Update package.json with test dependencies + +Co-Authored-By: Claude Opus 4.6 " +``` + +--- + +## Chunk 2: 加密工具模块 + +### Task 2.1: 创建加密工具函数 + +**Files:** +- Create: `desktop/src/lib/crypto-utils.ts` +- Create: `desktop/tests/lib/crypto-utils.test.ts` + +- [ ] **Step 1: 写失败的测试 - arrayToBase64** + +Create `desktop/tests/lib/crypto-utils.test.ts`: + +```typescript +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { arrayToBase64, base64ToArray, deriveKey, encrypt, decrypt } from '../../src/lib/crypto-utils'; + +describe('crypto-utils', () => { + describe('arrayToBase64', () => { + it('should convert Uint8Array to base64 string', () => { + const arr = new Uint8Array([72, 101, 108, 108, 111]); // "Hello" + const result = arrayToBase64(arr); + expect(result).toBe('SGVsbG8='); + }); + + it('should handle empty array', () => { + const arr = new Uint8Array([]); + const result = arrayToBase64(arr); + expect(result).toBe(''); + }); + }); + + describe('base64ToArray', () => { + it('should convert base64 string to Uint8Array', () => { + const base64 = 'SGVsbG8='; + const result = base64ToArray(base64); + expect(result).toEqual(new Uint8Array([72, 101, 108, 108, 111])); + }); + + it('should handle empty string', () => { + const result = base64ToArray(''); + expect(result).toEqual(new Uint8Array([])); + }); + }); +}); +``` + +- [ ] **Step 2: 运行测试确认失败** + +Run: +```bash +cd g:/ZClaw_openfang/desktop && pnpm test tests/lib/crypto-utils.test.ts +``` + +Expected: FAIL - "Cannot find module '../../src/lib/crypto-utils'" + +- [ ] **Step 3: 实现加密工具函数** + +Create `desktop/src/lib/crypto-utils.ts`: + +```typescript +/** + * Cryptographic utilities for secure storage + * Uses Web Crypto API for AES-GCM encryption + */ + +const SALT = new TextEncoder().encode('zclaw-secure-storage-salt'); +const ITERATIONS = 100000; + +/** + * Convert Uint8Array to base64 string + */ +export function arrayToBase64(array: Uint8Array): string { + if (array.length === 0) return ''; + + let binary = ''; + for (let i = 0; i < array.length; i++) { + binary += String.fromCharCode(array[i]); + } + return btoa(binary); +} + +/** + * Convert base64 string to Uint8Array + */ +export function base64ToArray(base64: string): Uint8Array { + if (!base64) return new Uint8Array([]); + + const binary = atob(base64); + const array = new Uint8Array(binary.length); + for (let i = 0; i < binary.length; i++) { + array[i] = binary.charCodeAt(i); + } + return array; +} + +/** + * Derive an encryption key from a master key + */ +export async function deriveKey( + masterKey: string, + salt: Uint8Array = SALT +): Promise { + const encoder = new TextEncoder(); + const keyMaterial = await crypto.subtle.importKey( + 'raw', + encoder.encode(masterKey), + 'PBKDF2', + false, + ['deriveBits', 'deriveKey'] + ); + + return crypto.subtle.deriveKey( + { + name: 'PBKDF2', + salt, + iterations: ITERATIONS, + hash: 'SHA-256', + }, + keyMaterial, + { name: 'AES-GCM', length: 256 }, + false, + ['encrypt', 'decrypt'] + ); +} + +/** + * Encrypt data using AES-GCM + */ +export async function encrypt( + plaintext: string, + key: CryptoKey +): Promise<{ iv: string; data: string }> { + const encoder = new TextEncoder(); + const iv = crypto.getRandomValues(new Uint8Array(12)); + + const encrypted = await crypto.subtle.encrypt( + { name: 'AES-GCM', iv }, + key, + encoder.encode(plaintext) + ); + + return { + iv: arrayToBase64(iv), + data: arrayToBase64(new Uint8Array(encrypted)), + }; +} + +/** + * Decrypt data using AES-GCM + */ +export async function decrypt( + encrypted: { iv: string; data: string }, + key: CryptoKey +): Promise { + const decoder = new TextDecoder(); + const decrypted = await crypto.subtle.decrypt( + { name: 'AES-GCM', iv: base64ToArray(encrypted.iv) }, + key, + base64ToArray(encrypted.data) + ); + + return decoder.decode(decrypted); +} + +/** + * Generate a random master key for encryption + */ +export function generateMasterKey(): string { + const array = crypto.getRandomValues(new Uint8Array(32)); + return arrayToBase64(array); +} +``` + +- [ ] **Step 4: 运行测试确认通过** + +Run: +```bash +cd g:/ZClaw_openfang/desktop && pnpm test tests/lib/crypto-utils.test.ts +``` + +Expected: PASS - 2 tests + +- [ ] **Step 5: 写失败的测试 - encrypt/decrypt** + +Add to `desktop/tests/lib/crypto-utils.test.ts`: + +```typescript +describe('encrypt and decrypt', () => { + it('should encrypt and decrypt text correctly', async () => { + // Mock crypto.subtle for Node.js environment + const originalCrypto = global.crypto; + + // Use a simple mock that actually works + const key = await crypto.subtle.generateKey( + { name: 'AES-GCM', length: 256 }, + true, + ['encrypt', 'decrypt'] + ); + + const plaintext = 'secret message'; + const encrypted = await encrypt(plaintext, key); + + expect(encrypted.iv).toBeDefined(); + expect(encrypted.data).toBeDefined(); + expect(encrypted.data).not.toBe(plaintext); + + const decrypted = await decrypt(encrypted, key); + expect(decrypted).toBe(plaintext); + }); +}); +``` + +- [ ] **Step 6: 运行测试** + +Run: +```bash +cd g:/ZClaw_openfang/desktop && pnpm test tests/lib/crypto-utils.test.ts +``` + +Expected: PASS - 3 tests + +- [ ] **Step 7: 提交加密工具模块** + +```bash +cd g:/ZClaw_openfang && git add desktop/src/lib/crypto-utils.ts desktop/tests/lib/crypto-utils.test.ts +git commit -m "feat(crypto): add AES-GCM encryption utilities + +- Add arrayToBase64/base64ToArray conversion functions +- Add deriveKey for PBKDF2 key derivation +- Add encrypt/decrypt using AES-GCM +- Add unit tests for all functions + +Co-Authored-By: Claude Opus 4.6 " +``` + +--- + +## Chunk 3: 凭据存储加密增强 + +### Task 3.1: 增强安全存储 + +**Files:** +- Modify: `desktop/src/lib/secure-storage.ts` +- Create: `desktop/tests/lib/secure-storage.test.ts` + +- [ ] **Step 1: 读取现有 secure-storage.ts** + +Read the full file to understand current implementation: +- File: `desktop/src/lib/secure-storage.ts` + +- [ ] **Step 2: 写失败的测试 - 加密回退** + +Create `desktop/tests/lib/secure-storage.test.ts`: + +```typescript +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { secureStorage, isSecureStorageAvailable } from '../../src/lib/secure-storage'; + +// Mock Tauri runtime to return false (non-Tauri environment) +vi.mock('../../src/lib/tauri-gateway', () => ({ + isTauriRuntime: () => false, +})); + +describe('secureStorage', () => { + beforeEach(() => { + localStorage.clear(); + vi.clearAllMocks(); + }); + + describe('encryption fallback', () => { + it('should encrypt data when storing to localStorage', async () => { + const key = 'test-key'; + const value = 'secret-value'; + + await secureStorage.set(key, value); + + // Check that localStorage doesn't contain plaintext + const stored = localStorage.getItem(key); + expect(stored).not.toBe(value); + expect(stored).not.toContain('secret-value'); + }); + + it('should decrypt data when retrieving from localStorage', async () => { + const key = 'test-key'; + const value = 'secret-value'; + + await secureStorage.set(key, value); + const retrieved = await secureStorage.get(key); + + expect(retrieved).toBe(value); + }); + + it('should handle special characters in values', async () => { + const key = 'test-key'; + const value = 'p@ssw0rd!#$%^&*(){}[]|\\:";\'<>?,./~`'; + + await secureStorage.set(key, value); + const retrieved = await secureStorage.get(key); + + expect(retrieved).toBe(value); + }); + }); + + describe('backward compatibility', () => { + it('should read unencrypted legacy data', async () => { + const key = 'legacy-key'; + const value = 'legacy-value'; + + // Simulate legacy unencrypted storage + localStorage.setItem(key, value); + + const retrieved = await secureStorage.get(key); + expect(retrieved).toBe(value); + }); + }); +}); +``` + +- [ ] **Step 3: 运行测试确认失败** + +Run: +```bash +cd g:/ZClaw_openfang/desktop && pnpm test tests/lib/secure-storage.test.ts +``` + +Expected: FAIL - localStorage 存储明文 + +- [ ] **Step 4: 实现加密回退** + +Modify `desktop/src/lib/secure-storage.ts`, add import and enhance the fallback functions: + +```typescript +// Add to imports +import { arrayToBase64, base64ToArray, deriveKey, encrypt, decrypt, generateMasterKey } from './crypto-utils'; + +// Add constant for encrypted key prefix +const ENCRYPTED_PREFIX = 'enc_'; +const MASTER_KEY_NAME = 'zclaw-master-key'; + +// Add helper functions after the existing code + +/** + * Get or create master key for encryption + */ +async function getOrCreateMasterKey(): Promise { + let masterKeyRaw = localStorage.getItem(MASTER_KEY_NAME); + + if (!masterKeyRaw) { + masterKeyRaw = generateMasterKey(); + localStorage.setItem(MASTER_KEY_NAME, masterKeyRaw); + } + + return deriveKey(masterKeyRaw); +} + +/** + * Check if a stored value is encrypted + */ +function isEncrypted(value: string): boolean { + try { + const parsed = JSON.parse(value); + return parsed && typeof parsed.iv === 'string' && typeof parsed.data === 'string'; + } catch { + return false; + } +} + +/** + * Write encrypted data to localStorage + */ +async function writeEncryptedLocalStorage(key: string, value: string): Promise { + if (!value) { + localStorage.removeItem(ENCRYPTED_PREFIX + key); + return; + } + + try { + const cryptoKey = await getOrCreateMasterKey(); + const encrypted = await encrypt(value, cryptoKey); + localStorage.setItem(ENCRYPTED_PREFIX + key, JSON.stringify(encrypted)); + } catch (error) { + console.error('[SecureStorage] Encryption failed:', error); + // Fallback to plaintext if encryption fails + localStorage.setItem(key, value); + } +} + +/** + * Read encrypted data from localStorage + */ +async function readEncryptedLocalStorage(key: string): Promise { + // Try encrypted version first + const encryptedRaw = localStorage.getItem(ENCRYPTED_PREFIX + key); + + if (encryptedRaw && isEncrypted(encryptedRaw)) { + try { + const cryptoKey = await getOrCreateMasterKey(); + const encrypted = JSON.parse(encryptedRaw); + return await decrypt(encrypted, cryptoKey); + } catch (error) { + console.error('[SecureStorage] Decryption failed:', error); + } + } + + // Fallback to unencrypted version for backward compatibility + return localStorage.getItem(key); +} +``` + +- [ ] **Step 5: 更新 secureStorage 接口使用加密函数** + +Modify the `secureStorage` object to use encrypted localStorage: + +```typescript +export const secureStorage = { + async set(key: string, value: string): Promise { + const trimmedValue = value.trim(); + + if (await isSecureStorageAvailable()) { + try { + if (trimmedValue) { + await invoke('secure_store_set', { key, value: trimmedValue }); + } else { + await invoke('secure_store_delete', { key }); + } + // Also write encrypted backup + await writeEncryptedLocalStorage(key, trimmedValue); + return; + } catch (error) { + console.warn('[SecureStorage] Failed to use keyring, falling back to encrypted localStorage:', error); + } + } + + // Fallback to encrypted localStorage + await writeEncryptedLocalStorage(key, trimmedValue); + }, + + async get(key: string): Promise { + if (await isSecureStorageAvailable()) { + try { + const value = await invoke('secure_store_get', { key }); + if (value !== null && value !== undefined && value !== '') { + return value; + } + // Try encrypted fallback for migration + return await readEncryptedLocalStorage(key); + } catch (error) { + console.warn('[SecureStorage] Failed to read from keyring, trying encrypted localStorage:', error); + } + } + + // Fallback to encrypted localStorage + return await readEncryptedLocalStorage(key); + }, + + // ... rest of the methods remain similar +}; +``` + +- [ ] **Step 6: 运行测试确认通过** + +Run: +```bash +cd g:/ZClaw_openfang/desktop && pnpm test tests/lib/secure-storage.test.ts +``` + +Expected: PASS - 4 tests + +- [ ] **Step 7: 提交凭据加密增强** + +```bash +cd g:/ZClaw_openfang && git add desktop/src/lib/secure-storage.ts desktop/tests/lib/secure-storage.test.ts +git commit -m "feat(security): add AES-GCM encryption for localStorage fallback + +- Encrypt credentials before storing in localStorage +- Decrypt on retrieval with automatic fallback +- Backward compatible with existing unencrypted data +- Add comprehensive unit tests + +Security: Credentials are now encrypted using AES-GCM when +OS keyring is unavailable, preventing plaintext exposure. + +Co-Authored-By: Claude Opus 4.6 " +``` + +--- + +## Chunk 4: 强制 WSS 连接 + +### Task 4.1: 增强网关安全 + +**Files:** +- Modify: `desktop/src/lib/gateway-client.ts` +- Create: `desktop/tests/lib/gateway-security.test.ts` + +- [ ] **Step 1: 读取现有 gateway-client.ts WebSocket 连接逻辑** + +Read file sections around WebSocket connection (lines 200-250 based on spec): + +```bash +# View the relevant section +head -n 250 g:/ZClaw_openfang/desktop/src/lib/gateway-client.ts | tail -n 100 +``` + +- [ ] **Step 2: 写失败的测试 - WSS 强制** + +Create `desktop/tests/lib/gateway-security.test.ts`: + +```typescript +import { describe, it, expect, vi } from 'vitest'; + +// Mock WebSocket +class MockWebSocket { + url: string; + readyState: number = WebSocket.CONNECTING; + onopen: (() => void) | null = null; + onerror: ((error: Error) => void) | null = null; + + constructor(url: string) { + this.url = url; + setTimeout(() => { + if (this.onopen) this.onopen(); + }, 0); + } + + close() { + this.readyState = WebSocket.CLOSED; + } +} + +vi.stubGlobal('WebSocket', MockWebSocket); + +describe('WebSocket Security', () => { + describe('isLocalhost', () => { + it('should identify localhost URLs', () => { + const localhostUrls = [ + 'ws://localhost:4200', + 'ws://127.0.0.1:4200', + 'ws://[::1]:4200', + 'wss://localhost:4200', + ]; + + localhostUrls.forEach(url => { + const parsed = new URL(url); + const isLocal = ['localhost', '127.0.0.1', '::1'].includes(parsed.hostname); + expect(isLocal).toBe(true); + }); + }); + + it('should identify non-localhost URLs', () => { + const remoteUrls = [ + 'ws://example.com:4200', + 'ws://192.168.1.1:4200', + 'wss://api.example.com/ws', + ]; + + remoteUrls.forEach(url => { + const parsed = new URL(url); + const isLocal = ['localhost', '127.0.0.1', '::1'].includes(parsed.hostname); + expect(isLocal).toBe(false); + }); + }); + }); + + describe('WSS enforcement', () => { + it('should allow ws:// for localhost', () => { + const url = 'ws://localhost:4200'; + const parsed = new URL(url); + const isSecure = url.startsWith('wss://') || + ['localhost', '127.0.0.1', '::1'].includes(parsed.hostname); + + expect(isSecure).toBe(true); + }); + + it('should reject ws:// for non-localhost', () => { + const url = 'ws://example.com:4200'; + const parsed = new URL(url); + const isSecure = url.startsWith('wss://') || + ['localhost', '127.0.0.1', '::1'].includes(parsed.hostname); + + expect(isSecure).toBe(false); + }); + + it('should allow wss:// for any host', () => { + const urls = [ + 'wss://example.com:4200', + 'wss://api.example.com/ws', + 'wss://192.168.1.1:4200', + ]; + + urls.forEach(url => { + expect(url.startsWith('wss://')).toBe(true); + }); + }); + }); +}); +``` + +- [ ] **Step 3: 运行测试确认通过 (测试逻辑)** + +Run: +```bash +cd g:/ZClaw_openfang/desktop && pnpm test tests/lib/gateway-security.test.ts +``` + +Expected: PASS - 6 tests (这些是纯逻辑测试) + +- [ ] **Step 4: 在 gateway-client.ts 中添加安全检查** + +Add helper function and modify connection logic in `desktop/src/lib/gateway-client.ts`: + +```typescript +// Add near the top of the file, after imports + +/** + * Check if URL points to localhost + */ +function isLocalhost(url: string): boolean { + try { + const parsed = new URL(url); + return ['localhost', '127.0.0.1', '::1'].includes(parsed.hostname); + } catch { + return false; + } +} + +/** + * Validate WebSocket URL security + * @throws SecurityError if non-localhost URL uses ws:// instead of wss:// + */ +function validateWebSocketSecurity(url: string): void { + if (!url.startsWith('wss://') && !isLocalhost(url)) { + throw new SecurityError( + 'Non-localhost connections must use WSS protocol for security. ' + + `URL: ${url.replace(/:[^:@]+@/, ':****@')}` + ); + } +} + +// Add SecurityError class if not already present +class SecurityError extends Error { + constructor(message: string) { + super(message); + this.name = 'SecurityError'; + } +} +``` + +- [ ] **Step 5: 在 connect 方法中调用安全验证** + +Find the `connect` method and add the validation call: + +```typescript +async connect(): Promise { + // Add security validation at the start + try { + validateWebSocketSecurity(this.url); + } catch (error) { + this.log('error', `Security validation failed: ${error}`); + throw error; + } + + // ... rest of existing connect logic +} +``` + +- [ ] **Step 6: 运行现有测试确保无回归** + +Run: +```bash +cd g:/ZClaw_openfang/desktop && pnpm test +``` + +Expected: All tests pass + +- [ ] **Step 7: 提交 WSS 强制策略** + +```bash +cd g:/ZClaw_openfang && git add desktop/src/lib/gateway-client.ts desktop/tests/lib/gateway-security.test.ts +git commit -m "feat(security): enforce WSS for non-localhost connections + +- Add validateWebSocketSecurity function +- Block ws:// connections to non-localhost hosts +- Add SecurityError class for clear error handling +- Add unit tests for security validation logic + +Security: Prevents man-in-the-middle attacks on remote connections +by requiring WSS protocol for all non-localhost WebSocket connections. + +Co-Authored-By: Claude Opus 4.6 " +``` + +--- + +## Chunk 5: ChatStore 基础测试 + +### Task 5.1: 创建 ChatStore 测试套件 + +**Files:** +- Create: `desktop/tests/store/chatStore.test.ts` + +- [ ] **Step 1: 读取 chatStore.ts 了解 API** + +Run: +```bash +head -n 100 g:/ZClaw_openfang/desktop/src/store/chatStore.ts +``` + +- [ ] **Step 2: 写失败的测试 - 基础功能** + +Create `desktop/tests/store/chatStore.test.ts`: + +```typescript +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { useChatStore } from '../../src/store/chatStore'; + +// Mock dependencies +vi.mock('../../src/lib/gateway-client', () => ({ + getGatewayClient: vi.fn(() => ({ + chatStream: vi.fn(), + chat: vi.fn(), + onAgentStream: vi.fn(() => () => {}), + })), +})); + +vi.mock('../../src/lib/intelligence-client', () => ({ + intelligenceClient: { + compactor: { + checkThreshold: vi.fn(() => Promise.resolve({ needsCompaction: false })), + }, + memory: { + search: vi.fn(() => Promise.resolve([])), + }, + reflection: { + shouldReflect: vi.fn(() => Promise.resolve(false)), + }, + }, +})); + +vi.mock('../../src/lib/memory-extractor', () => ({ + getMemoryExtractor: vi.fn(() => ({ + extract: vi.fn(() => Promise.resolve([])), + })), +})); + +describe('chatStore', () => { + beforeEach(() => { + // Reset store state before each test + useChatStore.setState({ + messages: [], + conversations: [], + currentConversationId: null, + isStreaming: false, + error: null, + }); + vi.clearAllMocks(); + }); + + describe('initial state', () => { + it('should have empty messages array', () => { + const state = useChatStore.getState(); + expect(state.messages).toEqual([]); + }); + + it('should not be streaming initially', () => { + const state = useChatStore.getState(); + expect(state.isStreaming).toBe(false); + }); + }); + + describe('addMessage', () => { + it('should add a message to the store', () => { + const { addMessage } = useChatStore.getState(); + const message = { + id: 'test-1', + role: 'user' as const, + content: 'Hello', + timestamp: Date.now(), + }; + + addMessage(message); + + const state = useChatStore.getState(); + expect(state.messages).toHaveLength(1); + expect(state.messages[0]).toEqual(message); + }); + + it('should append message to existing messages', () => { + const { addMessage } = useChatStore.getState(); + + addMessage({ + id: 'test-1', + role: 'user', + content: 'First', + timestamp: Date.now(), + }); + + addMessage({ + id: 'test-2', + role: 'assistant', + content: 'Second', + timestamp: Date.now(), + }); + + const state = useChatStore.getState(); + expect(state.messages).toHaveLength(2); + }); + }); + + describe('setStreaming', () => { + it('should update streaming state', () => { + const { setStreaming } = useChatStore.getState(); + + setStreaming(true); + expect(useChatStore.getState().isStreaming).toBe(true); + + setStreaming(false); + expect(useChatStore.getState().isStreaming).toBe(false); + }); + }); + + describe('clearMessages', () => { + it('should clear all messages', () => { + const { addMessage, clearMessages } = useChatStore.getState(); + + addMessage({ + id: 'test-1', + role: 'user', + content: 'Test', + timestamp: Date.now(), + }); + + clearMessages(); + + expect(useChatStore.getState().messages).toEqual([]); + }); + }); + + describe('error handling', () => { + it('should store error state', () => { + const { setError } = useChatStore.getState(); + const error = new Error('Test error'); + + setError(error); + + expect(useChatStore.getState().error).toBe(error); + }); + + it('should clear error with null', () => { + const { setError } = useChatStore.getState(); + + setError(new Error('Test')); + setError(null); + + expect(useChatStore.getState().error).toBeNull(); + }); + }); +}); +``` + +- [ ] **Step 3: 运行测试** + +Run: +```bash +cd g:/ZClaw_openfang/desktop && pnpm test tests/store/chatStore.test.ts +``` + +Expected: PASS - 8+ tests + +- [ ] **Step 4: 添加更多测试 - 消息更新** + +Add to `desktop/tests/store/chatStore.test.ts`: + +```typescript +describe('updateMessage', () => { + it('should update existing message content', () => { + const { addMessage, updateMessage } = useChatStore.getState(); + + addMessage({ + id: 'test-1', + role: 'assistant', + content: 'Initial', + timestamp: Date.now(), + }); + + updateMessage('test-1', { content: 'Updated' }); + + const state = useChatStore.getState(); + expect(state.messages[0].content).toBe('Updated'); + }); + + it('should not modify message if id not found', () => { + const { addMessage, updateMessage } = useChatStore.getState(); + + addMessage({ + id: 'test-1', + role: 'user', + content: 'Test', + timestamp: Date.now(), + }); + + updateMessage('non-existent', { content: 'Should not appear' }); + + const state = useChatStore.getState(); + expect(state.messages[0].content).toBe('Test'); + }); +}); +``` + +- [ ] **Step 5: 运行测试确认通过** + +Run: +```bash +cd g:/ZClaw_openfang/desktop && pnpm test tests/store/chatStore.test.ts +``` + +Expected: PASS - 10+ tests + +- [ ] **Step 6: 运行覆盖率报告** + +Run: +```bash +cd g:/ZClaw_openfang/desktop && pnpm test:coverage tests/store/chatStore.test.ts +``` + +Expected: Coverage > 80% for chatStore + +- [ ] **Step 7: 提交 ChatStore 测试** + +```bash +cd g:/ZClaw_openfang && git add desktop/tests/store/chatStore.test.ts +git commit -m "test(chat): add comprehensive unit tests for chatStore + +- Test initial state +- Test addMessage functionality +- Test setStreaming state management +- Test clearMessages functionality +- Test error handling +- Test updateMessage functionality + +Coverage: 80%+ for chatStore module + +Co-Authored-By: Claude Opus 4.6 " +``` + +--- + +## Chunk 6: 集成与验收 + +### Task 6.1: 运行完整测试套件 + +- [ ] **Step 1: 运行所有测试** + +Run: +```bash +cd g:/ZClaw_openfang/desktop && pnpm test +``` + +Expected: All tests pass + +- [ ] **Step 2: 生成覆盖率报告** + +Run: +```bash +cd g:/ZClaw_openfang/desktop && pnpm test:coverage +``` + +Expected: Overall coverage >= 60% + +- [ ] **Step 3: 验证覆盖率门禁** + +Check that coverage thresholds in `vitest.config.ts` are being enforced. + +### Task 6.2: 手动验证安全功能 + +- [ ] **Step 1: 验证加密存储** + +1. 启动应用: `pnpm tauri:dev` +2. 在开发者工具中检查 localStorage +3. 确认凭据值是加密的 (包含 `iv` 和 `data` 字段) + +- [ ] **Step 2: 验证 WSS 强制** + +1. 尝试连接 `ws://example.com:4200` +2. 确认连接被拒绝并显示安全错误 +3. 确认 `ws://localhost:4200` 仍然可以连接 + +### Task 6.3: 更新文档 + +- [ ] **Step 1: 更新 README 或创建变更日志** + +Create `docs/changelogs/2026-03-21-phase1-security.md`: + +```markdown +# Phase 1: Security + Testing + +## Changes + +### Security +- **凭据加密**: localStorage 回退现在使用 AES-GCM 加密 +- **WSS 强制**: 非 localhost 连接必须使用 WSS 协议 + +### Testing +- 建立 Vitest 测试框架 +- 添加 chatStore 单元测试 (80%+ 覆盖率) +- 添加加密工具测试 +- 添加网关安全测试 + +## Migration Notes +- 现有未加密的 localStorage 数据会在首次读取时自动迁移 +- 无需手动干预 + +## Breaking Changes +- `ws://` 协议不再允许用于非 localhost 连接 +- 如需使用远程服务器,请确保使用 `wss://` +``` + +- [ ] **Step 2: 提交文档更新** + +```bash +cd g:/ZClaw_openfang && git add docs/changelogs/2026-03-21-phase1-security.md +git commit -m "docs: add Phase 1 changelog + +Co-Authored-By: Claude Opus 4.6 " +``` + +### Task 6.4: 最终提交和标记 + +- [ ] **Step 1: 创建 Phase 1 完成标签** + +```bash +cd g:/ZClaw_openfang && git tag -a v0.3.0-phase1 -m "Phase 1: Security + Testing Complete" +``` + +- [ ] **Step 2: 推送所有提交** + +```bash +cd g:/ZClaw_openfang && git push origin main --tags +``` + +--- + +## Verification Checklist + +### Security Verification +- [ ] localStorage 凭据已加密 (AES-GCM) +- [ ] 非 localhost 连接强制 WSS +- [ ] 加密密钥不暴露在日志中 +- [ ] 向后兼容未加密数据 + +### Testing Verification +- [ ] 所有测试通过 +- [ ] 覆盖率 >= 60% +- [ ] chatStore 覆盖率 >= 80% +- [ ] CI 集成就绪 + +### Documentation Verification +- [ ] 变更日志已创建 +- [ ] 迁移说明已记录 +- [ ] 破坏性变更已标注 + +--- + +## Next Steps (Phase 2) + +完成 Phase 1 后,继续执行 Phase 2: 领域重组 + +- TASK-101: 创建 domains/ 目录结构 +- TASK-102: 迁移 Chat Store 到 Valtio +- TASK-103: 迁移 Hands Store + XState +- TASK-104: 增强 Intelligence 缓存 +- TASK-105: 提取共享模块 + +详见: `docs/superpowers/plans/2026-03-21-phase2-domain-reorganization.md` (待创建)