auramaxx 0.0.1

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 (418) hide show
  1. package/LICENSE +26 -0
  2. package/README.md +77 -0
  3. package/apps/desktop-electron/main.js +428 -0
  4. package/bin/auramaxx.js +1063 -0
  5. package/docs/ADAPTERS.md +466 -0
  6. package/docs/AGENT_SETUP.md +159 -0
  7. package/docs/API.md +127 -0
  8. package/docs/APPS.md +199 -0
  9. package/docs/ARCHITECTURE.md +235 -0
  10. package/docs/AUTH.md +318 -0
  11. package/docs/BEST-PRACTICES.md +82 -0
  12. package/docs/CLI.md +141 -0
  13. package/docs/DESKTOP_ELECTRON.md +26 -0
  14. package/docs/DEVELOPING-APPS.md +453 -0
  15. package/docs/MCP.md +122 -0
  16. package/docs/PACKAGING_POLICY.md +19 -0
  17. package/docs/PERMISSION.md +137 -0
  18. package/docs/PROTOCOL.md +142 -0
  19. package/docs/README.md +50 -0
  20. package/docs/SKILLS.md +132 -0
  21. package/docs/TROUBLESHOOTING.md +376 -0
  22. package/docs/WORKSPACE.md +673 -0
  23. package/docs/agent-auth.md +14 -0
  24. package/docs/api/authentication.md +79 -0
  25. package/docs/api/secrets/api-keys.md +28 -0
  26. package/docs/api/secrets/credentials.md +80 -0
  27. package/docs/api/secrets/sharing.md +48 -0
  28. package/docs/api/system.md +41 -0
  29. package/docs/api/wallets/apps-strategies.md +66 -0
  30. package/docs/api/wallets/core.md +46 -0
  31. package/docs/api/wallets/data-portfolio.md +42 -0
  32. package/docs/aura-file.md +48 -0
  33. package/docs/core-concepts/FEATURES.md +114 -0
  34. package/docs/credentials.md +120 -0
  35. package/docs/external/HOW_TO_AURAMAXX/GETTING_SECRETS.md +33 -0
  36. package/docs/external/HOW_TO_AURAMAXX/README.md +45 -0
  37. package/docs/external/getting-started.md +10 -0
  38. package/docs/external/overview.md +19 -0
  39. package/docs/external/persona-paths.md +7 -0
  40. package/docs/external/share-secret.md +76 -0
  41. package/docs/external/why-aura.md +7 -0
  42. package/docs/security.md +227 -0
  43. package/docs/templates/RELEASE_NOTES_TEMPLATE.md +22 -0
  44. package/docs/wallet/AI.md +508 -0
  45. package/docs/wallet/DEVELOPING-STRATEGIES.md +713 -0
  46. package/docs/wallet/README.md +47 -0
  47. package/docs/wallet/STRATEGY.md +89 -0
  48. package/next.config.ts +28 -0
  49. package/package.json +167 -0
  50. package/postcss.config.mjs +8 -0
  51. package/prisma/migrations/20260214170000_baseline/migration.sql +511 -0
  52. package/prisma/migrations/20260216214537_add_passkey_model/migration.sql +18 -0
  53. package/prisma/migrations/20260217150500_add_credential_access_audit/migration.sql +31 -0
  54. package/prisma/migrations/20260222090000_update_admin_ttl_default/migration.sql +10 -0
  55. package/prisma/migrations/migration_lock.toml +3 -0
  56. package/prisma/schema.prisma +447 -0
  57. package/public/logo.webp +0 -0
  58. package/scripts/add-app.js +245 -0
  59. package/server/abi/SwapHelper.json +438 -0
  60. package/server/cli/approval.ts +447 -0
  61. package/server/cli/commands/actions.ts +474 -0
  62. package/server/cli/commands/api.ts +220 -0
  63. package/server/cli/commands/apikey.ts +277 -0
  64. package/server/cli/commands/app.ts +204 -0
  65. package/server/cli/commands/auth.ts +464 -0
  66. package/server/cli/commands/cron.ts +24 -0
  67. package/server/cli/commands/diary.ts +274 -0
  68. package/server/cli/commands/doctor.ts +1247 -0
  69. package/server/cli/commands/env.ts +476 -0
  70. package/server/cli/commands/experimental.ts +69 -0
  71. package/server/cli/commands/init.ts +798 -0
  72. package/server/cli/commands/lock.ts +157 -0
  73. package/server/cli/commands/mcp.ts +285 -0
  74. package/server/cli/commands/quickhack.ts +86 -0
  75. package/server/cli/commands/release-check.ts +231 -0
  76. package/server/cli/commands/restore.ts +314 -0
  77. package/server/cli/commands/service.ts +320 -0
  78. package/server/cli/commands/shell-hook.ts +512 -0
  79. package/server/cli/commands/skill.ts +216 -0
  80. package/server/cli/commands/start.ts +139 -0
  81. package/server/cli/commands/status.ts +59 -0
  82. package/server/cli/commands/stop.ts +36 -0
  83. package/server/cli/commands/token.ts +180 -0
  84. package/server/cli/commands/unlock.ts +50 -0
  85. package/server/cli/commands/vault.ts +1323 -0
  86. package/server/cli/commands/wallet.ts +209 -0
  87. package/server/cli/index.ts +280 -0
  88. package/server/cli/lib/approval-poll.ts +94 -0
  89. package/server/cli/lib/aura-parser.ts +64 -0
  90. package/server/cli/lib/credential-create.ts +74 -0
  91. package/server/cli/lib/credential-resolve.ts +280 -0
  92. package/server/cli/lib/dotenv-migrate.ts +116 -0
  93. package/server/cli/lib/dotenv-parser.ts +146 -0
  94. package/server/cli/lib/escalation.ts +57 -0
  95. package/server/cli/lib/http.ts +91 -0
  96. package/server/cli/lib/init-steps.ts +76 -0
  97. package/server/cli/lib/local-agent-trust.ts +45 -0
  98. package/server/cli/lib/lock-unlock-helper.ts +71 -0
  99. package/server/cli/lib/process.ts +162 -0
  100. package/server/cli/lib/prompt.ts +294 -0
  101. package/server/cli/lib/theme.ts +240 -0
  102. package/server/cli/socket.ts +579 -0
  103. package/server/cli/transport-client.ts +50 -0
  104. package/server/cron/index.ts +137 -0
  105. package/server/cron/job.ts +31 -0
  106. package/server/cron/jobs/balance-sync.ts +436 -0
  107. package/server/cron/jobs/incoming-scan.ts +506 -0
  108. package/server/cron/jobs/native-price.ts +70 -0
  109. package/server/cron/jobs/orphan-cleanup.ts +40 -0
  110. package/server/cron/jobs/strategy-runner.ts +175 -0
  111. package/server/cron/scheduler.ts +125 -0
  112. package/server/index.ts +420 -0
  113. package/server/lib/adapters/factory.ts +119 -0
  114. package/server/lib/adapters/index.ts +19 -0
  115. package/server/lib/adapters/router.ts +297 -0
  116. package/server/lib/adapters/telegram.ts +645 -0
  117. package/server/lib/adapters/types.ts +89 -0
  118. package/server/lib/adapters/webhook.ts +95 -0
  119. package/server/lib/address.ts +49 -0
  120. package/server/lib/agent-auth/contracts.ts +1194 -0
  121. package/server/lib/agent-profiles.ts +419 -0
  122. package/server/lib/ai.ts +285 -0
  123. package/server/lib/api-registry/contracts.ts +86 -0
  124. package/server/lib/api-registry/validation.ts +172 -0
  125. package/server/lib/apikey-migration.ts +258 -0
  126. package/server/lib/app-installer.ts +505 -0
  127. package/server/lib/app-tokens.ts +247 -0
  128. package/server/lib/approval-link.ts +27 -0
  129. package/server/lib/auth.ts +314 -0
  130. package/server/lib/auto-execute.ts +160 -0
  131. package/server/lib/batch.ts +242 -0
  132. package/server/lib/cold.ts +1048 -0
  133. package/server/lib/config.ts +408 -0
  134. package/server/lib/credential-access-audit.ts +85 -0
  135. package/server/lib/credential-access-policy.ts +111 -0
  136. package/server/lib/credential-health.ts +343 -0
  137. package/server/lib/credential-import.ts +608 -0
  138. package/server/lib/credential-scope.ts +102 -0
  139. package/server/lib/credential-shares.ts +190 -0
  140. package/server/lib/credential-transport.ts +533 -0
  141. package/server/lib/credential-vault.ts +77 -0
  142. package/server/lib/credentials.ts +422 -0
  143. package/server/lib/crypto.ts +8 -0
  144. package/server/lib/db.ts +58 -0
  145. package/server/lib/defaults.ts +386 -0
  146. package/server/lib/dex/index.ts +80 -0
  147. package/server/lib/dex/relay.ts +235 -0
  148. package/server/lib/dex/types.ts +59 -0
  149. package/server/lib/dex/uniswap.ts +370 -0
  150. package/server/lib/diary.ts +34 -0
  151. package/server/lib/dont-ask-again-policy.ts +41 -0
  152. package/server/lib/e2e-agent/artifacts.ts +36 -0
  153. package/server/lib/e2e-agent/contracts.ts +112 -0
  154. package/server/lib/e2e-agent/validation.ts +135 -0
  155. package/server/lib/encrypt.ts +114 -0
  156. package/server/lib/error.ts +20 -0
  157. package/server/lib/events.ts +217 -0
  158. package/server/lib/feature-flags.ts +93 -0
  159. package/server/lib/hot.ts +357 -0
  160. package/server/lib/human-action-summary.ts +80 -0
  161. package/server/lib/key-fingerprint.ts +28 -0
  162. package/server/lib/logger.ts +340 -0
  163. package/server/lib/network.ts +137 -0
  164. package/server/lib/notifications.ts +230 -0
  165. package/server/lib/oauth2-refresh.ts +241 -0
  166. package/server/lib/oursecret.ts +71 -0
  167. package/server/lib/passkey-credential.ts +360 -0
  168. package/server/lib/passkey.ts +68 -0
  169. package/server/lib/permissions.ts +299 -0
  170. package/server/lib/pino.ts +24 -0
  171. package/server/lib/policy-preview.ts +138 -0
  172. package/server/lib/price.ts +338 -0
  173. package/server/lib/prices.ts +34 -0
  174. package/server/lib/project-scope.ts +297 -0
  175. package/server/lib/resolve-action.ts +328 -0
  176. package/server/lib/resolve.ts +36 -0
  177. package/server/lib/secret-gist-share.ts +296 -0
  178. package/server/lib/sessions.ts +634 -0
  179. package/server/lib/socket-path.ts +56 -0
  180. package/server/lib/solana/connection.ts +26 -0
  181. package/server/lib/solana/jupiter.ts +128 -0
  182. package/server/lib/solana/transfer.ts +108 -0
  183. package/server/lib/solana/wallet.ts +136 -0
  184. package/server/lib/strategy/emits.ts +21 -0
  185. package/server/lib/strategy/engine.ts +1305 -0
  186. package/server/lib/strategy/executor.ts +115 -0
  187. package/server/lib/strategy/hook-context.ts +159 -0
  188. package/server/lib/strategy/hooks.ts +990 -0
  189. package/server/lib/strategy/index.ts +28 -0
  190. package/server/lib/strategy/installer.ts +305 -0
  191. package/server/lib/strategy/loader.ts +256 -0
  192. package/server/lib/strategy/message.ts +237 -0
  193. package/server/lib/strategy/repository.ts +218 -0
  194. package/server/lib/strategy/session-logger.ts +693 -0
  195. package/server/lib/strategy/sources.ts +288 -0
  196. package/server/lib/strategy/state.ts +189 -0
  197. package/server/lib/strategy/templates.ts +403 -0
  198. package/server/lib/strategy/tick.ts +404 -0
  199. package/server/lib/strategy/types.ts +230 -0
  200. package/server/lib/swap.ts +3 -0
  201. package/server/lib/temp.ts +86 -0
  202. package/server/lib/token-metadata.ts +86 -0
  203. package/server/lib/token-safety.ts +200 -0
  204. package/server/lib/token-search.ts +444 -0
  205. package/server/lib/totp.ts +194 -0
  206. package/server/lib/transactions.ts +123 -0
  207. package/server/lib/transport.ts +84 -0
  208. package/server/lib/txhistory/decoder.ts +262 -0
  209. package/server/lib/txhistory/enricher.ts +652 -0
  210. package/server/lib/txhistory/index.ts +391 -0
  211. package/server/lib/txhistory/signatures.ts +59 -0
  212. package/server/lib/update-check.ts +35 -0
  213. package/server/lib/verified-summary.ts +414 -0
  214. package/server/lib/view-registry.ts +80 -0
  215. package/server/mcp/profile-policy.ts +30 -0
  216. package/server/mcp/server.ts +1589 -0
  217. package/server/mcp/tools.ts +276 -0
  218. package/server/middleware/auth.ts +119 -0
  219. package/server/middleware/requestLogger.ts +84 -0
  220. package/server/routes/actions.ts +539 -0
  221. package/server/routes/adapters.ts +711 -0
  222. package/server/routes/addressbook.ts +113 -0
  223. package/server/routes/ai.ts +34 -0
  224. package/server/routes/apikeys.ts +343 -0
  225. package/server/routes/apps.ts +601 -0
  226. package/server/routes/auth.ts +406 -0
  227. package/server/routes/backup.ts +404 -0
  228. package/server/routes/batch.ts +270 -0
  229. package/server/routes/bookmarks.ts +162 -0
  230. package/server/routes/credential-shares.ts +380 -0
  231. package/server/routes/credential-vaults.ts +159 -0
  232. package/server/routes/credentials.ts +1782 -0
  233. package/server/routes/dashboard.ts +97 -0
  234. package/server/routes/defaults.ts +124 -0
  235. package/server/routes/flags.ts +11 -0
  236. package/server/routes/fund.ts +225 -0
  237. package/server/routes/heartbeat.ts +375 -0
  238. package/server/routes/import.ts +364 -0
  239. package/server/routes/launch.ts +665 -0
  240. package/server/routes/lock.ts +54 -0
  241. package/server/routes/logs.ts +68 -0
  242. package/server/routes/nuke.ts +111 -0
  243. package/server/routes/passkey-credentials.ts +99 -0
  244. package/server/routes/passkey.ts +366 -0
  245. package/server/routes/portfolio.ts +217 -0
  246. package/server/routes/price.ts +63 -0
  247. package/server/routes/resolve.ts +31 -0
  248. package/server/routes/security.ts +45 -0
  249. package/server/routes/send-evm.ts +241 -0
  250. package/server/routes/send-solana.ts +281 -0
  251. package/server/routes/send.ts +178 -0
  252. package/server/routes/setup.ts +210 -0
  253. package/server/routes/strategy.ts +894 -0
  254. package/server/routes/swap-evm.ts +352 -0
  255. package/server/routes/swap-solana.ts +176 -0
  256. package/server/routes/swap.ts +356 -0
  257. package/server/routes/token.ts +247 -0
  258. package/server/routes/unlock.ts +467 -0
  259. package/server/routes/views.ts +41 -0
  260. package/server/routes/wallet-assets.ts +361 -0
  261. package/server/routes/wallet-transactions.ts +515 -0
  262. package/server/routes/wallet.ts +709 -0
  263. package/server/types.ts +146 -0
  264. package/shared/credential-field-schema.ts +248 -0
  265. package/skills/auramaxx/HEARTBEAT.md +78 -0
  266. package/skills/auramaxx/SKILL.md +745 -0
  267. package/skills/auramaxx/docs/AGENT_SETUP.md +155 -0
  268. package/skills/auramaxx/docs/API.md +127 -0
  269. package/skills/auramaxx/docs/AUTH.md +318 -0
  270. package/skills/auramaxx/docs/CLI.md +130 -0
  271. package/skills/auramaxx/docs/MCP.md +122 -0
  272. package/skills/auramaxx/docs/TROUBLESHOOTING.md +357 -0
  273. package/skills/auramaxx/docs/WORKSPACE.md +673 -0
  274. package/skills/auramaxx/docs/security.md +227 -0
  275. package/skills/task-lifecycle/SKILL.md +378 -0
  276. package/src/app/api/[...doc]/page.tsx +36 -0
  277. package/src/app/api/agent-requests/route.ts +30 -0
  278. package/src/app/api/apps/install/route.ts +132 -0
  279. package/src/app/api/apps/manifests/route.ts +16 -0
  280. package/src/app/api/apps/static/[...path]/route.ts +57 -0
  281. package/src/app/api/docs/plain/route.ts +74 -0
  282. package/src/app/api/events/route.ts +92 -0
  283. package/src/app/api/page.tsx +290 -0
  284. package/src/app/api/workspace/[id]/apps/[wid]/route.ts +119 -0
  285. package/src/app/api/workspace/[id]/apps/route.ts +81 -0
  286. package/src/app/api/workspace/[id]/export/route.ts +67 -0
  287. package/src/app/api/workspace/[id]/route.ts +168 -0
  288. package/src/app/api/workspace/auth.ts +40 -0
  289. package/src/app/api/workspace/config/route.ts +121 -0
  290. package/src/app/api/workspace/import/route.ts +127 -0
  291. package/src/app/api/workspace/route.ts +116 -0
  292. package/src/app/app-legacy-do-not-use/page.tsx +2245 -0
  293. package/src/app/apple-icon.png +0 -0
  294. package/src/app/approve/[actionId]/page.tsx +409 -0
  295. package/src/app/docs/DocsPageContent.tsx +269 -0
  296. package/src/app/docs/[...doc]/page.tsx +41 -0
  297. package/src/app/docs/page.tsx +38 -0
  298. package/src/app/favicon.ico +0 -0
  299. package/src/app/globals.css +819 -0
  300. package/src/app/health/page.tsx +5 -0
  301. package/src/app/hello/page.tsx +102 -0
  302. package/src/app/icon.png +0 -0
  303. package/src/app/layout.tsx +39 -0
  304. package/src/app/page.tsx +1964 -0
  305. package/src/app/privacy/page.tsx +63 -0
  306. package/src/app/providers.tsx +87 -0
  307. package/src/app/share/[token]/page.tsx +295 -0
  308. package/src/app/terms/page.tsx +80 -0
  309. package/src/components/ChainSelector.tsx +44 -0
  310. package/src/components/HumanActionBar.tsx +697 -0
  311. package/src/components/NotificationDrawer.tsx +387 -0
  312. package/src/components/PasskeyEnrollmentPrompt.tsx +235 -0
  313. package/src/components/apps/AgentKeysApp.tsx +490 -0
  314. package/src/components/apps/App.tsx +153 -0
  315. package/src/components/apps/AppGrid.tsx +15 -0
  316. package/src/components/apps/DetailedAddressDrawer.tsx +325 -0
  317. package/src/components/apps/DraggableApp.tsx +562 -0
  318. package/src/components/apps/IFrameApp.tsx +73 -0
  319. package/src/components/apps/LogsApp.tsx +360 -0
  320. package/src/components/apps/SendApp.tsx +394 -0
  321. package/src/components/apps/SetupWizardApp.tsx +1004 -0
  322. package/src/components/apps/SystemDefaultsApp.tsx +845 -0
  323. package/src/components/apps/ThirdPartyApp.tsx +428 -0
  324. package/src/components/apps/TokenApp.tsx +319 -0
  325. package/src/components/apps/TransactionsApp.tsx +438 -0
  326. package/src/components/apps/WalletDetailApp.tsx +1505 -0
  327. package/src/components/apps/index.ts +13 -0
  328. package/src/components/design-system/Button.tsx +88 -0
  329. package/src/components/design-system/ChainIndicator.tsx +65 -0
  330. package/src/components/design-system/ChainSelector.tsx +147 -0
  331. package/src/components/design-system/ConfirmationModal.tsx +107 -0
  332. package/src/components/design-system/ConfirmationPopover.tsx +81 -0
  333. package/src/components/design-system/DownloadButton.tsx +149 -0
  334. package/src/components/design-system/Drawer.tsx +133 -0
  335. package/src/components/design-system/FilterDropdown.tsx +183 -0
  336. package/src/components/design-system/ItemPicker.tsx +157 -0
  337. package/src/components/design-system/Modal.tsx +296 -0
  338. package/src/components/design-system/Popover.tsx +142 -0
  339. package/src/components/design-system/TextInput.tsx +85 -0
  340. package/src/components/design-system/Toggle.tsx +65 -0
  341. package/src/components/design-system/TyvekCollapsibleSection.tsx +55 -0
  342. package/src/components/design-system/index.ts +14 -0
  343. package/src/components/docs/ClientSideMarkdown.tsx +51 -0
  344. package/src/components/docs/DocsSearchBar.tsx +118 -0
  345. package/src/components/docs/DocsThemeToggle.tsx +38 -0
  346. package/src/components/docs/PersistentDocGroup.tsx +91 -0
  347. package/src/components/docs/ShareUrlButton.tsx +33 -0
  348. package/src/components/docs/SidebarScrollMemory.tsx +56 -0
  349. package/src/components/health/CredentialHealthDashboard.tsx +214 -0
  350. package/src/components/icons/ChainIcons.tsx +72 -0
  351. package/src/components/layout/AppStoreDrawer.tsx +369 -0
  352. package/src/components/layout/ContentArea.tsx +21 -0
  353. package/src/components/layout/CreateViewModal.tsx +88 -0
  354. package/src/components/layout/LeftRail.tsx +114 -0
  355. package/src/components/layout/TabBar.tsx +284 -0
  356. package/src/components/layout/WalletSidebar.tsx +1030 -0
  357. package/src/components/layout/index.ts +6 -0
  358. package/src/components/marketing/AuraMaxxSpecOverlay.tsx +653 -0
  359. package/src/components/marketing/DeviceMorphExperience.tsx +216 -0
  360. package/src/components/vault/ApiKeysConsole.tsx +1272 -0
  361. package/src/components/vault/AuditConsole.tsx +600 -0
  362. package/src/components/vault/CredentialDetail.tsx +625 -0
  363. package/src/components/vault/CredentialEmpty.tsx +55 -0
  364. package/src/components/vault/CredentialField.tsx +583 -0
  365. package/src/components/vault/CredentialForm.tsx +1484 -0
  366. package/src/components/vault/CredentialList.tsx +265 -0
  367. package/src/components/vault/CredentialRow.tsx +130 -0
  368. package/src/components/vault/CredentialShareModal.tsx +273 -0
  369. package/src/components/vault/CredentialVault.tsx +1662 -0
  370. package/src/components/vault/CredentialWalletWidget.tsx +103 -0
  371. package/src/components/vault/DocsConsole.tsx +113 -0
  372. package/src/components/vault/ImportCredentialsModal.tsx +578 -0
  373. package/src/components/vault/LargeTypeModal.tsx +88 -0
  374. package/src/components/vault/PasswordGenerator.tsx +232 -0
  375. package/src/components/vault/TOTPDisplay.tsx +108 -0
  376. package/src/components/vault/TotpSetupPanel.tsx +198 -0
  377. package/src/components/vault/VaultSidebar.tsx +881 -0
  378. package/src/components/vault/credentialFormName.ts +91 -0
  379. package/src/components/vault/hooks/useVaultKeyboardShortcuts.ts +69 -0
  380. package/src/components/vault/types.ts +56 -0
  381. package/src/context/AuthContext.tsx +365 -0
  382. package/src/context/PriceContext.tsx +113 -0
  383. package/src/context/ThemeContext.tsx +164 -0
  384. package/src/context/WebSocketContext.tsx +269 -0
  385. package/src/context/WorkspaceContext.tsx +668 -0
  386. package/src/hooks/index.ts +4 -0
  387. package/src/hooks/useAgentActions.ts +552 -0
  388. package/src/hooks/useBalance.ts +103 -0
  389. package/src/hooks/useBalances.ts +129 -0
  390. package/src/hooks/useTheme.ts +156 -0
  391. package/src/instrumentation.ts +12 -0
  392. package/src/lib/api-docs.ts +154 -0
  393. package/src/lib/api.ts +474 -0
  394. package/src/lib/app-loader.ts +148 -0
  395. package/src/lib/app-registry.ts +178 -0
  396. package/src/lib/app-sdk.ts +157 -0
  397. package/src/lib/audit-console-adapter.ts +151 -0
  398. package/src/lib/auth-client.ts +75 -0
  399. package/src/lib/config.ts +74 -0
  400. package/src/lib/credential-field-schema.ts +11 -0
  401. package/src/lib/crypto.ts +112 -0
  402. package/src/lib/db.ts +21 -0
  403. package/src/lib/docs.ts +544 -0
  404. package/src/lib/events.ts +363 -0
  405. package/src/lib/pino.ts +24 -0
  406. package/src/lib/theme-handlers.ts +168 -0
  407. package/src/lib/theme.ts +351 -0
  408. package/src/lib/tokenData.ts +378 -0
  409. package/src/lib/totp-import.ts +57 -0
  410. package/src/lib/vault-crypto.ts +129 -0
  411. package/src/lib/view-registry.ts +57 -0
  412. package/src/lib/websocket-server.ts +302 -0
  413. package/src/lib/websocket-setup.ts +79 -0
  414. package/src/lib/wordlist.ts +2050 -0
  415. package/src/lib/workspace-handlers.ts +285 -0
  416. package/start.sh +170 -0
  417. package/tailwind.config.ts +99 -0
  418. package/tsconfig.json +42 -0
@@ -0,0 +1,1484 @@
1
+ 'use client';
2
+
3
+ import React, { useState, useEffect, useCallback, useRef } from 'react';
4
+ import { Key, Eye, Loader2, CreditCard, FileText, RefreshCw, ArrowLeft, Wallet } from 'lucide-react';
5
+ import { Button, TextInput, FilterDropdown, Modal, Toggle, ItemPicker } from '@/components/design-system';
6
+ import { api, Api } from '@/lib/api';
7
+ import { decryptCredentialPayload } from '@/lib/vault-crypto';
8
+ import {
9
+ CREDENTIAL_FIELD_KEYS,
10
+ CREDENTIAL_FIELD_SCHEMA,
11
+ canonicalizeCredentialFieldKey,
12
+ NOTE_CONTENT_KEY,
13
+ getCredentialPrimaryFieldKey,
14
+ getCredentialPrimaryFieldSpec,
15
+ } from '@/lib/credential-field-schema';
16
+ import { PasswordGenerator } from './PasswordGenerator';
17
+ import { TotpSetupPanel } from './TotpSetupPanel';
18
+ import { deriveCredentialName, canDeriveName as canDeriveNameFromInputs } from './credentialFormName';
19
+ import type { VaultInfo, CredentialMeta, WalletLinkMetaV1 } from './types';
20
+
21
+ interface CredentialFormProps {
22
+ isOpen: boolean;
23
+ onClose: () => void;
24
+ onSaved: (credentialId?: string) => void | Promise<void>;
25
+ editCredentialId?: string;
26
+ vaults: VaultInfo[];
27
+ createStartStep?: 'type' | 'form';
28
+ createStartType?: FormType;
29
+ createPrefill?: {
30
+ vaultId?: string;
31
+ tags?: string[];
32
+ type?: FormType;
33
+ };
34
+ }
35
+
36
+ type FormType = 'login' | 'card' | 'note' | 'plain_note' | 'hot_wallet' | 'apikey' | 'oauth2' | 'ssh' | 'gpg' | 'custom';
37
+
38
+ const LOGIN_FIELD_KEYS = CREDENTIAL_FIELD_KEYS.login;
39
+ const CARD_FIELD_KEYS = CREDENTIAL_FIELD_KEYS.card;
40
+ const HOT_WALLET_FIELD_KEYS = CREDENTIAL_FIELD_KEYS.hot_wallet;
41
+ const APIKEY_FIELD_KEYS = CREDENTIAL_FIELD_KEYS.apikey;
42
+ const OAUTH2_FIELD_KEYS = CREDENTIAL_FIELD_KEYS.oauth2;
43
+ const SSH_FIELD_KEYS = CREDENTIAL_FIELD_KEYS.ssh;
44
+ const GPG_FIELD_KEYS = CREDENTIAL_FIELD_KEYS.gpg;
45
+
46
+ const TYPE_META: Record<FormType, {
47
+ label: string;
48
+ description: string;
49
+ icon: React.ComponentType<{ size?: number; className?: string }>;
50
+ }> = {
51
+ hot_wallet: { label: 'Hot Wallet', description: 'Generate wallet address + encrypted private key', icon: Wallet },
52
+ plain_note: { label: 'Plain Note', description: 'Readable note content stored without encryption', icon: FileText },
53
+ apikey: { label: 'API Key', description: 'Simple key/value secret', icon: Key },
54
+ login: { label: 'Login', description: 'Websites and app accounts', icon: Key },
55
+ note: { label: 'Secure Note', description: 'Private notes and secrets', icon: FileText },
56
+ card: { label: 'Credit Card', description: 'Payment cards and details', icon: CreditCard },
57
+ oauth2: { label: 'OAuth2', description: 'Auto-refreshing OAuth2 tokens', icon: RefreshCw },
58
+ ssh: { label: 'SSH Key', description: 'Private/public keypair for SSH', icon: Key },
59
+ gpg: { label: 'GPG Key', description: 'Armored key material and metadata', icon: FileText },
60
+ custom: { label: 'Key / Value', description: 'Define a custom key/value secret field', icon: FileText },
61
+ };
62
+
63
+ const FEATURED_TYPE_ORDER: FormType[] = ['apikey', 'login', 'plain_note'];
64
+ const REMAINING_TYPE_ORDER: FormType[] = ['custom', 'card', 'hot_wallet', 'note', 'oauth2', 'ssh', 'gpg'];
65
+
66
+ function normalizeHotWalletChain(raw: string | undefined): 'base' | 'solana' {
67
+ const chain = (raw || '').trim().toLowerCase();
68
+ if (chain === 'solana' || chain === 'solana-devnet') return 'solana';
69
+ return 'base';
70
+ }
71
+
72
+ const BRAND_OPTIONS = [
73
+ { value: 'visa', label: 'Visa' },
74
+ { value: 'mastercard', label: 'Mastercard' },
75
+ { value: 'amex', label: 'Amex' },
76
+ { value: 'discover', label: 'Discover' },
77
+ { value: 'other', label: 'Other' },
78
+ ];
79
+
80
+ const normalizeForRank = (value: string) => value.trim().toLowerCase();
81
+
82
+ const toTimestamp = (updatedAt?: string, createdAt?: string) => {
83
+ const updated = updatedAt ? Date.parse(updatedAt) : Number.NaN;
84
+ if (Number.isFinite(updated)) return updated;
85
+ const created = createdAt ? Date.parse(createdAt) : Number.NaN;
86
+ if (Number.isFinite(created)) return created;
87
+ return 0;
88
+ };
89
+
90
+ export const CredentialForm: React.FC<CredentialFormProps> = ({
91
+ isOpen,
92
+ onClose,
93
+ onSaved,
94
+ editCredentialId,
95
+ vaults,
96
+ createStartStep = 'form',
97
+ createStartType,
98
+ createPrefill,
99
+ }) => {
100
+ const [type, setType] = useState<FormType>('apikey');
101
+ const [createStep, setCreateStep] = useState<'type' | 'form'>('form');
102
+ const [name, setName] = useState('');
103
+ const [vaultId, setVaultId] = useState('');
104
+ const [favorite, setFavorite] = useState(false);
105
+ const [tags, setTags] = useState<string[]>([]);
106
+ const [tagInput, setTagInput] = useState('');
107
+
108
+ // Login fields
109
+ const [url, setUrl] = useState('');
110
+ const [username, setUsername] = useState('');
111
+ const [password, setPassword] = useState('');
112
+ const [loginNotes, setLoginNotes] = useState('');
113
+ const [totpSecret, setTotpSecret] = useState('');
114
+ const [totpIntent, setTotpIntent] = useState<'keep' | 'replace' | 'remove'>('keep');
115
+ const [hasExistingTotp, setHasExistingTotp] = useState(false);
116
+
117
+ // Card fields
118
+ const [cardholder, setCardholder] = useState('');
119
+ const [brand, setBrand] = useState('visa');
120
+ const [cardNumber, setCardNumber] = useState('');
121
+ const [expiry, setExpiry] = useState('');
122
+ const [cvv, setCvv] = useState('');
123
+ const [billingZip, setBillingZip] = useState('');
124
+ const [cardNotes, setCardNotes] = useState('');
125
+
126
+ // Note fields
127
+ const [noteContent, setNoteContent] = useState('');
128
+
129
+ // Hot wallet fields
130
+ const [hotWalletChain, setHotWalletChain] = useState('base');
131
+ const [hotWalletAddress, setHotWalletAddress] = useState('');
132
+
133
+ // API key fields
134
+ const [apiKeyName, setApiKeyName] = useState('');
135
+ const [apiKeyValue, setApiKeyValue] = useState('');
136
+ const [customFieldKey, setCustomFieldKey] = useState('value');
137
+ const [customFieldValue, setCustomFieldValue] = useState('');
138
+
139
+ // OAuth2 fields
140
+ const [oauth2TokenEndpoint, setOauth2TokenEndpoint] = useState('');
141
+ const [oauth2ClientId, setOauth2ClientId] = useState('');
142
+ const [oauth2ClientSecret, setOauth2ClientSecret] = useState('');
143
+ const [oauth2AccessToken, setOauth2AccessToken] = useState('');
144
+ const [oauth2RefreshToken, setOauth2RefreshToken] = useState('');
145
+ const [oauth2Scopes, setOauth2Scopes] = useState('');
146
+ const [oauth2AuthMethod, setOauth2AuthMethod] = useState('client_secret_post');
147
+ const [oauth2ExpiresAt, setOauth2ExpiresAt] = useState('');
148
+
149
+
150
+ // SSH fields
151
+ const [sshPublicKey, setSshPublicKey] = useState('');
152
+ const [sshPrivateKey, setSshPrivateKey] = useState('');
153
+ const [sshPassphrase, setSshPassphrase] = useState('');
154
+ const [sshHostsInput, setSshHostsInput] = useState('');
155
+ const [sshKeyType, setSshKeyType] = useState('');
156
+
157
+ // GPG fields
158
+ const [gpgPublicKey, setGpgPublicKey] = useState('');
159
+ const [gpgPrivateKey, setGpgPrivateKey] = useState('');
160
+ const [gpgKeyId, setGpgKeyId] = useState('');
161
+ const [gpgUidEmail, setGpgUidEmail] = useState('');
162
+ const [gpgExpiresAt, setGpgExpiresAt] = useState('');
163
+
164
+ // Preserve any existing wallet link metadata during edits.
165
+ const [walletLink, setWalletLink] = useState<WalletLinkMetaV1 | null>(null);
166
+
167
+ const [saving, setSaving] = useState(false);
168
+ const [error, setError] = useState<string | null>(null);
169
+ const [showPasswordGen, setShowPasswordGen] = useState(false);
170
+ const [showPasswordActions, setShowPasswordActions] = useState(false);
171
+ const [showGlobalAdvanced, setShowGlobalAdvanced] = useState(false);
172
+ const [showUsernameSuggestions, setShowUsernameSuggestions] = useState(false);
173
+ const [usernameSuggestions, setUsernameSuggestions] = useState<string[]>([]);
174
+ const [tagSuggestions, setTagSuggestions] = useState<string[]>([]);
175
+ const [revealingField, setRevealingField] = useState<string | null>(null);
176
+ const [sensitiveFieldCache, setSensitiveFieldCache] = useState<Record<string, string> | null>(null);
177
+ const [dirtySensitiveFields, setDirtySensitiveFields] = useState<Record<string, boolean>>({});
178
+ const [visibleSensitiveFields, setVisibleSensitiveFields] = useState<Record<string, boolean>>({});
179
+ const passwordActionsRef = useRef<HTMLDivElement | null>(null);
180
+
181
+ const isEdit = !!editCredentialId;
182
+
183
+ const setSensitiveFieldValue = useCallback((fieldKey: string, value: string, credentialType: FormType) => {
184
+ const canonicalFieldKey = canonicalizeCredentialFieldKey(credentialType, fieldKey);
185
+
186
+ switch (canonicalFieldKey) {
187
+ case LOGIN_FIELD_KEYS.password:
188
+ setPassword(value);
189
+ break;
190
+ case LOGIN_FIELD_KEYS.notes:
191
+ if (credentialType === 'login') setLoginNotes(value);
192
+ if (credentialType === 'card') setCardNotes(value);
193
+ break;
194
+ case CARD_FIELD_KEYS.number:
195
+ setCardNumber(value);
196
+ break;
197
+ case CARD_FIELD_KEYS.expiry:
198
+ setExpiry(value);
199
+ break;
200
+ case CARD_FIELD_KEYS.cvv:
201
+ setCvv(value);
202
+ break;
203
+ case NOTE_CONTENT_KEY:
204
+ setNoteContent(value);
205
+ break;
206
+ case APIKEY_FIELD_KEYS.value:
207
+ setApiKeyValue(value);
208
+ break;
209
+ case OAUTH2_FIELD_KEYS.accessToken:
210
+ setOauth2AccessToken(value);
211
+ break;
212
+ case OAUTH2_FIELD_KEYS.refreshToken:
213
+ setOauth2RefreshToken(value);
214
+ break;
215
+ case OAUTH2_FIELD_KEYS.clientId:
216
+ setOauth2ClientId(value);
217
+ break;
218
+ case OAUTH2_FIELD_KEYS.clientSecret:
219
+ setOauth2ClientSecret(value);
220
+ break;
221
+ case SSH_FIELD_KEYS.privateKey:
222
+ if (credentialType === 'ssh') setSshPrivateKey(value);
223
+ if (credentialType === 'gpg') setGpgPrivateKey(value);
224
+ break;
225
+ case SSH_FIELD_KEYS.passphrase:
226
+ setSshPassphrase(value);
227
+ break;
228
+ default:
229
+ if (credentialType === 'custom') setCustomFieldValue(value);
230
+ break;
231
+ }
232
+ }, []);
233
+
234
+ const markSensitiveDirty = useCallback((fieldKey: string) => {
235
+ setDirtySensitiveFields((prev) => ({ ...prev, [fieldKey]: true }));
236
+ }, []);
237
+
238
+ // Default vault
239
+ useEffect(() => {
240
+ if (vaults.length > 0 && !vaultId) {
241
+ const primary = vaults.find((v) => v.isPrimary);
242
+ setVaultId(primary?.id || vaults[0].id);
243
+ }
244
+ }, [vaults, vaultId]);
245
+
246
+ // Load existing credential for editing
247
+ const loadCredential = useCallback(async () => {
248
+ if (!editCredentialId) return;
249
+ try {
250
+ const res = await api.get<{ success: boolean; credential: CredentialMeta }>(Api.Wallet, `/credentials/${editCredentialId}`);
251
+ const cred = res.credential;
252
+ const credentialType = cred.type as FormType;
253
+ setName(cred.name);
254
+ setType(credentialType);
255
+ setVaultId(cred.vaultId);
256
+ setFavorite(cred.meta.favorite || false);
257
+ setTags(cred.meta.tags || []);
258
+ const existingWalletLink = cred.meta.walletLink as WalletLinkMetaV1 | undefined;
259
+ setWalletLink(existingWalletLink?.walletAddress && (existingWalletLink.tier === 'hot' || existingWalletLink.tier === 'cold') ? existingWalletLink : null);
260
+
261
+ if (cred.type === 'login') {
262
+ setUrl((cred.meta[LOGIN_FIELD_KEYS.url] as string) || '');
263
+ setUsername((cred.meta[LOGIN_FIELD_KEYS.username] as string) || '');
264
+ setHasExistingTotp(Boolean(cred.meta.has_totp));
265
+ } else if (cred.type === 'card') {
266
+ setCardholder((cred.meta[CARD_FIELD_KEYS.cardholder] as string) || '');
267
+ setBrand((cred.meta[CARD_FIELD_KEYS.brand] as string) || 'visa');
268
+ setBillingZip((cred.meta[CARD_FIELD_KEYS.billingZip] as string) || '');
269
+ } else if (cred.type === 'apikey') {
270
+ setApiKeyName((cred.meta[APIKEY_FIELD_KEYS.key] as string) || '');
271
+ setApiKeyValue((cred.meta[APIKEY_FIELD_KEYS.value] as string) || '');
272
+ } else if (cred.type === 'plain_note') {
273
+ setNoteContent(
274
+ (cred.meta[NOTE_CONTENT_KEY] as string)
275
+ || (cred.meta[APIKEY_FIELD_KEYS.value] as string)
276
+ || '',
277
+ );
278
+ } else if (cred.type === 'oauth2') {
279
+ setOauth2TokenEndpoint((cred.meta[OAUTH2_FIELD_KEYS.tokenEndpoint] as string) || '');
280
+ setOauth2Scopes((cred.meta[OAUTH2_FIELD_KEYS.scopes] as string) || '');
281
+ setOauth2AuthMethod((cred.meta[OAUTH2_FIELD_KEYS.authMethod] as string) || 'client_secret_post');
282
+ setOauth2ExpiresAt(
283
+ typeof cred.meta[OAUTH2_FIELD_KEYS.expiresAt] === 'number'
284
+ ? String(cred.meta[OAUTH2_FIELD_KEYS.expiresAt])
285
+ : typeof cred.meta[OAUTH2_FIELD_KEYS.expiresAt] === 'string'
286
+ ? cred.meta[OAUTH2_FIELD_KEYS.expiresAt] as string
287
+ : '',
288
+ );
289
+ } else if (cred.type === 'hot_wallet') {
290
+ setHotWalletAddress(
291
+ (cred.meta[HOT_WALLET_FIELD_KEYS.address] as string)
292
+ || existingWalletLink?.walletAddress
293
+ || '',
294
+ );
295
+ setHotWalletChain(
296
+ normalizeHotWalletChain(
297
+ (cred.meta[HOT_WALLET_FIELD_KEYS.chain] as string)
298
+ || existingWalletLink?.chain,
299
+ ),
300
+ );
301
+ } else if (cred.type === 'ssh') {
302
+ setSshPublicKey((cred.meta[SSH_FIELD_KEYS.publicKey] as string) || '');
303
+ setSshHostsInput(Array.isArray(cred.meta[SSH_FIELD_KEYS.hosts]) ? (cred.meta[SSH_FIELD_KEYS.hosts] as string[]).join('\n') : '');
304
+ setSshKeyType((cred.meta[SSH_FIELD_KEYS.keyType] as string) || '');
305
+ } else if (cred.type === 'gpg') {
306
+ setGpgPublicKey((cred.meta[GPG_FIELD_KEYS.publicKey] as string) || '');
307
+ setGpgKeyId((cred.meta[GPG_FIELD_KEYS.keyId] as string) || '');
308
+ setGpgUidEmail((cred.meta[GPG_FIELD_KEYS.uidEmail] as string) || '');
309
+ setGpgExpiresAt((cred.meta[GPG_FIELD_KEYS.expiresAt] as string) || '');
310
+ }
311
+
312
+ // Hydrate sensitive values on edit so the modal has actual secret values immediately.
313
+ const schema = CREDENTIAL_FIELD_SCHEMA[credentialType];
314
+ const hasSensitiveEditableFields = credentialType === 'custom'
315
+ || (credentialType !== 'hot_wallet' && (schema || []).some((field) => field.sensitive));
316
+ if (hasSensitiveEditableFields) {
317
+ try {
318
+ const readRes = await api.post<{ encrypted: string }>(Api.Wallet, `/credentials/${editCredentialId}/read`);
319
+ const plaintext = await decryptCredentialPayload(readRes.encrypted);
320
+ const parsed = JSON.parse(plaintext) as { fields?: Array<{ key: string; value: string }> };
321
+ const fieldMap: Record<string, string> = {};
322
+
323
+ (parsed.fields || []).forEach((field) => {
324
+ const canonicalFieldKey = canonicalizeCredentialFieldKey(credentialType, field.key);
325
+ fieldMap[canonicalFieldKey] = field.value;
326
+ });
327
+
328
+ setSensitiveFieldCache(fieldMap);
329
+ Object.entries(fieldMap).forEach(([fieldKey, fieldValue]) => {
330
+ setSensitiveFieldValue(fieldKey, fieldValue, credentialType);
331
+ });
332
+ } catch {
333
+ // Keep masked placeholders if read/decrypt fails; manual reveal can retry.
334
+ }
335
+ }
336
+ } catch (err) {
337
+ setError(err instanceof Error ? err.message : 'Failed to load credential');
338
+ }
339
+ }, [editCredentialId, setSensitiveFieldValue]);
340
+
341
+ const getSensitiveFieldMap = useCallback(async (): Promise<Record<string, string>> => {
342
+ if (!isEdit || !editCredentialId) return {};
343
+ if (sensitiveFieldCache) return sensitiveFieldCache;
344
+
345
+ const res = await api.post<{ encrypted: string }>(Api.Wallet, `/credentials/${editCredentialId}/read`);
346
+ const plaintext = await decryptCredentialPayload(res.encrypted);
347
+ const parsed = JSON.parse(plaintext) as { fields?: Array<{ key: string; value: string }> };
348
+ const fieldMap: Record<string, string> = {};
349
+
350
+ (parsed.fields || []).forEach((field) => {
351
+ const canonicalFieldKey = canonicalizeCredentialFieldKey(type, field.key);
352
+ fieldMap[canonicalFieldKey] = field.value;
353
+ });
354
+
355
+ setSensitiveFieldCache(fieldMap);
356
+ return fieldMap;
357
+ }, [isEdit, editCredentialId, sensitiveFieldCache, type]);
358
+
359
+ const handleRevealField = useCallback(async (fieldKey: string) => {
360
+ if (!isEdit) return;
361
+ setRevealingField(fieldKey);
362
+ setError(null);
363
+ try {
364
+ const fieldMap = await getSensitiveFieldMap();
365
+ const canonicalFieldKey = canonicalizeCredentialFieldKey(type, fieldKey);
366
+ setSensitiveFieldValue(
367
+ fieldKey,
368
+ fieldMap[canonicalFieldKey] || fieldMap[fieldKey] || '',
369
+ type,
370
+ );
371
+ } catch (err) {
372
+ setError(err instanceof Error ? err.message : 'Decryption failed -- try re-unlocking');
373
+ } finally {
374
+ setRevealingField(null);
375
+ }
376
+ }, [getSensitiveFieldMap, isEdit, setSensitiveFieldValue]);
377
+
378
+ const getSensitiveFieldValue = useCallback((fieldKey: string): string => {
379
+ const canonicalFieldKey = canonicalizeCredentialFieldKey(type, fieldKey);
380
+ switch (canonicalFieldKey) {
381
+ case LOGIN_FIELD_KEYS.password:
382
+ return password;
383
+ case APIKEY_FIELD_KEYS.value:
384
+ return apiKeyValue;
385
+ case CREDENTIAL_FIELD_KEYS.custom.value:
386
+ return customFieldValue;
387
+ default:
388
+ return type === 'custom' ? customFieldValue : '';
389
+ }
390
+ }, [apiKeyValue, customFieldValue, password, type]);
391
+
392
+ const isSensitiveFieldVisible = useCallback((fieldKey: string) => {
393
+ const canonicalFieldKey = canonicalizeCredentialFieldKey(type, fieldKey);
394
+ return !!visibleSensitiveFields[canonicalFieldKey];
395
+ }, [type, visibleSensitiveFields]);
396
+
397
+ const toggleSensitiveFieldVisibility = useCallback(async (fieldKey: string) => {
398
+ const canonicalFieldKey = canonicalizeCredentialFieldKey(type, fieldKey);
399
+ if (visibleSensitiveFields[canonicalFieldKey]) {
400
+ setVisibleSensitiveFields((prev) => ({ ...prev, [canonicalFieldKey]: false }));
401
+ return;
402
+ }
403
+
404
+ // Edit mode values may be masked placeholders until decrypted.
405
+ if (isEdit && !getSensitiveFieldValue(fieldKey).trim()) {
406
+ await handleRevealField(fieldKey);
407
+ }
408
+
409
+ setVisibleSensitiveFields((prev) => ({ ...prev, [canonicalFieldKey]: true }));
410
+ }, [getSensitiveFieldValue, handleRevealField, isEdit, type, visibleSensitiveFields]);
411
+
412
+ useEffect(() => {
413
+ if (isOpen) {
414
+ if (!isEdit) {
415
+ setCreateStep(createStartStep);
416
+ if (createStartType) {
417
+ setType(createStartType);
418
+ }
419
+ if (createPrefill?.type) {
420
+ setType(createPrefill.type);
421
+ }
422
+ if (createPrefill?.vaultId) {
423
+ setVaultId(createPrefill.vaultId);
424
+ }
425
+ if (createPrefill?.tags && createPrefill.tags.length > 0) {
426
+ setTags(createPrefill.tags);
427
+ }
428
+ } else {
429
+ setCreateStep('form');
430
+ }
431
+ setShowGlobalAdvanced(false);
432
+ setShowPasswordActions(false);
433
+ setVisibleSensitiveFields({});
434
+ setTotpIntent(isEdit ? 'keep' : 'replace');
435
+ }
436
+ }, [isOpen, isEdit, createStartStep, createStartType, createPrefill]);
437
+
438
+ useEffect(() => {
439
+ if (!isOpen || isEdit || type !== 'oauth2' || oauth2ExpiresAt) return;
440
+ setOauth2ExpiresAt(String(Math.floor(Date.now() / 1000) + 60 * 60));
441
+ }, [isOpen, isEdit, type, oauth2ExpiresAt]);
442
+
443
+ useEffect(() => {
444
+ if (!isOpen) return;
445
+
446
+ const loadSuggestions = async () => {
447
+ try {
448
+ const res = await api.get<{ success: boolean; credentials: CredentialMeta[] }>(Api.Wallet, '/credentials');
449
+ const creds = res.credentials || [];
450
+
451
+ const loginCreds = creds.filter((c) => c.type === 'login');
452
+ const usernameStats = new Map<string, { raw: string; frequency: number; recent: number }>();
453
+
454
+ loginCreds.forEach((credential) => {
455
+ const raw = String(credential.meta.username || '').trim();
456
+ if (!raw) return;
457
+ const normalized = normalizeForRank(raw);
458
+ const timestamp = toTimestamp(credential.updatedAt, credential.createdAt);
459
+ const existing = usernameStats.get(normalized);
460
+ if (!existing) {
461
+ usernameStats.set(normalized, { raw, frequency: 1, recent: timestamp });
462
+ return;
463
+ }
464
+ existing.frequency += 1;
465
+ if (timestamp > existing.recent) {
466
+ existing.recent = timestamp;
467
+ existing.raw = raw;
468
+ }
469
+ });
470
+
471
+ const usernames = Array.from(usernameStats.entries())
472
+ .sort((a, b) => {
473
+ if (b[1].recent !== a[1].recent) return b[1].recent - a[1].recent;
474
+ if (b[1].frequency !== a[1].frequency) return b[1].frequency - a[1].frequency;
475
+ return a[0].localeCompare(b[0]);
476
+ })
477
+ .map(([, value]) => value.raw)
478
+ .slice(0, 20);
479
+
480
+ const tagStats = new Map<string, { frequency: number; recent: number }>();
481
+ creds.forEach((credential) => {
482
+ const timestamp = toTimestamp(credential.updatedAt, credential.createdAt);
483
+ const uniqueTags = new Set((Array.isArray(credential.meta.tags) ? credential.meta.tags : [])
484
+ .map((t) => String(t).trim())
485
+ .filter(Boolean));
486
+ uniqueTags.forEach((tag) => {
487
+ const normalized = normalizeForRank(tag);
488
+ const existing = tagStats.get(normalized);
489
+ if (!existing) {
490
+ tagStats.set(normalized, { frequency: 1, recent: timestamp });
491
+ return;
492
+ }
493
+ existing.frequency += 1;
494
+ if (timestamp > existing.recent) existing.recent = timestamp;
495
+ });
496
+ });
497
+
498
+ const tagsFromVault = Array.from(tagStats.entries())
499
+ .sort((a, b) => {
500
+ if (b[1].recent !== a[1].recent) return b[1].recent - a[1].recent;
501
+ if (b[1].frequency !== a[1].frequency) return b[1].frequency - a[1].frequency;
502
+ return a[0].localeCompare(b[0]);
503
+ })
504
+ .map(([tag]) => tag)
505
+ .slice(0, 30);
506
+
507
+ setUsernameSuggestions(usernames);
508
+ setTagSuggestions(tagsFromVault);
509
+ } catch {
510
+ setUsernameSuggestions([]);
511
+ setTagSuggestions([]);
512
+ }
513
+ };
514
+
515
+ void loadSuggestions();
516
+ }, [isOpen]);
517
+
518
+ useEffect(() => {
519
+ if (isOpen && editCredentialId) {
520
+ setSensitiveFieldCache(null);
521
+ setDirtySensitiveFields({});
522
+ loadCredential();
523
+ }
524
+ }, [isOpen, editCredentialId, loadCredential]);
525
+
526
+ useEffect(() => {
527
+ if (!isOpen) {
528
+ setName('');
529
+ setVaultId('');
530
+ setFavorite(false);
531
+ setTags([]);
532
+ setTagInput('');
533
+ setUrl('');
534
+ setUsername('');
535
+ setPassword('');
536
+ setLoginNotes('');
537
+ setTotpSecret('');
538
+ setTotpIntent('keep');
539
+ setHasExistingTotp(false);
540
+ setCardholder('');
541
+ setBrand('visa');
542
+ setCardNumber('');
543
+ setExpiry('');
544
+ setCvv('');
545
+ setBillingZip('');
546
+ setCardNotes('');
547
+ setNoteContent('');
548
+ setHotWalletChain('base');
549
+ setHotWalletAddress('');
550
+ setApiKeyName('');
551
+ setApiKeyValue('');
552
+ setCustomFieldKey('value');
553
+ setCustomFieldValue('');
554
+ setOauth2TokenEndpoint('');
555
+ setOauth2ClientId('');
556
+ setOauth2ClientSecret('');
557
+ setOauth2AccessToken('');
558
+ setOauth2RefreshToken('');
559
+ setOauth2Scopes('');
560
+ setOauth2AuthMethod('client_secret_post');
561
+ setOauth2ExpiresAt('');
562
+ setSshPublicKey('');
563
+ setSshPrivateKey('');
564
+ setSshPassphrase('');
565
+ setSshHostsInput('');
566
+ setSshKeyType('');
567
+ setGpgPublicKey('');
568
+ setGpgPrivateKey('');
569
+ setGpgKeyId('');
570
+ setGpgUidEmail('');
571
+ setGpgExpiresAt('');
572
+ setWalletLink(null);
573
+ setError(null);
574
+ setSaving(false);
575
+ setShowPasswordActions(false);
576
+ setRevealingField(null);
577
+ setSensitiveFieldCache(null);
578
+ setDirtySensitiveFields({});
579
+ setVisibleSensitiveFields({});
580
+ setShowGlobalAdvanced(false);
581
+ setShowUsernameSuggestions(false);
582
+ }
583
+ }, [isOpen]);
584
+
585
+ useEffect(() => {
586
+ if (type !== 'login') {
587
+ setShowPasswordActions(false);
588
+ }
589
+ }, [type]);
590
+
591
+ const vaultOptions = vaults.map((v) => ({
592
+ value: v.id,
593
+ label: v.name || (v.isPrimary ? 'Primary' : v.id.slice(0, 8)),
594
+ }));
595
+
596
+ const handleTagKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
597
+ if (e.key === 'Enter') {
598
+ e.preventDefault();
599
+ const tag = tagInput.trim();
600
+ if (tag && !tags.includes(tag)) {
601
+ setTags([...tags, tag]);
602
+ }
603
+ setTagInput('');
604
+ }
605
+ };
606
+
607
+ const removeTag = (tag: string) => {
608
+ setTags(tags.filter((t) => t !== tag));
609
+ };
610
+
611
+ const visibleTagSuggestions = tagSuggestions
612
+ .filter((tag) => !tags.includes(tag))
613
+ .filter((tag) => !tagInput.trim() || tag.toLowerCase().includes(tagInput.trim().toLowerCase()))
614
+ .slice(0, 6);
615
+
616
+ const buildPayload = () => {
617
+ const resolvedName = deriveCredentialName({
618
+ type,
619
+ name,
620
+ apiKeyName,
621
+ username,
622
+ url,
623
+ noteContent,
624
+ customFieldKey,
625
+ hotWalletChain,
626
+ cardholder,
627
+ cardNumber,
628
+ oauth2TokenEndpoint,
629
+ sshHostsInput,
630
+ gpgKeyId,
631
+ gpgUidEmail,
632
+ });
633
+ const resolvedCustomFieldKey = customFieldKey.trim() || getCredentialPrimaryFieldKey(type);
634
+ const fields: { key: string; value: string; type: string; sensitive: boolean }[] = [];
635
+ let meta: Record<string, unknown> = { tags, favorite };
636
+
637
+ switch (type) {
638
+ case 'login':
639
+ if (url) fields.push({ key: LOGIN_FIELD_KEYS.url, value: url, type: 'text', sensitive: false });
640
+ fields.push({ key: LOGIN_FIELD_KEYS.username, value: username, type: 'text', sensitive: false });
641
+ fields.push({ key: LOGIN_FIELD_KEYS.password, value: password, type: 'secret', sensitive: true });
642
+ if (loginNotes.trim() || (isEdit && dirtySensitiveFields[LOGIN_FIELD_KEYS.notes])) fields.push({ key: LOGIN_FIELD_KEYS.notes, value: loginNotes, type: 'text', sensitive: true });
643
+ if (isEdit) {
644
+ if (totpIntent === 'replace' && totpSecret.trim()) {
645
+ fields.push({ key: LOGIN_FIELD_KEYS.totp, value: totpSecret.trim().replace(/\s+/g, '').toUpperCase(), type: 'secret', sensitive: true });
646
+ }
647
+ if (totpIntent === 'remove') {
648
+ fields.push({ key: LOGIN_FIELD_KEYS.totp, value: '', type: 'secret', sensitive: true });
649
+ }
650
+ } else if (totpSecret.trim()) {
651
+ fields.push({ key: LOGIN_FIELD_KEYS.totp, value: totpSecret.trim().replace(/\s+/g, '').toUpperCase(), type: 'secret', sensitive: true });
652
+ }
653
+ meta = { ...meta, [LOGIN_FIELD_KEYS.url]: url, [LOGIN_FIELD_KEYS.username]: username };
654
+ break;
655
+ case 'card':
656
+ fields.push({ key: CARD_FIELD_KEYS.cardholder, value: cardholder, type: 'text', sensitive: false });
657
+ fields.push({ key: CARD_FIELD_KEYS.number, value: cardNumber, type: 'text', sensitive: true });
658
+ fields.push({ key: CARD_FIELD_KEYS.cvv, value: cvv, type: 'secret', sensitive: true });
659
+ fields.push({ key: CARD_FIELD_KEYS.expiry, value: expiry, type: 'text', sensitive: true });
660
+ if (cardNotes.trim() || (isEdit && dirtySensitiveFields[CARD_FIELD_KEYS.notes])) fields.push({ key: CARD_FIELD_KEYS.notes, value: cardNotes, type: 'text', sensitive: true });
661
+ meta = {
662
+ ...meta,
663
+ [CARD_FIELD_KEYS.brand]: brand,
664
+ [CARD_FIELD_KEYS.cardholder]: cardholder,
665
+ [CARD_FIELD_KEYS.last4]: cardNumber.slice(-4),
666
+ [CARD_FIELD_KEYS.billingZip]: billingZip,
667
+ };
668
+ break;
669
+ case 'note':
670
+ fields.push({ key: NOTE_CONTENT_KEY, value: noteContent, type: 'text', sensitive: true });
671
+ break;
672
+ case 'hot_wallet':
673
+ if (isEdit && hotWalletAddress.trim()) {
674
+ fields.push({ key: HOT_WALLET_FIELD_KEYS.address, value: hotWalletAddress.trim(), type: 'text', sensitive: false });
675
+ }
676
+ meta = {
677
+ ...meta,
678
+ [HOT_WALLET_FIELD_KEYS.chain]: hotWalletChain,
679
+ ...(hotWalletAddress.trim() ? { [HOT_WALLET_FIELD_KEYS.address]: hotWalletAddress.trim() } : {}),
680
+ };
681
+ break;
682
+ case 'plain_note':
683
+ fields.push({ key: NOTE_CONTENT_KEY, value: noteContent, type: 'text', sensitive: false });
684
+ meta = { ...meta, [NOTE_CONTENT_KEY]: noteContent };
685
+ break;
686
+ case 'apikey':
687
+ fields.push({ key: APIKEY_FIELD_KEYS.key, value: apiKeyName, type: 'text', sensitive: false });
688
+ fields.push({ key: APIKEY_FIELD_KEYS.value, value: apiKeyValue, type: 'secret', sensitive: true });
689
+ meta = { ...meta, [APIKEY_FIELD_KEYS.key]: apiKeyName };
690
+ break;
691
+ case 'custom':
692
+ fields.push({ key: resolvedCustomFieldKey, value: customFieldValue, type: 'secret', sensitive: true });
693
+ meta = { ...meta, primaryKey: resolvedCustomFieldKey };
694
+ break;
695
+ case 'oauth2': {
696
+ const expiresAtSeconds = Number.parseInt(oauth2ExpiresAt, 10);
697
+ fields.push({ key: OAUTH2_FIELD_KEYS.accessToken, value: oauth2AccessToken, type: 'secret', sensitive: true });
698
+ fields.push({ key: OAUTH2_FIELD_KEYS.refreshToken, value: oauth2RefreshToken, type: 'secret', sensitive: true });
699
+ fields.push({ key: OAUTH2_FIELD_KEYS.clientId, value: oauth2ClientId, type: 'secret', sensitive: true });
700
+ fields.push({ key: OAUTH2_FIELD_KEYS.clientSecret, value: oauth2ClientSecret, type: 'secret', sensitive: true });
701
+ meta = {
702
+ ...meta,
703
+ [OAUTH2_FIELD_KEYS.tokenEndpoint]: oauth2TokenEndpoint,
704
+ [OAUTH2_FIELD_KEYS.scopes]: oauth2Scopes,
705
+ [OAUTH2_FIELD_KEYS.authMethod]: oauth2AuthMethod,
706
+ [OAUTH2_FIELD_KEYS.expiresAt]: Number.isFinite(expiresAtSeconds) ? expiresAtSeconds : null,
707
+ };
708
+ break;
709
+ }
710
+
711
+ case 'ssh': {
712
+ const hosts = sshHostsInput
713
+ .split(/\n|,/)
714
+ .map((host) => host.trim())
715
+ .filter(Boolean);
716
+ fields.push({ key: SSH_FIELD_KEYS.privateKey, value: sshPrivateKey, type: 'secret', sensitive: true });
717
+ if (sshPassphrase.trim()) fields.push({ key: SSH_FIELD_KEYS.passphrase, value: sshPassphrase, type: 'secret', sensitive: true });
718
+ if (sshPublicKey.trim()) fields.push({ key: SSH_FIELD_KEYS.publicKey, value: sshPublicKey, type: 'text', sensitive: false });
719
+ meta = {
720
+ ...meta,
721
+ [SSH_FIELD_KEYS.publicKey]: sshPublicKey,
722
+ [SSH_FIELD_KEYS.hosts]: hosts,
723
+ [SSH_FIELD_KEYS.keyType]: sshKeyType || undefined,
724
+ };
725
+ break;
726
+ }
727
+ case 'gpg':
728
+ fields.push({ key: GPG_FIELD_KEYS.privateKey, value: gpgPrivateKey, type: 'secret', sensitive: true });
729
+ if (gpgPublicKey.trim()) fields.push({ key: GPG_FIELD_KEYS.publicKey, value: gpgPublicKey, type: 'text', sensitive: false });
730
+ meta = {
731
+ ...meta,
732
+ [GPG_FIELD_KEYS.publicKey]: gpgPublicKey,
733
+ [GPG_FIELD_KEYS.keyId]: gpgKeyId,
734
+ [GPG_FIELD_KEYS.uidEmail]: gpgUidEmail,
735
+ [GPG_FIELD_KEYS.expiresAt]: gpgExpiresAt || undefined,
736
+ };
737
+ break;
738
+ }
739
+
740
+ if (walletLink) {
741
+ meta = { ...meta, walletLink };
742
+ }
743
+
744
+ return { vaultId, type, name: resolvedName, fields, meta };
745
+ };
746
+
747
+ const handleSave = async () => {
748
+ const canDeriveName = canDeriveNameFromInputs({
749
+ type,
750
+ apiKeyName,
751
+ username,
752
+ url,
753
+ noteContent,
754
+ customFieldKey,
755
+ hotWalletChain,
756
+ cardholder,
757
+ cardNumber,
758
+ oauth2TokenEndpoint,
759
+ sshHostsInput,
760
+ gpgKeyId,
761
+ gpgUidEmail,
762
+ });
763
+ if (!name.trim() && !canDeriveName) {
764
+ setError('Name is required');
765
+ return;
766
+ }
767
+ if (type === 'login' && !isEdit && !password.trim()) {
768
+ setError('Password is required for login');
769
+ return;
770
+ }
771
+ if (type === 'login' && isEdit && totpIntent === 'replace' && !totpSecret.trim()) {
772
+ setError('TOTP replace requires a valid secret or setup link');
773
+ return;
774
+ }
775
+ if (type === 'plain_note' && !noteContent.trim()) {
776
+ setError('Content is required for plain note');
777
+ return;
778
+ }
779
+ if (type === 'apikey' && (!apiKeyName.trim() || !apiKeyValue.trim())) {
780
+ setError('Key and value are required for API key');
781
+ return;
782
+ }
783
+ if (type === 'custom' && (!customFieldKey.trim() || !customFieldValue.trim())) {
784
+ setError('Primary key and value are required for custom type');
785
+ return;
786
+ }
787
+ if (type === 'ssh' && !sshPrivateKey.trim() && !isEdit) {
788
+ setError('SSH private key is required');
789
+ return;
790
+ }
791
+ if (type === 'gpg' && !gpgPrivateKey.trim() && !isEdit) {
792
+ setError('GPG private key is required');
793
+ return;
794
+ }
795
+ if (type === 'oauth2') {
796
+ if (!oauth2TokenEndpoint.trim()) {
797
+ setError('OAuth2 requires a token endpoint. Add a valid token endpoint URL.');
798
+ return;
799
+ }
800
+
801
+ const expiresAtNumber = Number.parseInt(oauth2ExpiresAt, 10);
802
+ if (!Number.isFinite(expiresAtNumber) || expiresAtNumber <= 0) {
803
+ setError('OAuth2 expiry is invalid. Enter a positive unix timestamp in seconds.');
804
+ return;
805
+ }
806
+ if (!isEdit) {
807
+ if (!oauth2AccessToken.trim()) {
808
+ setError('OAuth2 requires an access token.');
809
+ return;
810
+ }
811
+ if (!oauth2RefreshToken.trim()) {
812
+ setError('OAuth2 requires a refresh token.');
813
+ return;
814
+ }
815
+ if (!oauth2ClientId.trim()) {
816
+ setError('OAuth2 requires a client ID.');
817
+ return;
818
+ }
819
+ if (!oauth2ClientSecret.trim()) {
820
+ setError('OAuth2 requires a client secret.');
821
+ return;
822
+ }
823
+ }
824
+ }
825
+ setSaving(true);
826
+ setError(null);
827
+
828
+ try {
829
+ const payload = buildPayload();
830
+ let savedCredentialId: string | undefined;
831
+
832
+ if (isEdit) {
833
+ // Only submit changed sensitive fields for edit.
834
+ const editFields = payload.fields.filter((f) => !f.sensitive || !!dirtySensitiveFields[f.key]);
835
+ await api.put(Api.Wallet, `/credentials/${editCredentialId}`, {
836
+ ...payload,
837
+ fields: editFields,
838
+ });
839
+ savedCredentialId = editCredentialId;
840
+ } else {
841
+ const createRes = await api.post<{ success: boolean; credential?: { id?: string } }>(Api.Wallet, '/credentials', payload);
842
+ savedCredentialId = createRes?.credential?.id;
843
+ }
844
+
845
+ await onSaved(savedCredentialId);
846
+ onClose();
847
+ } catch (err) {
848
+ setError(err instanceof Error ? err.message : 'Failed to save credential');
849
+ } finally {
850
+ setSaving(false);
851
+ }
852
+ };
853
+
854
+ const textareaClassName =
855
+ 'w-full h-24 bg-[var(--color-background-alt,#f4f4f5)] border border-[var(--color-border,#d4d4d8)] font-mono text-sm p-3 resize-none focus:border-[var(--color-border-focus,#0a0a0a)] outline-none text-[var(--color-text,#0a0a0a)] placeholder-[var(--color-text-muted,#6b7280)]';
856
+
857
+ const selectedTypeLabel = TYPE_META[type]?.label || 'Credential';
858
+ const primaryFieldKey = getCredentialPrimaryFieldKey(type);
859
+ const primaryFieldSpec = getCredentialPrimaryFieldSpec(type);
860
+ const primaryFieldLabel = primaryFieldSpec?.label || 'Primary Value';
861
+ const createHeaderTitle = createStep === 'form' ? `Create ${selectedTypeLabel}` : 'Create';
862
+
863
+ return (
864
+ <>
865
+ <Modal
866
+ isOpen={isOpen}
867
+ onClose={onClose}
868
+ title={isEdit ? 'Edit Credential' : createHeaderTitle}
869
+ size="lg"
870
+ headerActionPosition="left"
871
+ headerAction={!isEdit && createStep === 'form' ? (
872
+ <button
873
+ type="button"
874
+ data-testid="back-to-type-picker"
875
+ onClick={() => setCreateStep('type')}
876
+ className="inline-flex items-center justify-center w-6 h-6 text-[var(--color-text-muted,#6b7280)] hover:text-[var(--color-text,#0a0a0a)] transition-colors"
877
+ aria-label="Back to type picker"
878
+ >
879
+ <ArrowLeft size={12} />
880
+ </button>
881
+ ) : undefined}
882
+ footer={
883
+ <div className="flex justify-end gap-2">
884
+ <Button variant="secondary" size="sm" onClick={onClose}>
885
+ Cancel
886
+ </Button>
887
+ {(!isEdit && createStep === 'type') ? null : (
888
+ <Button variant="primary" size="sm" onClick={handleSave} loading={saving}>
889
+ Save
890
+ </Button>
891
+ )}
892
+ </div>
893
+ }
894
+ >
895
+ {!isEdit && createStep === 'type' ? (
896
+ <div className="space-y-4">
897
+ <div data-testid="featured-types-section" className="space-y-2">
898
+ <p className="font-mono text-[9px] font-bold uppercase tracking-widest text-[var(--color-text-muted,#6b7280)]">
899
+ Common
900
+ </p>
901
+ <ItemPicker
902
+ ariaLabel="Common types"
903
+ options={FEATURED_TYPE_ORDER.map((t) => {
904
+ const Icon = TYPE_META[t].icon;
905
+ return {
906
+ value: t,
907
+ label: TYPE_META[t].label,
908
+ description: TYPE_META[t].description,
909
+ icon: <Icon size={16} />,
910
+ quickActionLabel: `Quick create ${TYPE_META[t].label}`,
911
+ onQuickAction: () => {
912
+ setType(t);
913
+ setError(null);
914
+ setCreateStep('form');
915
+ },
916
+ };
917
+ })}
918
+ value={type}
919
+ onChange={(val) => {
920
+ setType(val as FormType);
921
+ setError(null);
922
+ setCreateStep('form');
923
+ }}
924
+ />
925
+ </div>
926
+
927
+ <div data-testid="other-types-section" className="space-y-2">
928
+ <p className="font-mono text-[9px] font-bold uppercase tracking-widest text-[var(--color-text-muted,#6b7280)]">
929
+ Other Types
930
+ </p>
931
+ <ItemPicker
932
+ ariaLabel="Other types"
933
+ options={REMAINING_TYPE_ORDER.map((t) => {
934
+ const Icon = TYPE_META[t].icon;
935
+ return {
936
+ value: t,
937
+ label: TYPE_META[t].label,
938
+ description: TYPE_META[t].description,
939
+ icon: <Icon size={16} />,
940
+ quickActionLabel: `Quick create ${TYPE_META[t].label}`,
941
+ onQuickAction: () => {
942
+ setType(t);
943
+ setError(null);
944
+ setCreateStep('form');
945
+ },
946
+ };
947
+ })}
948
+ value={type}
949
+ onChange={(val) => {
950
+ setType(val as FormType);
951
+ setError(null);
952
+ setCreateStep('form');
953
+ }}
954
+ />
955
+ </div>
956
+ </div>
957
+ ) : (
958
+ <div data-testid="credential-form-layout" className="space-y-4 gap-4">
959
+ <div className="space-y-2">
960
+ <div className="flex items-center justify-between px-1">
961
+ <span className="font-mono text-[9px] font-bold uppercase tracking-widest text-[var(--color-text-muted,#6b7280)]">
962
+ Type: {selectedTypeLabel}
963
+ </span>
964
+ </div>
965
+ </div>
966
+
967
+ {/* Type-specific fields */}
968
+ {type === 'login' && (
969
+ <div className="space-y-3">
970
+ <TextInput
971
+ label="Username"
972
+ value={username}
973
+ onChange={(e) => setUsername(e.target.value)}
974
+ onFocus={() => setShowUsernameSuggestions(true)}
975
+ onBlur={() => setTimeout(() => setShowUsernameSuggestions(false), 50)}
976
+ placeholder="user@example.com"
977
+ list="login-username-suggestions"
978
+ />
979
+ {showUsernameSuggestions && usernameSuggestions.length > 0 && (
980
+ <datalist id="login-username-suggestions">
981
+ {usernameSuggestions.map((suggestion) => (
982
+ <option key={suggestion} value={suggestion} />
983
+ ))}
984
+ </datalist>
985
+ )}
986
+
987
+ <TextInput
988
+ label="Website"
989
+ value={url}
990
+ onChange={(e) => setUrl(e.target.value)}
991
+ placeholder="https://example.com"
992
+ />
993
+
994
+ <div className="relative">
995
+ <TextInput
996
+ label="Password"
997
+ type={isSensitiveFieldVisible(LOGIN_FIELD_KEYS.password) ? 'text' : 'password'}
998
+ value={password}
999
+ onChange={(e) => {
1000
+ setPassword(e.target.value);
1001
+ if (isEdit) markSensitiveDirty(LOGIN_FIELD_KEYS.password);
1002
+ }}
1003
+ onFocus={() => setShowPasswordActions(true)}
1004
+ onClick={() => setShowPasswordActions(true)}
1005
+ onBlur={(e) => {
1006
+ const nextTarget = e.relatedTarget as Node | null;
1007
+ if (nextTarget && passwordActionsRef.current?.contains(nextTarget)) return;
1008
+ setShowPasswordActions(false);
1009
+ }}
1010
+ placeholder={isEdit ? '\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022' : ''}
1011
+ rightElement={(
1012
+ <button
1013
+ type="button"
1014
+ onClick={() => { void toggleSensitiveFieldVisibility(LOGIN_FIELD_KEYS.password); }}
1015
+ aria-label={isSensitiveFieldVisible(LOGIN_FIELD_KEYS.password) ? 'Hide Password' : 'Reveal Password'}
1016
+ className="text-[var(--color-text-muted,#6b7280)] hover:text-[var(--color-text,#0a0a0a)] transition-colors"
1017
+ >
1018
+ {revealingField === LOGIN_FIELD_KEYS.password ? <Loader2 size={11} className="animate-spin" /> : <Eye size={12} />}
1019
+ </button>
1020
+ )}
1021
+ />
1022
+ {showPasswordActions && (
1023
+ <div
1024
+ ref={passwordActionsRef}
1025
+ className="absolute right-0 top-full mt-1 z-20 min-w-[13rem] border border-[var(--color-border,#d4d4d8)] bg-[var(--color-surface,#ffffff)] shadow-[2px_2px_0_rgba(10,10,10,0.08)]"
1026
+ >
1027
+ <button
1028
+ type="button"
1029
+ onMouseDown={(e) => e.preventDefault()}
1030
+ onClick={() => {
1031
+ setShowPasswordGen(true);
1032
+ setShowPasswordActions(false);
1033
+ }}
1034
+ className="w-full text-left px-3 py-2 font-mono text-[9px] uppercase tracking-widest text-[var(--color-text,#0a0a0a)] hover:bg-[var(--color-background-alt,#f4f4f5)]"
1035
+ >
1036
+ Generate New Password
1037
+ </button>
1038
+ </div>
1039
+ )}
1040
+ </div>
1041
+
1042
+ {showGlobalAdvanced && (
1043
+ <div className="space-y-3 border border-[var(--color-border,#d4d4d8)] p-3 bg-[var(--color-background-alt,#f4f4f5)]">
1044
+ <div>
1045
+ <div className="flex items-center justify-between mb-1.5 px-1">
1046
+ <label className="block font-mono text-[9px] font-bold uppercase tracking-widest text-[var(--color-text-muted,#6b7280)]">
1047
+ Notes
1048
+ </label>
1049
+ {isEdit && (
1050
+ <button
1051
+ type="button"
1052
+ onClick={() => { void handleRevealField(LOGIN_FIELD_KEYS.notes); }}
1053
+ aria-label="Reveal Notes"
1054
+ className="font-mono text-[8px] uppercase tracking-widest text-[var(--color-text-muted,#6b7280)] hover:text-[var(--color-text,#0a0a0a)] transition-colors inline-flex items-center gap-1"
1055
+ >
1056
+ {revealingField === LOGIN_FIELD_KEYS.notes ? <Loader2 size={9} className="animate-spin" /> : <Eye size={9} />}
1057
+ Reveal
1058
+ </button>
1059
+ )}
1060
+ </div>
1061
+ <textarea
1062
+ className={textareaClassName}
1063
+ value={loginNotes}
1064
+ onChange={(e) => {
1065
+ setLoginNotes(e.target.value);
1066
+ if (isEdit) markSensitiveDirty(LOGIN_FIELD_KEYS.notes);
1067
+ }}
1068
+ placeholder={isEdit ? '\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022' : ''}
1069
+ />
1070
+ </div>
1071
+ <TotpSetupPanel
1072
+ isEdit={isEdit}
1073
+ hasExistingTotp={hasExistingTotp}
1074
+ onIntentChange={(intent) => {
1075
+ setTotpIntent(intent);
1076
+ if (isEdit && intent === 'remove') {
1077
+ markSensitiveDirty(LOGIN_FIELD_KEYS.totp);
1078
+ }
1079
+ }}
1080
+ onSecretChange={(secret, markDirty) => {
1081
+ setTotpSecret(secret);
1082
+ if (isEdit) {
1083
+ if (markDirty) {
1084
+ markSensitiveDirty(LOGIN_FIELD_KEYS.totp);
1085
+ if (secret.trim()) {
1086
+ setTotpIntent('replace');
1087
+ }
1088
+ } else if (totpIntent !== 'remove') {
1089
+ setTotpIntent(hasExistingTotp ? 'keep' : 'replace');
1090
+ }
1091
+ }
1092
+ }}
1093
+ />
1094
+ </div>
1095
+ )}
1096
+ </div>
1097
+ )}
1098
+
1099
+ {type === 'card' && (
1100
+ <div className="space-y-3">
1101
+ <TextInput
1102
+ label="Cardholder"
1103
+ value={cardholder}
1104
+ onChange={(e) => setCardholder(e.target.value)}
1105
+ placeholder="John Doe"
1106
+ />
1107
+ <TextInput
1108
+ label="Card Number"
1109
+ value={cardNumber}
1110
+ onChange={(e) => {
1111
+ setCardNumber(e.target.value);
1112
+ if (isEdit) markSensitiveDirty(CARD_FIELD_KEYS.number);
1113
+ }}
1114
+ placeholder={isEdit ? '\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022' : ''}
1115
+ />
1116
+ <div className="flex gap-3">
1117
+ <div className="flex-1">
1118
+ <TextInput
1119
+ label="Expiry"
1120
+ value={expiry}
1121
+ onChange={(e) => {
1122
+ setExpiry(e.target.value);
1123
+ if (isEdit) markSensitiveDirty(CARD_FIELD_KEYS.expiry);
1124
+ }}
1125
+ placeholder="MM/YY"
1126
+ />
1127
+ </div>
1128
+ <div className="flex-1">
1129
+ <TextInput
1130
+ label="CVV"
1131
+ type="password"
1132
+ value={cvv}
1133
+ onChange={(e) => {
1134
+ setCvv(e.target.value);
1135
+ if (isEdit) markSensitiveDirty(CARD_FIELD_KEYS.cvv);
1136
+ }}
1137
+ placeholder={isEdit ? '\u2022\u2022\u2022\u2022' : ''}
1138
+ />
1139
+ </div>
1140
+ </div>
1141
+ {showGlobalAdvanced && (
1142
+ <div className="space-y-3 border border-[var(--color-border,#d4d4d8)] p-3 bg-[var(--color-background-alt,#f4f4f5)]">
1143
+ <FilterDropdown options={BRAND_OPTIONS} value={brand} onChange={setBrand} label="Brand" />
1144
+ <TextInput label="Billing ZIP" value={billingZip} onChange={(e) => setBillingZip(e.target.value)} placeholder="12345" />
1145
+ </div>
1146
+ )}
1147
+ </div>
1148
+ )}
1149
+
1150
+ {type === 'note' && (
1151
+ <div>
1152
+ <div className="flex items-center justify-between mb-1.5 px-1">
1153
+ <label className="block font-mono text-[9px] font-bold uppercase tracking-widest text-[var(--color-text-muted,#6b7280)]">
1154
+ Content
1155
+ </label>
1156
+ {isEdit && (
1157
+ <button
1158
+ type="button"
1159
+ onClick={() => { void handleRevealField(NOTE_CONTENT_KEY); }}
1160
+ aria-label="Reveal Content"
1161
+ className="font-mono text-[8px] uppercase tracking-widest text-[var(--color-text-muted,#6b7280)] hover:text-[var(--color-text,#0a0a0a)] transition-colors inline-flex items-center gap-1"
1162
+ >
1163
+ {revealingField === NOTE_CONTENT_KEY ? <Loader2 size={9} className="animate-spin" /> : <Eye size={9} />}
1164
+ Reveal
1165
+ </button>
1166
+ )}
1167
+ </div>
1168
+ <textarea
1169
+ className={textareaClassName.replace('h-24', 'h-48')}
1170
+ value={noteContent}
1171
+ onChange={(e) => {
1172
+ setNoteContent(e.target.value);
1173
+ if (isEdit) markSensitiveDirty(NOTE_CONTENT_KEY);
1174
+ }}
1175
+ placeholder={isEdit ? '\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022' : ''}
1176
+ />
1177
+ </div>
1178
+ )}
1179
+
1180
+ {type === 'plain_note' && (
1181
+ <div className="space-y-3">
1182
+ <p className="font-mono text-[10px] text-[var(--color-text-muted,#6b7280)]">
1183
+ Plain Note stores readable content in metadata (not encrypted).
1184
+ </p>
1185
+ <div>
1186
+ <label className="block font-mono text-[9px] font-bold uppercase tracking-widest text-[var(--color-text-muted,#6b7280)] mb-1.5 px-1">
1187
+ Content
1188
+ </label>
1189
+ <textarea
1190
+ className={textareaClassName.replace('h-24', 'h-48')}
1191
+ value={noteContent}
1192
+ onChange={(e) => setNoteContent(e.target.value)}
1193
+ placeholder=""
1194
+ />
1195
+ </div>
1196
+ </div>
1197
+ )}
1198
+
1199
+ {type === 'hot_wallet' && (
1200
+ <div className="space-y-3">
1201
+ <p className="font-mono text-[10px] text-[var(--color-text-muted,#6b7280)]">
1202
+ Generates a hot wallet via backend wallet creation logic and stores the private key as encrypted primary secret.
1203
+ </p>
1204
+ <div className="space-y-1">
1205
+ <label className="block font-mono text-[9px] font-bold uppercase tracking-widest text-[var(--color-text-muted,#6b7280)]">
1206
+ Chain
1207
+ </label>
1208
+ <div data-testid="hot-wallet-chain-select">
1209
+ <ItemPicker
1210
+ ariaLabel="Chain"
1211
+ options={[
1212
+ { value: 'base', label: 'Base / EVM', description: 'EVM-compatible chains' },
1213
+ { value: 'solana', label: 'Solana', description: 'Solana network wallets' },
1214
+ ]}
1215
+ value={hotWalletChain}
1216
+ onChange={(val) => setHotWalletChain(val as 'base' | 'solana')}
1217
+ />
1218
+ </div>
1219
+ </div>
1220
+ {isEdit && (
1221
+ <TextInput
1222
+ label="Address"
1223
+ value={hotWalletAddress}
1224
+ onChange={(e) => setHotWalletAddress(e.target.value)}
1225
+ placeholder="Generated wallet address"
1226
+ />
1227
+ )}
1228
+ </div>
1229
+ )}
1230
+
1231
+ {type === 'apikey' && (
1232
+ <div className="space-y-3">
1233
+ <p className="font-mono text-[10px] text-[var(--color-text-muted,#6b7280)]">
1234
+ Minimal flow: key name + key value. If Name is blank, key name becomes the credential title.
1235
+ </p>
1236
+ <TextInput
1237
+ label="Key"
1238
+ value={apiKeyName}
1239
+ onChange={(e) => setApiKeyName(e.target.value)}
1240
+ placeholder="Service key name"
1241
+ />
1242
+ <TextInput
1243
+ label="Value"
1244
+ type={isSensitiveFieldVisible(APIKEY_FIELD_KEYS.value) ? 'text' : 'password'}
1245
+ value={apiKeyValue}
1246
+ onChange={(e) => {
1247
+ setApiKeyValue(e.target.value);
1248
+ if (isEdit) markSensitiveDirty(APIKEY_FIELD_KEYS.value);
1249
+ }}
1250
+ placeholder={isEdit ? '\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022' : ''}
1251
+ rightElement={(
1252
+ <button
1253
+ type="button"
1254
+ onClick={() => { void toggleSensitiveFieldVisibility(APIKEY_FIELD_KEYS.value); }}
1255
+ aria-label={isSensitiveFieldVisible(APIKEY_FIELD_KEYS.value) ? 'Hide Value' : 'Reveal Value'}
1256
+ className="text-[var(--color-text-muted,#6b7280)] hover:text-[var(--color-text,#0a0a0a)] transition-colors"
1257
+ >
1258
+ {revealingField === APIKEY_FIELD_KEYS.value ? <Loader2 size={11} className="animate-spin" /> : <Eye size={12} />}
1259
+ </button>
1260
+ )}
1261
+ />
1262
+ </div>
1263
+ )}
1264
+
1265
+ {type === 'custom' && (
1266
+ <div className="space-y-3">
1267
+ <p className="font-mono text-[10px] text-[var(--color-text-muted,#6b7280)]">
1268
+ Canonical primary key: {primaryFieldKey}
1269
+ </p>
1270
+ <TextInput
1271
+ label="Primary Key"
1272
+ value={customFieldKey}
1273
+ onChange={(e) => setCustomFieldKey(e.target.value)}
1274
+ placeholder="value"
1275
+ />
1276
+ <TextInput
1277
+ label={primaryFieldLabel}
1278
+ type={isSensitiveFieldVisible(CREDENTIAL_FIELD_KEYS.custom.value) ? 'text' : 'password'}
1279
+ value={customFieldValue}
1280
+ onChange={(e) => setCustomFieldValue(e.target.value)}
1281
+ placeholder={isEdit ? '\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022' : ''}
1282
+ rightElement={(
1283
+ <button
1284
+ type="button"
1285
+ onClick={() => { void toggleSensitiveFieldVisibility(CREDENTIAL_FIELD_KEYS.custom.value); }}
1286
+ aria-label={isSensitiveFieldVisible(CREDENTIAL_FIELD_KEYS.custom.value) ? 'Hide Value' : 'Reveal Value'}
1287
+ className="text-[var(--color-text-muted,#6b7280)] hover:text-[var(--color-text,#0a0a0a)] transition-colors"
1288
+ >
1289
+ {revealingField === CREDENTIAL_FIELD_KEYS.custom.value ? <Loader2 size={11} className="animate-spin" /> : <Eye size={12} />}
1290
+ </button>
1291
+ )}
1292
+ />
1293
+ </div>
1294
+ )}
1295
+
1296
+ {type === 'oauth2' && (
1297
+ <div className="space-y-3">
1298
+ <TextInput label="Token Endpoint" value={oauth2TokenEndpoint} onChange={(e) => setOauth2TokenEndpoint(e.target.value)} placeholder="https://accounts.google.com/o/oauth2/token" />
1299
+ <TextInput
1300
+ label="Access Token"
1301
+ type="password"
1302
+ value={oauth2AccessToken}
1303
+ onChange={(e) => {
1304
+ setOauth2AccessToken(e.target.value);
1305
+ if (isEdit) markSensitiveDirty(OAUTH2_FIELD_KEYS.accessToken);
1306
+ }}
1307
+ placeholder={isEdit ? '\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022' : 'Optional — will be fetched on first use'}
1308
+ />
1309
+ <TextInput label="Expires At (unix seconds)" type="text" value={oauth2ExpiresAt} onChange={(e) => setOauth2ExpiresAt(e.target.value)} placeholder={String(Math.floor(Date.now() / 1000) + 60 * 60)} />
1310
+
1311
+ {showGlobalAdvanced && (
1312
+ <div className="space-y-3 border border-[var(--color-border,#d4d4d8)] p-3 bg-[var(--color-background-alt,#f4f4f5)]">
1313
+ <TextInput
1314
+ label="Client ID"
1315
+ value={oauth2ClientId}
1316
+ onChange={(e) => {
1317
+ setOauth2ClientId(e.target.value);
1318
+ if (isEdit) markSensitiveDirty(OAUTH2_FIELD_KEYS.clientId);
1319
+ }}
1320
+ placeholder="your-client-id"
1321
+ />
1322
+ <TextInput
1323
+ label="Client Secret"
1324
+ type="password"
1325
+ value={oauth2ClientSecret}
1326
+ onChange={(e) => {
1327
+ setOauth2ClientSecret(e.target.value);
1328
+ if (isEdit) markSensitiveDirty(OAUTH2_FIELD_KEYS.clientSecret);
1329
+ }}
1330
+ placeholder={isEdit ? '\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022' : ''}
1331
+ />
1332
+ <TextInput
1333
+ label="Refresh Token"
1334
+ type="password"
1335
+ value={oauth2RefreshToken}
1336
+ onChange={(e) => {
1337
+ setOauth2RefreshToken(e.target.value);
1338
+ if (isEdit) markSensitiveDirty(OAUTH2_FIELD_KEYS.refreshToken);
1339
+ }}
1340
+ placeholder={isEdit ? '\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022' : ''}
1341
+ />
1342
+ <TextInput label="Scopes" value={oauth2Scopes} onChange={(e) => setOauth2Scopes(e.target.value)} placeholder="read write (space-separated)" />
1343
+ <FilterDropdown
1344
+ options={[
1345
+ { value: 'client_secret_post', label: 'Client Secret (POST body)' },
1346
+ { value: 'client_secret_basic', label: 'Client Secret (Basic Auth)' },
1347
+ ]}
1348
+ value={oauth2AuthMethod}
1349
+ onChange={setOauth2AuthMethod}
1350
+ label="Auth Method"
1351
+ />
1352
+ </div>
1353
+ )}
1354
+ </div>
1355
+ )}
1356
+
1357
+
1358
+ {type === 'ssh' && (
1359
+ <div className="space-y-3">
1360
+ <TextInput label="Public Key (optional)" value={sshPublicKey} onChange={(e) => setSshPublicKey(e.target.value)} placeholder="ssh-ed25519 AAAA..." />
1361
+ <div>
1362
+ <label className="block font-mono text-[9px] font-bold uppercase tracking-widest text-[var(--color-text-muted,#6b7280)]">Private Key</label>
1363
+ <textarea className={textareaClassName.replace('h-24', 'h-40')} value={sshPrivateKey} onChange={(e) => { setSshPrivateKey(e.target.value); if (isEdit) markSensitiveDirty(SSH_FIELD_KEYS.privateKey); }} placeholder={isEdit ? '••••••••' : '-----BEGIN OPENSSH PRIVATE KEY-----'} />
1364
+ </div>
1365
+ <TextInput label="Passphrase (optional)" type="password" value={sshPassphrase} onChange={(e) => { setSshPassphrase(e.target.value); if (isEdit) markSensitiveDirty(SSH_FIELD_KEYS.passphrase); }} />
1366
+ <div>
1367
+ <label className="block font-mono text-[9px] font-bold uppercase tracking-widest text-[var(--color-text-muted,#6b7280)]">Associated Hosts (comma or newline)</label>
1368
+ <textarea className={textareaClassName} value={sshHostsInput} onChange={(e) => setSshHostsInput(e.target.value)} placeholder="github.com
1369
+ prod.example.com" />
1370
+ </div>
1371
+ </div>
1372
+ )}
1373
+
1374
+ {type === 'gpg' && (
1375
+ <div className="space-y-3">
1376
+ <div>
1377
+ <label className="block font-mono text-[9px] font-bold uppercase tracking-widest text-[var(--color-text-muted,#6b7280)]">Private Key (armored)</label>
1378
+ <textarea className={textareaClassName.replace('h-24', 'h-40')} value={gpgPrivateKey} onChange={(e) => { setGpgPrivateKey(e.target.value); if (isEdit) markSensitiveDirty(GPG_FIELD_KEYS.privateKey); }} placeholder={isEdit ? '••••••••' : '-----BEGIN PGP PRIVATE KEY BLOCK-----'} />
1379
+ </div>
1380
+ <div>
1381
+ <label className="block font-mono text-[9px] font-bold uppercase tracking-widest text-[var(--color-text-muted,#6b7280)]">Public Key (optional)</label>
1382
+ <textarea className={textareaClassName} value={gpgPublicKey} onChange={(e) => setGpgPublicKey(e.target.value)} placeholder="-----BEGIN PGP PUBLIC KEY BLOCK-----" />
1383
+ </div>
1384
+ <TextInput label="Key ID (optional)" value={gpgKeyId} onChange={(e) => setGpgKeyId(e.target.value)} />
1385
+ <TextInput label="UID Email (optional)" value={gpgUidEmail} onChange={(e) => setGpgUidEmail(e.target.value)} />
1386
+ <TextInput label="Expires At (optional)" value={gpgExpiresAt} onChange={(e) => setGpgExpiresAt(e.target.value)} placeholder="2027-01-01" />
1387
+ </div>
1388
+ )}
1389
+
1390
+ <button
1391
+ type="button"
1392
+ onClick={() => setShowGlobalAdvanced((prev) => !prev)}
1393
+ className="font-mono text-[9px] uppercase tracking-widest text-[var(--color-text-muted,#6b7280)] hover:text-[var(--color-text,#0a0a0a)] text-left"
1394
+ >
1395
+ {showGlobalAdvanced ? 'Hide Advanced' : 'Show Advanced'}
1396
+ </button>
1397
+
1398
+ {showGlobalAdvanced && (
1399
+ <div className="space-y-4 border border-[var(--color-border,#d4d4d8)] p-3 bg-[var(--color-background-alt,#f4f4f5)]">
1400
+ {!isEdit && vaultOptions.length > 0 && (
1401
+ <FilterDropdown
1402
+ options={vaultOptions}
1403
+ value={vaultId}
1404
+ onChange={setVaultId}
1405
+ label="Vault"
1406
+ />
1407
+ )}
1408
+
1409
+ {/* Tags */}
1410
+ <div>
1411
+ <TextInput
1412
+ label="Tags"
1413
+ value={tagInput}
1414
+ onChange={(e) => setTagInput(e.target.value)}
1415
+ onKeyDown={handleTagKeyDown}
1416
+ placeholder="Add tag and press Enter"
1417
+ />
1418
+ {visibleTagSuggestions.length > 0 && (
1419
+ <div className="flex flex-wrap gap-1 mt-1">
1420
+ {visibleTagSuggestions.map((suggestedTag) => (
1421
+ <button
1422
+ key={suggestedTag}
1423
+ type="button"
1424
+ onClick={() => {
1425
+ if (!tags.includes(suggestedTag)) setTags([...tags, suggestedTag]);
1426
+ setTagInput('');
1427
+ }}
1428
+ className="font-mono text-[9px] px-2 py-0.5 border border-[var(--color-border,#d4d4d8)] hover:border-[var(--color-border-focus,#0a0a0a)]"
1429
+ >
1430
+ + {suggestedTag}
1431
+ </button>
1432
+ ))}
1433
+ </div>
1434
+ )}
1435
+ {tags.length > 0 && (
1436
+ <div className="flex flex-wrap gap-1 mt-2">
1437
+ {tags.map((tag) => (
1438
+ <span
1439
+ key={tag}
1440
+ className="inline-flex items-center gap-1 bg-[var(--color-accent,#ccff00)]/10 text-[var(--color-text,#0a0a0a)] text-[9px] font-mono px-2 py-0.5"
1441
+ >
1442
+ {tag}
1443
+ <button
1444
+ type="button"
1445
+ onClick={() => removeTag(tag)}
1446
+ className="text-[var(--color-text-muted,#6b7280)] hover:text-[var(--color-text,#0a0a0a)] ml-0.5"
1447
+ >
1448
+ x
1449
+ </button>
1450
+ </span>
1451
+ ))}
1452
+ </div>
1453
+ )}
1454
+ </div>
1455
+
1456
+ <Toggle size="sm" checked={favorite} onChange={setFavorite} label="Favorite" />
1457
+ </div>
1458
+ )}
1459
+
1460
+ {/* Error */}
1461
+ {error && (
1462
+ <div className="font-mono text-[10px] text-[var(--color-danger,#ef4444)] bg-[var(--color-danger,#ef4444)]/5 border border-[var(--color-danger,#ef4444)]/20 px-3 py-2">
1463
+ {error}
1464
+ </div>
1465
+ )}
1466
+
1467
+
1468
+ </div>
1469
+ )}
1470
+ </Modal>
1471
+
1472
+ {/* Password Generator */}
1473
+ <PasswordGenerator
1474
+ isOpen={showPasswordGen}
1475
+ onClose={() => setShowPasswordGen(false)}
1476
+ onUse={(pw) => {
1477
+ setPassword(pw);
1478
+ if (isEdit) markSensitiveDirty(LOGIN_FIELD_KEYS.password);
1479
+ setShowPasswordGen(false);
1480
+ }}
1481
+ />
1482
+ </>
1483
+ );
1484
+ };