auramaxx 1.0.0-alpha.4

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 (363) hide show
  1. package/LICENSE +26 -0
  2. package/README.md +112 -0
  3. package/bin/aurawallet.js +121 -0
  4. package/docs/ADAPTERS.md +467 -0
  5. package/docs/API.md +2679 -0
  6. package/docs/APPS.md +198 -0
  7. package/docs/ARCHITECTURE.md +350 -0
  8. package/docs/AUTH.md +698 -0
  9. package/docs/BEST-PRACTICES.md +121 -0
  10. package/docs/CLI.md +61 -0
  11. package/docs/DEVELOPING-APPS.md +452 -0
  12. package/docs/EXTENSION.md +97 -0
  13. package/docs/JOBS.md +33 -0
  14. package/docs/MCP.md +76 -0
  15. package/docs/PROTOCOL.md +142 -0
  16. package/docs/SETUP.md +219 -0
  17. package/docs/WORKSPACE.md +672 -0
  18. package/docs/agent-auth.md +63 -0
  19. package/docs/aura-file.md +48 -0
  20. package/docs/credentials.md +53 -0
  21. package/docs/external/getting-started.md +65 -0
  22. package/docs/external/overview.md +45 -0
  23. package/docs/external/use-cases.md +48 -0
  24. package/docs/external/why-aura.md +35 -0
  25. package/docs/jobs/connect-agent.md +77 -0
  26. package/docs/jobs/migrate-from-dotenv.md +79 -0
  27. package/docs/jobs/recover-from-lockout.md +72 -0
  28. package/docs/jobs/secure-ci.md +63 -0
  29. package/docs/oauth2.md +42 -0
  30. package/docs/passkeys.md +60 -0
  31. package/docs/security.md +540 -0
  32. package/docs/specs/aura-open-protocol.md +61 -0
  33. package/docs/specs/aura-provider-plugin.md +24 -0
  34. package/docs/specs/aura-registry-model.md +31 -0
  35. package/docs/specs/fixtures/invalid-bad-key.aura +1 -0
  36. package/docs/specs/fixtures/invalid-bad-unicode-escape.aura +1 -0
  37. package/docs/specs/fixtures/invalid-duplicate-key.aura +2 -0
  38. package/docs/specs/fixtures/valid-basic.aura +4 -0
  39. package/docs/specs/fixtures/valid-provider-ref.aura +1 -0
  40. package/docs/specs/fixtures/valid-quoted-escapes.aura +2 -0
  41. package/docs/templates/RELEASE_NOTES_TEMPLATE.md +22 -0
  42. package/docs/totp.md +40 -0
  43. package/docs/wallet/AI.md +508 -0
  44. package/docs/wallet/DEVELOPING-STRATEGIES.md +713 -0
  45. package/docs/wallet/README.md +47 -0
  46. package/docs/wallet/STRATEGY.md +89 -0
  47. package/next.config.ts +21 -0
  48. package/package.json +151 -0
  49. package/postcss.config.mjs +8 -0
  50. package/prisma/migrations/20260214170000_baseline/migration.sql +511 -0
  51. package/prisma/migrations/20260216214537_add_passkey_model/migration.sql +18 -0
  52. package/prisma/migrations/20260217150500_add_credential_access_audit/migration.sql +31 -0
  53. package/prisma/migrations/migration_lock.toml +3 -0
  54. package/prisma/schema.prisma +447 -0
  55. package/public/logo-chevron.svg +31 -0
  56. package/public/logo-concentric.svg +31 -0
  57. package/public/logo-crosshatch.svg +39 -0
  58. package/public/logo-dashed.svg +39 -0
  59. package/public/logo-horizontal.svg +31 -0
  60. package/public/logo-m56.svg +64 -0
  61. package/public/logo.webp +0 -0
  62. package/scripts/add-app.js +245 -0
  63. package/scripts/init.sh +57 -0
  64. package/scripts/migrate-apikeys-to-credentials.ts +35 -0
  65. package/scripts/sandbox-agent-flow.sh +235 -0
  66. package/scripts/sandbox.sh +175 -0
  67. package/scripts/validate-job-docs.mjs +125 -0
  68. package/server/abi/SwapHelper.json +438 -0
  69. package/server/cli/approval.ts +447 -0
  70. package/server/cli/commands/app.ts +204 -0
  71. package/server/cli/commands/cron.ts +24 -0
  72. package/server/cli/commands/doctor.ts +1007 -0
  73. package/server/cli/commands/env.ts +456 -0
  74. package/server/cli/commands/init.ts +752 -0
  75. package/server/cli/commands/mcp.ts +125 -0
  76. package/server/cli/commands/restore.ts +314 -0
  77. package/server/cli/commands/shell-hook.ts +468 -0
  78. package/server/cli/commands/start.ts +62 -0
  79. package/server/cli/commands/status.ts +59 -0
  80. package/server/cli/commands/stop.ts +14 -0
  81. package/server/cli/commands/token.ts +180 -0
  82. package/server/cli/commands/unlock.ts +49 -0
  83. package/server/cli/commands/vault.ts +417 -0
  84. package/server/cli/index.ts +328 -0
  85. package/server/cli/lib/aura-parser.ts +64 -0
  86. package/server/cli/lib/credential-create.ts +74 -0
  87. package/server/cli/lib/credential-resolve.ts +254 -0
  88. package/server/cli/lib/dotenv-migrate.ts +116 -0
  89. package/server/cli/lib/dotenv-parser.ts +146 -0
  90. package/server/cli/lib/http.ts +91 -0
  91. package/server/cli/lib/init-steps.ts +76 -0
  92. package/server/cli/lib/local-agent-trust.ts +45 -0
  93. package/server/cli/lib/process.ts +136 -0
  94. package/server/cli/lib/prompt.ts +85 -0
  95. package/server/cli/lib/theme.ts +240 -0
  96. package/server/cli/socket.ts +570 -0
  97. package/server/cli/transport-client.ts +50 -0
  98. package/server/cron/index.ts +137 -0
  99. package/server/cron/job.ts +31 -0
  100. package/server/cron/jobs/balance-sync.ts +436 -0
  101. package/server/cron/jobs/incoming-scan.ts +506 -0
  102. package/server/cron/jobs/native-price.ts +70 -0
  103. package/server/cron/jobs/orphan-cleanup.ts +40 -0
  104. package/server/cron/jobs/strategy-runner.ts +175 -0
  105. package/server/cron/scheduler.ts +125 -0
  106. package/server/index.ts +406 -0
  107. package/server/lib/adapters/factory.ts +110 -0
  108. package/server/lib/adapters/index.ts +19 -0
  109. package/server/lib/adapters/router.ts +297 -0
  110. package/server/lib/adapters/telegram.ts +645 -0
  111. package/server/lib/adapters/types.ts +89 -0
  112. package/server/lib/adapters/webhook.ts +95 -0
  113. package/server/lib/address.ts +49 -0
  114. package/server/lib/agent-auth/contracts.ts +1194 -0
  115. package/server/lib/agent-profiles.ts +328 -0
  116. package/server/lib/ai.ts +285 -0
  117. package/server/lib/api-registry/contracts.ts +86 -0
  118. package/server/lib/api-registry/validation.ts +172 -0
  119. package/server/lib/apikey-migration.ts +189 -0
  120. package/server/lib/app-installer.ts +505 -0
  121. package/server/lib/app-tokens.ts +247 -0
  122. package/server/lib/auth.ts +314 -0
  123. package/server/lib/batch.ts +242 -0
  124. package/server/lib/cold.ts +874 -0
  125. package/server/lib/config.ts +381 -0
  126. package/server/lib/credential-access-audit.ts +85 -0
  127. package/server/lib/credential-access-policy.ts +110 -0
  128. package/server/lib/credential-health.ts +343 -0
  129. package/server/lib/credential-import.ts +487 -0
  130. package/server/lib/credential-scope.ts +87 -0
  131. package/server/lib/credential-shares.ts +190 -0
  132. package/server/lib/credential-transport.ts +342 -0
  133. package/server/lib/credential-vault.ts +77 -0
  134. package/server/lib/credentials.ts +333 -0
  135. package/server/lib/crypto.ts +8 -0
  136. package/server/lib/db.ts +15 -0
  137. package/server/lib/defaults.ts +366 -0
  138. package/server/lib/dex/index.ts +80 -0
  139. package/server/lib/dex/relay.ts +235 -0
  140. package/server/lib/dex/types.ts +59 -0
  141. package/server/lib/dex/uniswap.ts +370 -0
  142. package/server/lib/e2e-agent/artifacts.ts +36 -0
  143. package/server/lib/e2e-agent/contracts.ts +112 -0
  144. package/server/lib/e2e-agent/validation.ts +135 -0
  145. package/server/lib/encrypt.ts +128 -0
  146. package/server/lib/error.ts +20 -0
  147. package/server/lib/events.ts +205 -0
  148. package/server/lib/hot.ts +357 -0
  149. package/server/lib/key-fingerprint.ts +28 -0
  150. package/server/lib/logger.ts +331 -0
  151. package/server/lib/network.ts +137 -0
  152. package/server/lib/notifications.ts +219 -0
  153. package/server/lib/oauth2-refresh.ts +241 -0
  154. package/server/lib/oursecret.ts +54 -0
  155. package/server/lib/passkey-credential.ts +360 -0
  156. package/server/lib/passkey.ts +68 -0
  157. package/server/lib/permissions.ts +248 -0
  158. package/server/lib/pino.ts +24 -0
  159. package/server/lib/policy-preview.ts +138 -0
  160. package/server/lib/price.ts +338 -0
  161. package/server/lib/prices.ts +34 -0
  162. package/server/lib/project-scope.ts +239 -0
  163. package/server/lib/resolve-action.ts +427 -0
  164. package/server/lib/resolve.ts +36 -0
  165. package/server/lib/sessions.ts +632 -0
  166. package/server/lib/solana/connection.ts +26 -0
  167. package/server/lib/solana/jupiter.ts +128 -0
  168. package/server/lib/solana/transfer.ts +108 -0
  169. package/server/lib/solana/wallet.ts +136 -0
  170. package/server/lib/strategy/emits.ts +21 -0
  171. package/server/lib/strategy/engine.ts +1305 -0
  172. package/server/lib/strategy/executor.ts +115 -0
  173. package/server/lib/strategy/hook-context.ts +158 -0
  174. package/server/lib/strategy/hooks.ts +990 -0
  175. package/server/lib/strategy/index.ts +28 -0
  176. package/server/lib/strategy/installer.ts +305 -0
  177. package/server/lib/strategy/loader.ts +256 -0
  178. package/server/lib/strategy/message.ts +235 -0
  179. package/server/lib/strategy/repository.ts +218 -0
  180. package/server/lib/strategy/session-logger.ts +693 -0
  181. package/server/lib/strategy/sources.ts +288 -0
  182. package/server/lib/strategy/state.ts +189 -0
  183. package/server/lib/strategy/templates.ts +403 -0
  184. package/server/lib/strategy/tick.ts +404 -0
  185. package/server/lib/strategy/types.ts +230 -0
  186. package/server/lib/swap.ts +3 -0
  187. package/server/lib/temp.ts +86 -0
  188. package/server/lib/token-metadata.ts +86 -0
  189. package/server/lib/token-safety.ts +200 -0
  190. package/server/lib/token-search.ts +444 -0
  191. package/server/lib/totp.ts +194 -0
  192. package/server/lib/transactions.ts +123 -0
  193. package/server/lib/transport.ts +75 -0
  194. package/server/lib/txhistory/decoder.ts +262 -0
  195. package/server/lib/txhistory/enricher.ts +652 -0
  196. package/server/lib/txhistory/index.ts +391 -0
  197. package/server/lib/txhistory/signatures.ts +59 -0
  198. package/server/lib/verified-summary.ts +421 -0
  199. package/server/mcp/profile-policy.ts +30 -0
  200. package/server/mcp/server.ts +619 -0
  201. package/server/mcp/tools.ts +523 -0
  202. package/server/middleware/auth.ts +119 -0
  203. package/server/middleware/requestLogger.ts +84 -0
  204. package/server/routes/actions.ts +459 -0
  205. package/server/routes/adapters.ts +703 -0
  206. package/server/routes/addressbook.ts +113 -0
  207. package/server/routes/ai.ts +34 -0
  208. package/server/routes/apikeys.ts +295 -0
  209. package/server/routes/apps.ts +601 -0
  210. package/server/routes/auth.ts +457 -0
  211. package/server/routes/backup.ts +340 -0
  212. package/server/routes/batch.ts +270 -0
  213. package/server/routes/bookmarks.ts +162 -0
  214. package/server/routes/credential-shares.ts +198 -0
  215. package/server/routes/credential-vaults.ts +154 -0
  216. package/server/routes/credentials.ts +1290 -0
  217. package/server/routes/dashboard.ts +71 -0
  218. package/server/routes/defaults.ts +124 -0
  219. package/server/routes/fund.ts +229 -0
  220. package/server/routes/import.ts +352 -0
  221. package/server/routes/launch.ts +665 -0
  222. package/server/routes/lock.ts +54 -0
  223. package/server/routes/logs.ts +68 -0
  224. package/server/routes/nuke.ts +111 -0
  225. package/server/routes/passkey-credentials.ts +99 -0
  226. package/server/routes/passkey.ts +346 -0
  227. package/server/routes/portfolio.ts +217 -0
  228. package/server/routes/price.ts +63 -0
  229. package/server/routes/resolve.ts +31 -0
  230. package/server/routes/security.ts +45 -0
  231. package/server/routes/send-evm.ts +241 -0
  232. package/server/routes/send-solana.ts +281 -0
  233. package/server/routes/send.ts +178 -0
  234. package/server/routes/setup.ts +210 -0
  235. package/server/routes/strategy.ts +894 -0
  236. package/server/routes/swap-evm.ts +353 -0
  237. package/server/routes/swap-solana.ts +177 -0
  238. package/server/routes/swap.ts +356 -0
  239. package/server/routes/token.ts +247 -0
  240. package/server/routes/unlock.ts +403 -0
  241. package/server/routes/wallet-assets.ts +361 -0
  242. package/server/routes/wallet-transactions.ts +515 -0
  243. package/server/routes/wallet.ts +710 -0
  244. package/server/types.ts +146 -0
  245. package/skills/aurawallet/SKILL.md +739 -0
  246. package/skills/aurawallet-setup/SKILL.md +74 -0
  247. package/skills/security-review/SKILL.md +148 -0
  248. package/src/app/api/agent-requests/route.ts +30 -0
  249. package/src/app/api/apps/install/route.ts +126 -0
  250. package/src/app/api/apps/manifests/route.ts +16 -0
  251. package/src/app/api/apps/static/[...path]/route.ts +57 -0
  252. package/src/app/api/events/route.ts +92 -0
  253. package/src/app/api/page.tsx +212 -0
  254. package/src/app/api/workspace/[id]/apps/[wid]/route.ts +119 -0
  255. package/src/app/api/workspace/[id]/apps/route.ts +81 -0
  256. package/src/app/api/workspace/[id]/export/route.ts +67 -0
  257. package/src/app/api/workspace/[id]/route.ts +168 -0
  258. package/src/app/api/workspace/auth.ts +34 -0
  259. package/src/app/api/workspace/config/route.ts +106 -0
  260. package/src/app/api/workspace/import/route.ts +127 -0
  261. package/src/app/api/workspace/route.ts +116 -0
  262. package/src/app/app/page.tsx +2122 -0
  263. package/src/app/apple-icon.png +0 -0
  264. package/src/app/docs/page.tsx +178 -0
  265. package/src/app/favicon.ico +0 -0
  266. package/src/app/globals.css +572 -0
  267. package/src/app/health/page.tsx +5 -0
  268. package/src/app/hello/page.tsx +15 -0
  269. package/src/app/icon.png +0 -0
  270. package/src/app/layout.tsx +34 -0
  271. package/src/app/page.tsx +986 -0
  272. package/src/app/providers.tsx +90 -0
  273. package/src/app/share/[token]/page.tsx +295 -0
  274. package/src/components/ChainSelector.tsx +144 -0
  275. package/src/components/HumanActionBar.tsx +695 -0
  276. package/src/components/NotificationDrawer.tsx +129 -0
  277. package/src/components/apps/AgentKeysApp.tsx +490 -0
  278. package/src/components/apps/App.tsx +153 -0
  279. package/src/components/apps/AppGrid.tsx +15 -0
  280. package/src/components/apps/DetailedAddressDrawer.tsx +325 -0
  281. package/src/components/apps/DraggableApp.tsx +562 -0
  282. package/src/components/apps/IFrameApp.tsx +73 -0
  283. package/src/components/apps/LogsApp.tsx +360 -0
  284. package/src/components/apps/SendApp.tsx +394 -0
  285. package/src/components/apps/SetupWizardApp.tsx +1004 -0
  286. package/src/components/apps/SystemDefaultsApp.tsx +845 -0
  287. package/src/components/apps/ThirdPartyApp.tsx +428 -0
  288. package/src/components/apps/TokenApp.tsx +319 -0
  289. package/src/components/apps/TransactionsApp.tsx +438 -0
  290. package/src/components/apps/WalletDetailApp.tsx +1505 -0
  291. package/src/components/apps/index.ts +13 -0
  292. package/src/components/design-system/Button.tsx +53 -0
  293. package/src/components/design-system/ChainIndicator.tsx +65 -0
  294. package/src/components/design-system/ChainSelector.tsx +137 -0
  295. package/src/components/design-system/ConfirmationModal.tsx +106 -0
  296. package/src/components/design-system/ConfirmationPopover.tsx +81 -0
  297. package/src/components/design-system/Drawer.tsx +123 -0
  298. package/src/components/design-system/FilterDropdown.tsx +72 -0
  299. package/src/components/design-system/Modal.tsx +206 -0
  300. package/src/components/design-system/Popover.tsx +142 -0
  301. package/src/components/design-system/TextInput.tsx +85 -0
  302. package/src/components/design-system/Toggle.tsx +58 -0
  303. package/src/components/design-system/index.ts +11 -0
  304. package/src/components/docs/DocsThemeToggle.tsx +49 -0
  305. package/src/components/health/CredentialHealthDashboard.tsx +214 -0
  306. package/src/components/icons/ChainIcons.tsx +72 -0
  307. package/src/components/layout/AppStoreDrawer.tsx +369 -0
  308. package/src/components/layout/ContentArea.tsx +21 -0
  309. package/src/components/layout/TabBar.tsx +278 -0
  310. package/src/components/layout/WalletSidebar.tsx +1033 -0
  311. package/src/components/layout/index.ts +4 -0
  312. package/src/components/marketing/AuraWalletSpecOverlay.tsx +635 -0
  313. package/src/components/marketing/DeviceMorphExperience.tsx +216 -0
  314. package/src/components/vault/ApiKeysConsole.tsx +1080 -0
  315. package/src/components/vault/AuditConsole.tsx +584 -0
  316. package/src/components/vault/CredentialDetail.tsx +455 -0
  317. package/src/components/vault/CredentialEmpty.tsx +55 -0
  318. package/src/components/vault/CredentialField.tsx +361 -0
  319. package/src/components/vault/CredentialForm.tsx +1212 -0
  320. package/src/components/vault/CredentialList.tsx +165 -0
  321. package/src/components/vault/CredentialRow.tsx +97 -0
  322. package/src/components/vault/CredentialShareModal.tsx +178 -0
  323. package/src/components/vault/CredentialVault.tsx +754 -0
  324. package/src/components/vault/CredentialWalletWidget.tsx +103 -0
  325. package/src/components/vault/ImportCredentialsModal.tsx +515 -0
  326. package/src/components/vault/LargeTypeModal.tsx +64 -0
  327. package/src/components/vault/PasswordGenerator.tsx +224 -0
  328. package/src/components/vault/TOTPDisplay.tsx +123 -0
  329. package/src/components/vault/VaultSidebar.tsx +413 -0
  330. package/src/components/vault/types.ts +54 -0
  331. package/src/context/AuthContext.tsx +337 -0
  332. package/src/context/PriceContext.tsx +113 -0
  333. package/src/context/ThemeContext.tsx +164 -0
  334. package/src/context/WebSocketContext.tsx +269 -0
  335. package/src/context/WorkspaceContext.tsx +668 -0
  336. package/src/hooks/index.ts +3 -0
  337. package/src/hooks/useAgentActions.ts +368 -0
  338. package/src/hooks/useBalance.ts +103 -0
  339. package/src/hooks/useBalances.ts +129 -0
  340. package/src/instrumentation.ts +12 -0
  341. package/src/lib/api.ts +449 -0
  342. package/src/lib/app-loader.ts +148 -0
  343. package/src/lib/app-registry.ts +178 -0
  344. package/src/lib/app-sdk.ts +157 -0
  345. package/src/lib/audit-console-adapter.ts +151 -0
  346. package/src/lib/auth-client.ts +75 -0
  347. package/src/lib/config.ts +74 -0
  348. package/src/lib/crypto.ts +112 -0
  349. package/src/lib/db.ts +21 -0
  350. package/src/lib/docs.ts +390 -0
  351. package/src/lib/events.ts +361 -0
  352. package/src/lib/pino.ts +24 -0
  353. package/src/lib/theme-handlers.ts +168 -0
  354. package/src/lib/theme.ts +351 -0
  355. package/src/lib/tokenData.ts +378 -0
  356. package/src/lib/vault-crypto.ts +129 -0
  357. package/src/lib/websocket-server.ts +302 -0
  358. package/src/lib/websocket-setup.ts +79 -0
  359. package/src/lib/wordlist.ts +2050 -0
  360. package/src/lib/workspace-handlers.ts +285 -0
  361. package/start.sh +80 -0
  362. package/tailwind.config.ts +99 -0
  363. package/tsconfig.json +42 -0
@@ -0,0 +1,986 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect, useCallback, useMemo } from 'react';
4
+ import Link from 'next/link';
5
+ import { Bot, ChevronDown, KeyRound } from 'lucide-react';
6
+ import { useAuth } from '@/context/AuthContext';
7
+ import { api, Api, unlockWallet, setupWallet, rekeySession, changePrimaryVaultPassword } from '@/lib/api';
8
+ import { generateVaultKeypair, getVaultPrivateKey } from '@/lib/vault-crypto';
9
+ import { CredentialVault } from '@/components/vault/CredentialVault';
10
+ import DocsThemeToggle from '@/components/docs/DocsThemeToggle';
11
+ import { Drawer } from '@/components/design-system/Drawer';
12
+ import { Modal } from '@/components/design-system/Modal';
13
+
14
+ interface VaultInfo {
15
+ id: string;
16
+ name?: string;
17
+ address: string;
18
+ solanaAddress?: string;
19
+ isUnlocked: boolean;
20
+ isPrimary: boolean;
21
+ createdAt: string;
22
+ }
23
+
24
+ interface WalletData {
25
+ address: string;
26
+ tier: 'cold' | 'hot' | 'temp';
27
+ chain: string;
28
+ balance?: string;
29
+ }
30
+
31
+ type PageState = 'loading' | 'setup' | 'locked' | 'unlocked';
32
+ type LocalAgentMode = 'strict' | 'dev' | 'admin';
33
+ type SetupOnboardingStep = 'seed' | 'trust';
34
+ type LocalPolicySettings = {
35
+ profile: LocalAgentMode;
36
+ profileVersion: 'v1';
37
+ autoApprove: boolean;
38
+ };
39
+
40
+ const LOCAL_POLICY_PROFILES: LocalAgentMode[] = ['strict', 'dev', 'admin'];
41
+
42
+ type OnboardingSeedDraft = {
43
+ mnemonic: string;
44
+ createdAt: number;
45
+ };
46
+
47
+ const ONBOARDING_SEED_STORAGE_KEY = 'aura:onboarding-seed-draft';
48
+ const ONBOARDING_SEED_TTL_MS = 15 * 60 * 1000;
49
+
50
+ export default function UnlockPage() {
51
+ const { token, setToken, clearToken } = useAuth();
52
+
53
+ const [pageState, setPageState] = useState<PageState>('loading');
54
+ const [password, setPassword] = useState('');
55
+ const [loading, setLoading] = useState(false);
56
+ const [error, setError] = useState<string | null>(null);
57
+ const [mnemonic, setMnemonic] = useState<string | null>(null);
58
+ const [seedAcknowledged, setSeedAcknowledged] = useState(false);
59
+ const [seedRecoveryNotice, setSeedRecoveryNotice] = useState<string | null>(null);
60
+ const [localAgentMode, setLocalAgentMode] = useState<LocalAgentMode>('dev');
61
+ const [setupOnboardingStep, setSetupOnboardingStep] = useState<SetupOnboardingStep>('seed');
62
+ const [onboardingToken, setOnboardingToken] = useState<string | null>(null);
63
+ const [showSettingsDrawer, setShowSettingsDrawer] = useState(false);
64
+ const [policySettings, setPolicySettings] = useState<LocalPolicySettings | null>(null);
65
+ const [policyForm, setPolicyForm] = useState<LocalPolicySettings>({ profile: 'dev', profileVersion: 'v1', autoApprove: true });
66
+ const [policyLoadError, setPolicyLoadError] = useState<string | null>(null);
67
+ const [policySaveError, setPolicySaveError] = useState<string | null>(null);
68
+ const [policySaveSuccess, setPolicySaveSuccess] = useState<string | null>(null);
69
+ const [policyLoading, setPolicyLoading] = useState(false);
70
+ const [policySaving, setPolicySaving] = useState(false);
71
+ const [dangerConfirmOpen, setDangerConfirmOpen] = useState(false);
72
+ const [agentSettingsOpen, setAgentSettingsOpen] = useState(false);
73
+ const [securitySettingsOpen, setSecuritySettingsOpen] = useState(false);
74
+ const [showPasswordModal, setShowPasswordModal] = useState(false);
75
+ const [currentPasswordValue, setCurrentPasswordValue] = useState('');
76
+ const [newPasswordValue, setNewPasswordValue] = useState('');
77
+ const [confirmPasswordValue, setConfirmPasswordValue] = useState('');
78
+ const [passwordChangeError, setPasswordChangeError] = useState<string | null>(null);
79
+ const [passwordChangeSuccess, setPasswordChangeSuccess] = useState<string | null>(null);
80
+ const [passwordChanging, setPasswordChanging] = useState(false);
81
+
82
+ const hasPendingSeedConfirmation = useMemo(() => Boolean(mnemonic), [mnemonic]);
83
+
84
+ useEffect(() => {
85
+ try {
86
+ const rawDraft = sessionStorage.getItem(ONBOARDING_SEED_STORAGE_KEY);
87
+ if (!rawDraft) return;
88
+ const parsed = JSON.parse(rawDraft) as OnboardingSeedDraft;
89
+ if (!parsed?.mnemonic || typeof parsed.createdAt !== 'number') {
90
+ sessionStorage.removeItem(ONBOARDING_SEED_STORAGE_KEY);
91
+ return;
92
+ }
93
+ if (Date.now() - parsed.createdAt > ONBOARDING_SEED_TTL_MS) {
94
+ sessionStorage.removeItem(ONBOARDING_SEED_STORAGE_KEY);
95
+ setSeedRecoveryNotice('Recovery phrase draft expired. Restart setup to generate a new phrase.');
96
+ return;
97
+ }
98
+
99
+ setMnemonic(parsed.mnemonic);
100
+ setSetupOnboardingStep('seed');
101
+ setPageState('setup');
102
+ setSeedRecoveryNotice('Recovered your in-progress recovery phrase for this tab. Confirm after you store it safely.');
103
+ } catch {
104
+ sessionStorage.removeItem(ONBOARDING_SEED_STORAGE_KEY);
105
+ }
106
+ }, []);
107
+
108
+ useEffect(() => {
109
+ if (!mnemonic) {
110
+ sessionStorage.removeItem(ONBOARDING_SEED_STORAGE_KEY);
111
+ setSetupOnboardingStep('seed');
112
+ return;
113
+ }
114
+ const draft: OnboardingSeedDraft = {
115
+ mnemonic,
116
+ createdAt: Date.now(),
117
+ };
118
+ sessionStorage.setItem(ONBOARDING_SEED_STORAGE_KEY, JSON.stringify(draft));
119
+ }, [mnemonic]);
120
+
121
+ const fetchState = useCallback(async () => {
122
+ // Page reload recovery: keypair is memory-only and lost on reload.
123
+ // If token exists but keypair is gone, regenerate keypair and re-key the session.
124
+ if (token && !getVaultPrivateKey()) {
125
+ try {
126
+ const { publicKeyBase64 } = await generateVaultKeypair();
127
+ const result = await rekeySession(publicKeyBase64);
128
+ if (result.token) {
129
+ setToken(result.token);
130
+ }
131
+ // Keypair restored, continue to fetch state normally
132
+ } catch {
133
+ // Re-key failed (token expired, server restarted, vault locked) — fall back to locked
134
+ clearToken();
135
+ setPageState('locked');
136
+ return;
137
+ }
138
+ }
139
+
140
+ try {
141
+ const data = await api.get<{ wallets: WalletData[]; unlocked: boolean; vaults: VaultInfo[] }>(
142
+ Api.Wallet,
143
+ '/wallets',
144
+ { includeHidden: true }
145
+ );
146
+
147
+ const configured = data.wallets.some(w => w.tier === 'cold') || (data.vaults && data.vaults.some(v => v.isPrimary));
148
+
149
+ if (!configured) {
150
+ setPageState('setup');
151
+ } else if (hasPendingSeedConfirmation) {
152
+ setPageState('setup');
153
+ } else if (!data.unlocked || !token) {
154
+ setPageState('locked');
155
+ } else {
156
+ setPageState('unlocked');
157
+ }
158
+ } catch {
159
+ setPageState('setup');
160
+ }
161
+ }, [token, clearToken, hasPendingSeedConfirmation]);
162
+
163
+ useEffect(() => {
164
+ fetchState();
165
+ }, [fetchState]);
166
+
167
+ const loadLocalPolicySettings = useCallback(async () => {
168
+ if (!token) {
169
+ setPolicyLoadError('Unlock vault first to manage local socket policy.');
170
+ return;
171
+ }
172
+
173
+ setPolicyLoading(true);
174
+ setPolicyLoadError(null);
175
+ setPolicySaveError(null);
176
+ setPolicySaveSuccess(null);
177
+ try {
178
+ const headers = { Authorization: `Bearer ${token}` };
179
+ const baseUrl = api.getBaseUrl(Api.Wallet);
180
+ const defaultsRes = await fetch(`${baseUrl}/defaults`, { headers });
181
+ if (!defaultsRes.ok) {
182
+ throw new Error('Failed to load canonical trust policy defaults.');
183
+ }
184
+
185
+ const defaultsJson = await defaultsRes.json() as {
186
+ success?: boolean;
187
+ defaults?: Record<string, Array<{ key: string; value: unknown }>>;
188
+ };
189
+ if (defaultsJson.success === false) {
190
+ throw new Error('Failed to load canonical trust policy defaults.');
191
+ }
192
+ const flatDefaults = Object.values(defaultsJson.defaults || {}).flat();
193
+ const findDefault = (key: string): unknown => flatDefaults.find((item) => item.key === key)?.value;
194
+
195
+ const loadedProfile = String(findDefault('trust.localProfile') ?? '').trim() as LocalAgentMode;
196
+ if (!LOCAL_POLICY_PROFILES.includes(loadedProfile)) {
197
+ throw new Error(`Unknown persisted local profile: ${loadedProfile || '(empty)'}`);
198
+ }
199
+
200
+ const loaded: LocalPolicySettings = {
201
+ profile: loadedProfile,
202
+ profileVersion: 'v1',
203
+ autoApprove: Boolean(findDefault('trust.localAutoApprove')),
204
+ };
205
+
206
+ if (String(findDefault('trust.localProfileVersion') ?? 'v1').trim() !== 'v1') {
207
+ throw new Error('Unknown local profile version; refusing to edit settings.');
208
+ }
209
+
210
+ setPolicySettings(loaded);
211
+ setPolicyForm(loaded);
212
+ } catch (err) {
213
+ setPolicyLoadError((err as Error).message || 'Failed to load policy settings');
214
+ setPolicySettings(null);
215
+ } finally {
216
+ setPolicyLoading(false);
217
+ }
218
+ }, [token]);
219
+
220
+ useEffect(() => {
221
+ if (showSettingsDrawer) {
222
+ void loadLocalPolicySettings();
223
+ }
224
+ }, [showSettingsDrawer, loadLocalPolicySettings]);
225
+
226
+ const persistLocalPolicySettings = useCallback(async () => {
227
+ if (!token) throw new Error('Missing auth token for save.');
228
+ if (!LOCAL_POLICY_PROFILES.includes(policyForm.profile)) {
229
+ throw new Error('Unknown profile selected; refusing to persist.');
230
+ }
231
+ const baseUrl = api.getBaseUrl(Api.Wallet);
232
+ const headers = {
233
+ 'Content-Type': 'application/json',
234
+ Authorization: `Bearer ${token}`,
235
+ };
236
+
237
+ const profileRes = await fetch(`${baseUrl}/defaults/trust.localProfile`, {
238
+ method: 'PATCH',
239
+ headers,
240
+ body: JSON.stringify({ value: policyForm.profile }),
241
+ });
242
+ const profileVersionRes = await fetch(`${baseUrl}/defaults/trust.localProfileVersion`, {
243
+ method: 'PATCH',
244
+ headers,
245
+ body: JSON.stringify({ value: 'v1' }),
246
+ });
247
+ const autoApproveRes = await fetch(`${baseUrl}/defaults/trust.localAutoApprove`, {
248
+ method: 'PATCH',
249
+ headers,
250
+ body: JSON.stringify({ value: policyForm.autoApprove }),
251
+ });
252
+ if (!profileRes.ok || !profileVersionRes.ok || !autoApproveRes.ok) {
253
+ throw new Error('Failed to save canonical trust policy defaults.');
254
+ }
255
+ }, [policyForm, token]);
256
+
257
+ const handleSaveLocalPolicy = useCallback(async () => {
258
+ if (!policySettings || policyLoading || policySaving) return;
259
+ const enablingDangerous = policySettings.profile !== 'admin' && policyForm.profile === 'admin';
260
+ if (enablingDangerous && !dangerConfirmOpen) {
261
+ setDangerConfirmOpen(true);
262
+ return;
263
+ }
264
+
265
+ setPolicySaving(true);
266
+ setPolicySaveError(null);
267
+ setPolicySaveSuccess(null);
268
+ try {
269
+ await persistLocalPolicySettings();
270
+ const saved = { ...policyForm, profileVersion: 'v1' as const };
271
+ setPolicySettings(saved);
272
+ setPolicyForm(saved);
273
+ setDangerConfirmOpen(false);
274
+ setPolicySaveSuccess('Local trust policy saved.');
275
+ } catch (err) {
276
+ setPolicySaveError((err as Error).message || 'Failed to save policy settings');
277
+ if (policySettings) {
278
+ setPolicyForm(policySettings);
279
+ }
280
+ setDangerConfirmOpen(false);
281
+ } finally {
282
+ setPolicySaving(false);
283
+ }
284
+ }, [dangerConfirmOpen, persistLocalPolicySettings, policyForm, policyLoading, policySaving, policySettings]);
285
+
286
+ const closePasswordModal = useCallback(() => {
287
+ setShowPasswordModal(false);
288
+ setCurrentPasswordValue('');
289
+ setNewPasswordValue('');
290
+ setConfirmPasswordValue('');
291
+ setPasswordChangeError(null);
292
+ setPasswordChanging(false);
293
+ }, []);
294
+
295
+ const handleChangePrimaryPassword = useCallback(async (e: React.FormEvent) => {
296
+ e.preventDefault();
297
+ setPasswordChangeError(null);
298
+ setPasswordChangeSuccess(null);
299
+
300
+ if (newPasswordValue.length < 8) {
301
+ setPasswordChangeError('New password must be at least 8 characters.');
302
+ return;
303
+ }
304
+ if (newPasswordValue !== confirmPasswordValue) {
305
+ setPasswordChangeError('New password and confirmation do not match.');
306
+ return;
307
+ }
308
+
309
+ setPasswordChanging(true);
310
+ try {
311
+ await changePrimaryVaultPassword(currentPasswordValue, newPasswordValue);
312
+ setPasswordChangeSuccess('Primary vault password updated.');
313
+ closePasswordModal();
314
+ } catch (err) {
315
+ setPasswordChangeError((err as Error).message || 'Failed to change primary password.');
316
+ } finally {
317
+ setPasswordChanging(false);
318
+ }
319
+ }, [closePasswordModal, confirmPasswordValue, currentPasswordValue, newPasswordValue]);
320
+
321
+ const handleUnlock = async (e: React.FormEvent) => {
322
+ e.preventDefault();
323
+ if (!password) return;
324
+
325
+ setLoading(true);
326
+ setError(null);
327
+ try {
328
+ // Generate keypair before unlock so the token is minted with our pubkey
329
+ const { publicKeyBase64 } = await generateVaultKeypair();
330
+ const data = await unlockWallet(password, undefined, publicKeyBase64);
331
+ if (data.token) {
332
+ setToken(data.token);
333
+ }
334
+ setPassword('');
335
+ fetchState();
336
+ } catch (err) {
337
+ setError((err as Error).message || 'Unlock failed');
338
+ } finally {
339
+ setLoading(false);
340
+ }
341
+ };
342
+
343
+ const handleSetup = async (e: React.FormEvent) => {
344
+ e.preventDefault();
345
+ if (password.length < 8) return;
346
+
347
+ setLoading(true);
348
+ setError(null);
349
+ try {
350
+ // Generate keypair before setup so the initial token has our pubkey
351
+ const { publicKeyBase64 } = await generateVaultKeypair();
352
+ const result = await setupWallet(password, publicKeyBase64);
353
+ if (result.token) {
354
+ setToken(result.token);
355
+ setOnboardingToken(result.token);
356
+ }
357
+ if (result.mnemonic) {
358
+ setMnemonic(result.mnemonic);
359
+ setSeedAcknowledged(false);
360
+ setSetupOnboardingStep('seed');
361
+ setSeedRecoveryNotice(null);
362
+ }
363
+ setPassword('');
364
+ if (!result.mnemonic) fetchState();
365
+ } catch (err) {
366
+ setError((err as Error).message || 'Setup failed');
367
+ } finally {
368
+ setLoading(false);
369
+ }
370
+ };
371
+
372
+ const persistLocalAgentMode = useCallback(async () => {
373
+ const authToken = onboardingToken || token;
374
+ if (!authToken) {
375
+ throw new Error('Session token unavailable. Unlock again and retry setup.');
376
+ }
377
+
378
+ const profile = localAgentMode;
379
+ const profileVersion = 'v1';
380
+ const autoApprove = profile !== 'strict';
381
+ const baseUrl = api.getBaseUrl(Api.Wallet);
382
+ const headers = {
383
+ 'Content-Type': 'application/json',
384
+ 'Authorization': `Bearer ${authToken}`,
385
+ };
386
+
387
+ await fetch(`${baseUrl}/defaults/trust.localProfile`, {
388
+ method: 'PATCH',
389
+ headers,
390
+ body: JSON.stringify({ value: profile }),
391
+ });
392
+ await fetch(`${baseUrl}/defaults/trust.localProfileVersion`, {
393
+ method: 'PATCH',
394
+ headers,
395
+ body: JSON.stringify({ value: profileVersion }),
396
+ });
397
+ await fetch(`${baseUrl}/defaults/trust.localAutoApprove`, {
398
+ method: 'PATCH',
399
+ headers,
400
+ body: JSON.stringify({ value: autoApprove }),
401
+ });
402
+ }, [localAgentMode, onboardingToken, token]);
403
+
404
+ const handleFinalizeOnboarding = useCallback(async () => {
405
+ setLoading(true);
406
+ setError(null);
407
+ try {
408
+ await persistLocalAgentMode();
409
+ setMnemonic(null);
410
+ setSeedAcknowledged(false);
411
+ setSeedRecoveryNotice(null);
412
+ setOnboardingToken(null);
413
+ setSetupOnboardingStep('seed');
414
+ fetchState();
415
+ } catch (err) {
416
+ setError((err as Error).message || 'Failed to save local agent mode');
417
+ } finally {
418
+ setLoading(false);
419
+ }
420
+ }, [fetchState, persistLocalAgentMode]);
421
+
422
+ const handleLock = useCallback(() => {
423
+ setPageState('locked');
424
+ }, []);
425
+
426
+ // Unlocked: render full-screen vault + root settings drawer controls
427
+ if (pageState === 'unlocked') {
428
+ return (
429
+ <div className="relative h-screen">
430
+ <CredentialVault onLock={handleLock} onSettings={() => setShowSettingsDrawer(true)} />
431
+
432
+ <Drawer
433
+ isOpen={showSettingsDrawer}
434
+ onClose={() => {
435
+ setShowSettingsDrawer(false);
436
+ setDangerConfirmOpen(false);
437
+ setPolicySaveError(null);
438
+ setPasswordChangeError(null);
439
+ setAgentSettingsOpen(false);
440
+ setSecuritySettingsOpen(false);
441
+ setShowPasswordModal(false);
442
+ if (policySettings) setPolicyForm(policySettings);
443
+ }}
444
+ title="ROOT SETTINGS"
445
+ subtitle="Local socket policy"
446
+ >
447
+ <div className="space-y-4">
448
+ {passwordChangeSuccess && (
449
+ <div className="text-[10px] text-[var(--color-info,#0047ff)] border border-[var(--color-info,#0047ff)]/30 bg-[var(--color-info,#0047ff)]/10 px-3 py-2">
450
+ {passwordChangeSuccess}
451
+ </div>
452
+ )}
453
+
454
+ <div className="bg-[var(--color-surface,#fff)] border border-[var(--color-border,#d4d4d8)]">
455
+ <button
456
+ type="button"
457
+ onClick={() => setAgentSettingsOpen((open) => !open)}
458
+ className="w-full p-4 flex items-center justify-between hover:bg-[var(--color-surface-alt,#fafafa)] transition-colors"
459
+ >
460
+ <div className="flex items-center gap-2">
461
+ <Bot size={12} className="text-[var(--color-text-muted,#6b7280)]" />
462
+ <span className="font-mono text-[10px] tracking-widest text-[var(--color-text-muted,#6b7280)]">DEFAULT_AGENT_PROFILE</span>
463
+ </div>
464
+ <ChevronDown size={12} className={`text-[var(--color-text-muted,#6b7280)] transition-transform ${agentSettingsOpen ? 'rotate-180' : ''}`} />
465
+ </button>
466
+ {agentSettingsOpen && (
467
+ <div className="px-4 pb-4 space-y-3 border-t border-[var(--color-border,#d4d4d8)]">
468
+ {policyLoadError && (
469
+ <div className="space-y-2 pt-3">
470
+ <div className="text-[10px] text-[var(--color-danger,#ef4444)] border border-[var(--color-danger,#ef4444)]/30 bg-[var(--color-danger,#ef4444)]/10 px-3 py-2">
471
+ {policyLoadError}
472
+ </div>
473
+ <button
474
+ type="button"
475
+ onClick={() => void loadLocalPolicySettings()}
476
+ disabled={policyLoading}
477
+ className="h-9 px-3 border border-[var(--color-border,#d4d4d8)] font-mono text-[10px] tracking-widest"
478
+ >
479
+ {policyLoading ? 'RETRYING...' : 'RETRY LOAD'}
480
+ </button>
481
+ </div>
482
+ )}
483
+
484
+ {!policyLoadError && (
485
+ <>
486
+ <label className="pt-3 flex items-center justify-between text-[10px] font-mono text-[var(--color-text,#0a0a0a)]">
487
+ <span>AUTO-APPROVE LOCAL REQUESTS</span>
488
+ <input
489
+ type="checkbox"
490
+ checked={policyForm.autoApprove}
491
+ onChange={(e) => setPolicyForm((prev) => ({ ...prev, autoApprove: e.target.checked }))}
492
+ disabled={policyLoading || policySaving}
493
+ />
494
+ </label>
495
+
496
+ <div>
497
+ <label className="block text-[9px] text-[var(--color-text-faint,#9ca3af)] tracking-widest mb-2">LOCAL PROFILE</label>
498
+ <select
499
+ aria-label="LOCAL PROFILE"
500
+ value={policyForm.profile}
501
+ onChange={(e) => setPolicyForm((prev) => ({ ...prev, profile: e.target.value as LocalAgentMode }))}
502
+ disabled={policyLoading || policySaving}
503
+ className="w-full h-10 px-2 border border-[var(--color-border,#d4d4d8)] bg-[var(--color-surface,#fff)] text-[11px] font-mono"
504
+ >
505
+ <option value="strict">strict</option>
506
+ <option value="dev">dev</option>
507
+ <option value="admin">admin (dangerous)</option>
508
+ </select>
509
+ </div>
510
+
511
+ {dangerConfirmOpen && (
512
+ <div className="text-[10px] text-[var(--color-danger,#ef4444)] border border-[var(--color-danger,#ef4444)]/30 bg-[var(--color-danger,#ef4444)]/10 px-3 py-2 space-y-2">
513
+ <div>Enabling admin mode is dangerous and broadens token scope.</div>
514
+ <div className="flex gap-2">
515
+ <button
516
+ type="button"
517
+ onClick={() => {
518
+ setDangerConfirmOpen(false);
519
+ if (policySettings) setPolicyForm(policySettings);
520
+ }}
521
+ className="h-8 px-3 border border-[var(--color-border,#d4d4d8)] text-[10px] font-mono"
522
+ >
523
+ CANCEL
524
+ </button>
525
+ <button
526
+ type="button"
527
+ onClick={() => { void handleSaveLocalPolicy(); }}
528
+ disabled={policySaving}
529
+ className="h-8 px-3 bg-[var(--color-danger,#ef4444)] text-white text-[10px] font-mono"
530
+ >
531
+ CONFIRM ADMIN MODE
532
+ </button>
533
+ </div>
534
+ </div>
535
+ )}
536
+
537
+ {policySaveError && <div className="text-[10px] text-[var(--color-danger,#ef4444)]">{policySaveError}</div>}
538
+ {policySaveSuccess && <div className="text-[10px] text-[var(--color-info,#0047ff)]">{policySaveSuccess}</div>}
539
+
540
+ <button
541
+ type="button"
542
+ onClick={() => { void handleSaveLocalPolicy(); }}
543
+ disabled={Boolean(policyLoadError) || policyLoading || policySaving || !policySettings}
544
+ className="w-full h-10 bg-[var(--color-text,#0a0a0a)] text-white text-[10px] font-mono tracking-widest disabled:opacity-40"
545
+ >
546
+ {policySaving ? 'SAVING...' : 'SAVE LOCAL POLICY'}
547
+ </button>
548
+ </>
549
+ )}
550
+ </div>
551
+ )}
552
+ </div>
553
+
554
+ <div className="bg-[var(--color-surface,#fff)] border border-[var(--color-border,#d4d4d8)]">
555
+ <button
556
+ type="button"
557
+ onClick={() => setSecuritySettingsOpen((open) => !open)}
558
+ className="w-full p-4 flex items-center justify-between hover:bg-[var(--color-surface-alt,#fafafa)] transition-colors"
559
+ >
560
+ <div className="flex items-center gap-2">
561
+ <KeyRound size={12} className="text-[var(--color-text-muted,#6b7280)]" />
562
+ <span className="font-mono text-[10px] tracking-widest text-[var(--color-text-muted,#6b7280)]">PRIMARY_PASSWORD</span>
563
+ </div>
564
+ <ChevronDown size={12} className={`text-[var(--color-text-muted,#6b7280)] transition-transform ${securitySettingsOpen ? 'rotate-180' : ''}`} />
565
+ </button>
566
+ {securitySettingsOpen && (
567
+ <div className="px-4 pb-4 pt-3 space-y-3 border-t border-[var(--color-border,#d4d4d8)]">
568
+ <div className="text-[9px] text-[var(--color-text-muted,#6b7280)] leading-relaxed">
569
+ Rotate your primary vault password. This updates the vault wrapper encryption and keeps existing credentials intact.
570
+ </div>
571
+ {passwordChangeError && (
572
+ <div className="text-[10px] text-[var(--color-danger,#ef4444)] border border-[var(--color-danger,#ef4444)]/30 bg-[var(--color-danger,#ef4444)]/10 px-3 py-2">
573
+ {passwordChangeError}
574
+ </div>
575
+ )}
576
+ <button
577
+ type="button"
578
+ onClick={() => {
579
+ setPasswordChangeError(null);
580
+ setShowPasswordModal(true);
581
+ }}
582
+ className="w-full h-10 border border-[var(--color-border,#d4d4d8)] bg-[var(--color-surface,#fff)] text-[10px] font-mono tracking-widest hover:border-[var(--color-text,#0a0a0a)]"
583
+ >
584
+ CHANGE PRIMARY PASSWORD
585
+ </button>
586
+ </div>
587
+ )}
588
+ </div>
589
+ </div>
590
+ </Drawer>
591
+
592
+ <Modal
593
+ isOpen={showPasswordModal}
594
+ onClose={closePasswordModal}
595
+ title="Change Primary Password"
596
+ subtitle="Security"
597
+ size="sm"
598
+ >
599
+ <form onSubmit={handleChangePrimaryPassword} className="space-y-3">
600
+ <div>
601
+ <label className="block text-[9px] text-[var(--color-text-faint,#9ca3af)] tracking-widest mb-2">CURRENT PASSWORD</label>
602
+ <input
603
+ type="password"
604
+ aria-label="CURRENT PASSWORD"
605
+ value={currentPasswordValue}
606
+ onChange={(e) => setCurrentPasswordValue(e.target.value)}
607
+ autoFocus
608
+ className="w-full h-10 px-3 border border-[var(--color-border,#d4d4d8)] bg-[var(--color-surface,#fff)] text-[11px] font-mono"
609
+ />
610
+ </div>
611
+ <div>
612
+ <label className="block text-[9px] text-[var(--color-text-faint,#9ca3af)] tracking-widest mb-2">NEW PASSWORD</label>
613
+ <input
614
+ type="password"
615
+ aria-label="NEW PASSWORD"
616
+ value={newPasswordValue}
617
+ onChange={(e) => setNewPasswordValue(e.target.value)}
618
+ className="w-full h-10 px-3 border border-[var(--color-border,#d4d4d8)] bg-[var(--color-surface,#fff)] text-[11px] font-mono"
619
+ />
620
+ </div>
621
+ <div>
622
+ <label className="block text-[9px] text-[var(--color-text-faint,#9ca3af)] tracking-widest mb-2">CONFIRM NEW PASSWORD</label>
623
+ <input
624
+ type="password"
625
+ aria-label="CONFIRM NEW PASSWORD"
626
+ value={confirmPasswordValue}
627
+ onChange={(e) => setConfirmPasswordValue(e.target.value)}
628
+ className="w-full h-10 px-3 border border-[var(--color-border,#d4d4d8)] bg-[var(--color-surface,#fff)] text-[11px] font-mono"
629
+ />
630
+ </div>
631
+ {passwordChangeError && <div className="text-[10px] text-[var(--color-danger,#ef4444)]">{passwordChangeError}</div>}
632
+ <div className="flex gap-2 pt-2">
633
+ <button
634
+ type="button"
635
+ onClick={closePasswordModal}
636
+ disabled={passwordChanging}
637
+ className="flex-1 h-10 border border-[var(--color-border,#d4d4d8)] text-[10px] font-mono tracking-widest"
638
+ >
639
+ CANCEL
640
+ </button>
641
+ <button
642
+ type="submit"
643
+ disabled={passwordChanging || !currentPasswordValue || !newPasswordValue || !confirmPasswordValue}
644
+ className="flex-1 h-10 bg-[var(--color-text,#0a0a0a)] text-white text-[10px] font-mono tracking-widest disabled:opacity-40"
645
+ >
646
+ {passwordChanging ? 'UPDATING...' : 'UPDATE PASSWORD'}
647
+ </button>
648
+ </div>
649
+ </form>
650
+ </Modal>
651
+ </div>
652
+ );
653
+ }
654
+
655
+ return (
656
+ <div className="min-h-screen bg-[var(--color-background,#f4f4f5)] relative flex items-center justify-center p-4">
657
+ {/* Background — sterile field (same as docs/api) */}
658
+ <div className="fixed inset-0 pointer-events-none z-0 overflow-hidden">
659
+ <div className="absolute inset-0 bg-grid-adaptive bg-[size:4rem_4rem] opacity-30" />
660
+ <div className="absolute inset-0 tyvek-texture opacity-40 mix-blend-multiply" />
661
+
662
+ {/* Giant background typography */}
663
+ <div className="absolute top-[5%] left-[5%] opacity-5 select-none">
664
+ <h1 className="text-[15vw] font-bold leading-none text-[var(--color-text,#0a0a0a)] font-mono tracking-tighter">
665
+ AURA
666
+ </h1>
667
+ </div>
668
+ <div className="absolute bottom-[5%] right-[5%] opacity-5 select-none">
669
+ <h1 className="text-[15vw] font-bold leading-none text-[var(--color-text,#0a0a0a)] font-mono tracking-tighter text-right">
670
+ WALLET
671
+ </h1>
672
+ </div>
673
+
674
+ {/* Corner finder patterns */}
675
+ <div className="absolute top-10 left-10 w-32 h-32 border-l-4 border-t-4 border-[var(--color-text,#0a0a0a)] opacity-10">
676
+ <div className="absolute top-2 left-2 w-4 h-4 bg-[var(--color-text,#0a0a0a)]" />
677
+ </div>
678
+ <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">
679
+ <div className="absolute bottom-2 right-2 w-4 h-4 bg-[var(--color-text,#0a0a0a)]" />
680
+ </div>
681
+ </div>
682
+
683
+ {/* Logo header */}
684
+ <div className="fixed top-6 left-6 z-50 flex items-center gap-3">
685
+ <div className="w-10 h-10">
686
+ <img src="/logo.webp" alt="AuraWallet" className="w-full h-full object-contain" />
687
+ </div>
688
+ <div className="font-black text-xl tracking-tighter text-[var(--color-text,#0a0a0a)]">AURAWALLET</div>
689
+ </div>
690
+
691
+ {/* Nav */}
692
+ <div className="fixed top-7 right-6 z-50 flex items-center gap-3 font-mono text-[10px] tracking-widest">
693
+ <Link href="/docs" className="text-[var(--color-text-muted,#6b7280)] hover:text-[var(--color-text,#0a0a0a)] transition-colors">DOCS</Link>
694
+ <Link href="/api" className="text-[var(--color-text-muted,#6b7280)] hover:text-[var(--color-text,#0a0a0a)] transition-colors">API</Link>
695
+ <Link href="/app" className="text-[var(--color-text-muted,#6b7280)] hover:text-[var(--color-text,#0a0a0a)] transition-colors">APP</Link>
696
+ <DocsThemeToggle />
697
+ </div>
698
+
699
+ {/* Unlock card */}
700
+ <div className="relative z-10 w-full max-w-[380px]">
701
+ <div className="bg-[var(--color-surface,#f4f4f2)] border border-[var(--color-border,#d4d4d8)] shadow-lg overflow-hidden font-mono">
702
+ {/* Card header bar */}
703
+ <div className="px-5 py-3 border-b border-[var(--color-border,#d4d4d8)] bg-[var(--color-surface-alt,#fafafa)] flex items-center justify-between">
704
+ <span className="font-sans font-bold text-sm text-[var(--color-text,#0a0a0a)] uppercase tracking-tight">
705
+ {pageState === 'setup' ? 'Initialize' : 'Unlock'}
706
+ </span>
707
+ <span className="text-[9px] text-[var(--color-text-faint,#9ca3af)] font-bold tracking-widest">
708
+ {pageState === 'loading' ? 'CONNECTING...' : pageState === 'setup' ? 'NO_VAULT' : 'LOCKED'}
709
+ </span>
710
+ </div>
711
+
712
+ <div className="p-6">
713
+ {pageState === 'loading' && (
714
+ <div className="flex flex-col items-center py-12">
715
+ <div className="w-6 h-6 border-2 border-[var(--color-border,#d4d4d8)] border-t-[var(--color-text,#0a0a0a)] animate-spin" />
716
+ <div className="mt-4 text-[10px] text-[var(--color-text-muted,#6b7280)] tracking-widest">CONNECTING</div>
717
+ </div>
718
+ )}
719
+
720
+ {pageState === 'setup' && mnemonic && setupOnboardingStep === 'seed' && (
721
+ <div className="flex flex-col items-center">
722
+ <div className="w-16 h-16 mb-4">
723
+ <img src="/logo.webp" alt="AuraWallet" className="w-full h-full object-contain" />
724
+ </div>
725
+ <div className="text-[10px] text-[var(--color-text-muted,#6b7280)] tracking-widest text-center mb-4">
726
+ SAVE YOUR RECOVERY PHRASE
727
+ </div>
728
+ <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">
729
+ Write this down and store it securely. You will stay on this screen until you explicitly confirm.
730
+ </div>
731
+ {seedRecoveryNotice && (
732
+ <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">
733
+ {seedRecoveryNotice}
734
+ </div>
735
+ )}
736
+ <div className="grid grid-cols-3 gap-2 w-full mb-4">
737
+ {mnemonic.split(' ').map((word, i) => (
738
+ <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)]">
739
+ <span className="text-[var(--color-text-faint,#9ca3af)] mr-1">{i + 1}.</span>{word}
740
+ </div>
741
+ ))}
742
+ </div>
743
+ <label className="flex items-start gap-2 w-full mb-3 cursor-pointer">
744
+ <input
745
+ type="checkbox"
746
+ checked={seedAcknowledged}
747
+ onChange={(e) => setSeedAcknowledged(e.target.checked)}
748
+ className="mt-0.5"
749
+ />
750
+ <span className="text-[9px] text-[var(--color-text-muted,#6b7280)]">
751
+ I have written and verified this recovery phrase in a secure location.
752
+ </span>
753
+ </label>
754
+ <button
755
+ onClick={() => {
756
+ if (!seedAcknowledged) return;
757
+ setSetupOnboardingStep('trust');
758
+ }}
759
+ disabled={!seedAcknowledged}
760
+ 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"
761
+ >
762
+ CONTINUE TO AGENT MODE
763
+ </button>
764
+ <div className="w-full mt-3 text-[8px] text-[var(--color-text-faint,#9ca3af)] text-center">
765
+ If you leave before confirming, this phrase is recoverable only temporarily in this tab session. If recovery expires, restart onboarding to regenerate.
766
+ </div>
767
+ </div>
768
+ )}
769
+
770
+ {pageState === 'setup' && mnemonic && setupOnboardingStep === 'trust' && (
771
+ <>
772
+ <div className="flex flex-col items-center mb-6">
773
+ <div className="w-16 h-16 mb-4">
774
+ <img src="/logo.webp" alt="AuraWallet" className="w-full h-full object-contain" />
775
+ </div>
776
+ <div className="text-[10px] text-[var(--color-text-muted,#6b7280)] tracking-widest text-center">
777
+ LOCAL AGENT MODE
778
+ </div>
779
+ </div>
780
+
781
+ <div className="space-y-4">
782
+ <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)]">
783
+ Choose how local Unix-socket agents are issued default permissions. You can change this later in settings.
784
+ </div>
785
+
786
+ <fieldset className="border border-[var(--color-border,#d4d4d8)] p-2.5 bg-[var(--color-background,#f4f4f5)]">
787
+ <legend className="text-[8px] text-[var(--color-text-faint,#9ca3af)] tracking-widest uppercase px-1">
788
+ Local Agent Mode
789
+ </legend>
790
+ <div className="space-y-2">
791
+ <label className="flex items-start gap-2 cursor-pointer">
792
+ <input
793
+ type="radio"
794
+ name="local-agent-mode"
795
+ checked={localAgentMode === 'dev'}
796
+ onChange={() => setLocalAgentMode('dev')}
797
+ className="mt-0.5"
798
+ />
799
+ <span className="text-[9px] text-[var(--color-text-muted,#6b7280)] leading-relaxed">
800
+ Dev (recommended): auto-approve enabled with scoped non-financial profile.
801
+ </span>
802
+ </label>
803
+ <label className="flex items-start gap-2 cursor-pointer">
804
+ <input
805
+ type="radio"
806
+ name="local-agent-mode"
807
+ checked={localAgentMode === 'strict'}
808
+ onChange={() => setLocalAgentMode('strict')}
809
+ className="mt-0.5"
810
+ />
811
+ <span className="text-[9px] text-[var(--color-text-muted,#6b7280)] leading-relaxed">
812
+ Strict: disable local auto-approve. Every local agent token request needs manual approval.
813
+ </span>
814
+ </label>
815
+ <label className="flex items-start gap-2 cursor-pointer">
816
+ <input
817
+ type="radio"
818
+ name="local-agent-mode"
819
+ checked={localAgentMode === 'admin'}
820
+ onChange={() => setLocalAgentMode('admin')}
821
+ className="mt-0.5"
822
+ />
823
+ <span className="text-[9px] text-[var(--color-danger,#ef4444)] leading-relaxed">
824
+ Admin (dangerous): broad local agent access. Not recommended for primary vault workflows.
825
+ </span>
826
+ </label>
827
+ </div>
828
+ </fieldset>
829
+
830
+ {error && (
831
+ <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">
832
+ {error}
833
+ </div>
834
+ )}
835
+
836
+ <button
837
+ onClick={() => { void handleFinalizeOnboarding(); }}
838
+ disabled={loading}
839
+ 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"
840
+ >
841
+ {loading ? (
842
+ <>
843
+ <div className="w-3 h-3 border border-[var(--color-surface,#ffffff)] border-t-transparent animate-spin" />
844
+ SAVING...
845
+ </>
846
+ ) : (
847
+ 'SAVE MODE AND CONTINUE'
848
+ )}
849
+ </button>
850
+ </div>
851
+ </>
852
+ )}
853
+
854
+ {pageState === 'setup' && !mnemonic && (
855
+ <>
856
+ {/* Logo centered */}
857
+ <div className="flex flex-col items-center mb-6">
858
+ <div className="w-16 h-16 mb-4">
859
+ <img src="/logo.webp" alt="AuraWallet" className="w-full h-full object-contain" />
860
+ </div>
861
+ <div className="text-[10px] text-[var(--color-text-muted,#6b7280)] tracking-widest text-center">
862
+ CREATE YOUR ENCRYPTED VAULT
863
+ </div>
864
+ </div>
865
+
866
+ <form onSubmit={handleSetup} className="space-y-4">
867
+ <div>
868
+ <label className="block text-[8px] text-[var(--color-text-faint,#9ca3af)] tracking-widest mb-1.5 uppercase">
869
+ Encryption Password
870
+ </label>
871
+ <input
872
+ type="password"
873
+ value={password}
874
+ onChange={(e) => { setPassword(e.target.value); setError(null); }}
875
+ placeholder="Minimum 8 characters"
876
+ 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"
877
+ autoFocus
878
+ />
879
+ </div>
880
+
881
+ {error && (
882
+ <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">
883
+ {error}
884
+ </div>
885
+ )}
886
+
887
+ <button
888
+ type="submit"
889
+ disabled={loading || password.length < 8}
890
+ 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"
891
+ >
892
+ {loading ? (
893
+ <>
894
+ <div className="w-3 h-3 border border-[var(--color-surface,#ffffff)] border-t-transparent animate-spin" />
895
+ INITIALIZING...
896
+ </>
897
+ ) : (
898
+ 'INITIALIZE VAULT'
899
+ )}
900
+ </button>
901
+ </form>
902
+
903
+ <div className="mt-4 pt-4 border-t border-[var(--color-border,#d4d4d8)]">
904
+ <div className="flex items-start gap-2">
905
+ <div className="w-1 h-1 bg-[var(--color-text-muted,#6b7280)] mt-1.5 flex-shrink-0" />
906
+ <span className="text-[8px] text-[var(--color-text-faint,#9ca3af)] leading-relaxed">
907
+ This password encrypts your seed phrase locally. It never leaves your machine.
908
+ </span>
909
+ </div>
910
+ </div>
911
+ </>
912
+ )}
913
+
914
+ {pageState === 'locked' && (
915
+ <>
916
+ {/* Logo centered */}
917
+ <div className="flex flex-col items-center mb-6">
918
+ <div className="w-16 h-16 mb-4">
919
+ <img src="/logo.webp" alt="AuraWallet" className="w-full h-full object-contain" />
920
+ </div>
921
+ <div className="text-[10px] text-[var(--color-text-muted,#6b7280)] tracking-widest text-center">
922
+ ENTER PASSWORD TO UNLOCK
923
+ </div>
924
+ </div>
925
+
926
+ <form onSubmit={handleUnlock} className="space-y-4">
927
+ <div>
928
+ <label className="block text-[8px] text-[var(--color-text-faint,#9ca3af)] tracking-widest mb-1.5 uppercase">
929
+ Password
930
+ </label>
931
+ <input
932
+ type="password"
933
+ value={password}
934
+ onChange={(e) => { setPassword(e.target.value); setError(null); }}
935
+ placeholder="Enter vault password"
936
+ 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"
937
+ autoFocus
938
+ />
939
+ </div>
940
+
941
+ {error && (
942
+ <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">
943
+ {error}
944
+ </div>
945
+ )}
946
+
947
+ <button
948
+ type="submit"
949
+ disabled={loading || !password}
950
+ 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"
951
+ >
952
+ {loading ? (
953
+ <>
954
+ <div className="w-3 h-3 border border-[var(--color-surface,#ffffff)] border-t-transparent animate-spin" />
955
+ UNLOCKING...
956
+ </>
957
+ ) : (
958
+ 'UNLOCK'
959
+ )}
960
+ </button>
961
+ </form>
962
+ </>
963
+ )}
964
+ </div>
965
+
966
+ {/* Barcode + stripe */}
967
+ <div className="flex items-center gap-3 px-5 py-2 border-t border-[var(--color-border,#d4d4d8)]">
968
+ <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" />
969
+ <span className="text-[8px] text-[var(--color-text-faint,#9ca3af)] tracking-wider">AURAWALLET</span>
970
+ </div>
971
+ <div className="h-2 w-full" style={{
972
+ backgroundImage: 'repeating-linear-gradient(45deg, var(--color-text, #000), var(--color-text, #000) 5px, transparent 5px, transparent 10px)',
973
+ opacity: 0.1,
974
+ }} />
975
+ </div>
976
+
977
+ {/* Specimen label below card */}
978
+ <div className="mt-4 text-center">
979
+ <span className="text-[8px] text-[var(--color-text-faint,#9ca3af)] tracking-[0.2em] font-mono">
980
+ SECURE LOCAL WALLETS FOR AI AGENTS
981
+ </span>
982
+ </div>
983
+ </div>
984
+ </div>
985
+ );
986
+ }