opencode-account-manager 0.6.4 → 0.6.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/README.md +235 -216
  2. package/README_VI.md +235 -216
  3. package/dist/cli.js +83 -0
  4. package/dist/cli.js.map +1 -1
  5. package/dist/core/config-store.d.ts +12 -0
  6. package/dist/core/config-store.d.ts.map +1 -1
  7. package/dist/core/config-store.js +98 -0
  8. package/dist/core/config-store.js.map +1 -1
  9. package/dist/core/health-log.d.ts +9 -0
  10. package/dist/core/health-log.d.ts.map +1 -0
  11. package/dist/core/health-log.js +154 -0
  12. package/dist/core/health-log.js.map +1 -0
  13. package/dist/core/health-oauth.d.ts +5 -0
  14. package/dist/core/health-oauth.d.ts.map +1 -0
  15. package/dist/core/health-oauth.js +147 -0
  16. package/dist/core/health-oauth.js.map +1 -0
  17. package/dist/core/health-orchestrator.d.ts +32 -0
  18. package/dist/core/health-orchestrator.d.ts.map +1 -0
  19. package/dist/core/health-orchestrator.js +148 -0
  20. package/dist/core/health-orchestrator.js.map +1 -0
  21. package/dist/core/health-utils.d.ts +15 -0
  22. package/dist/core/health-utils.d.ts.map +1 -0
  23. package/dist/core/health-utils.js +60 -0
  24. package/dist/core/health-utils.js.map +1 -0
  25. package/dist/core/paths.d.ts +1 -0
  26. package/dist/core/paths.d.ts.map +1 -1
  27. package/dist/core/paths.js +4 -0
  28. package/dist/core/paths.js.map +1 -1
  29. package/dist/core/types.d.ts +26 -0
  30. package/dist/core/types.d.ts.map +1 -1
  31. package/dist/tui/Dashboard.d.ts.map +1 -1
  32. package/dist/tui/Dashboard.js +69 -2
  33. package/dist/tui/Dashboard.js.map +1 -1
  34. package/dist/tui/components/AccountList.d.ts +5 -3
  35. package/dist/tui/components/AccountList.d.ts.map +1 -1
  36. package/dist/tui/components/AccountList.js +9 -3
  37. package/dist/tui/components/AccountList.js.map +1 -1
  38. package/dist/tui/components/DashboardView.d.ts +3 -2
  39. package/dist/tui/components/DashboardView.d.ts.map +1 -1
  40. package/dist/tui/components/DashboardView.js +50 -4
  41. package/dist/tui/components/DashboardView.js.map +1 -1
  42. package/dist/tui/components/HealthBadge.d.ts +9 -0
  43. package/dist/tui/components/HealthBadge.d.ts.map +1 -0
  44. package/dist/tui/components/HealthBadge.js +56 -0
  45. package/dist/tui/components/HealthBadge.js.map +1 -0
  46. package/dist/tui/components/StatusBadge.d.ts +2 -1
  47. package/dist/tui/components/StatusBadge.d.ts.map +1 -1
  48. package/dist/tui/components/StatusBadge.js +30 -2
  49. package/dist/tui/components/StatusBadge.js.map +1 -1
  50. package/dist/tui/components/index.d.ts +1 -0
  51. package/dist/tui/components/index.d.ts.map +1 -1
  52. package/dist/tui/components/index.js +3 -1
  53. package/dist/tui/components/index.js.map +1 -1
  54. package/docs/BLUEPRINT.md +476 -476
  55. package/docs/ROADMAP.md +125 -107
  56. package/package.json +36 -36
  57. package/src/cli.ts +139 -38
  58. package/src/core/config-store.ts +278 -171
  59. package/src/core/crypto.ts +162 -162
  60. package/src/core/health-log.ts +173 -0
  61. package/src/core/health-oauth.ts +190 -0
  62. package/src/core/health-orchestrator.ts +224 -0
  63. package/src/core/importers/amExport.ts +177 -177
  64. package/src/core/opencode-config.ts +217 -217
  65. package/src/core/paths.ts +10 -6
  66. package/src/core/types.ts +193 -147
  67. package/src/tui/Dashboard.tsx +557 -478
  68. package/src/tui/components/AccountList.tsx +122 -104
  69. package/src/tui/components/ActionPalette.tsx +117 -117
  70. package/src/tui/components/Box.tsx +7 -7
  71. package/src/tui/components/DashboardView.tsx +285 -230
  72. package/src/tui/components/ExportModal.tsx +255 -255
  73. package/src/tui/components/FileBrowser.tsx +393 -393
  74. package/src/tui/components/Header.tsx +26 -26
  75. package/src/tui/components/HealthBadge.tsx +64 -0
  76. package/src/tui/components/ImportModal.tsx +334 -334
  77. package/src/tui/components/McpServerList.tsx +67 -67
  78. package/src/tui/components/Menu.tsx +61 -61
  79. package/src/tui/components/PasswordInput.tsx +159 -159
  80. package/src/tui/components/ProviderList.tsx +59 -59
  81. package/src/tui/components/SectionBox.tsx +35 -35
  82. package/src/tui/components/StatsRow.tsx +33 -33
  83. package/src/tui/components/StatusBadge.tsx +36 -3
  84. package/src/tui/components/index.ts +15 -14
  85. package/test-minimal.js +26 -26
  86. package/test-with-accounts.js +58 -58
package/docs/BLUEPRINT.md CHANGED
@@ -1,476 +1,476 @@
1
- # OpenCode Account Manager - Blueprint v0.4.0
2
-
3
- ## Overview
4
-
5
- Technical specification for the **Encrypted Export/Import** feature.
6
-
7
- ---
8
-
9
- ## 1. Architecture
10
-
11
- ```
12
- ┌─────────────────────────────────────────────────────────────────────────┐
13
- │ TUI Layer │
14
- ├─────────────────────────────────────────────────────────────────────────┤
15
- │ Dashboard.tsx │
16
- │ ├── ExportModal.tsx (format selection → folder → password → save) │
17
- │ ├── ImportModal.tsx (file selection → password → preview → import) │
18
- │ └── Components: │
19
- │ ├── FileBrowser.tsx (folder/file selection UI) │
20
- │ ├── PasswordInput.tsx (masked password input) │
21
- │ └── FormatSelector.tsx (encrypted vs plain) │
22
- ├─────────────────────────────────────────────────────────────────────────┤
23
- │ Core Layer │
24
- ├─────────────────────────────────────────────────────────────────────────┤
25
- │ crypto.ts - AES-256-GCM encryption/decryption │
26
- │ config-store.ts - Persist user preferences (last folder, etc.) │
27
- │ accounts.ts - Extended with encrypt/decrypt functions │
28
- │ types.ts - New types for encrypted files │
29
- └─────────────────────────────────────────────────────────────────────────┘
30
- ```
31
-
32
- ---
33
-
34
- ## 2. File Specifications
35
-
36
- ### 2.1 Encrypted Export File (.ocam)
37
-
38
- **Extension:** `.ocam` (OpenCode Account Manager)
39
-
40
- **Structure:**
41
- ```typescript
42
- interface EncryptedExportFile {
43
- // Header (not encrypted)
44
- version: 1;
45
- format: "encrypted";
46
- algorithm: "aes-256-gcm";
47
-
48
- // Encryption parameters
49
- salt: string; // 32 bytes, hex encoded (for key derivation)
50
- iv: string; // 12 bytes, hex encoded (initialization vector)
51
- authTag: string; // 16 bytes, hex encoded (authentication tag)
52
-
53
- // Encrypted payload
54
- data: string; // Encrypted JSON, hex encoded
55
-
56
- // Metadata (not encrypted)
57
- exportedAt: number; // Unix timestamp ms
58
- accountCount: number; // Number of accounts (for display)
59
- exportedFrom: string; // App identifier
60
- }
61
- ```
62
-
63
- **Example:**
64
- ```json
65
- {
66
- "version": 1,
67
- "format": "encrypted",
68
- "algorithm": "aes-256-gcm",
69
- "salt": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2",
70
- "iv": "1234567890abcdef12345678",
71
- "authTag": "fedcba0987654321fedcba0987654321",
72
- "data": "encrypted_hex_data_here...",
73
- "exportedAt": 1707123456789,
74
- "accountCount": 5,
75
- "exportedFrom": "opencode-account-manager"
76
- }
77
- ```
78
-
79
- ### 2.2 Plain Export File (.json)
80
-
81
- Keep existing format for backward compatibility:
82
- ```typescript
83
- interface PortableExportFile {
84
- version: number;
85
- exportedAt: number;
86
- exportedFrom: "opencode-account-manager";
87
- accounts: Account[];
88
- }
89
- ```
90
-
91
- ### 2.3 App Config File
92
-
93
- **Location:** `%APPDATA%/opencode/ocam-config.json` (Windows)
94
- `~/.config/opencode/ocam-config.json` (Linux/Mac)
95
-
96
- ```typescript
97
- interface AppConfig {
98
- lastExportFolder?: string;
99
- lastImportFolder?: string;
100
- defaultExportFormat?: "encrypted" | "plain";
101
- recentFolders?: string[]; // Max 5 recent folders
102
- }
103
- ```
104
-
105
- ---
106
-
107
- ## 3. Encryption Specification
108
-
109
- ### 3.1 Algorithm: AES-256-GCM
110
-
111
- - **Key Derivation:** scrypt (N=16384, r=8, p=1)
112
- - **Key Length:** 256 bits (32 bytes)
113
- - **Salt Length:** 256 bits (32 bytes, random)
114
- - **IV Length:** 96 bits (12 bytes, random)
115
- - **Auth Tag:** 128 bits (16 bytes)
116
-
117
- ### 3.2 Encryption Flow
118
-
119
- ```
120
- Password (user input)
121
-
122
-
123
- ┌───────────────────────────────────────┐
124
- │ scrypt(password, salt, N=16384) │
125
- │ Output: 32-byte key │
126
- └───────────────────────────────────────┘
127
-
128
-
129
- ┌───────────────────────────────────────┐
130
- │ AES-256-GCM.encrypt( │
131
- │ key: derived_key, │
132
- │ iv: random_12_bytes, │
133
- │ plaintext: JSON.stringify(data), │
134
- │ aad: none │
135
- │ ) │
136
- │ Output: ciphertext + authTag │
137
- └───────────────────────────────────────┘
138
-
139
-
140
- { salt, iv, authTag, data: ciphertext }
141
- ```
142
-
143
- ### 3.3 Decryption Flow
144
-
145
- ```
146
- { salt, iv, authTag, data } + Password
147
-
148
-
149
- ┌───────────────────────────────────────┐
150
- │ scrypt(password, salt, N=16384) │
151
- │ Output: 32-byte key │
152
- └───────────────────────────────────────┘
153
-
154
-
155
- ┌───────────────────────────────────────┐
156
- │ AES-256-GCM.decrypt( │
157
- │ key: derived_key, │
158
- │ iv: iv, │
159
- │ ciphertext: data, │
160
- │ authTag: authTag │
161
- │ ) │
162
- │ Output: plaintext (or throw error) │
163
- └───────────────────────────────────────┘
164
-
165
-
166
- JSON.parse(plaintext) → Account[]
167
- ```
168
-
169
- ---
170
-
171
- ## 4. UI Components
172
-
173
- ### 4.1 ExportModal
174
-
175
- **States:**
176
- 1. `format-select` - Choose encrypted or plain
177
- 2. `folder-select` - Choose destination folder
178
- 3. `password-input` - Enter password (only for encrypted)
179
- 4. `exporting` - Show progress
180
- 5. `success` - Show result
181
- 6. `error` - Show error message
182
-
183
- **Props:**
184
- ```typescript
185
- interface ExportModalProps {
186
- accounts: Account[];
187
- onComplete: (filePath: string) => void;
188
- onCancel: () => void;
189
- }
190
- ```
191
-
192
- ### 4.2 ImportModal
193
-
194
- **States:**
195
- 1. `file-select` - Choose file to import
196
- 2. `password-input` - Enter password (only for .ocam)
197
- 3. `preview` - Show accounts to import with conflict info
198
- 4. `importing` - Show progress
199
- 5. `success` - Show result
200
- 6. `error` - Show error message
201
-
202
- **Props:**
203
- ```typescript
204
- interface ImportModalProps {
205
- existingAccounts: Account[];
206
- onComplete: (imported: number, overwritten: number) => void;
207
- onCancel: () => void;
208
- }
209
- ```
210
-
211
- ### 4.3 FileBrowser
212
-
213
- **Features:**
214
- - Quick locations (Current Dir, Desktop, Documents, Recent)
215
- - Folder navigation with arrow keys
216
- - Text input for pasting path
217
- - Filter by extension (for import)
218
- - Show file metadata (size, date, encrypted/plain)
219
-
220
- **Props:**
221
- ```typescript
222
- interface FileBrowserProps {
223
- mode: "folder" | "file";
224
- initialPath?: string;
225
- extensions?: string[]; // e.g., [".ocam", ".json"]
226
- onSelect: (path: string) => void;
227
- onCancel: () => void;
228
- }
229
- ```
230
-
231
- ### 4.4 PasswordInput
232
-
233
- **Features:**
234
- - Masked input (show dots)
235
- - Optional confirmation field (for export)
236
- - Password mismatch warning
237
- - Enter to submit, Escape to cancel
238
-
239
- **Props:**
240
- ```typescript
241
- interface PasswordInputProps {
242
- mode: "single" | "confirm";
243
- title?: string;
244
- warning?: string;
245
- onSubmit: (password: string) => void;
246
- onCancel: () => void;
247
- }
248
- ```
249
-
250
- ---
251
-
252
- ## 5. User Flows
253
-
254
- ### 5.1 Export Flow
255
-
256
- ```
257
- [E] pressed
258
-
259
-
260
- ┌─────────────────────────────────┐
261
- │ Select export format: │
262
- │ [1] Encrypted (.ocam) │
263
- │ [2] Plain JSON │
264
- └─────────────────────────────────┘
265
-
266
- ├─── [1] ──────────────────────────────────────┐
267
- │ │
268
- ▼ ▼
269
- ┌─────────────────────────────────┐ ┌─────────────────────────────────┐
270
- │ Select folder │ │ Select folder │
271
- │ (FileBrowser mode="folder") │ │ (FileBrowser mode="folder") │
272
- └─────────────────────────────────┘ └─────────────────────────────────┘
273
- │ │
274
- ▼ ▼
275
- ┌─────────────────────────────────┐ ┌─────────────────────────────────┐
276
- │ Enter password │ │ Save file │
277
- │ (PasswordInput mode="confirm") │ │ filename: accounts-{date}.json │
278
- └─────────────────────────────────┘ └─────────────────────────────────┘
279
- │ │
280
- ▼ ▼
281
- ┌─────────────────────────────────┐ ┌─────────────────────────────────┐
282
- │ Encrypt & Save │ │ Success message │
283
- │ filename: accounts-{date}.ocam │ └─────────────────────────────────┘
284
- └─────────────────────────────────┘
285
-
286
-
287
- ┌─────────────────────────────────┐
288
- │ Success message │
289
- └─────────────────────────────────┘
290
- ```
291
-
292
- ### 5.2 Import Flow
293
-
294
- ```
295
- [I] pressed
296
-
297
-
298
- ┌─────────────────────────────────┐
299
- │ Select file │
300
- │ (FileBrowser mode="file") │
301
- │ extensions: [".ocam", ".json"] │
302
- └─────────────────────────────────┘
303
-
304
- ├─── .ocam ────────────────────────────────────┐
305
- │ │
306
- ▼ ▼
307
- ┌─────────────────────────────────┐ ┌─────────────────────────────────┐
308
- │ Enter password │ │ .json file │
309
- │ (PasswordInput mode="single") │ │ Parse directly │
310
- └─────────────────────────────────┘ └─────────────────────────────────┘
311
- │ │
312
- ├─── Wrong password ───┐ │
313
- │ ▼ │
314
- │ ┌─────────────────────────────────┐ │
315
- │ │ Error: Invalid password │ │
316
- │ │ [Try again] [Cancel] │ │
317
- │ └─────────────────────────────────┘ │
318
- │ │
319
- ▼ ▼
320
- ┌─────────────────────────────────────────────────────────────┐
321
- │ Preview accounts │
322
- │ Show: email, exists? (will overwrite) │
323
- │ [Enter] Import [Esc] Cancel │
324
- └─────────────────────────────────────────────────────────────┘
325
-
326
-
327
- ┌─────────────────────────────────┐
328
- │ Import accounts (overwrite) │
329
- └─────────────────────────────────┘
330
-
331
-
332
- ┌─────────────────────────────────┐
333
- │ Success: X imported, Y new │
334
- └─────────────────────────────────┘
335
- ```
336
-
337
- ---
338
-
339
- ## 6. Implementation Checklist
340
-
341
- ### Core Layer
342
- - [ ] `src/core/crypto.ts`
343
- - [ ] `generateSalt(): string`
344
- - [ ] `generateIV(): string`
345
- - [ ] `deriveKey(password: string, salt: string): Buffer`
346
- - [ ] `encrypt(data: object, password: string): EncryptedData`
347
- - [ ] `decrypt(encrypted: EncryptedData, password: string): object`
348
-
349
- - [ ] `src/core/config-store.ts`
350
- - [ ] `getConfigPath(): string`
351
- - [ ] `readConfig(): AppConfig`
352
- - [ ] `writeConfig(config: AppConfig): void`
353
- - [ ] `updateLastExportFolder(folder: string): void`
354
- - [ ] `updateLastImportFolder(folder: string): void`
355
- - [ ] `getRecentFolders(): string[]`
356
-
357
- - [ ] `src/core/types.ts`
358
- - [ ] `EncryptedExportFile` interface
359
- - [ ] `AppConfig` interface
360
- - [ ] Update `PortableExportFile.exportedFrom`
361
-
362
- - [ ] `src/core/accounts.ts`
363
- - [ ] `encryptAndExport(accounts: Account[], password: string): EncryptedExportFile`
364
- - [ ] `decryptAndImport(file: EncryptedExportFile, password: string): Account[]`
365
- - [ ] `isEncryptedFile(data: unknown): boolean`
366
-
367
- ### TUI Layer
368
- - [ ] `src/tui/components/PasswordInput.tsx`
369
- - [ ] Masked input display
370
- - [ ] Confirm mode (two fields)
371
- - [ ] Mismatch warning
372
- - [ ] Enter/Escape handling
373
-
374
- - [ ] `src/tui/components/FileBrowser.tsx`
375
- - [ ] Quick locations list
376
- - [ ] Folder navigation
377
- - [ ] Text input for path
378
- - [ ] File filtering by extension
379
- - [ ] File metadata display
380
-
381
- - [ ] `src/tui/components/ExportModal.tsx`
382
- - [ ] Format selection step
383
- - [ ] Folder selection step
384
- - [ ] Password input step
385
- - [ ] Export execution
386
- - [ ] Success/Error display
387
-
388
- - [ ] `src/tui/components/ImportModal.tsx`
389
- - [ ] File selection step
390
- - [ ] Password input step (if encrypted)
391
- - [ ] Preview with conflict detection
392
- - [ ] Import execution
393
- - [ ] Success/Error display
394
-
395
- - [ ] `src/tui/Dashboard.tsx`
396
- - [ ] Modal state management
397
- - [ ] Export handler
398
- - [ ] Import handler
399
-
400
- - [ ] `src/tui/components/Menu.tsx`
401
- - [ ] Update export action
402
- - [ ] Update import action
403
-
404
- - [ ] `src/tui/components/index.ts`
405
- - [ ] Export new components
406
-
407
- ---
408
-
409
- ## 7. Testing
410
-
411
- ### Manual Test Cases
412
-
413
- 1. **Export Encrypted**
414
- - Export 5 accounts with password "test123"
415
- - Verify .ocam file created
416
- - Verify file content is encrypted (not readable)
417
-
418
- 2. **Import Encrypted**
419
- - Import the exported .ocam file
420
- - Enter correct password → success
421
- - Enter wrong password → error with retry option
422
-
423
- 3. **Export Plain**
424
- - Export 5 accounts as plain JSON
425
- - Verify .json file created
426
- - Verify accounts are readable in file
427
-
428
- 4. **Import Plain**
429
- - Import a plain .json file
430
- - No password required
431
- - Accounts imported successfully
432
-
433
- 5. **Overwrite Existing**
434
- - Export 3 accounts
435
- - Modify 1 account locally
436
- - Import the file
437
- - Verify the account is overwritten
438
-
439
- 6. **Remember Folder**
440
- - Export to custom folder
441
- - Close and reopen app
442
- - Export again → should show last folder as recent
443
-
444
- ---
445
-
446
- ## 8. Security Considerations
447
-
448
- 1. **Password not stored** - Never save password to disk
449
- 2. **Memory cleanup** - Clear password from memory after use
450
- 3. **Auth tag verification** - Detect tampered files
451
- 4. **No password hints** - Don't store any password metadata
452
- 5. **Salt per file** - Each export uses unique salt
453
- 6. **Warning on plain export** - Show security warning
454
-
455
- ---
456
-
457
- ## 9. Error Handling
458
-
459
- | Error | User Message |
460
- |-------|--------------|
461
- | Wrong password | "Invalid password. Please try again." |
462
- | Corrupted file | "File is corrupted or invalid format." |
463
- | File not found | "File not found: {path}" |
464
- | Permission denied | "Cannot write to folder: {path}" |
465
- | Disk full | "Not enough disk space." |
466
- | Invalid JSON | "Invalid file format." |
467
-
468
- ---
469
-
470
- ## 10. Dependencies
471
-
472
- No new npm packages required. Using Node.js built-in:
473
- - `crypto` - AES-256-GCM, scrypt
474
- - `fs` - File operations
475
- - `path` - Path manipulation
476
- - `os` - Home directory, platform detection
1
+ # OpenCode Account Manager - Blueprint v0.4.0
2
+
3
+ ## Overview
4
+
5
+ Technical specification for the **Encrypted Export/Import** feature.
6
+
7
+ ---
8
+
9
+ ## 1. Architecture
10
+
11
+ ```
12
+ ┌─────────────────────────────────────────────────────────────────────────┐
13
+ │ TUI Layer │
14
+ ├─────────────────────────────────────────────────────────────────────────┤
15
+ │ Dashboard.tsx │
16
+ │ ├── ExportModal.tsx (format selection → folder → password → save) │
17
+ │ ├── ImportModal.tsx (file selection → password → preview → import) │
18
+ │ └── Components: │
19
+ │ ├── FileBrowser.tsx (folder/file selection UI) │
20
+ │ ├── PasswordInput.tsx (masked password input) │
21
+ │ └── FormatSelector.tsx (encrypted vs plain) │
22
+ ├─────────────────────────────────────────────────────────────────────────┤
23
+ │ Core Layer │
24
+ ├─────────────────────────────────────────────────────────────────────────┤
25
+ │ crypto.ts - AES-256-GCM encryption/decryption │
26
+ │ config-store.ts - Persist user preferences (last folder, etc.) │
27
+ │ accounts.ts - Extended with encrypt/decrypt functions │
28
+ │ types.ts - New types for encrypted files │
29
+ └─────────────────────────────────────────────────────────────────────────┘
30
+ ```
31
+
32
+ ---
33
+
34
+ ## 2. File Specifications
35
+
36
+ ### 2.1 Encrypted Export File (.ocam)
37
+
38
+ **Extension:** `.ocam` (OpenCode Account Manager)
39
+
40
+ **Structure:**
41
+ ```typescript
42
+ interface EncryptedExportFile {
43
+ // Header (not encrypted)
44
+ version: 1;
45
+ format: "encrypted";
46
+ algorithm: "aes-256-gcm";
47
+
48
+ // Encryption parameters
49
+ salt: string; // 32 bytes, hex encoded (for key derivation)
50
+ iv: string; // 12 bytes, hex encoded (initialization vector)
51
+ authTag: string; // 16 bytes, hex encoded (authentication tag)
52
+
53
+ // Encrypted payload
54
+ data: string; // Encrypted JSON, hex encoded
55
+
56
+ // Metadata (not encrypted)
57
+ exportedAt: number; // Unix timestamp ms
58
+ accountCount: number; // Number of accounts (for display)
59
+ exportedFrom: string; // App identifier
60
+ }
61
+ ```
62
+
63
+ **Example:**
64
+ ```json
65
+ {
66
+ "version": 1,
67
+ "format": "encrypted",
68
+ "algorithm": "aes-256-gcm",
69
+ "salt": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2",
70
+ "iv": "1234567890abcdef12345678",
71
+ "authTag": "fedcba0987654321fedcba0987654321",
72
+ "data": "encrypted_hex_data_here...",
73
+ "exportedAt": 1707123456789,
74
+ "accountCount": 5,
75
+ "exportedFrom": "opencode-account-manager"
76
+ }
77
+ ```
78
+
79
+ ### 2.2 Plain Export File (.json)
80
+
81
+ Keep existing format for backward compatibility:
82
+ ```typescript
83
+ interface PortableExportFile {
84
+ version: number;
85
+ exportedAt: number;
86
+ exportedFrom: "opencode-account-manager";
87
+ accounts: Account[];
88
+ }
89
+ ```
90
+
91
+ ### 2.3 App Config File
92
+
93
+ **Location:** `%APPDATA%/opencode/ocam-config.json` (Windows)
94
+ `~/.config/opencode/ocam-config.json` (Linux/Mac)
95
+
96
+ ```typescript
97
+ interface AppConfig {
98
+ lastExportFolder?: string;
99
+ lastImportFolder?: string;
100
+ defaultExportFormat?: "encrypted" | "plain";
101
+ recentFolders?: string[]; // Max 5 recent folders
102
+ }
103
+ ```
104
+
105
+ ---
106
+
107
+ ## 3. Encryption Specification
108
+
109
+ ### 3.1 Algorithm: AES-256-GCM
110
+
111
+ - **Key Derivation:** scrypt (N=16384, r=8, p=1)
112
+ - **Key Length:** 256 bits (32 bytes)
113
+ - **Salt Length:** 256 bits (32 bytes, random)
114
+ - **IV Length:** 96 bits (12 bytes, random)
115
+ - **Auth Tag:** 128 bits (16 bytes)
116
+
117
+ ### 3.2 Encryption Flow
118
+
119
+ ```
120
+ Password (user input)
121
+
122
+
123
+ ┌───────────────────────────────────────┐
124
+ │ scrypt(password, salt, N=16384) │
125
+ │ Output: 32-byte key │
126
+ └───────────────────────────────────────┘
127
+
128
+
129
+ ┌───────────────────────────────────────┐
130
+ │ AES-256-GCM.encrypt( │
131
+ │ key: derived_key, │
132
+ │ iv: random_12_bytes, │
133
+ │ plaintext: JSON.stringify(data), │
134
+ │ aad: none │
135
+ │ ) │
136
+ │ Output: ciphertext + authTag │
137
+ └───────────────────────────────────────┘
138
+
139
+
140
+ { salt, iv, authTag, data: ciphertext }
141
+ ```
142
+
143
+ ### 3.3 Decryption Flow
144
+
145
+ ```
146
+ { salt, iv, authTag, data } + Password
147
+
148
+
149
+ ┌───────────────────────────────────────┐
150
+ │ scrypt(password, salt, N=16384) │
151
+ │ Output: 32-byte key │
152
+ └───────────────────────────────────────┘
153
+
154
+
155
+ ┌───────────────────────────────────────┐
156
+ │ AES-256-GCM.decrypt( │
157
+ │ key: derived_key, │
158
+ │ iv: iv, │
159
+ │ ciphertext: data, │
160
+ │ authTag: authTag │
161
+ │ ) │
162
+ │ Output: plaintext (or throw error) │
163
+ └───────────────────────────────────────┘
164
+
165
+
166
+ JSON.parse(plaintext) → Account[]
167
+ ```
168
+
169
+ ---
170
+
171
+ ## 4. UI Components
172
+
173
+ ### 4.1 ExportModal
174
+
175
+ **States:**
176
+ 1. `format-select` - Choose encrypted or plain
177
+ 2. `folder-select` - Choose destination folder
178
+ 3. `password-input` - Enter password (only for encrypted)
179
+ 4. `exporting` - Show progress
180
+ 5. `success` - Show result
181
+ 6. `error` - Show error message
182
+
183
+ **Props:**
184
+ ```typescript
185
+ interface ExportModalProps {
186
+ accounts: Account[];
187
+ onComplete: (filePath: string) => void;
188
+ onCancel: () => void;
189
+ }
190
+ ```
191
+
192
+ ### 4.2 ImportModal
193
+
194
+ **States:**
195
+ 1. `file-select` - Choose file to import
196
+ 2. `password-input` - Enter password (only for .ocam)
197
+ 3. `preview` - Show accounts to import with conflict info
198
+ 4. `importing` - Show progress
199
+ 5. `success` - Show result
200
+ 6. `error` - Show error message
201
+
202
+ **Props:**
203
+ ```typescript
204
+ interface ImportModalProps {
205
+ existingAccounts: Account[];
206
+ onComplete: (imported: number, overwritten: number) => void;
207
+ onCancel: () => void;
208
+ }
209
+ ```
210
+
211
+ ### 4.3 FileBrowser
212
+
213
+ **Features:**
214
+ - Quick locations (Current Dir, Desktop, Documents, Recent)
215
+ - Folder navigation with arrow keys
216
+ - Text input for pasting path
217
+ - Filter by extension (for import)
218
+ - Show file metadata (size, date, encrypted/plain)
219
+
220
+ **Props:**
221
+ ```typescript
222
+ interface FileBrowserProps {
223
+ mode: "folder" | "file";
224
+ initialPath?: string;
225
+ extensions?: string[]; // e.g., [".ocam", ".json"]
226
+ onSelect: (path: string) => void;
227
+ onCancel: () => void;
228
+ }
229
+ ```
230
+
231
+ ### 4.4 PasswordInput
232
+
233
+ **Features:**
234
+ - Masked input (show dots)
235
+ - Optional confirmation field (for export)
236
+ - Password mismatch warning
237
+ - Enter to submit, Escape to cancel
238
+
239
+ **Props:**
240
+ ```typescript
241
+ interface PasswordInputProps {
242
+ mode: "single" | "confirm";
243
+ title?: string;
244
+ warning?: string;
245
+ onSubmit: (password: string) => void;
246
+ onCancel: () => void;
247
+ }
248
+ ```
249
+
250
+ ---
251
+
252
+ ## 5. User Flows
253
+
254
+ ### 5.1 Export Flow
255
+
256
+ ```
257
+ [E] pressed
258
+
259
+
260
+ ┌─────────────────────────────────┐
261
+ │ Select export format: │
262
+ │ [1] Encrypted (.ocam) │
263
+ │ [2] Plain JSON │
264
+ └─────────────────────────────────┘
265
+
266
+ ├─── [1] ──────────────────────────────────────┐
267
+ │ │
268
+ ▼ ▼
269
+ ┌─────────────────────────────────┐ ┌─────────────────────────────────┐
270
+ │ Select folder │ │ Select folder │
271
+ │ (FileBrowser mode="folder") │ │ (FileBrowser mode="folder") │
272
+ └─────────────────────────────────┘ └─────────────────────────────────┘
273
+ │ │
274
+ ▼ ▼
275
+ ┌─────────────────────────────────┐ ┌─────────────────────────────────┐
276
+ │ Enter password │ │ Save file │
277
+ │ (PasswordInput mode="confirm") │ │ filename: accounts-{date}.json │
278
+ └─────────────────────────────────┘ └─────────────────────────────────┘
279
+ │ │
280
+ ▼ ▼
281
+ ┌─────────────────────────────────┐ ┌─────────────────────────────────┐
282
+ │ Encrypt & Save │ │ Success message │
283
+ │ filename: accounts-{date}.ocam │ └─────────────────────────────────┘
284
+ └─────────────────────────────────┘
285
+
286
+
287
+ ┌─────────────────────────────────┐
288
+ │ Success message │
289
+ └─────────────────────────────────┘
290
+ ```
291
+
292
+ ### 5.2 Import Flow
293
+
294
+ ```
295
+ [I] pressed
296
+
297
+
298
+ ┌─────────────────────────────────┐
299
+ │ Select file │
300
+ │ (FileBrowser mode="file") │
301
+ │ extensions: [".ocam", ".json"] │
302
+ └─────────────────────────────────┘
303
+
304
+ ├─── .ocam ────────────────────────────────────┐
305
+ │ │
306
+ ▼ ▼
307
+ ┌─────────────────────────────────┐ ┌─────────────────────────────────┐
308
+ │ Enter password │ │ .json file │
309
+ │ (PasswordInput mode="single") │ │ Parse directly │
310
+ └─────────────────────────────────┘ └─────────────────────────────────┘
311
+ │ │
312
+ ├─── Wrong password ───┐ │
313
+ │ ▼ │
314
+ │ ┌─────────────────────────────────┐ │
315
+ │ │ Error: Invalid password │ │
316
+ │ │ [Try again] [Cancel] │ │
317
+ │ └─────────────────────────────────┘ │
318
+ │ │
319
+ ▼ ▼
320
+ ┌─────────────────────────────────────────────────────────────┐
321
+ │ Preview accounts │
322
+ │ Show: email, exists? (will overwrite) │
323
+ │ [Enter] Import [Esc] Cancel │
324
+ └─────────────────────────────────────────────────────────────┘
325
+
326
+
327
+ ┌─────────────────────────────────┐
328
+ │ Import accounts (overwrite) │
329
+ └─────────────────────────────────┘
330
+
331
+
332
+ ┌─────────────────────────────────┐
333
+ │ Success: X imported, Y new │
334
+ └─────────────────────────────────┘
335
+ ```
336
+
337
+ ---
338
+
339
+ ## 6. Implementation Checklist
340
+
341
+ ### Core Layer
342
+ - [ ] `src/core/crypto.ts`
343
+ - [ ] `generateSalt(): string`
344
+ - [ ] `generateIV(): string`
345
+ - [ ] `deriveKey(password: string, salt: string): Buffer`
346
+ - [ ] `encrypt(data: object, password: string): EncryptedData`
347
+ - [ ] `decrypt(encrypted: EncryptedData, password: string): object`
348
+
349
+ - [ ] `src/core/config-store.ts`
350
+ - [ ] `getConfigPath(): string`
351
+ - [ ] `readConfig(): AppConfig`
352
+ - [ ] `writeConfig(config: AppConfig): void`
353
+ - [ ] `updateLastExportFolder(folder: string): void`
354
+ - [ ] `updateLastImportFolder(folder: string): void`
355
+ - [ ] `getRecentFolders(): string[]`
356
+
357
+ - [ ] `src/core/types.ts`
358
+ - [ ] `EncryptedExportFile` interface
359
+ - [ ] `AppConfig` interface
360
+ - [ ] Update `PortableExportFile.exportedFrom`
361
+
362
+ - [ ] `src/core/accounts.ts`
363
+ - [ ] `encryptAndExport(accounts: Account[], password: string): EncryptedExportFile`
364
+ - [ ] `decryptAndImport(file: EncryptedExportFile, password: string): Account[]`
365
+ - [ ] `isEncryptedFile(data: unknown): boolean`
366
+
367
+ ### TUI Layer
368
+ - [ ] `src/tui/components/PasswordInput.tsx`
369
+ - [ ] Masked input display
370
+ - [ ] Confirm mode (two fields)
371
+ - [ ] Mismatch warning
372
+ - [ ] Enter/Escape handling
373
+
374
+ - [ ] `src/tui/components/FileBrowser.tsx`
375
+ - [ ] Quick locations list
376
+ - [ ] Folder navigation
377
+ - [ ] Text input for path
378
+ - [ ] File filtering by extension
379
+ - [ ] File metadata display
380
+
381
+ - [ ] `src/tui/components/ExportModal.tsx`
382
+ - [ ] Format selection step
383
+ - [ ] Folder selection step
384
+ - [ ] Password input step
385
+ - [ ] Export execution
386
+ - [ ] Success/Error display
387
+
388
+ - [ ] `src/tui/components/ImportModal.tsx`
389
+ - [ ] File selection step
390
+ - [ ] Password input step (if encrypted)
391
+ - [ ] Preview with conflict detection
392
+ - [ ] Import execution
393
+ - [ ] Success/Error display
394
+
395
+ - [ ] `src/tui/Dashboard.tsx`
396
+ - [ ] Modal state management
397
+ - [ ] Export handler
398
+ - [ ] Import handler
399
+
400
+ - [ ] `src/tui/components/Menu.tsx`
401
+ - [ ] Update export action
402
+ - [ ] Update import action
403
+
404
+ - [ ] `src/tui/components/index.ts`
405
+ - [ ] Export new components
406
+
407
+ ---
408
+
409
+ ## 7. Testing
410
+
411
+ ### Manual Test Cases
412
+
413
+ 1. **Export Encrypted**
414
+ - Export 5 accounts with password "test123"
415
+ - Verify .ocam file created
416
+ - Verify file content is encrypted (not readable)
417
+
418
+ 2. **Import Encrypted**
419
+ - Import the exported .ocam file
420
+ - Enter correct password → success
421
+ - Enter wrong password → error with retry option
422
+
423
+ 3. **Export Plain**
424
+ - Export 5 accounts as plain JSON
425
+ - Verify .json file created
426
+ - Verify accounts are readable in file
427
+
428
+ 4. **Import Plain**
429
+ - Import a plain .json file
430
+ - No password required
431
+ - Accounts imported successfully
432
+
433
+ 5. **Overwrite Existing**
434
+ - Export 3 accounts
435
+ - Modify 1 account locally
436
+ - Import the file
437
+ - Verify the account is overwritten
438
+
439
+ 6. **Remember Folder**
440
+ - Export to custom folder
441
+ - Close and reopen app
442
+ - Export again → should show last folder as recent
443
+
444
+ ---
445
+
446
+ ## 8. Security Considerations
447
+
448
+ 1. **Password not stored** - Never save password to disk
449
+ 2. **Memory cleanup** - Clear password from memory after use
450
+ 3. **Auth tag verification** - Detect tampered files
451
+ 4. **No password hints** - Don't store any password metadata
452
+ 5. **Salt per file** - Each export uses unique salt
453
+ 6. **Warning on plain export** - Show security warning
454
+
455
+ ---
456
+
457
+ ## 9. Error Handling
458
+
459
+ | Error | User Message |
460
+ |-------|--------------|
461
+ | Wrong password | "Invalid password. Please try again." |
462
+ | Corrupted file | "File is corrupted or invalid format." |
463
+ | File not found | "File not found: {path}" |
464
+ | Permission denied | "Cannot write to folder: {path}" |
465
+ | Disk full | "Not enough disk space." |
466
+ | Invalid JSON | "Invalid file format." |
467
+
468
+ ---
469
+
470
+ ## 10. Dependencies
471
+
472
+ No new npm packages required. Using Node.js built-in:
473
+ - `crypto` - AES-256-GCM, scrypt
474
+ - `fs` - File operations
475
+ - `path` - Path manipulation
476
+ - `os` - Home directory, platform detection