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,1964 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect, useCallback, useMemo } from 'react';
4
+ import * as bip39 from 'bip39';
5
+ import Link from 'next/link';
6
+ import { Bot, KeyRound, Fingerprint, Database, Plus, RotateCcw, Loader2, Settings } from 'lucide-react';
7
+ import { useAuth } from '@/context/AuthContext';
8
+ import { api, Api, unlockWallet, setupWallet, rekeySession, changePrimaryVaultPassword, recoverWalletAccess } from '@/lib/api';
9
+ import { generateVaultKeypair, getVaultPrivateKey } from '@/lib/vault-crypto';
10
+ import { CredentialVault } from '@/components/vault/CredentialVault';
11
+ import { NotificationDrawer } from '@/components/NotificationDrawer';
12
+ import { useAgentActions } from '@/hooks/useAgentActions';
13
+ import DocsThemeToggle from '@/components/docs/DocsThemeToggle';
14
+ import { Drawer, Modal, Button, ItemPicker, Toggle, TextInput, TyvekCollapsibleSection, ConfirmationModal } from '@/components/design-system';
15
+ import { PasskeyEnrollmentPrompt } from '@/components/PasskeyEnrollmentPrompt';
16
+ import { useTheme, type ColorMode, type UiScale } from '@/hooks/useTheme';
17
+
18
+ interface VaultInfo {
19
+ id: string;
20
+ name?: string;
21
+ address: string;
22
+ solanaAddress?: string;
23
+ isUnlocked: boolean;
24
+ isPrimary: boolean;
25
+ createdAt: string;
26
+ }
27
+
28
+ interface WalletData {
29
+ address: string;
30
+ tier: 'cold' | 'hot' | 'temp';
31
+ chain: string;
32
+ balance?: string;
33
+ }
34
+
35
+ type PageState = 'loading' | 'setup' | 'locked' | 'transition' | 'unlocked';
36
+ type LocalAgentMode = 'strict' | 'dev' | 'admin';
37
+ type ProjectScopeMode = 'auto' | 'strict' | 'off';
38
+ type SetupOnboardingStep = 'seed' | 'trust';
39
+ type SeedPhraseActionStatus = 'copied' | 'copy-failed' | 'downloaded' | 'download-failed';
40
+ type LocalPolicySettings = {
41
+ profile: LocalAgentMode;
42
+ profileVersion: 'v1';
43
+ autoApprove: boolean;
44
+ projectScopeMode: ProjectScopeMode;
45
+ };
46
+
47
+ const LOCAL_POLICY_PROFILES: LocalAgentMode[] = ['strict', 'dev', 'admin'];
48
+ const LOCAL_PROJECT_SCOPE_MODES: ProjectScopeMode[] = ['auto', 'strict', 'off'];
49
+ const LOCAL_PROFILE_ITEM_OPTIONS = [
50
+ {
51
+ value: 'strict',
52
+ label: 'strict',
53
+ description: 'Minimal local token scope with explicit allowlists.',
54
+ },
55
+ {
56
+ value: 'dev',
57
+ label: 'dev',
58
+ description: 'Balanced local automation with scoped defaults.',
59
+ },
60
+ {
61
+ value: 'admin',
62
+ label: 'admin (dangerous)',
63
+ description: 'Broad token scope. Use only in tightly controlled local environments.',
64
+ },
65
+ ] as const;
66
+ const LOCAL_PROJECT_SCOPE_ITEM_OPTIONS = [
67
+ {
68
+ value: 'auto',
69
+ label: 'auto (recommended)',
70
+ description: 'Uses `.aura` when present and safely falls back to token scope when missing.',
71
+ },
72
+ {
73
+ value: 'strict',
74
+ label: 'strict (require .aura)',
75
+ description: 'Requires explicit `.aura` mappings for secret access.',
76
+ },
77
+ {
78
+ value: 'off',
79
+ label: 'off (disable project allowlist)',
80
+ description: 'Disables project allowlist checks for local token access.',
81
+ },
82
+ ] as const;
83
+ const ONBOARDING_LOCAL_AGENT_MODE_OPTIONS = [
84
+ {
85
+ value: 'dev',
86
+ label: 'dev',
87
+ description: 'Recommended: auto-approve enabled with scoped non-financial profile.',
88
+ },
89
+ {
90
+ value: 'strict',
91
+ label: 'local',
92
+ description: 'Disable local auto-approve. Every local agent token request needs manual approval.',
93
+ },
94
+ {
95
+ value: 'admin',
96
+ label: 'work',
97
+ description: 'Dangerous: broad local agent access. Not recommended for primary vault workflows.',
98
+ },
99
+ ] as const;
100
+ const VAULT_COLOR_MODE_OPTIONS = [
101
+ { value: 'light', label: 'Light' },
102
+ { value: 'dark', label: 'Dark' },
103
+ ] as const;
104
+ const VAULT_UI_SCALE_OPTIONS = [
105
+ { value: 'normal', label: 'Normal' },
106
+ { value: 'big', label: 'Big' },
107
+ ] as const;
108
+
109
+ type OnboardingSeedDraft = {
110
+ mnemonic: string;
111
+ createdAt: number;
112
+ };
113
+
114
+ const ONBOARDING_SEED_STORAGE_KEY = 'aura:onboarding-seed-draft';
115
+ const ONBOARDING_SEED_TTL_MS = 15 * 60 * 1000;
116
+ const DASHBOARD_TRANSITION_TIMEOUT_MS = 1500;
117
+ const BIP39_WORD_SET = new Set((bip39.wordlists.english as string[]).map((word) => word.toLowerCase()));
118
+
119
+ const normalizeRecoveryWords = (raw: string): string[] => raw
120
+ .trim()
121
+ .toLowerCase()
122
+ .split(/\s+/)
123
+ .filter(Boolean);
124
+
125
+ export default function UnlockPage() {
126
+ const { token, setToken, clearToken } = useAuth();
127
+ const { colorMode, uiScale, setColorMode, setUiScale } = useTheme();
128
+
129
+ const [pageState, setPageState] = useState<PageState>('loading');
130
+ const [password, setPassword] = useState('');
131
+ const [trustDevice, setTrustDevice] = useState(true);
132
+ const [loading, setLoading] = useState(false);
133
+ const [error, setError] = useState<string | null>(null);
134
+ const [mnemonic, setMnemonic] = useState<string | null>(null);
135
+ const [seedAcknowledged, setSeedAcknowledged] = useState(false);
136
+ const [seedRecoveryNotice, setSeedRecoveryNotice] = useState<string | null>(null);
137
+ const [seedPhraseActionStatus, setSeedPhraseActionStatus] = useState<SeedPhraseActionStatus | null>(null);
138
+ const [localAgentMode, setLocalAgentMode] = useState<LocalAgentMode>('dev');
139
+ const [setupOnboardingStep, setSetupOnboardingStep] = useState<SetupOnboardingStep>('seed');
140
+ const [onboardingToken, setOnboardingToken] = useState<string | null>(null);
141
+ const [dashboardTransitionTimedOut, setDashboardTransitionTimedOut] = useState(false);
142
+ const [showSettingsDrawer, setShowSettingsDrawer] = useState(false);
143
+ const { notifications: pageNotifications, dismissNotification: pageDismissNotification } = useAgentActions({ autoFetch: !!token });
144
+ const [confirmNuke, setConfirmNuke] = useState(false);
145
+ const [nuking, setNuking] = useState(false);
146
+ const [nukeError, setNukeError] = useState<string | null>(null);
147
+ const [policySettings, setPolicySettings] = useState<LocalPolicySettings | null>(null);
148
+ const [policyForm, setPolicyForm] = useState<LocalPolicySettings>({
149
+ profile: 'dev',
150
+ profileVersion: 'v1',
151
+ autoApprove: true,
152
+ projectScopeMode: 'auto',
153
+ });
154
+ const [policyLoadError, setPolicyLoadError] = useState<string | null>(null);
155
+ const [policySaveError, setPolicySaveError] = useState<string | null>(null);
156
+ const [policyFormErrors, setPolicyFormErrors] = useState<Record<string, string>>({});
157
+ const [policySaveSuccess, setPolicySaveSuccess] = useState<string | null>(null);
158
+ const [policyLoading, setPolicyLoading] = useState(false);
159
+ const [policySaving, setPolicySaving] = useState(false);
160
+ const [dangerConfirmOpen, setDangerConfirmOpen] = useState(false);
161
+ const [discardConfirmOpen, setDiscardConfirmOpen] = useState(false);
162
+ const [vaultThemeOpen, setVaultThemeOpen] = useState(true);
163
+ const [agentSettingsOpen, setAgentSettingsOpen] = useState(false);
164
+ const [securitySettingsOpen, setSecuritySettingsOpen] = useState(false);
165
+ const [dangerZoneOpen, setDangerZoneOpen] = useState(false);
166
+ const [backupSectionOpen, setBackupSectionOpen] = useState(false);
167
+ const [backups, setBackups] = useState<Array<{ filename: string; timestamp: string; size: number; date: string }>>([]);
168
+ const [backupsLoading, setBackupsLoading] = useState(false);
169
+ const [creatingBackup, setCreatingBackup] = useState(false);
170
+ const [restoringBackup, setRestoringBackup] = useState<string | null>(null);
171
+ const [exportingDb, setExportingDb] = useState(false);
172
+ const [restoreConfirmOpen, setRestoreConfirmOpen] = useState<string | null>(null);
173
+ const [restoreAnchorEl, setRestoreAnchorEl] = useState<HTMLElement | null>(null);
174
+ const [showPasswordModal, setShowPasswordModal] = useState(false);
175
+ const [currentPasswordValue, setCurrentPasswordValue] = useState('');
176
+ const [newPasswordValue, setNewPasswordValue] = useState('');
177
+ const [confirmPasswordValue, setConfirmPasswordValue] = useState('');
178
+ const [passwordChangeError, setPasswordChangeError] = useState<string | null>(null);
179
+ const [passwordChangeSuccess, setPasswordChangeSuccess] = useState<string | null>(null);
180
+ const [passwordChanging, setPasswordChanging] = useState(false);
181
+ const [showSeedRecovery, setShowSeedRecovery] = useState(false);
182
+ const [recoveryWordCount, setRecoveryWordCount] = useState<12 | 24>(12);
183
+ const [recoveryWords, setRecoveryWords] = useState<string[]>(Array(12).fill(''));
184
+
185
+ // Passkey biometric unlock state
186
+ const [passkeyAvailable, setPasskeyAvailable] = useState(false);
187
+ const [passkeyLoading, setPasskeyLoading] = useState(false);
188
+ const [recoveryNewPassword, setRecoveryNewPassword] = useState('');
189
+ const [recoveryError, setRecoveryError] = useState<string | null>(null);
190
+ const [recoveryLoading, setRecoveryLoading] = useState(false);
191
+
192
+ const hasPendingSeedConfirmation = useMemo(() => Boolean(mnemonic), [mnemonic]);
193
+ const recoveryWordsFilled = useMemo(() => recoveryWords.filter((word) => word.length > 0).length, [recoveryWords]);
194
+ const normalizedRecoveryPhrase = useMemo(() => recoveryWords.map((word) => word.trim().toLowerCase()).join(' ').trim(), [recoveryWords]);
195
+ const invalidRecoveryIndexes = useMemo(() => {
196
+ const invalid = new Set<number>();
197
+ recoveryWords.forEach((word, index) => {
198
+ if (!word) return;
199
+ if (!BIP39_WORD_SET.has(word.trim().toLowerCase())) {
200
+ invalid.add(index);
201
+ }
202
+ });
203
+ return invalid;
204
+ }, [recoveryWords]);
205
+ const isRecoveryPhraseStructurallyValid = useMemo(() => {
206
+ if (recoveryWordsFilled !== recoveryWordCount) return false;
207
+ if (invalidRecoveryIndexes.size > 0) return false;
208
+ return bip39.validateMnemonic(normalizedRecoveryPhrase);
209
+ }, [invalidRecoveryIndexes.size, normalizedRecoveryPhrase, recoveryWordCount, recoveryWordsFilled]);
210
+
211
+ const isDangerousPolicySelection = useCallback((settings: LocalPolicySettings) => {
212
+ return settings.profile === 'admin';
213
+ }, []);
214
+
215
+ const policyFormDirty = useMemo(() => {
216
+ if (!policySettings) return false;
217
+ return JSON.stringify(policySettings) !== JSON.stringify(policyForm);
218
+ }, [policyForm, policySettings]);
219
+
220
+ useEffect(() => {
221
+ try {
222
+ const rawDraft = sessionStorage.getItem(ONBOARDING_SEED_STORAGE_KEY);
223
+ if (!rawDraft) return;
224
+ const parsed = JSON.parse(rawDraft) as OnboardingSeedDraft;
225
+ if (!parsed?.mnemonic || typeof parsed.createdAt !== 'number') {
226
+ sessionStorage.removeItem(ONBOARDING_SEED_STORAGE_KEY);
227
+ return;
228
+ }
229
+ if (Date.now() - parsed.createdAt > ONBOARDING_SEED_TTL_MS) {
230
+ sessionStorage.removeItem(ONBOARDING_SEED_STORAGE_KEY);
231
+ setSeedRecoveryNotice('Recovery phrase draft expired. Restart setup to generate a new phrase.');
232
+ return;
233
+ }
234
+
235
+ setMnemonic(parsed.mnemonic);
236
+ setSetupOnboardingStep('seed');
237
+ setPageState('setup');
238
+ setSeedRecoveryNotice('Recovered your in-progress recovery phrase for this tab. Confirm after you store it safely.');
239
+ } catch {
240
+ sessionStorage.removeItem(ONBOARDING_SEED_STORAGE_KEY);
241
+ }
242
+ }, []);
243
+
244
+ useEffect(() => {
245
+ if (!mnemonic) {
246
+ sessionStorage.removeItem(ONBOARDING_SEED_STORAGE_KEY);
247
+ setSetupOnboardingStep('seed');
248
+ setSeedPhraseActionStatus(null);
249
+ return;
250
+ }
251
+ const draft: OnboardingSeedDraft = {
252
+ mnemonic,
253
+ createdAt: Date.now(),
254
+ };
255
+ sessionStorage.setItem(ONBOARDING_SEED_STORAGE_KEY, JSON.stringify(draft));
256
+ }, [mnemonic]);
257
+
258
+ useEffect(() => {
259
+ if (pageState !== 'locked') {
260
+ setShowSeedRecovery(false);
261
+ }
262
+ }, [pageState]);
263
+
264
+ // Check if passkey biometric unlock is usable (registered + vault unlocked server-side)
265
+ useEffect(() => {
266
+ if (pageState !== 'locked') {
267
+ setPasskeyAvailable(false);
268
+ return;
269
+ }
270
+ if (typeof window === 'undefined' || !window.PublicKeyCredential) return;
271
+ // Electron's Chromium reports WebAuthn as available but the sandbox
272
+ // can't complete the ceremony — skip passkey in desktop app.
273
+ if (typeof window !== 'undefined' && (window as unknown as Record<string, unknown>).auraDesktop) return;
274
+
275
+ let cancelled = false;
276
+ (async () => {
277
+ try {
278
+ const status = await api.get<{ registered: boolean }>(Api.Wallet, '/auth/passkey/status');
279
+ if (cancelled || !status.registered) return;
280
+ // Probe authenticate/options to confirm vault is unlocked server-side.
281
+ // If the server returns vault_locked, biometric auth won't work.
282
+ await api.post(Api.Wallet, '/auth/passkey/authenticate/options', {});
283
+ if (!cancelled) setPasskeyAvailable(true);
284
+ } catch { /* vault_locked or no passkeys — hide button */ }
285
+ })();
286
+ return () => { cancelled = true; };
287
+ }, [pageState]);
288
+
289
+ const fetchState = useCallback(async () => {
290
+ // Page reload recovery: keypair is memory-only and lost on reload.
291
+ // If token exists but keypair is gone, regenerate keypair and re-key the session.
292
+ if (token && !getVaultPrivateKey()) {
293
+ try {
294
+ const { publicKeyBase64 } = await generateVaultKeypair();
295
+ const result = await rekeySession(publicKeyBase64);
296
+ if (result.token) {
297
+ setToken(result.token);
298
+ }
299
+ // Keypair restored, continue to fetch state normally
300
+ } catch {
301
+ // Re-key failed (token expired, server restarted, vault locked) — fall back to locked
302
+ clearToken();
303
+ setPageState('locked');
304
+ return;
305
+ }
306
+ }
307
+
308
+ try {
309
+ // Use /setup (lightweight status check) instead of /wallets (which fetches RPC balances
310
+ // and can hang on cold start). /setup only checks in-memory vault state — no RPC calls.
311
+ const [status, vaultData] = await Promise.all([
312
+ api.get<{ hasWallet: boolean; unlocked: boolean }>(Api.Wallet, '/setup'),
313
+ api.get<{ vaults: VaultInfo[] }>(Api.Wallet, '/setup/vaults'),
314
+ ]);
315
+
316
+ const configured = status.hasWallet || (vaultData.vaults && vaultData.vaults.some(v => v.isPrimary));
317
+
318
+ if (!configured) {
319
+ setPageState('setup');
320
+ } else if (hasPendingSeedConfirmation) {
321
+ setPageState('setup');
322
+ } else if (!status.unlocked || !token) {
323
+ setPageState('locked');
324
+ } else {
325
+ setPageState('unlocked');
326
+ }
327
+ } catch {
328
+ setPageState('setup');
329
+ }
330
+ }, [token, clearToken, hasPendingSeedConfirmation]);
331
+
332
+ useEffect(() => {
333
+ fetchState();
334
+ }, [fetchState]);
335
+
336
+ const loadLocalPolicySettings = useCallback(async () => {
337
+ if (!token) {
338
+ setPolicyLoadError('Unlock vault first to manage local socket policy.');
339
+ return;
340
+ }
341
+
342
+ setPolicyLoading(true);
343
+ setPolicyLoadError(null);
344
+ setPolicySaveError(null);
345
+ setPolicySaveSuccess(null);
346
+ try {
347
+ const headers = { Authorization: `Bearer ${token}` };
348
+ const baseUrl = api.getBaseUrl(Api.Wallet);
349
+ const defaultsRes = await fetch(`${baseUrl}/defaults`, { headers });
350
+ if (!defaultsRes.ok) {
351
+ throw new Error('Failed to load canonical trust policy defaults.');
352
+ }
353
+
354
+ const defaultsJson = await defaultsRes.json() as {
355
+ success?: boolean;
356
+ defaults?: Record<string, Array<{ key: string; value: unknown }>>;
357
+ };
358
+ if (defaultsJson.success === false) {
359
+ throw new Error('Failed to load canonical trust policy defaults.');
360
+ }
361
+ const flatDefaults = Object.values(defaultsJson.defaults || {}).flat();
362
+ const findDefault = (key: string): unknown => flatDefaults.find((item) => item.key === key)?.value;
363
+
364
+ const loadedProfile = String(findDefault('trust.localProfile') ?? '').trim() as LocalAgentMode;
365
+ if (!LOCAL_POLICY_PROFILES.includes(loadedProfile)) {
366
+ throw new Error(`Unknown persisted local profile: ${loadedProfile || '(empty)'}`);
367
+ }
368
+
369
+ const profileVersion = String(findDefault('trust.localProfileVersion') ?? 'v1').trim();
370
+ if (profileVersion !== 'v1') {
371
+ throw new Error('Unknown local profile version; refusing to edit settings.');
372
+ }
373
+
374
+ const loadedProjectScopeMode = String(findDefault('trust.projectScopeMode') ?? 'auto').trim() as ProjectScopeMode;
375
+ if (!LOCAL_PROJECT_SCOPE_MODES.includes(loadedProjectScopeMode)) {
376
+ throw new Error(`Unknown persisted project scope mode: ${loadedProjectScopeMode || '(empty)'}`);
377
+ }
378
+
379
+ const loaded: LocalPolicySettings = {
380
+ profile: loadedProfile,
381
+ profileVersion: 'v1',
382
+ autoApprove: Boolean(findDefault('trust.localAutoApprove')),
383
+ projectScopeMode: loadedProjectScopeMode,
384
+ };
385
+
386
+ setPolicySettings(loaded);
387
+ setPolicyForm(loaded);
388
+ } catch (err) {
389
+ setPolicyLoadError((err as Error).message || 'Failed to load policy settings');
390
+ setPolicySettings(null);
391
+ } finally {
392
+ setPolicyLoading(false);
393
+ }
394
+ }, [token]);
395
+
396
+ useEffect(() => {
397
+ if (showSettingsDrawer) {
398
+ void loadLocalPolicySettings();
399
+ }
400
+ }, [showSettingsDrawer, loadLocalPolicySettings]);
401
+
402
+ const persistLocalPolicySettings = useCallback(async () => {
403
+ if (!token) throw new Error('Missing auth token for save.');
404
+ if (!LOCAL_POLICY_PROFILES.includes(policyForm.profile)) {
405
+ throw new Error('Unknown profile selected; refusing to persist.');
406
+ }
407
+ if (!LOCAL_PROJECT_SCOPE_MODES.includes(policyForm.projectScopeMode)) {
408
+ throw new Error('Unknown project scope mode selected; refusing to persist.');
409
+ }
410
+
411
+ const baseUrl = api.getBaseUrl(Api.Wallet);
412
+ const headers = {
413
+ 'Content-Type': 'application/json',
414
+ Authorization: `Bearer ${token}`,
415
+ };
416
+
417
+ const updates: Array<[string, unknown]> = [
418
+ ['trust.localProfile', policyForm.profile],
419
+ ['trust.localProfileVersion', 'v1'],
420
+ ['trust.localAutoApprove', policyForm.autoApprove],
421
+ ['trust.projectScopeMode', policyForm.projectScopeMode],
422
+ ];
423
+
424
+ const results = await Promise.all(
425
+ updates.map(async ([key, value]) => {
426
+ const response = await fetch(`${baseUrl}/defaults/${key}`, {
427
+ method: 'PATCH',
428
+ headers,
429
+ body: JSON.stringify({ value }),
430
+ });
431
+ return response.ok;
432
+ })
433
+ );
434
+
435
+ if (!results.every(Boolean)) {
436
+ throw new Error('Failed to save canonical trust policy defaults.');
437
+ }
438
+
439
+ return { ...policyForm, profileVersion: 'v1' } as LocalPolicySettings;
440
+ }, [policyForm, token]);
441
+
442
+ const handleSaveLocalPolicy = useCallback(async () => {
443
+ if (!policySettings || policyLoading || policySaving) return;
444
+ const enablingDangerous = !isDangerousPolicySelection(policySettings) && isDangerousPolicySelection(policyForm);
445
+ if (enablingDangerous && !dangerConfirmOpen) {
446
+ setDangerConfirmOpen(true);
447
+ return;
448
+ }
449
+
450
+ setPolicySaving(true);
451
+ setPolicySaveError(null);
452
+ setPolicySaveSuccess(null);
453
+ try {
454
+ const saved = await persistLocalPolicySettings();
455
+ setPolicySettings(saved);
456
+ setPolicyForm(saved);
457
+ setDangerConfirmOpen(false);
458
+ setPolicySaveSuccess('Local trust policy saved. Changes apply to newly issued local tokens only.');
459
+ } catch (err) {
460
+ const message = (err as Error).message || 'Failed to save policy settings';
461
+ setDangerConfirmOpen(false);
462
+ if (
463
+ message.includes('Unknown profile selected')
464
+ || message.includes('Unknown project scope mode selected')
465
+ ) {
466
+ setPolicySaveError(message);
467
+ } else {
468
+ await loadLocalPolicySettings();
469
+ setPolicySaveError(`${message} Server values were reloaded.`);
470
+ }
471
+ } finally {
472
+ setPolicySaving(false);
473
+ }
474
+ }, [dangerConfirmOpen, isDangerousPolicySelection, loadLocalPolicySettings, persistLocalPolicySettings, policyForm, policyLoading, policySaving, policySettings]);
475
+
476
+ const closePasswordModal = useCallback(() => {
477
+ setShowPasswordModal(false);
478
+ setCurrentPasswordValue('');
479
+ setNewPasswordValue('');
480
+ setConfirmPasswordValue('');
481
+ setPasswordChangeError(null);
482
+ setPasswordChanging(false);
483
+ }, []);
484
+
485
+ const handleChangePrimaryPassword = useCallback(async (e: React.FormEvent) => {
486
+ e.preventDefault();
487
+ setPasswordChangeError(null);
488
+ setPasswordChangeSuccess(null);
489
+
490
+ if (newPasswordValue.length < 8) {
491
+ setPasswordChangeError('New password must be at least 8 characters.');
492
+ return;
493
+ }
494
+ if (newPasswordValue !== confirmPasswordValue) {
495
+ setPasswordChangeError('New password and confirmation do not match.');
496
+ return;
497
+ }
498
+
499
+ setPasswordChanging(true);
500
+ try {
501
+ await changePrimaryVaultPassword(currentPasswordValue, newPasswordValue);
502
+ setPasswordChangeSuccess('Primary vault password updated.');
503
+ closePasswordModal();
504
+ } catch (err) {
505
+ setPasswordChangeError((err as Error).message || 'Failed to change primary password.');
506
+ } finally {
507
+ setPasswordChanging(false);
508
+ }
509
+ }, [closePasswordModal, confirmPasswordValue, currentPasswordValue, newPasswordValue]);
510
+
511
+ const handleRecoveryWordChange = useCallback((index: number, value: string) => {
512
+ const normalized = value.trim().toLowerCase();
513
+
514
+ if (normalized.includes(' ')) {
515
+ const parsed = normalizeRecoveryWords(normalized);
516
+ if (parsed.length > 1) {
517
+ const nextWords = [...recoveryWords];
518
+ parsed.forEach((word, offset) => {
519
+ const targetIndex = index + offset;
520
+ if (targetIndex < nextWords.length) nextWords[targetIndex] = word;
521
+ });
522
+ setRecoveryWords(nextWords);
523
+ setRecoveryError(null);
524
+ return;
525
+ }
526
+ }
527
+
528
+ const nextWords = [...recoveryWords];
529
+ nextWords[index] = normalized;
530
+ setRecoveryWords(nextWords);
531
+ setRecoveryError(null);
532
+ }, [recoveryWords]);
533
+
534
+ const handleRecoveryPaste = useCallback((index: number, text: string) => {
535
+ const parsed = normalizeRecoveryWords(text);
536
+ if (parsed.length <= 1) return false;
537
+
538
+ const nextWordCount: 12 | 24 = parsed.length > 12 ? 24 : recoveryWordCount;
539
+ if (nextWordCount !== recoveryWordCount) {
540
+ setRecoveryWordCount(nextWordCount);
541
+ const nextWords = Array(nextWordCount).fill('');
542
+ parsed.slice(0, nextWordCount).forEach((word, wordIndex) => {
543
+ nextWords[wordIndex] = word;
544
+ });
545
+ setRecoveryWords(nextWords);
546
+ setRecoveryError(null);
547
+ return true;
548
+ }
549
+
550
+ const nextWords = [...recoveryWords];
551
+ parsed.forEach((word, offset) => {
552
+ const targetIndex = index + offset;
553
+ if (targetIndex < nextWords.length) nextWords[targetIndex] = word;
554
+ });
555
+ setRecoveryWords(nextWords);
556
+ setRecoveryError(null);
557
+ return true;
558
+ }, [recoveryWordCount, recoveryWords]);
559
+
560
+ const handleRecoverAccess = useCallback(async (e: React.FormEvent) => {
561
+ e.preventDefault();
562
+ setRecoveryError(null);
563
+
564
+ if (recoveryNewPassword.length < 8) {
565
+ setRecoveryError('New password must be at least 8 characters.');
566
+ return;
567
+ }
568
+ if (!isRecoveryPhraseStructurallyValid) {
569
+ setRecoveryError('Enter a valid 12 or 24-word BIP-39 seed phrase.');
570
+ return;
571
+ }
572
+
573
+ setRecoveryLoading(true);
574
+ try {
575
+ const { publicKeyBase64 } = await generateVaultKeypair();
576
+ const result = await recoverWalletAccess(normalizedRecoveryPhrase, recoveryNewPassword, publicKeyBase64);
577
+ if (result.token) {
578
+ setToken(result.token, { persist: trustDevice ? 'local' : 'session' });
579
+ }
580
+ setPassword('');
581
+ setRecoveryNewPassword('');
582
+ setRecoveryWords(Array(recoveryWordCount).fill(''));
583
+ // Route through transition state to avoid jarring layout shift
584
+ setPageState('transition');
585
+ void bootstrapDashboardTransition();
586
+ } catch (err) {
587
+ setRecoveryError((err as Error).message || 'Recovery failed.');
588
+ } finally {
589
+ setRecoveryLoading(false);
590
+ }
591
+ }, [fetchState, isRecoveryPhraseStructurallyValid, normalizedRecoveryPhrase, recoveryNewPassword, recoveryWordCount, setToken, trustDevice]);
592
+
593
+ const handleUnlock = async (e: React.FormEvent) => {
594
+ e.preventDefault();
595
+ if (!password) return;
596
+
597
+ setLoading(true);
598
+ setError(null);
599
+ try {
600
+ // Generate keypair before unlock so the token is minted with our pubkey
601
+ const { publicKeyBase64 } = await generateVaultKeypair();
602
+ const data = await unlockWallet(password, undefined, publicKeyBase64);
603
+ if (data.token) {
604
+ setToken(data.token, { persist: trustDevice ? 'local' : 'session' });
605
+ }
606
+ setPassword('');
607
+ // Route through transition state to avoid jarring layout shift
608
+ setPageState('transition');
609
+ void bootstrapDashboardTransition();
610
+ } catch (err) {
611
+ setError((err as Error).message || 'Unlock failed');
612
+ } finally {
613
+ setLoading(false);
614
+ }
615
+ };
616
+
617
+ const handleSetup = async (e: React.FormEvent) => {
618
+ e.preventDefault();
619
+ if (password.length < 8) return;
620
+
621
+ setLoading(true);
622
+ setError(null);
623
+ try {
624
+ // Generate keypair before setup so the initial token has our pubkey
625
+ const { publicKeyBase64 } = await generateVaultKeypair();
626
+ const result = await setupWallet(password, publicKeyBase64);
627
+ if (result.token) {
628
+ setToken(result.token, { persist: trustDevice ? 'local' : 'session' });
629
+ setOnboardingToken(result.token);
630
+ }
631
+ if (result.mnemonic) {
632
+ setMnemonic(result.mnemonic);
633
+ setSeedAcknowledged(false);
634
+ setSetupOnboardingStep('seed');
635
+ setSeedRecoveryNotice(null);
636
+ setSeedPhraseActionStatus(null);
637
+ }
638
+ setPassword('');
639
+ if (!result.mnemonic) fetchState();
640
+ } catch (err) {
641
+ setError((err as Error).message || 'Setup failed');
642
+ } finally {
643
+ setLoading(false);
644
+ }
645
+ };
646
+
647
+ const handleCopySeedPhrase = useCallback(async () => {
648
+ if (!mnemonic) return;
649
+
650
+ let copied = false;
651
+ if (typeof navigator !== 'undefined' && navigator.clipboard?.writeText) {
652
+ try {
653
+ await navigator.clipboard.writeText(mnemonic);
654
+ copied = true;
655
+ } catch {
656
+ copied = false;
657
+ }
658
+ }
659
+
660
+ if (copied) {
661
+ setSeedPhraseActionStatus('copied');
662
+ return;
663
+ }
664
+
665
+ const fallbackTextarea = document.createElement('textarea');
666
+ fallbackTextarea.value = mnemonic;
667
+ fallbackTextarea.setAttribute('readonly', 'true');
668
+ fallbackTextarea.style.position = 'fixed';
669
+ fallbackTextarea.style.left = '-9999px';
670
+ document.body.appendChild(fallbackTextarea);
671
+ fallbackTextarea.focus();
672
+ fallbackTextarea.select();
673
+
674
+ let fallbackCopied = false;
675
+ try {
676
+ fallbackCopied = document.execCommand('copy');
677
+ } catch {
678
+ fallbackCopied = false;
679
+ } finally {
680
+ fallbackTextarea.remove();
681
+ }
682
+
683
+ setSeedPhraseActionStatus(fallbackCopied ? 'copied' : 'copy-failed');
684
+ }, [mnemonic]);
685
+
686
+ const handleDownloadSeedBackup = useCallback(() => {
687
+ if (!mnemonic) return;
688
+
689
+ const dateStamp = new Date().toISOString().slice(0, 10);
690
+ const numberedWords = mnemonic
691
+ .split(' ')
692
+ .map((word, index) => `${index + 1}. ${word}`)
693
+ .join('\n');
694
+ const markdown = `# Aura Vault Seed Phrase Backup\n\n**Date:** ${dateStamp}\n**WARNING:** Keep this file safe and private. Anyone with this phrase can access your vault.\n\n${numberedWords}\n`;
695
+ const filename = `aura-seed-backup-${dateStamp}.md`;
696
+
697
+ let objectUrl = '';
698
+ const downloadLink = document.createElement('a');
699
+ try {
700
+ const blob = new Blob([markdown], { type: 'text/markdown;charset=utf-8' });
701
+ objectUrl = URL.createObjectURL(blob);
702
+ downloadLink.href = objectUrl;
703
+ downloadLink.download = filename;
704
+ downloadLink.style.display = 'none';
705
+ document.body.appendChild(downloadLink);
706
+ downloadLink.click();
707
+ setSeedPhraseActionStatus('downloaded');
708
+ } catch {
709
+ setSeedPhraseActionStatus('download-failed');
710
+ } finally {
711
+ downloadLink.remove();
712
+ if (objectUrl) {
713
+ URL.revokeObjectURL(objectUrl);
714
+ }
715
+ }
716
+ }, [mnemonic]);
717
+
718
+ const persistLocalAgentMode = useCallback(async () => {
719
+ const authToken = onboardingToken || token;
720
+ if (!authToken) {
721
+ throw new Error('Session token unavailable. Unlock again and retry setup.');
722
+ }
723
+
724
+ const profile = localAgentMode;
725
+ const profileVersion = 'v1';
726
+ const autoApprove = profile !== 'strict';
727
+ const baseUrl = api.getBaseUrl(Api.Wallet);
728
+ const headers = {
729
+ 'Content-Type': 'application/json',
730
+ 'Authorization': `Bearer ${authToken}`,
731
+ };
732
+
733
+ await fetch(`${baseUrl}/defaults/trust.localProfile`, {
734
+ method: 'PATCH',
735
+ headers,
736
+ body: JSON.stringify({ value: profile }),
737
+ });
738
+ await fetch(`${baseUrl}/defaults/trust.localProfileVersion`, {
739
+ method: 'PATCH',
740
+ headers,
741
+ body: JSON.stringify({ value: profileVersion }),
742
+ });
743
+ await fetch(`${baseUrl}/defaults/trust.localAutoApprove`, {
744
+ method: 'PATCH',
745
+ headers,
746
+ body: JSON.stringify({ value: autoApprove }),
747
+ });
748
+ }, [localAgentMode, onboardingToken, token]);
749
+
750
+ const bootstrapDashboardTransition = useCallback(async () => {
751
+ setDashboardTransitionTimedOut(false);
752
+ let finished = false;
753
+ const timeout = window.setTimeout(() => {
754
+ if (!finished) setDashboardTransitionTimedOut(true);
755
+ }, DASHBOARD_TRANSITION_TIMEOUT_MS);
756
+
757
+ try {
758
+ await fetchState();
759
+ finished = true;
760
+ } finally {
761
+ window.clearTimeout(timeout);
762
+ }
763
+ }, [fetchState]);
764
+
765
+ const handlePasskeyUnlock = useCallback(async () => {
766
+ setPasskeyLoading(true);
767
+ setError(null);
768
+ try {
769
+ const { publicKeyBase64 } = await generateVaultKeypair();
770
+
771
+ const options = await api.post<{
772
+ challenge: string;
773
+ rpId: string;
774
+ allowCredentials: Array<{ id: string; transports?: string[] }>;
775
+ timeout: number;
776
+ userVerification: string;
777
+ }>(Api.Wallet, '/auth/passkey/authenticate/options', {});
778
+
779
+ const toBuffer = (b: string): ArrayBuffer => {
780
+ let s = b.replace(/-/g, '+').replace(/_/g, '/');
781
+ while (s.length % 4) s += '=';
782
+ const bin = atob(s);
783
+ const a = new Uint8Array(bin.length);
784
+ for (let i = 0; i < bin.length; i++) a[i] = bin.charCodeAt(i);
785
+ return a.buffer;
786
+ };
787
+ const toBase64url = (b: ArrayBuffer): string => {
788
+ const bytes = new Uint8Array(b);
789
+ let bin = '';
790
+ for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);
791
+ return btoa(bin).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
792
+ };
793
+
794
+ const publicKey: PublicKeyCredentialRequestOptions = {
795
+ challenge: toBuffer(options.challenge),
796
+ rpId: options.rpId,
797
+ allowCredentials: (options.allowCredentials || []).map((c) => ({
798
+ type: 'public-key' as const,
799
+ id: toBuffer(c.id),
800
+ transports: c.transports as AuthenticatorTransport[] | undefined,
801
+ })),
802
+ timeout: options.timeout,
803
+ userVerification: (options.userVerification || 'required') as UserVerificationRequirement,
804
+ };
805
+
806
+ const credential = await navigator.credentials.get({ publicKey }) as PublicKeyCredential | null;
807
+ if (!credential) {
808
+ setPasskeyLoading(false);
809
+ return;
810
+ }
811
+
812
+ const response = credential.response as AuthenticatorAssertionResponse;
813
+
814
+ const result = await api.post<{ success: boolean; token?: string; error?: string }>(
815
+ Api.Wallet,
816
+ '/auth/passkey/authenticate/verify',
817
+ {
818
+ credential: {
819
+ id: toBase64url(credential.rawId),
820
+ rawId: toBase64url(credential.rawId),
821
+ type: credential.type,
822
+ response: {
823
+ clientDataJSON: toBase64url(response.clientDataJSON),
824
+ authenticatorData: toBase64url(response.authenticatorData),
825
+ signature: toBase64url(response.signature),
826
+ userHandle: response.userHandle ? toBase64url(response.userHandle) : undefined,
827
+ },
828
+ },
829
+ pubkey: publicKeyBase64,
830
+ },
831
+ );
832
+
833
+ if (result.success && result.token) {
834
+ setToken(result.token, { persist: trustDevice ? 'local' : 'session' });
835
+ setPageState('transition');
836
+ void bootstrapDashboardTransition();
837
+ } else {
838
+ setError(result.error || 'Biometric authentication failed');
839
+ }
840
+ } catch (err) {
841
+ if (err instanceof Error && err.name === 'NotAllowedError') {
842
+ setPasskeyLoading(false);
843
+ return;
844
+ }
845
+ const msg = err instanceof Error ? err.message : 'Biometric authentication failed';
846
+ if (msg.includes('vault_locked')) {
847
+ // Vault was locked between probe and attempt — just hide the button
848
+ setPasskeyAvailable(false);
849
+ } else {
850
+ setError(msg);
851
+ }
852
+ } finally {
853
+ setPasskeyLoading(false);
854
+ }
855
+ }, [trustDevice, setToken, bootstrapDashboardTransition]);
856
+
857
+ const handleFinalizeOnboarding = useCallback(async () => {
858
+ setLoading(true);
859
+ setError(null);
860
+ try {
861
+ await persistLocalAgentMode();
862
+ setMnemonic(null);
863
+ setSeedAcknowledged(false);
864
+ setSeedRecoveryNotice(null);
865
+ setOnboardingToken(null);
866
+ setSetupOnboardingStep('seed');
867
+ setPageState('transition');
868
+ void bootstrapDashboardTransition();
869
+ } catch (err) {
870
+ setError((err as Error).message || 'Failed to save local agent mode');
871
+ } finally {
872
+ setLoading(false);
873
+ }
874
+ }, [bootstrapDashboardTransition, persistLocalAgentMode]);
875
+
876
+ // CredentialVault already calls POST /lock and clearToken() before invoking onLock.
877
+ // This handler only needs to transition the page state.
878
+ const handleLock = useCallback(() => {
879
+ setError(null);
880
+ setPageState('locked');
881
+ }, []);
882
+
883
+ const handleNuke = useCallback(async () => {
884
+ if (!confirmNuke) {
885
+ setConfirmNuke(true);
886
+ setNukeError(null);
887
+ return;
888
+ }
889
+
890
+ setNuking(true);
891
+ setNukeError(null);
892
+ try {
893
+ await api.post(Api.Wallet, '/nuke', {});
894
+ setShowSettingsDrawer(false);
895
+ setDangerConfirmOpen(false);
896
+ setPolicySaveError(null);
897
+ setPolicyFormErrors({});
898
+ setPasswordChangeError(null);
899
+ setVaultThemeOpen(true);
900
+ setAgentSettingsOpen(false);
901
+ setSecuritySettingsOpen(false);
902
+ setDangerZoneOpen(false);
903
+ setShowPasswordModal(false);
904
+ setConfirmNuke(false);
905
+ setNuking(false);
906
+ setNukeError(null);
907
+ if (policySettings) setPolicyForm(policySettings);
908
+ window.location.reload();
909
+ } catch (err) {
910
+ setNukeError((err as Error).message || 'Failed to nuke wallet');
911
+ console.error('[UnlockPage] Nuke failed:', err);
912
+ } finally {
913
+ setNuking(false);
914
+ }
915
+ }, [confirmNuke, policySettings]);
916
+
917
+ // --- Backup / Export handlers ---
918
+
919
+ const fetchBackups = useCallback(async () => {
920
+ if (!token) { setBackups([]); return; }
921
+ setBackupsLoading(true);
922
+ try {
923
+ const data = await api.get<{ success: boolean; backups: Array<{ filename: string; timestamp: string; size: number; date: string }> }>(Api.Wallet, '/backup');
924
+ if (data.success) setBackups(data.backups);
925
+ } catch { setBackups([]); }
926
+ finally { setBackupsLoading(false); }
927
+ }, [token]);
928
+
929
+ const handleCreateBackup = useCallback(async () => {
930
+ setCreatingBackup(true);
931
+ try {
932
+ const data = await api.post<{ success: boolean; error?: string }>(Api.Wallet, '/backup');
933
+ if (data.success) await fetchBackups();
934
+ } catch (err) { console.error('Backup create failed', err); }
935
+ finally { setCreatingBackup(false); }
936
+ }, [fetchBackups]);
937
+
938
+ const handleExportDb = useCallback(async () => {
939
+ setExportingDb(true);
940
+ try {
941
+ const baseUrl = api.getBaseUrl(Api.Wallet);
942
+ const res = await fetch(`${baseUrl}/backup/export`, {
943
+ headers: { Authorization: `Bearer ${token || ''}` },
944
+ });
945
+ if (!res.ok) return;
946
+ const blob = await res.blob();
947
+ const disposition = res.headers.get('Content-Disposition') || '';
948
+ const match = disposition.match(/filename="?([^"]+)"?/);
949
+ const filename = match ? match[1] : 'auramaxx-export.db';
950
+ const url = URL.createObjectURL(blob);
951
+ const a = document.createElement('a');
952
+ a.href = url;
953
+ a.download = filename;
954
+ document.body.appendChild(a);
955
+ a.click();
956
+ document.body.removeChild(a);
957
+ URL.revokeObjectURL(url);
958
+ } catch (err) { console.error('Export failed', err); }
959
+ finally { setExportingDb(false); }
960
+ }, [token]);
961
+
962
+ const handleRestoreBackup = useCallback(async (filename: string) => {
963
+ setRestoringBackup(filename);
964
+ try {
965
+ const data = await api.put<{ success: boolean; error?: string }>(Api.Wallet, '/backup', { filename });
966
+ if (data.success) window.location.reload();
967
+ } catch (err) { console.error('Restore failed', err); }
968
+ finally { setRestoringBackup(null); }
969
+ }, []);
970
+
971
+ const formatBackupDate = (ts: string) => {
972
+ const y = ts.slice(0, 4), m = ts.slice(4, 6), d = ts.slice(6, 8), h = ts.slice(9, 11), min = ts.slice(11, 13);
973
+ return `${y}-${m}-${d} ${h}:${min}`;
974
+ };
975
+
976
+ const formatSize = (bytes: number) => {
977
+ if (bytes < 1024) return `${bytes} B`;
978
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
979
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
980
+ };
981
+
982
+ const closeSettingsDrawer = useCallback(() => {
983
+ if (policyFormDirty) {
984
+ setDiscardConfirmOpen(true);
985
+ return;
986
+ }
987
+ setShowSettingsDrawer(false);
988
+ setDangerConfirmOpen(false);
989
+ setPolicySaveError(null);
990
+ setPolicyFormErrors({});
991
+ setPasswordChangeError(null);
992
+ setVaultThemeOpen(true);
993
+ setAgentSettingsOpen(false);
994
+ setSecuritySettingsOpen(false);
995
+ setDangerZoneOpen(false);
996
+ setBackupSectionOpen(false);
997
+ setRestoreConfirmOpen(null);
998
+ setRestoreAnchorEl(null);
999
+ setShowPasswordModal(false);
1000
+ setConfirmNuke(false);
1001
+ setNuking(false);
1002
+ setNukeError(null);
1003
+ if (policySettings) setPolicyForm(policySettings);
1004
+ }, [policyFormDirty, policySettings]);
1005
+
1006
+ // Transition: full-screen loader between locked and unlocked
1007
+ if (pageState === 'transition') {
1008
+ return (
1009
+ <div className="h-screen bg-[var(--color-background,#f4f4f5)] flex flex-col items-center justify-center animate-fade-in-up">
1010
+ <div className="w-6 h-6 border-2 border-[var(--color-border,#d4d4d8)] border-t-[var(--color-text,#0a0a0a)] animate-spin" />
1011
+ <div className="mt-4 label-specimen text-[var(--color-text-muted,#6b7280)] animate-pulse">
1012
+ DECRYPTING VAULT
1013
+ </div>
1014
+ <div className="mt-3 w-32 h-[2px] skeleton-mech" />
1015
+ {dashboardTransitionTimedOut && (
1016
+ <div className="mt-6 w-full max-w-[280px] space-y-3 text-center">
1017
+ <div className="text-[9px] text-[var(--color-danger,#ef4444)] bg-[var(--color-danger,#ef4444)]/10 px-3 py-2 border border-[var(--color-danger,#ef4444)]/20">
1018
+ Dashboard took too long. You can retry without re-running onboarding.
1019
+ </div>
1020
+ <button
1021
+ onClick={() => { void bootstrapDashboardTransition(); }}
1022
+ className="w-full py-2.5 bg-[var(--color-text,#0a0a0a)] text-[var(--color-surface,#ffffff)] font-mono text-xs tracking-widest font-bold hover:opacity-90 transition-opacity clip-specimen-sm"
1023
+ >
1024
+ RETRY
1025
+ </button>
1026
+ </div>
1027
+ )}
1028
+ </div>
1029
+ );
1030
+ }
1031
+
1032
+ // Unlocked: render full-screen vault + root settings drawer controls
1033
+ if (pageState === 'unlocked') {
1034
+ return (
1035
+ <div className="relative h-screen vault-surface">
1036
+ {/* Top-right icon bar: Settings, Notifications, Dark mode */}
1037
+ <div className="fixed top-3 right-6 z-50 flex items-center gap-1.5">
1038
+ <button
1039
+ onClick={() => setShowSettingsDrawer(true)}
1040
+ className="p-1.5 text-[var(--color-text-muted,#6b7280)] hover:text-[var(--color-text,#0a0a0a)] hover:bg-[var(--color-surface,#ffffff)]/50 transition-colors rounded"
1041
+ title="Settings"
1042
+ aria-label="Settings"
1043
+ >
1044
+ <Settings size={14} />
1045
+ </button>
1046
+ <NotificationDrawer
1047
+ notifications={pageNotifications}
1048
+ onDismiss={pageDismissNotification}
1049
+ />
1050
+ <DocsThemeToggle />
1051
+ </div>
1052
+
1053
+ {/* Bottom-right links: Docs, API, GitHub, X, Help */}
1054
+ <div className="fixed bottom-16 right-6 z-50 flex items-center gap-3 font-mono text-[10px] tracking-widest">
1055
+ <Link href="/docs" className="text-[var(--color-text-muted,#6b7280)] hover:text-[var(--color-text,#0a0a0a)] transition-colors">DOCS</Link>
1056
+ <Link href="/api" className="text-[var(--color-text-muted,#6b7280)] hover:text-[var(--color-text,#0a0a0a)] transition-colors">API</Link>
1057
+ <a href="https://github.com/Aura-Industry/auramaxx" target="_blank" rel="noopener noreferrer" className="text-[var(--color-text-muted,#6b7280)] hover:text-[var(--color-text,#0a0a0a)] transition-colors">GITHUB</a>
1058
+ <a href="https://x.com/npxauramaxx" target="_blank" rel="noopener noreferrer" className="text-[var(--color-text-muted,#6b7280)] hover:text-[var(--color-text,#0a0a0a)] transition-colors">X</a>
1059
+ <a href="https://x.com/hi_im_nico" target="_blank" rel="noopener noreferrer" className="text-[var(--color-text-muted,#6b7280)] hover:text-[var(--color-text,#0a0a0a)] transition-colors">HELP</a>
1060
+ </div>
1061
+
1062
+ <CredentialVault
1063
+ onLock={handleLock}
1064
+ onSettings={() => setShowSettingsDrawer(true)}
1065
+ />
1066
+
1067
+ <Drawer
1068
+ isOpen={showSettingsDrawer}
1069
+ onClose={closeSettingsDrawer}
1070
+ title="ROOT SETTINGS"
1071
+ subtitle="Local socket policy"
1072
+ footerLabel=""
1073
+ >
1074
+ <div className="space-y-4">
1075
+ {passwordChangeSuccess && (
1076
+ <div className="text-[10px] text-[var(--color-info)] border border-[var(--color-info)]/30 bg-[var(--color-info)]/10 px-3 py-2">
1077
+ {passwordChangeSuccess}
1078
+ </div>
1079
+ )}
1080
+
1081
+ <TyvekCollapsibleSection
1082
+ title="VAULT_THEME"
1083
+ isOpen={vaultThemeOpen}
1084
+ onToggle={() => setVaultThemeOpen((open) => !open)}
1085
+ className="overflow-hidden"
1086
+ contentClassName="p-[var(--space-md)] border-t border-[var(--color-border)]"
1087
+ >
1088
+ <label className="block text-[9px] text-[var(--color-text-faint)] tracking-widest mb-2">COLOR MODE</label>
1089
+ <ItemPicker
1090
+ options={[...VAULT_COLOR_MODE_OPTIONS]}
1091
+ value={colorMode}
1092
+ onChange={(value) => setColorMode(value as ColorMode)}
1093
+ />
1094
+ <label className="block mt-3 text-[9px] text-[var(--color-text-faint)] tracking-widest mb-2">UI SCALE</label>
1095
+ <ItemPicker
1096
+ options={[...VAULT_UI_SCALE_OPTIONS]}
1097
+ value={uiScale}
1098
+ onChange={(value) => setUiScale(value as UiScale)}
1099
+ />
1100
+ <div className="mt-2 text-[9px] text-[var(--color-text-muted)]">
1101
+ Dark adjusts vault color contrast. Big increases typography, spacing, radius, and shadows.
1102
+ </div>
1103
+ </TyvekCollapsibleSection>
1104
+
1105
+ <TyvekCollapsibleSection
1106
+ title="DEFAULT_AGENT_PROFILE"
1107
+ icon={<Bot size={12} />}
1108
+ isOpen={agentSettingsOpen}
1109
+ onToggle={() => setAgentSettingsOpen((open) => !open)}
1110
+ className="overflow-hidden"
1111
+ contentClassName="px-4 pb-4 space-y-3 border-t border-[var(--color-border)]"
1112
+ >
1113
+ {policyLoadError && (
1114
+ <div className="space-y-2 pt-3">
1115
+ <div className="text-[10px] text-[var(--color-danger)] border border-[var(--color-danger)]/30 bg-[var(--color-danger)]/10 px-3 py-2">
1116
+ {policyLoadError}
1117
+ </div>
1118
+ <Button
1119
+ type="button"
1120
+ onClick={() => void loadLocalPolicySettings()}
1121
+ disabled={policyLoading}
1122
+ variant="secondary"
1123
+ size="md"
1124
+ >
1125
+ {policyLoading ? 'RETRYING...' : 'RETRY LOAD'}
1126
+ </Button>
1127
+ </div>
1128
+ )}
1129
+
1130
+ {!policyLoadError && (
1131
+ <>
1132
+ <div className="pt-3 flex items-center justify-between text-[10px] font-mono text-[var(--color-text)]">
1133
+ <span>AUTO-APPROVE LOCAL REQUESTS</span>
1134
+ <Toggle
1135
+ size="sm"
1136
+ checked={policyForm.autoApprove}
1137
+ onChange={(checked) => setPolicyForm((prev) => ({ ...prev, autoApprove: checked }))}
1138
+ disabled={policyLoading || policySaving}
1139
+ />
1140
+ </div>
1141
+
1142
+ <div>
1143
+ <label className="block text-[9px] text-[var(--color-text-faint)] tracking-widest mb-2">LOCAL PROFILE</label>
1144
+ <ItemPicker
1145
+ ariaLabel="LOCAL PROFILE"
1146
+ options={[...LOCAL_PROFILE_ITEM_OPTIONS]}
1147
+ value={policyForm.profile}
1148
+ onChange={(value) => setPolicyForm((prev) => ({ ...prev, profile: value as LocalAgentMode }))}
1149
+ disabled={policyLoading || policySaving}
1150
+ />
1151
+ </div>
1152
+
1153
+ <div>
1154
+ <label className="block text-[9px] text-[var(--color-text-faint)] tracking-widest mb-2">PROJECT SCOPE MODE</label>
1155
+ <ItemPicker
1156
+ ariaLabel="PROJECT SCOPE MODE"
1157
+ options={[...LOCAL_PROJECT_SCOPE_ITEM_OPTIONS]}
1158
+ value={policyForm.projectScopeMode}
1159
+ onChange={(value) => setPolicyForm((prev) => ({ ...prev, projectScopeMode: value as ProjectScopeMode }))}
1160
+ disabled={policyLoading || policySaving}
1161
+ />
1162
+ <div className="mt-1 text-[9px] text-[var(--color-text-muted)]">
1163
+ Strict requires `.aura` mappings for `get_secret`. Auto allows token-scope fallback when `.aura` is missing.
1164
+ </div>
1165
+ </div>
1166
+
1167
+ {dangerConfirmOpen && (
1168
+ <div className="text-[10px] text-[var(--color-danger)] border border-[var(--color-danger)]/30 bg-[var(--color-danger)]/10 px-3 py-2 space-y-2">
1169
+ <div>Dangerous mode broadens local token scope (admin profile or super-scopes).</div>
1170
+ <div className="flex gap-2">
1171
+ <Button
1172
+ type="button"
1173
+ onClick={() => {
1174
+ setDangerConfirmOpen(false);
1175
+ if (policySettings) setPolicyForm(policySettings);
1176
+ }}
1177
+ variant="secondary"
1178
+ size="sm"
1179
+ className="!h-8"
1180
+ >
1181
+ CANCEL
1182
+ </Button>
1183
+ <Button
1184
+ type="button"
1185
+ onClick={() => { void handleSaveLocalPolicy(); }}
1186
+ disabled={policySaving}
1187
+ variant="danger"
1188
+ size="sm"
1189
+ className="!h-8"
1190
+ >
1191
+ CONFIRM DANGEROUS MODE
1192
+ </Button>
1193
+ </div>
1194
+ </div>
1195
+ )}
1196
+
1197
+ <div className="text-[9px] text-[var(--color-text-muted)] px-3 py-2">
1198
+ Policy changes apply to newly issued local tokens only.
1199
+ </div>
1200
+
1201
+ {policySaveError && <div className="text-[10px] text-[var(--color-danger)]">{policySaveError}</div>}
1202
+ {policySaveSuccess && <div className="text-[10px] text-[var(--color-info)]">{policySaveSuccess}</div>}
1203
+
1204
+ <Button
1205
+ type="button"
1206
+ onClick={() => { void handleSaveLocalPolicy(); }}
1207
+ disabled={Boolean(policyLoadError) || policyLoading || policySaving || !policySettings}
1208
+ variant="primary"
1209
+ size="lg"
1210
+ className="w-full"
1211
+ >
1212
+ {policySaving ? 'SAVING...' : 'SAVE LOCAL POLICY'}
1213
+ </Button>
1214
+ </>
1215
+ )}
1216
+ </TyvekCollapsibleSection>
1217
+
1218
+ <TyvekCollapsibleSection
1219
+ title="PRIMARY_PASSWORD"
1220
+ icon={<KeyRound size={12} />}
1221
+ isOpen={securitySettingsOpen}
1222
+ onToggle={() => setSecuritySettingsOpen((open) => !open)}
1223
+ className="overflow-hidden"
1224
+ contentClassName="px-4 pb-4 pt-3 space-y-3 border-t border-[var(--color-border)]"
1225
+ >
1226
+ <div className="text-[9px] text-[var(--color-text-muted)] leading-relaxed">
1227
+ Rotate your primary vault password. This updates the vault wrapper encryption and keeps existing credentials intact.
1228
+ </div>
1229
+ {passwordChangeError && (
1230
+ <div className="text-[10px] text-[var(--color-danger)] border border-[var(--color-danger)]/30 bg-[var(--color-danger)]/10 px-3 py-2">
1231
+ {passwordChangeError}
1232
+ </div>
1233
+ )}
1234
+ <Button
1235
+ type="button"
1236
+ onClick={() => {
1237
+ setPasswordChangeError(null);
1238
+ setShowPasswordModal(true);
1239
+ }}
1240
+ variant="secondary"
1241
+ size="lg"
1242
+ className="w-full"
1243
+ >
1244
+ CHANGE PRIMARY PASSWORD
1245
+ </Button>
1246
+ </TyvekCollapsibleSection>
1247
+
1248
+ <TyvekCollapsibleSection
1249
+ title="DATABASE_BACKUP"
1250
+ icon={<Database size={12} />}
1251
+ isOpen={backupSectionOpen}
1252
+ onToggle={() => {
1253
+ const next = !backupSectionOpen;
1254
+ setBackupSectionOpen(next);
1255
+ if (next) void fetchBackups();
1256
+ }}
1257
+ className="overflow-hidden"
1258
+ contentClassName="px-4 pb-4 pt-3 space-y-3 border-t border-[var(--color-border)]"
1259
+ >
1260
+ <div className="flex gap-2">
1261
+ <Button
1262
+ variant="secondary"
1263
+ size="lg"
1264
+ onClick={() => void handleCreateBackup()}
1265
+ disabled={creatingBackup}
1266
+ loading={creatingBackup}
1267
+ icon={!creatingBackup ? <Plus size={12} /> : undefined}
1268
+ className="flex-1"
1269
+ >
1270
+ {creatingBackup ? 'CREATING...' : 'CREATE BACKUP'}
1271
+ </Button>
1272
+ <Button
1273
+ variant="secondary"
1274
+ size="lg"
1275
+ onClick={() => void handleExportDb()}
1276
+ disabled={exportingDb}
1277
+ loading={exportingDb}
1278
+ icon={!exportingDb ? <Database size={12} /> : undefined}
1279
+ className="flex-1"
1280
+ >
1281
+ {exportingDb ? 'EXPORTING...' : 'EXPORT DB'}
1282
+ </Button>
1283
+ </div>
1284
+
1285
+ {backupsLoading ? (
1286
+ <div className="py-4 flex items-center justify-center">
1287
+ <Loader2 size={16} className="animate-spin text-[var(--color-text-muted)]" />
1288
+ </div>
1289
+ ) : backups.length === 0 ? (
1290
+ <div className="py-3 text-center">
1291
+ <div className="font-mono text-[9px] text-[var(--color-text-muted)]">No backups found</div>
1292
+ </div>
1293
+ ) : (
1294
+ <div className="space-y-1 max-h-48 overflow-y-auto">
1295
+ {backups.map((backup) => (
1296
+ <div key={backup.filename} className="relative">
1297
+ <Button
1298
+ type="button"
1299
+ variant="secondary"
1300
+ size="md"
1301
+ onClick={(e) => {
1302
+ setRestoreAnchorEl(e.currentTarget as unknown as HTMLElement);
1303
+ setRestoreConfirmOpen(backup.filename);
1304
+ }}
1305
+ className="w-full !h-auto !px-2 !py-2 !justify-between group"
1306
+ >
1307
+ <div className="flex items-center gap-2">
1308
+ <RotateCcw size={10} className="text-[var(--color-text-muted)] group-hover:text-[var(--color-text)]" />
1309
+ <span className="font-mono text-[10px] text-[var(--color-text)]">
1310
+ {formatBackupDate(backup.timestamp)}
1311
+ </span>
1312
+ </div>
1313
+ <span className="font-mono text-[9px] text-[var(--color-text-muted)]">
1314
+ {formatSize(backup.size)}
1315
+ </span>
1316
+ </Button>
1317
+ <ConfirmationModal
1318
+ isOpen={restoreConfirmOpen === backup.filename}
1319
+ onClose={() => { setRestoreConfirmOpen(null); setRestoreAnchorEl(null); }}
1320
+ onConfirm={() => {
1321
+ setRestoreConfirmOpen(null);
1322
+ setRestoreAnchorEl(null);
1323
+ void handleRestoreBackup(backup.filename);
1324
+ }}
1325
+ title="Restore Backup"
1326
+ message="Restore to this backup? You will lose all data since this backup was created."
1327
+ confirmText="RESTORE"
1328
+ variant="warning"
1329
+ loading={restoringBackup === backup.filename}
1330
+ />
1331
+ </div>
1332
+ ))}
1333
+ </div>
1334
+ )}
1335
+
1336
+ <div className="pt-2 border-t border-dashed border-[var(--color-border)]">
1337
+ <div className="font-mono text-[8px] text-[var(--color-text-faint)] leading-relaxed">
1338
+ Backups are tied to the current database schema. Restoring after migrations may cause issues.
1339
+ </div>
1340
+ </div>
1341
+ </TyvekCollapsibleSection>
1342
+
1343
+ <TyvekCollapsibleSection
1344
+ title="DANGER_ZONE"
1345
+ isOpen={dangerZoneOpen}
1346
+ onToggle={() => setDangerZoneOpen((open) => !open)}
1347
+ tone="warning"
1348
+ className="overflow-hidden"
1349
+ contentClassName="space-y-3 p-[var(--space-md)] border-t border-[var(--color-border)]"
1350
+ >
1351
+ <div className="text-[9px] text-[var(--color-warning)] leading-relaxed">
1352
+ Permanently delete your vault, wallets, credentials, and local configuration. This action cannot be undone.
1353
+ </div>
1354
+ {nukeError && (
1355
+ <div
1356
+ className="text-[10px] border px-3 py-2"
1357
+ style={{
1358
+ color: 'var(--color-warning)',
1359
+ borderColor: 'color-mix(in srgb, var(--color-warning) 30%, transparent)',
1360
+ background: 'color-mix(in srgb, var(--color-warning) 10%, transparent)',
1361
+ }}
1362
+ >
1363
+ {nukeError}
1364
+ </div>
1365
+ )}
1366
+ <Button
1367
+ type="button"
1368
+ onClick={() => { void handleNuke(); }}
1369
+ disabled={nuking}
1370
+ variant="danger"
1371
+ size="lg"
1372
+ className={`w-full ${
1373
+ confirmNuke
1374
+ ? '!bg-[var(--color-warning)] !text-[var(--color-surface)] !border-[var(--color-warning)]'
1375
+ : ''
1376
+ }`}
1377
+ >
1378
+ {nuking ? 'NUKING...' : confirmNuke ? 'CONFIRM' : 'NUKE'}
1379
+ </Button>
1380
+ </TyvekCollapsibleSection>
1381
+ </div>
1382
+ </Drawer>
1383
+
1384
+ <ConfirmationModal
1385
+ isOpen={discardConfirmOpen}
1386
+ onClose={() => setDiscardConfirmOpen(false)}
1387
+ onConfirm={() => {
1388
+ setDiscardConfirmOpen(false);
1389
+ setShowSettingsDrawer(false);
1390
+ setDangerConfirmOpen(false);
1391
+ setPolicySaveError(null);
1392
+ setPolicyFormErrors({});
1393
+ setPasswordChangeError(null);
1394
+ setVaultThemeOpen(true);
1395
+ setAgentSettingsOpen(false);
1396
+ setSecuritySettingsOpen(false);
1397
+ setDangerZoneOpen(false);
1398
+ setBackupSectionOpen(false);
1399
+ setRestoreConfirmOpen(null);
1400
+ setRestoreAnchorEl(null);
1401
+ setShowPasswordModal(false);
1402
+ setConfirmNuke(false);
1403
+ setNuking(false);
1404
+ setNukeError(null);
1405
+ if (policySettings) setPolicyForm(policySettings);
1406
+ }}
1407
+ variant="warning"
1408
+ title="Discard Changes"
1409
+ message="You have unsaved local policy changes. Discard them?"
1410
+ confirmText="DISCARD"
1411
+ cancelText="KEEP EDITING"
1412
+ />
1413
+
1414
+ <Modal
1415
+ isOpen={showPasswordModal}
1416
+ onClose={closePasswordModal}
1417
+ title="Change Primary Password"
1418
+ subtitle="Security"
1419
+ size="sm"
1420
+ >
1421
+ <form onSubmit={handleChangePrimaryPassword} className="space-y-3">
1422
+ <TextInput
1423
+ type="password"
1424
+ label="CURRENT PASSWORD"
1425
+ aria-label="CURRENT PASSWORD"
1426
+ value={currentPasswordValue}
1427
+ onChange={(e) => setCurrentPasswordValue(e.target.value)}
1428
+ autoFocus
1429
+ compact
1430
+ />
1431
+ <TextInput
1432
+ type="password"
1433
+ label="NEW PASSWORD"
1434
+ aria-label="NEW PASSWORD"
1435
+ value={newPasswordValue}
1436
+ onChange={(e) => setNewPasswordValue(e.target.value)}
1437
+ compact
1438
+ />
1439
+ <TextInput
1440
+ type="password"
1441
+ label="CONFIRM NEW PASSWORD"
1442
+ aria-label="CONFIRM NEW PASSWORD"
1443
+ value={confirmPasswordValue}
1444
+ onChange={(e) => setConfirmPasswordValue(e.target.value)}
1445
+ compact
1446
+ />
1447
+ {passwordChangeError && <div className="text-[10px] text-[var(--color-danger)]">{passwordChangeError}</div>}
1448
+ <div className="flex gap-2 pt-2">
1449
+ <Button
1450
+ type="button"
1451
+ onClick={closePasswordModal}
1452
+ disabled={passwordChanging}
1453
+ variant="secondary"
1454
+ size="lg"
1455
+ className="flex-1"
1456
+ >
1457
+ CANCEL
1458
+ </Button>
1459
+ <Button
1460
+ type="submit"
1461
+ disabled={passwordChanging || !currentPasswordValue || !newPasswordValue || !confirmPasswordValue}
1462
+ variant="primary"
1463
+ size="lg"
1464
+ className="flex-1"
1465
+ >
1466
+ {passwordChanging ? 'UPDATING...' : 'UPDATE PASSWORD'}
1467
+ </Button>
1468
+ </div>
1469
+ </form>
1470
+ </Modal>
1471
+
1472
+ <PasskeyEnrollmentPrompt isUnlocked={pageState === 'unlocked'} />
1473
+ </div>
1474
+ );
1475
+ }
1476
+
1477
+ return (
1478
+ <div className="min-h-screen bg-[var(--color-background,#f4f4f5)] relative flex items-center justify-center p-4">
1479
+ {/* Background — sterile field (same as docs/api) */}
1480
+ <div className="fixed inset-0 pointer-events-none z-0 overflow-hidden">
1481
+ <div className="absolute inset-0 bg-grid-adaptive bg-[size:4rem_4rem] opacity-30" />
1482
+ <div className="absolute inset-0 tyvek-texture opacity-40 mix-blend-multiply" />
1483
+
1484
+ {/* Giant background typography */}
1485
+ <div className="absolute bottom-[5%] right-[5%] opacity-5 select-none" data-testid="home-background-branding">
1486
+ <h1 className="text-[15vw] font-bold leading-none text-[var(--color-text,#0a0a0a)] font-mono tracking-tighter text-right">
1487
+ AURAMAXX
1488
+ </h1>
1489
+ </div>
1490
+
1491
+ {/* Corner finder patterns */}
1492
+ <div className="absolute top-10 left-10 w-32 h-32 border-l-4 border-t-4 border-[var(--color-text,#0a0a0a)] opacity-10">
1493
+ <div className="absolute top-2 left-2 w-4 h-4 bg-[var(--color-text,#0a0a0a)]" />
1494
+ </div>
1495
+ <div className="absolute bottom-10 right-10 w-32 h-32 border-r-4 border-b-4 border-[var(--color-text,#0a0a0a)] opacity-10 flex items-end justify-end">
1496
+ <div className="absolute bottom-2 right-2 w-4 h-4 bg-[var(--color-text,#0a0a0a)]" />
1497
+ </div>
1498
+ </div>
1499
+
1500
+ {/* Logo header */}
1501
+ <div className="fixed top-6 left-6 z-50 flex items-center gap-3">
1502
+ <div className="w-10 h-10">
1503
+ <img src="/logo.webp" alt="AuraMaxx" className="w-full h-full object-contain" />
1504
+ </div>
1505
+ </div>
1506
+
1507
+ {/* Nav */}
1508
+ <div className="fixed top-7 right-6 z-50 flex items-center gap-3 font-mono text-[10px] tracking-widest">
1509
+ <Link href="/docs" className="text-[var(--color-text-muted,#6b7280)] hover:text-[var(--color-text,#0a0a0a)] transition-colors">DOCS</Link>
1510
+ <Link href="/api" className="text-[var(--color-text-muted,#6b7280)] hover:text-[var(--color-text,#0a0a0a)] transition-colors">API</Link>
1511
+ <a href="https://github.com/Aura-Industry/auramaxx" target="_blank" rel="noopener noreferrer" className="text-[var(--color-text-muted,#6b7280)] hover:text-[var(--color-text,#0a0a0a)] transition-colors">GITHUB</a>
1512
+ <a href="https://x.com/npxauramaxx" target="_blank" rel="noopener noreferrer" className="text-[var(--color-text-muted,#6b7280)] hover:text-[var(--color-text,#0a0a0a)] transition-colors">X</a>
1513
+ <a href="https://x.com/hi_im_nico" target="_blank" rel="noopener noreferrer" className="text-[var(--color-text-muted,#6b7280)] hover:text-[var(--color-text,#0a0a0a)] transition-colors">HELP</a>
1514
+ <DocsThemeToggle />
1515
+ </div>
1516
+
1517
+ {/* Unlock card */}
1518
+ <div className="relative z-10 w-full max-w-[380px]">
1519
+ {/* Vertical specimen label */}
1520
+ <div className="absolute -left-8 top-1/2 -translate-y-1/2 text-vertical label-specimen-sm text-[var(--color-text-faint,#9ca3af)] select-none hidden sm:block">
1521
+ VAULT&nbsp;ACCESS
1522
+ </div>
1523
+ <div className="bg-[var(--color-surface,#f4f4f2)] clip-specimen border-mech shadow-mech overflow-hidden font-mono corner-marks">
1524
+ {/* Card header bar */}
1525
+ <div className="px-5 py-3 border-b border-[var(--color-border,#d4d4d8)] bg-[var(--color-surface-alt,#fafafa)] flex items-center justify-between">
1526
+ <span className="font-sans font-bold text-sm text-[var(--color-text,#0a0a0a)] uppercase tracking-tight">
1527
+ {pageState === 'setup' ? 'Initialize' : 'Unlock'}
1528
+ </span>
1529
+ <span className="text-[9px] text-[var(--color-text-faint,#9ca3af)] font-bold tracking-widest">
1530
+ {pageState === 'loading'
1531
+ ? 'CONNECTING...'
1532
+ : pageState === 'setup'
1533
+ ? 'NO_VAULT'
1534
+ : 'LOCKED'}
1535
+ </span>
1536
+ </div>
1537
+
1538
+ <div className="p-6">
1539
+ {pageState === 'loading' && (
1540
+ <div className="flex flex-col items-center py-12">
1541
+ <div className="w-6 h-6 border-2 border-[var(--color-border,#d4d4d8)] border-t-[var(--color-text,#0a0a0a)] animate-spin" />
1542
+ <div className="mt-4 label-specimen text-[var(--color-text-muted,#6b7280)] animate-pulse">CONNECTING</div>
1543
+ <div className="mt-2 w-20 h-[2px] skeleton-mech" />
1544
+ </div>
1545
+ )}
1546
+
1547
+ {pageState === 'setup' && mnemonic && setupOnboardingStep === 'seed' && (
1548
+ <div className="flex flex-col items-center">
1549
+ <div className="w-16 h-16 mb-4">
1550
+ <img src="/logo.webp" alt="AuraMaxx" className="w-full h-full object-contain" />
1551
+ </div>
1552
+ <div className="text-[10px] text-[var(--color-text-muted,#6b7280)] tracking-widest text-center mb-4">
1553
+ SAVE YOUR RECOVERY PHRASE
1554
+ </div>
1555
+ <div className="text-[9px] text-[var(--color-danger,#ef4444)] bg-[var(--color-danger,#ef4444)]/10 px-3 py-2 border border-[var(--color-danger,#ef4444)]/20 mb-3">
1556
+ Write this down and store it securely. You will stay on this screen until you explicitly confirm.
1557
+ </div>
1558
+ {seedRecoveryNotice && (
1559
+ <div className="text-[9px] text-[var(--color-info,#0047ff)] bg-[var(--color-info,#0047ff)]/10 px-3 py-2 border border-[var(--color-info,#0047ff)]/20 mb-3">
1560
+ {seedRecoveryNotice}
1561
+ </div>
1562
+ )}
1563
+ <div className="grid grid-cols-3 gap-2 w-full mb-4">
1564
+ {mnemonic.split(' ').map((word, i) => (
1565
+ <div key={i} className="text-[10px] font-mono text-[var(--color-text,#0a0a0a)] bg-[var(--color-background,#f4f4f5)] px-2 py-1 border border-[var(--color-border,#d4d4d8)]">
1566
+ <span className="text-[var(--color-text-faint,#9ca3af)] mr-1">{i + 1}.</span>{word}
1567
+ </div>
1568
+ ))}
1569
+ </div>
1570
+ <div className="grid grid-cols-2 gap-2 w-full mb-3">
1571
+ <button
1572
+ type="button"
1573
+ onClick={() => { void handleCopySeedPhrase(); }}
1574
+ className="h-9 px-2 border border-[var(--color-border,#d4d4d8)] font-mono text-[9px] tracking-widest text-[var(--color-text,#0a0a0a)] hover:bg-[var(--color-surface-alt,#fafafa)] transition-colors"
1575
+ >
1576
+ COPY SEED PHRASE
1577
+ </button>
1578
+ <button
1579
+ type="button"
1580
+ onClick={handleDownloadSeedBackup}
1581
+ className="h-9 px-2 border border-[var(--color-border,#d4d4d8)] font-mono text-[9px] tracking-widest text-[var(--color-text,#0a0a0a)] hover:bg-[var(--color-surface-alt,#fafafa)] transition-colors"
1582
+ >
1583
+ DOWNLOAD BACKUP (.MD)
1584
+ </button>
1585
+ </div>
1586
+ {seedPhraseActionStatus && (
1587
+ <div
1588
+ className="w-full mb-3 text-[9px] text-[var(--color-text-muted,#6b7280)]"
1589
+ aria-live="polite"
1590
+ data-testid="seed-phrase-action-status"
1591
+ >
1592
+ {seedPhraseActionStatus}
1593
+ </div>
1594
+ )}
1595
+ <label className="flex items-start gap-2 w-full mb-3 cursor-pointer">
1596
+ <input
1597
+ type="checkbox"
1598
+ checked={seedAcknowledged}
1599
+ onChange={(e) => setSeedAcknowledged(e.target.checked)}
1600
+ className="mt-0.5"
1601
+ />
1602
+ <span className="text-[9px] text-[var(--color-text-muted,#6b7280)]">
1603
+ I have written and verified this recovery phrase in a secure location.
1604
+ </span>
1605
+ </label>
1606
+ <button
1607
+ onClick={() => {
1608
+ if (!seedAcknowledged) return;
1609
+ setSetupOnboardingStep('trust');
1610
+ }}
1611
+ disabled={!seedAcknowledged}
1612
+ className="w-full py-2.5 bg-[var(--color-text,#0a0a0a)] text-[var(--color-surface,#ffffff)] font-mono text-xs tracking-widest font-bold hover:opacity-90 transition-opacity disabled:opacity-30 disabled:cursor-not-allowed"
1613
+ >
1614
+ CONTINUE TO AGENT MODE
1615
+ </button>
1616
+ <div className="w-full mt-3 text-[8px] text-[var(--color-text-faint,#9ca3af)] text-center">
1617
+ If you leave before confirming, this phrase is recoverable only temporarily in this tab session. If recovery expires, restart onboarding to regenerate.
1618
+ </div>
1619
+ </div>
1620
+ )}
1621
+
1622
+ {pageState === 'setup' && mnemonic && setupOnboardingStep === 'trust' && (
1623
+ <>
1624
+ <div className="flex flex-col items-center mb-6">
1625
+ <div className="w-16 h-16 mb-4">
1626
+ <img src="/logo.webp" alt="AuraMaxx" className="w-full h-full object-contain" />
1627
+ </div>
1628
+ <div className="text-[10px] text-[var(--color-text-muted,#6b7280)] tracking-widest text-center">
1629
+ LOCAL AGENT MODE
1630
+ </div>
1631
+ </div>
1632
+
1633
+ <div className="space-y-4">
1634
+ <div className="text-[9px] text-[var(--color-text-muted,#6b7280)] bg-[var(--color-background,#f4f4f5)] px-3 py-2 border border-[var(--color-border,#d4d4d8)]">
1635
+ Choose how local Unix-socket agents are issued default permissions. You can change this later in settings.
1636
+ </div>
1637
+
1638
+ <fieldset className="border border-[var(--color-border,#d4d4d8)] p-2.5 bg-[var(--color-background,#f4f4f5)]">
1639
+ <legend className="text-[8px] text-[var(--color-text-faint,#9ca3af)] tracking-widest uppercase px-1">
1640
+ Local Agent Mode
1641
+ </legend>
1642
+ <ItemPicker
1643
+ options={[...ONBOARDING_LOCAL_AGENT_MODE_OPTIONS]}
1644
+ value={localAgentMode}
1645
+ onChange={(value) => setLocalAgentMode(value as LocalAgentMode)}
1646
+ ariaLabel="Onboarding local agent mode"
1647
+ />
1648
+ </fieldset>
1649
+
1650
+ {error && (
1651
+ <div className="text-[9px] text-[var(--color-danger,#ef4444)] bg-[var(--color-danger,#ef4444)]/10 px-3 py-2 border border-[var(--color-danger,#ef4444)]/20">
1652
+ {error}
1653
+ </div>
1654
+ )}
1655
+
1656
+ <button
1657
+ onClick={() => { void handleFinalizeOnboarding(); }}
1658
+ disabled={loading}
1659
+ className="w-full py-2.5 bg-[var(--color-text,#0a0a0a)] text-[var(--color-surface,#ffffff)] font-mono text-xs tracking-widest font-bold hover:opacity-90 transition-opacity disabled:opacity-30 disabled:cursor-not-allowed flex items-center justify-center gap-2"
1660
+ >
1661
+ {loading ? (
1662
+ <>
1663
+ <div className="w-3 h-3 border border-[var(--color-surface,#ffffff)] border-t-transparent animate-spin" />
1664
+ SAVING...
1665
+ </>
1666
+ ) : (
1667
+ 'SAVE MODE AND CONTINUE'
1668
+ )}
1669
+ </button>
1670
+ </div>
1671
+ </>
1672
+ )}
1673
+
1674
+ {pageState === 'setup' && !mnemonic && (
1675
+ <>
1676
+ {/* Logo centered */}
1677
+ <div className="flex flex-col items-center mb-6">
1678
+ <div className="w-16 h-16 mb-4">
1679
+ <img src="/logo.webp" alt="AuraMaxx" className="w-full h-full object-contain" />
1680
+ </div>
1681
+ <div className="text-[10px] text-[var(--color-text-muted,#6b7280)] tracking-widest text-center">
1682
+ CREATE YOUR ENCRYPTED VAULT
1683
+ </div>
1684
+ </div>
1685
+
1686
+ <form onSubmit={handleSetup} className="space-y-4">
1687
+ <div>
1688
+ <label className="block text-[8px] text-[var(--color-text-faint,#9ca3af)] tracking-widest mb-1.5 uppercase">
1689
+ Encryption Password
1690
+ </label>
1691
+ <input
1692
+ type="password"
1693
+ value={password}
1694
+ onChange={(e) => { setPassword(e.target.value); setError(null); }}
1695
+ placeholder="Minimum 8 characters"
1696
+ className="w-full px-3 py-2.5 border border-[var(--color-border,#d4d4d8)] font-mono text-sm text-[var(--color-text,#0a0a0a)] focus:outline-none focus:border-[var(--color-text,#0a0a0a)] bg-[var(--color-surface,#ffffff)] placeholder-[var(--color-text-faint,#9ca3af)] transition-colors"
1697
+ autoFocus
1698
+ />
1699
+ </div>
1700
+ <label className="flex items-center justify-between gap-3 text-[8px] tracking-widest uppercase text-[var(--color-text-muted,#6b7280)]">
1701
+ <span className="inline-flex items-center gap-2">
1702
+ <input
1703
+ type="checkbox"
1704
+ checked={trustDevice}
1705
+ onChange={(e) => setTrustDevice(e.target.checked)}
1706
+ className="h-3.5 w-3.5 border border-[var(--color-border,#d4d4d8)] accent-[var(--color-text,#0a0a0a)]"
1707
+ />
1708
+ Trusted device
1709
+ </span>
1710
+ <span>{trustDevice ? 'PERSISTENT' : 'TAB ONLY'}</span>
1711
+ </label>
1712
+
1713
+ {error && (
1714
+ <div className="text-[9px] text-[var(--color-danger,#ef4444)] bg-[var(--color-danger,#ef4444)]/10 px-3 py-2 border border-[var(--color-danger,#ef4444)]/20">
1715
+ {error}
1716
+ </div>
1717
+ )}
1718
+
1719
+ <button
1720
+ type="submit"
1721
+ disabled={loading || password.length < 8}
1722
+ className="w-full py-2.5 bg-[var(--color-text,#0a0a0a)] text-[var(--color-surface,#ffffff)] font-mono text-xs tracking-widest font-bold hover:opacity-90 transition-opacity disabled:opacity-30 disabled:cursor-not-allowed flex items-center justify-center gap-2"
1723
+ >
1724
+ {loading ? (
1725
+ <>
1726
+ <div className="w-3 h-3 border border-[var(--color-surface,#ffffff)] border-t-transparent animate-spin" />
1727
+ INITIALIZING...
1728
+ </>
1729
+ ) : (
1730
+ 'INITIALIZE VAULT'
1731
+ )}
1732
+ </button>
1733
+ </form>
1734
+
1735
+ <div className="mt-4 pt-4 border-t border-[var(--color-border,#d4d4d8)]">
1736
+ <div className="flex items-start gap-2">
1737
+ <div className="w-1 h-1 bg-[var(--color-text-muted,#6b7280)] mt-1.5 flex-shrink-0" />
1738
+ <span className="text-[8px] text-[var(--color-text-faint,#9ca3af)] leading-relaxed">
1739
+ This password encrypts your seed phrase locally. It never leaves your machine.
1740
+ </span>
1741
+ </div>
1742
+ </div>
1743
+ </>
1744
+ )}
1745
+
1746
+ {pageState === 'locked' && (
1747
+ <>
1748
+ {/* Logo centered */}
1749
+ <div className="flex flex-col items-center mb-6">
1750
+ <div className="w-16 h-16 mb-4">
1751
+ <img src="/logo.webp" alt="AuraMaxx" className="w-full h-full object-contain" />
1752
+ </div>
1753
+ <div className="text-[10px] text-[var(--color-text-muted,#6b7280)] tracking-widest text-center">
1754
+ {showSeedRecovery ? 'RECOVER WITH SEED PHRASE' : passkeyAvailable ? 'UNLOCK VAULT' : 'ENTER PASSWORD TO UNLOCK'}
1755
+ </div>
1756
+ </div>
1757
+
1758
+ {!showSeedRecovery && (
1759
+ <>
1760
+ {passkeyAvailable && (
1761
+ <div className="mb-4">
1762
+ <button
1763
+ type="button"
1764
+ onClick={() => { void handlePasskeyUnlock(); }}
1765
+ disabled={passkeyLoading}
1766
+ className="w-full py-3 bg-[var(--color-text,#0a0a0a)] text-[var(--color-surface,#ffffff)] font-mono text-xs tracking-widest font-bold hover:opacity-90 transition-opacity disabled:opacity-50 flex items-center justify-center gap-2"
1767
+ >
1768
+ {passkeyLoading ? (
1769
+ <>
1770
+ <div className="w-3 h-3 border border-[var(--color-surface,#ffffff)] border-t-transparent animate-spin" />
1771
+ AUTHENTICATING...
1772
+ </>
1773
+ ) : (
1774
+ <>
1775
+ <Fingerprint size={14} />
1776
+ UNLOCK WITH PASSKEY
1777
+ </>
1778
+ )}
1779
+ </button>
1780
+ <div className="mt-3 flex items-center gap-3">
1781
+ <div className="flex-1 h-px bg-[var(--color-border,#d4d4d8)]" />
1782
+ <span className="text-[8px] text-[var(--color-text-faint,#9ca3af)] tracking-widest">OR USE PASSWORD</span>
1783
+ <div className="flex-1 h-px bg-[var(--color-border,#d4d4d8)]" />
1784
+ </div>
1785
+ </div>
1786
+ )}
1787
+ <form onSubmit={handleUnlock} className="space-y-4">
1788
+ <div>
1789
+ <label className="block text-[8px] text-[var(--color-text-faint,#9ca3af)] tracking-widest mb-1.5 uppercase">
1790
+ Password
1791
+ </label>
1792
+ <input
1793
+ type="password"
1794
+ value={password}
1795
+ onChange={(e) => { setPassword(e.target.value); setError(null); }}
1796
+ placeholder="Enter vault password"
1797
+ className="w-full px-3 py-2.5 border border-[var(--color-border,#d4d4d8)] font-mono text-sm text-[var(--color-text,#0a0a0a)] focus:outline-none focus:border-[var(--color-text,#0a0a0a)] bg-[var(--color-surface,#ffffff)] placeholder-[var(--color-text-faint,#9ca3af)] transition-colors"
1798
+ autoFocus
1799
+ />
1800
+ </div>
1801
+ <label className="flex items-center justify-between gap-3 text-[8px] tracking-widest uppercase text-[var(--color-text-muted,#6b7280)]">
1802
+ <span className="inline-flex items-center gap-2">
1803
+ <input
1804
+ type="checkbox"
1805
+ checked={trustDevice}
1806
+ onChange={(e) => setTrustDevice(e.target.checked)}
1807
+ className="h-3.5 w-3.5 border border-[var(--color-border,#d4d4d8)] accent-[var(--color-text,#0a0a0a)]"
1808
+ />
1809
+ Trusted device
1810
+ </span>
1811
+ <span>{trustDevice ? 'PERSISTENT' : 'TAB ONLY'}</span>
1812
+ </label>
1813
+
1814
+ {error && (
1815
+ <div
1816
+ data-testid="unlock-error-banner"
1817
+ className="text-[9px] text-[var(--color-danger,#ef4444)] px-3 py-2 border"
1818
+ style={{
1819
+ borderColor: 'color-mix(in srgb, var(--color-danger,#ef4444) 35%, transparent)',
1820
+ background: 'color-mix(in srgb, var(--color-danger,#ef4444) 12%, transparent)',
1821
+ }}
1822
+ >
1823
+ {error}
1824
+ </div>
1825
+ )}
1826
+
1827
+ <button
1828
+ type="submit"
1829
+ disabled={loading || !password}
1830
+ className="w-full py-2.5 bg-[var(--color-text,#0a0a0a)] text-[var(--color-surface,#ffffff)] font-mono text-xs tracking-widest font-bold hover:opacity-90 transition-opacity disabled:opacity-30 disabled:cursor-not-allowed flex items-center justify-center gap-2"
1831
+ >
1832
+ {loading ? (
1833
+ <>
1834
+ <div className="w-3 h-3 border border-[var(--color-surface,#ffffff)] border-t-transparent animate-spin" />
1835
+ UNLOCKING...
1836
+ </>
1837
+ ) : (
1838
+ 'UNLOCK'
1839
+ )}
1840
+ </button>
1841
+ </form>
1842
+
1843
+ <div className="mt-4 border-t border-[var(--color-border,#d4d4d8)] pt-3 text-center">
1844
+ <button
1845
+ type="button"
1846
+ onClick={() => {
1847
+ setShowSeedRecovery(true);
1848
+ setRecoveryError(null);
1849
+ }}
1850
+ className="text-[10px] underline underline-offset-2 text-[var(--color-text-muted,#6b7280)] hover:text-[var(--color-text,#0a0a0a)] transition-colors"
1851
+ >
1852
+ Forgot password?
1853
+ </button>
1854
+ </div>
1855
+ </>
1856
+ )}
1857
+
1858
+ {showSeedRecovery && (
1859
+ <div className="mt-4 border-t border-[var(--color-border,#d4d4d8)] pt-3 text-center">
1860
+ <button
1861
+ type="button"
1862
+ onClick={() => {
1863
+ setShowSeedRecovery(false);
1864
+ setRecoveryError(null);
1865
+ }}
1866
+ className="text-[10px] underline underline-offset-2 text-[var(--color-text-muted,#6b7280)] hover:text-[var(--color-text,#0a0a0a)] transition-colors"
1867
+ >
1868
+ Back to unlock
1869
+ </button>
1870
+ </div>
1871
+ )}
1872
+
1873
+ {showSeedRecovery && (
1874
+ <form onSubmit={handleRecoverAccess} className="mt-4 space-y-3">
1875
+ <div className="flex items-center justify-between">
1876
+ <div className="text-[9px] tracking-widest uppercase text-[var(--color-text-muted,#6b7280)]">Seed Recovery</div>
1877
+ <div className="flex items-center gap-1">
1878
+ <button
1879
+ type="button"
1880
+ onClick={() => { setRecoveryWordCount(12); setRecoveryWords(Array(12).fill('')); }}
1881
+ className={`px-2 py-1 text-[8px] border ${recoveryWordCount === 12 ? 'bg-[var(--color-text,#0a0a0a)] text-[var(--color-surface,#fff)] border-[var(--color-text,#0a0a0a)]' : 'border-[var(--color-border,#d4d4d8)] text-[var(--color-text-muted,#6b7280)]'}`}
1882
+ >12</button>
1883
+ <button
1884
+ type="button"
1885
+ onClick={() => { setRecoveryWordCount(24); setRecoveryWords(Array(24).fill('')); }}
1886
+ className={`px-2 py-1 text-[8px] border ${recoveryWordCount === 24 ? 'bg-[var(--color-text,#0a0a0a)] text-[var(--color-surface,#fff)] border-[var(--color-text,#0a0a0a)]' : 'border-[var(--color-border,#d4d4d8)] text-[var(--color-text-muted,#6b7280)]'}`}
1887
+ >24</button>
1888
+ </div>
1889
+ </div>
1890
+
1891
+ <div className="grid grid-cols-3 gap-1.5">
1892
+ {recoveryWords.map((word, index) => {
1893
+ const invalid = invalidRecoveryIndexes.has(index);
1894
+ return (
1895
+ <TextInput
1896
+ key={`recovery-word-${index}`}
1897
+ compact
1898
+ error={invalid}
1899
+ aria-label={`Recovery word ${index + 1}`}
1900
+ value={word}
1901
+ onChange={(e) => handleRecoveryWordChange(index, e.target.value)}
1902
+ onPaste={(e) => {
1903
+ const didSplit = handleRecoveryPaste(index, e.clipboardData.getData('text'));
1904
+ if (didSplit) e.preventDefault();
1905
+ }}
1906
+ placeholder={`${index + 1}`}
1907
+ className="min-w-0"
1908
+ />
1909
+ );
1910
+ })}
1911
+ </div>
1912
+
1913
+ <div className="text-[9px] text-[var(--color-text-faint,#9ca3af)]">
1914
+ {recoveryWordsFilled}/{recoveryWordCount} words · {invalidRecoveryIndexes.size > 0 ? `${invalidRecoveryIndexes.size} invalid word(s)` : (isRecoveryPhraseStructurallyValid ? 'BIP-39 phrase valid' : 'Waiting for valid BIP-39 phrase')}
1915
+ </div>
1916
+
1917
+ <TextInput
1918
+ type="password"
1919
+ compact
1920
+ value={recoveryNewPassword}
1921
+ onChange={(e) => { setRecoveryNewPassword(e.target.value); setRecoveryError(null); }}
1922
+ placeholder="New password"
1923
+ aria-label="New password"
1924
+ />
1925
+ {recoveryError && (
1926
+ <div className="text-[9px] text-[var(--color-danger,#ef4444)] bg-[var(--color-danger,#ef4444)]/10 px-3 py-2 border border-[var(--color-danger,#ef4444)]/20">
1927
+ {recoveryError}
1928
+ </div>
1929
+ )}
1930
+
1931
+ <button
1932
+ type="submit"
1933
+ disabled={recoveryLoading}
1934
+ className="w-full py-2.5 bg-[var(--color-text,#0a0a0a)] text-[var(--color-surface,#ffffff)] font-mono text-xs tracking-widest font-bold hover:opacity-90 transition-opacity disabled:opacity-30"
1935
+ >
1936
+ {recoveryLoading ? 'RECOVERING...' : 'RECOVER & UNLOCK'}
1937
+ </button>
1938
+ </form>
1939
+ )}
1940
+ </>
1941
+ )}
1942
+ </div>
1943
+
1944
+ {/* Barcode + stripe */}
1945
+ <div className="flex items-center gap-3 px-5 py-2 border-t border-[var(--color-border,#d4d4d8)]">
1946
+ <div className="h-4 flex-1 bg-[repeating-linear-gradient(90deg,var(--color-text,#000),var(--color-text,#000)_1px,transparent_1px,transparent_3px)] opacity-30" />
1947
+ <span className="text-[8px] text-[var(--color-text-faint,#9ca3af)] tracking-wider">AURAMAXX</span>
1948
+ </div>
1949
+ <div className="h-2 w-full" style={{
1950
+ backgroundImage: 'repeating-linear-gradient(45deg, var(--color-text, #000), var(--color-text, #000) 5px, transparent 5px, transparent 10px)',
1951
+ opacity: 0.1,
1952
+ }} />
1953
+ </div>
1954
+
1955
+ {/* Specimen label below card */}
1956
+ <div className="mt-4 text-center">
1957
+ <span className="text-[8px] text-[var(--color-text-faint,#9ca3af)] tracking-[0.2em] font-mono">
1958
+ SECURE LOCAL WALLETS FOR AI AGENTS
1959
+ </span>
1960
+ </div>
1961
+ </div>
1962
+ </div>
1963
+ );
1964
+ }