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,1048 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { ethers } from 'ethers';
4
+ import { Keypair, Transaction, VersionedTransaction } from '@solana/web3.js';
5
+ import { WalletInfo, EncryptedData } from '../types';
6
+ import { encryptPrivateKey, decryptPrivateKey } from './encrypt';
7
+ import { DATA_PATHS } from './config';
8
+ import { deriveSolanaColdKeypair } from './solana/wallet';
9
+ import { unlockCredentialVault, lockCredentialVault, lockAllCredentialVaults } from './credential-vault';
10
+ import { ensureDontLookForVault, ensureGettingStartedForVault, ensureOurSecretForVault } from './oursecret';
11
+
12
+ // Cold wallet derivation path: m/44'/60'/0'/0/0
13
+ const COLD_PATH = "m/44'/60'/0'/0/0";
14
+ const COLD_FILE = 'cold.json';
15
+ const VAULT_PREFIX = 'vault-';
16
+ const PRIMARY_VAULT_ID = 'primary';
17
+ export const AGENT_VAULT_NAME = 'agent';
18
+ export const DAILY_LOGS_VAULT_NAME = 'daily-logs';
19
+
20
+ export type VaultMode = 'primary' | 'linked' | 'independent';
21
+
22
+ // ---------------------------------------------------------------------------
23
+ // Vault session stored in memory while a vault is unlocked
24
+ // ---------------------------------------------------------------------------
25
+ interface VaultSession {
26
+ id: string;
27
+ mnemonic: string;
28
+ address: string; // EVM
29
+ solanaKeypair: Keypair; // Solana
30
+ }
31
+
32
+ // In-memory storage - survives as long as process runs
33
+ const vaultSessions = new Map<string, VaultSession>();
34
+ let primaryVaultId: string | null = null;
35
+ let primaryVaultPassword: string | null = null; // stored while primary is unlocked
36
+
37
+ // ---------------------------------------------------------------------------
38
+ // Vault file on disk
39
+ // ---------------------------------------------------------------------------
40
+ interface VaultFile {
41
+ address: string;
42
+ solanaAddress?: string;
43
+ encrypted: EncryptedData;
44
+ createdAt: string;
45
+ name?: string;
46
+ mode?: VaultMode;
47
+ linkedTo?: string;
48
+ }
49
+
50
+ // ---------------------------------------------------------------------------
51
+ // Public vault info (safe to expose)
52
+ // ---------------------------------------------------------------------------
53
+ export interface VaultInfo {
54
+ id: string;
55
+ name?: string;
56
+ address: string;
57
+ solanaAddress?: string;
58
+ mode: VaultMode;
59
+ parentVaultId?: string;
60
+ linkedTo?: string;
61
+ isUnlocked: boolean;
62
+ isPrimary: boolean;
63
+ createdAt: string;
64
+ }
65
+
66
+ export interface CreateVaultResult {
67
+ id: string;
68
+ address: string;
69
+ solanaAddress: string;
70
+ mnemonic: string;
71
+ name?: string;
72
+ mode: VaultMode;
73
+ parentVaultId?: string;
74
+ linkedTo?: string;
75
+ }
76
+
77
+ export interface CreateVaultOptions {
78
+ mode?: Exclude<VaultMode, 'primary'>;
79
+ parentVaultId?: string;
80
+ linkedTo?: string;
81
+ seedOnboardingSecret?: boolean;
82
+ }
83
+
84
+ // ---------------------------------------------------------------------------
85
+ // File path helpers
86
+ // ---------------------------------------------------------------------------
87
+ function getVaultFilePath(id: string): string {
88
+ return path.join(DATA_PATHS.wallets, `${VAULT_PREFIX}${id}.json`);
89
+ }
90
+
91
+ function getLegacyColdFilePath(): string {
92
+ return path.join(DATA_PATHS.wallets, COLD_FILE);
93
+ }
94
+
95
+ /**
96
+ * Auto-migrate cold.json → vault-primary.json if needed.
97
+ * Called on first access that needs to list vaults.
98
+ */
99
+ let migrationDone = false;
100
+ function ensureMigration(): void {
101
+ if (migrationDone) return;
102
+ migrationDone = true;
103
+
104
+ const legacyPath = getLegacyColdFilePath();
105
+ const primaryPath = getVaultFilePath(PRIMARY_VAULT_ID);
106
+
107
+ if (fs.existsSync(legacyPath) && !fs.existsSync(primaryPath)) {
108
+ // Migrate: copy cold.json content into vault-primary.json
109
+ const raw = fs.readFileSync(legacyPath, 'utf-8');
110
+ fs.writeFileSync(primaryPath, raw);
111
+ // Remove old file
112
+ fs.unlinkSync(legacyPath);
113
+ }
114
+
115
+ // If primary vault file exists, ensure primaryVaultId is set
116
+ if (fs.existsSync(primaryPath) && !primaryVaultId) {
117
+ primaryVaultId = PRIMARY_VAULT_ID;
118
+ }
119
+ }
120
+
121
+ function isVaultMode(value: unknown): value is VaultMode {
122
+ return value === 'primary' || value === 'linked' || value === 'independent';
123
+ }
124
+
125
+ function readVaultFile(id: string): VaultFile {
126
+ const filePath = getVaultFilePath(id);
127
+ return JSON.parse(fs.readFileSync(filePath, 'utf-8')) as VaultFile;
128
+ }
129
+
130
+ function writeVaultFile(id: string, data: VaultFile): void {
131
+ const filePath = getVaultFilePath(id);
132
+ fs.writeFileSync(filePath, JSON.stringify(data, null, 2));
133
+ }
134
+
135
+ function resolveVaultMode(id: string, data: VaultFile): VaultMode {
136
+ if (id === PRIMARY_VAULT_ID || id === primaryVaultId) return 'primary';
137
+ if (isVaultMode(data.mode) && data.mode !== 'primary') return data.mode;
138
+ return 'independent';
139
+ }
140
+
141
+ function resolveParentVaultId(id: string, data: VaultFile, mode: VaultMode): string | undefined {
142
+ if (mode !== 'linked') return undefined;
143
+ if (typeof data.linkedTo === 'string' && data.linkedTo.trim()) return data.linkedTo.trim();
144
+ // Default linked target is primary when omitted.
145
+ return primaryVaultId || PRIMARY_VAULT_ID;
146
+ }
147
+
148
+ // ---------------------------------------------------------------------------
149
+ // Vault CRUD
150
+ // ---------------------------------------------------------------------------
151
+
152
+ /**
153
+ * List all vault files on disk. Returns VaultInfo for each.
154
+ */
155
+ export function listVaults(): VaultInfo[] {
156
+ ensureMigration();
157
+
158
+ const walletsDir = DATA_PATHS.wallets;
159
+ if (!fs.existsSync(walletsDir)) return [];
160
+
161
+ const files = fs.readdirSync(walletsDir);
162
+ const vaults: VaultInfo[] = [];
163
+
164
+ for (const file of files) {
165
+ if (!file.startsWith(VAULT_PREFIX) || !file.endsWith('.json')) continue;
166
+ const id = file.slice(VAULT_PREFIX.length, -5); // strip prefix and .json
167
+
168
+ try {
169
+ const raw = fs.readFileSync(path.join(walletsDir, file), 'utf-8');
170
+ const data: VaultFile = JSON.parse(raw);
171
+ const mode = resolveVaultMode(id, data);
172
+ const parentVaultId = resolveParentVaultId(id, data, mode);
173
+ vaults.push({
174
+ id,
175
+ name: data.name,
176
+ address: data.address,
177
+ solanaAddress: data.solanaAddress,
178
+ mode,
179
+ parentVaultId,
180
+ linkedTo: parentVaultId,
181
+ isUnlocked: vaultSessions.has(id),
182
+ isPrimary: id === primaryVaultId || id === PRIMARY_VAULT_ID,
183
+ createdAt: data.createdAt,
184
+ });
185
+ } catch {
186
+ // skip corrupt files
187
+ }
188
+ }
189
+
190
+ // Sort: primary first, linked next, then independent by createdAt
191
+ vaults.sort((a, b) => {
192
+ if (a.isPrimary) return -1;
193
+ if (b.isPrimary) return 1;
194
+ if (a.mode === 'linked' && b.mode !== 'linked') return -1;
195
+ if (b.mode === 'linked' && a.mode !== 'linked') return 1;
196
+ return new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime();
197
+ });
198
+
199
+ return vaults;
200
+ }
201
+
202
+ /**
203
+ * Create a new vault with a fresh mnemonic.
204
+ */
205
+ export function createVault(password: string, name?: string, options: CreateVaultOptions = {}): CreateVaultResult {
206
+ ensureMigration();
207
+
208
+ const id = generateVaultId();
209
+ const filePath = getVaultFilePath(id);
210
+
211
+ if (fs.existsSync(filePath)) {
212
+ throw new Error(`Vault file already exists: ${id}`);
213
+ }
214
+
215
+ const mnemonic = ethers.Mnemonic.entropyToPhrase(ethers.randomBytes(16));
216
+ const hdNode = ethers.HDNodeWallet.fromPhrase(mnemonic, undefined, COLD_PATH);
217
+ const solanaKeypair = deriveSolanaColdKeypair(mnemonic);
218
+ const solanaAddress = solanaKeypair.publicKey.toBase58();
219
+
220
+ const mode: Exclude<VaultMode, 'primary'> = options.mode || 'independent';
221
+ const parentVaultId = mode === 'linked'
222
+ ? (options.parentVaultId || options.linkedTo || primaryVaultId || PRIMARY_VAULT_ID)
223
+ : undefined;
224
+ if (mode === 'linked') {
225
+ if (!parentVaultId) throw new Error('linked vault requires a parent vault target');
226
+ if (!fs.existsSync(getVaultFilePath(parentVaultId))) {
227
+ throw new Error(`parent vault not found: ${parentVaultId}`);
228
+ }
229
+ }
230
+
231
+ const encrypted = encryptPrivateKey(mnemonic, password);
232
+
233
+ const vaultFile: VaultFile = {
234
+ address: hdNode.address,
235
+ solanaAddress,
236
+ encrypted,
237
+ createdAt: new Date().toISOString(),
238
+ name,
239
+ mode,
240
+ linkedTo: parentVaultId,
241
+ };
242
+
243
+ fs.writeFileSync(filePath, JSON.stringify(vaultFile, null, 2));
244
+
245
+ // Auto-unlock
246
+ vaultSessions.set(id, {
247
+ id,
248
+ mnemonic,
249
+ address: hdNode.address,
250
+ solanaKeypair,
251
+ });
252
+ unlockCredentialVault(id);
253
+
254
+ if (options.seedOnboardingSecret !== false) {
255
+ try {
256
+ ensureOurSecretForVault(id);
257
+ } catch (err) {
258
+ console.warn('[cold] Failed to seed OURSECRET onboarding note:', err);
259
+ }
260
+ }
261
+
262
+ return {
263
+ id,
264
+ address: hdNode.address,
265
+ solanaAddress,
266
+ mnemonic,
267
+ name,
268
+ mode,
269
+ parentVaultId,
270
+ linkedTo: parentVaultId,
271
+ };
272
+ }
273
+
274
+ /**
275
+ * Import a vault from an existing mnemonic.
276
+ */
277
+ export function importVault(mnemonic: string, password: string, name?: string, options: CreateVaultOptions = {}): VaultInfo {
278
+ ensureMigration();
279
+
280
+ // Normalize
281
+ const normalizedMnemonic = mnemonic.trim().toLowerCase().split(/\s+/).join(' ');
282
+ if (!ethers.Mnemonic.isValidMnemonic(normalizedMnemonic)) {
283
+ throw new Error('Invalid seed phrase');
284
+ }
285
+
286
+ const id = generateVaultId();
287
+ const filePath = getVaultFilePath(id);
288
+
289
+ const hdNode = ethers.HDNodeWallet.fromPhrase(normalizedMnemonic, undefined, COLD_PATH);
290
+ const solanaKeypair = deriveSolanaColdKeypair(normalizedMnemonic);
291
+ const solanaAddress = solanaKeypair.publicKey.toBase58();
292
+
293
+ const mode: Exclude<VaultMode, 'primary'> = options.mode || 'independent';
294
+ const parentVaultId = mode === 'linked'
295
+ ? (options.parentVaultId || options.linkedTo || primaryVaultId || PRIMARY_VAULT_ID)
296
+ : undefined;
297
+ if (mode === 'linked') {
298
+ if (!parentVaultId) throw new Error('linked vault requires a parent vault target');
299
+ if (!fs.existsSync(getVaultFilePath(parentVaultId))) {
300
+ throw new Error(`parent vault not found: ${parentVaultId}`);
301
+ }
302
+ }
303
+
304
+ const encrypted = encryptPrivateKey(normalizedMnemonic, password);
305
+
306
+ const vaultFile: VaultFile = {
307
+ address: hdNode.address,
308
+ solanaAddress,
309
+ encrypted,
310
+ createdAt: new Date().toISOString(),
311
+ name,
312
+ mode,
313
+ linkedTo: parentVaultId,
314
+ };
315
+
316
+ fs.writeFileSync(filePath, JSON.stringify(vaultFile, null, 2));
317
+
318
+ // Auto-unlock
319
+ vaultSessions.set(id, {
320
+ id,
321
+ mnemonic: normalizedMnemonic,
322
+ address: hdNode.address,
323
+ solanaKeypair,
324
+ });
325
+ unlockCredentialVault(id);
326
+
327
+ if (options.seedOnboardingSecret !== false) {
328
+ try {
329
+ ensureOurSecretForVault(id);
330
+ } catch (err) {
331
+ console.warn('[cold] Failed to seed OURSECRET onboarding note:', err);
332
+ }
333
+ }
334
+
335
+ return {
336
+ id,
337
+ name,
338
+ address: hdNode.address,
339
+ solanaAddress,
340
+ mode,
341
+ parentVaultId,
342
+ linkedTo: parentVaultId,
343
+ isUnlocked: true,
344
+ isPrimary: false,
345
+ createdAt: vaultFile.createdAt,
346
+ };
347
+ }
348
+
349
+ /**
350
+ * Unlock a specific vault.
351
+ */
352
+ export function unlockVault(id: string, password: string): boolean {
353
+ ensureMigration();
354
+
355
+ const filePath = getVaultFilePath(id);
356
+ if (!fs.existsSync(filePath)) {
357
+ throw new Error(`Vault not found: ${id}`);
358
+ }
359
+
360
+ const data: VaultFile = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
361
+
362
+ try {
363
+ const mnemonic = decryptPrivateKey(data.encrypted, password);
364
+ const solanaKeypair = deriveSolanaColdKeypair(mnemonic);
365
+
366
+ vaultSessions.set(id, {
367
+ id,
368
+ mnemonic,
369
+ address: data.address,
370
+ solanaKeypair,
371
+ });
372
+ unlockCredentialVault(id);
373
+
374
+ // Cache primary password while unlocked so linked vaults can auto-unlock.
375
+ if (id === primaryVaultId || id === PRIMARY_VAULT_ID) {
376
+ primaryVaultPassword = password;
377
+ }
378
+
379
+ return true;
380
+ } catch {
381
+ return false;
382
+ }
383
+ }
384
+
385
+ /**
386
+ * Lock a specific vault.
387
+ */
388
+ export function lockVault(id: string): void {
389
+ vaultSessions.delete(id);
390
+ lockCredentialVault(id);
391
+ if (id === primaryVaultId || id === PRIMARY_VAULT_ID) {
392
+ primaryVaultPassword = null;
393
+ }
394
+ }
395
+
396
+ /**
397
+ * Lock ALL vaults.
398
+ */
399
+ export function lockAllVaults(): void {
400
+ vaultSessions.clear();
401
+ primaryVaultPassword = null;
402
+ lockAllCredentialVaults();
403
+ }
404
+
405
+ /**
406
+ * Get the mnemonic for a specific vault (must be unlocked).
407
+ */
408
+ export function getVaultMnemonic(id: string): string | null {
409
+ return vaultSessions.get(id)?.mnemonic ?? null;
410
+ }
411
+
412
+ /**
413
+ * Check if a specific vault is unlocked.
414
+ */
415
+ export function isVaultUnlocked(id: string): boolean {
416
+ return vaultSessions.has(id);
417
+ }
418
+
419
+ /**
420
+ * Get a vault's EVM address (from file, doesn't need unlock).
421
+ */
422
+ export function getVaultAddress(id: string): string | null {
423
+ const session = vaultSessions.get(id);
424
+ if (session) return session.address;
425
+
426
+ const filePath = getVaultFilePath(id);
427
+ if (!fs.existsSync(filePath)) return null;
428
+
429
+ const data: VaultFile = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
430
+ return data.address;
431
+ }
432
+
433
+ /**
434
+ * Get a vault's Solana address (from file, doesn't need unlock).
435
+ */
436
+ export function getVaultSolanaAddress(id: string): string | null {
437
+ const session = vaultSessions.get(id);
438
+ if (session) return session.solanaKeypair.publicKey.toBase58();
439
+
440
+ const filePath = getVaultFilePath(id);
441
+ if (!fs.existsSync(filePath)) return null;
442
+
443
+ const data: VaultFile = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
444
+ return data.solanaAddress || null;
445
+ }
446
+
447
+ /**
448
+ * Get a vault's Solana keypair (requires unlock).
449
+ */
450
+ export function getVaultSolanaKeypair(id: string): Keypair | null {
451
+ return vaultSessions.get(id)?.solanaKeypair ?? null;
452
+ }
453
+
454
+ /**
455
+ * Delete a vault file and lock it.
456
+ */
457
+ export function deleteVault(id: string): void {
458
+ lockVault(id);
459
+
460
+ const filePath = getVaultFilePath(id);
461
+ if (fs.existsSync(filePath)) {
462
+ fs.unlinkSync(filePath);
463
+ }
464
+
465
+ if (primaryVaultId === id) {
466
+ primaryVaultId = null;
467
+ }
468
+ }
469
+
470
+ /**
471
+ * Export seed for a specific vault (must be unlocked).
472
+ */
473
+ export function exportVaultSeed(id: string): string | null {
474
+ return getVaultMnemonic(id);
475
+ }
476
+
477
+ /**
478
+ * Sign an EVM transaction with a specific vault.
479
+ */
480
+ export async function signWithVault(
481
+ vaultId: string,
482
+ transaction: ethers.TransactionRequest,
483
+ provider: ethers.Provider
484
+ ): Promise<string> {
485
+ const session = vaultSessions.get(vaultId);
486
+ if (!session) {
487
+ throw new Error(`Vault ${vaultId} is locked. Unlock it first.`);
488
+ }
489
+
490
+ const hdNode = ethers.HDNodeWallet.fromPhrase(session.mnemonic, undefined, COLD_PATH);
491
+ const wallet = hdNode.connect(provider);
492
+ const tx = await wallet.sendTransaction(transaction);
493
+ return tx.hash;
494
+ }
495
+
496
+ /**
497
+ * Sign a Solana transaction with a specific vault's keypair.
498
+ */
499
+ export function signSolanaVaultTransaction(vaultId: string, tx: Transaction): void {
500
+ const session = vaultSessions.get(vaultId);
501
+ if (!session) {
502
+ throw new Error(`Vault ${vaultId} is locked. Unlock it first.`);
503
+ }
504
+ tx.partialSign(session.solanaKeypair);
505
+ }
506
+
507
+ // ---------------------------------------------------------------------------
508
+ // Generate short vault ID
509
+ // ---------------------------------------------------------------------------
510
+ function generateVaultId(): string {
511
+ const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
512
+ let id = '';
513
+ const bytes = ethers.randomBytes(6);
514
+ for (let i = 0; i < 6; i++) {
515
+ id += chars[bytes[i] % chars.length];
516
+ }
517
+ return id;
518
+ }
519
+
520
+ /**
521
+ * Get the cached primary vault password (available while primary is unlocked).
522
+ */
523
+ export function getPrimaryVaultPassword(): string | null {
524
+ const rootVaultId = primaryVaultId || PRIMARY_VAULT_ID;
525
+ if (!isVaultUnlocked(rootVaultId)) {
526
+ primaryVaultPassword = null;
527
+ return null;
528
+ }
529
+ return primaryVaultPassword;
530
+ }
531
+
532
+ /**
533
+ * Ensure the default linked hierarchy exists:
534
+ * primary -> agent -> daily-logs
535
+ * This is called during primary creation/import and is idempotent.
536
+ */
537
+ export function ensureDefaultLinkedAgentVault(): { created: boolean; vaultId: string | null } {
538
+ ensureMigration();
539
+ if (!primaryVaultId || !primaryVaultPassword) {
540
+ return { created: false, vaultId: null };
541
+ }
542
+
543
+ const vaults = listVaults();
544
+ const existing = vaults.find(
545
+ v => (v.name || '').trim().toLowerCase() === AGENT_VAULT_NAME && (v.parentVaultId || v.linkedTo) === primaryVaultId,
546
+ ) || vaults.find(v => (v.name || '').trim().toLowerCase() === AGENT_VAULT_NAME);
547
+
548
+ if (existing) {
549
+ return { created: false, vaultId: existing.id };
550
+ }
551
+
552
+ const created = createVault(primaryVaultPassword, AGENT_VAULT_NAME, {
553
+ mode: 'linked',
554
+ parentVaultId: primaryVaultId,
555
+ seedOnboardingSecret: false,
556
+ });
557
+ return { created: true, vaultId: created.id };
558
+ }
559
+
560
+ export function ensureDefaultDailyLogsVault(): { created: boolean; vaultId: string | null; parentVaultId: string | null } {
561
+ ensureMigration();
562
+ if (!primaryVaultId || !primaryVaultPassword) {
563
+ return { created: false, vaultId: null, parentVaultId: null };
564
+ }
565
+
566
+ const agentResult = ensureDefaultLinkedAgentVault();
567
+ const agentVaultId = agentResult.vaultId;
568
+ if (!agentVaultId) {
569
+ return { created: false, vaultId: null, parentVaultId: null };
570
+ }
571
+
572
+ const vaults = listVaults();
573
+ const existing = vaults.find(
574
+ v => (v.name || '').trim().toLowerCase() === DAILY_LOGS_VAULT_NAME && (v.parentVaultId || v.linkedTo) === agentVaultId,
575
+ );
576
+ if (existing) {
577
+ return { created: false, vaultId: existing.id, parentVaultId: agentVaultId };
578
+ }
579
+
580
+ const created = createVault(primaryVaultPassword, DAILY_LOGS_VAULT_NAME, {
581
+ mode: 'linked',
582
+ parentVaultId: agentVaultId,
583
+ seedOnboardingSecret: false,
584
+ });
585
+ return { created: true, vaultId: created.id, parentVaultId: agentVaultId };
586
+ }
587
+
588
+ export function ensureDefaultVaultHierarchy(): { agentVaultId: string | null; dailyLogsVaultId: string | null } {
589
+ const agent = ensureDefaultLinkedAgentVault();
590
+ const dailyLogs = ensureDefaultDailyLogsVault();
591
+ return { agentVaultId: agent.vaultId, dailyLogsVaultId: dailyLogs.vaultId };
592
+ }
593
+
594
+ function sortVaultsByCreatedAt(a: VaultInfo, b: VaultInfo): number {
595
+ const createdDelta = new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime();
596
+ if (createdDelta !== 0) return createdDelta;
597
+ return a.id.localeCompare(b.id);
598
+ }
599
+
600
+ function buildChildrenMap(vaults: VaultInfo[]): Map<string, VaultInfo[]> {
601
+ const byParent = new Map<string, VaultInfo[]>();
602
+ for (const vault of vaults) {
603
+ if (vault.mode !== 'linked') continue;
604
+ const parentVaultId = vault.parentVaultId || vault.linkedTo;
605
+ if (!parentVaultId) continue;
606
+ const bucket = byParent.get(parentVaultId) || [];
607
+ bucket.push(vault);
608
+ byParent.set(parentVaultId, bucket);
609
+ }
610
+ for (const children of byParent.values()) {
611
+ children.sort(sortVaultsByCreatedAt);
612
+ }
613
+ return byParent;
614
+ }
615
+
616
+ function collectDescendantVaultIds(rootVaultId: string, vaults: VaultInfo[]): string[] {
617
+ const byParent = buildChildrenMap(vaults);
618
+ const descendants: string[] = [];
619
+ const queue = [...(byParent.get(rootVaultId) || [])];
620
+ const visited = new Set<string>([rootVaultId]);
621
+
622
+ while (queue.length > 0) {
623
+ const next = queue.shift()!;
624
+ if (visited.has(next.id)) continue;
625
+ visited.add(next.id);
626
+ descendants.push(next.id);
627
+ const children = byParent.get(next.id);
628
+ if (children) queue.push(...children);
629
+ }
630
+
631
+ return descendants;
632
+ }
633
+
634
+ /**
635
+ * Unlock all descendant child vaults for a parent using the same password.
636
+ * Returns number of child vaults newly unlocked in this call.
637
+ */
638
+ export function autoUnlockChildVaults(parentVaultId: string, password: string): number {
639
+ const vaults = listVaults();
640
+ const byParent = buildChildrenMap(vaults);
641
+ const queue = [...(byParent.get(parentVaultId) || [])];
642
+ const visited = new Set<string>([parentVaultId]);
643
+ let unlocked = 0;
644
+
645
+ while (queue.length > 0) {
646
+ const next = queue.shift()!;
647
+ if (visited.has(next.id)) continue;
648
+ visited.add(next.id);
649
+
650
+ if (!isVaultUnlocked(next.id)) {
651
+ try {
652
+ if (unlockVault(next.id, password)) unlocked++;
653
+ } catch (err) {
654
+ console.warn(
655
+ `[cold] Child vault unlock failed for "${next.id}"${next.name ? ` (${next.name})` : ''}: ${String(err)}`
656
+ );
657
+ }
658
+ }
659
+
660
+ const children = byParent.get(next.id);
661
+ if (children) queue.push(...children);
662
+ }
663
+
664
+ return unlocked;
665
+ }
666
+
667
+ /**
668
+ * Auto-unlock linked vaults when primary is unlocked.
669
+ * Independent vaults remain locked until explicitly unlocked with their own password.
670
+ */
671
+ export function autoUnlockLinkedVaults(): number {
672
+ const rootVaultId = primaryVaultId || PRIMARY_VAULT_ID;
673
+ if (!isVaultUnlocked(rootVaultId)) {
674
+ primaryVaultPassword = null;
675
+ return 0;
676
+ }
677
+ if (!primaryVaultPassword) return 0;
678
+ return autoUnlockChildVaults(rootVaultId, primaryVaultPassword);
679
+ }
680
+
681
+ /**
682
+ * Resolve vault subtree for UI filtering.
683
+ * - Selecting any vault returns that vault plus all descendants.
684
+ */
685
+ export function getLinkedVaultGroup(vaultId: string): string[] {
686
+ const vaults = listVaults();
687
+ const selected = vaults.find(v => v.id === vaultId);
688
+ if (!selected) return [vaultId];
689
+ return [selected.id, ...collectDescendantVaultIds(selected.id, vaults)];
690
+ }
691
+
692
+ // ---------------------------------------------------------------------------
693
+ // Reset for testing (clears all module state)
694
+ // ---------------------------------------------------------------------------
695
+ export function _resetForTesting(): void {
696
+ vaultSessions.clear();
697
+ primaryVaultId = null;
698
+ primaryVaultPassword = null;
699
+ migrationDone = false;
700
+ }
701
+
702
+ // ===========================================================================
703
+ // BACKWARD COMPATIBILITY — all original exports still work via primary vault
704
+ // ===========================================================================
705
+
706
+ /**
707
+ * Get the primary vault ID (or null if none exists).
708
+ */
709
+ export function getPrimaryVaultId(): string | null {
710
+ ensureMigration();
711
+ return primaryVaultId;
712
+ }
713
+
714
+ export function hasColdWallet(): boolean {
715
+ ensureMigration();
716
+ if (primaryVaultId && fs.existsSync(getVaultFilePath(primaryVaultId))) {
717
+ return true;
718
+ }
719
+ // Also check legacy path as fallback
720
+ return fs.existsSync(getLegacyColdFilePath());
721
+ }
722
+
723
+ export function isUnlocked(): boolean {
724
+ // Returns true if ANY vault is unlocked (backward compat)
725
+ return vaultSessions.size > 0;
726
+ }
727
+
728
+ export interface CreateWalletResult extends WalletInfo {
729
+ mnemonic: string;
730
+ }
731
+
732
+ export function createColdWallet(password: string): CreateWalletResult {
733
+ ensureMigration();
734
+
735
+ if (hasColdWallet()) {
736
+ throw new Error('Cold wallet already exists. Delete it first if you want to recreate.');
737
+ }
738
+
739
+ // Create as the primary vault
740
+ const result = createVault(password, undefined, { seedOnboardingSecret: false });
741
+
742
+ // Set as primary
743
+ primaryVaultId = result.id;
744
+
745
+ // Rename to vault-primary.json
746
+ const currentPath = getVaultFilePath(result.id);
747
+ const primaryPath = getVaultFilePath(PRIMARY_VAULT_ID);
748
+ fs.renameSync(currentPath, primaryPath);
749
+
750
+ // Update session key
751
+ const session = vaultSessions.get(result.id)!;
752
+ vaultSessions.delete(result.id);
753
+ vaultSessions.set(PRIMARY_VAULT_ID, { ...session, id: PRIMARY_VAULT_ID });
754
+ primaryVaultId = PRIMARY_VAULT_ID;
755
+ primaryVaultPassword = password;
756
+ unlockCredentialVault(PRIMARY_VAULT_ID);
757
+
758
+ // Persist primary mode metadata after rename.
759
+ try {
760
+ const primaryFile = readVaultFile(PRIMARY_VAULT_ID);
761
+ primaryFile.mode = 'primary';
762
+ delete primaryFile.linkedTo;
763
+ writeVaultFile(PRIMARY_VAULT_ID, primaryFile);
764
+ } catch (err) {
765
+ console.warn('[cold] Failed to persist primary vault mode metadata:', err);
766
+ }
767
+
768
+ // Seed primary onboarding secret after final vault ID is stable.
769
+ try {
770
+ ensureOurSecretForVault(PRIMARY_VAULT_ID);
771
+ } catch (err) {
772
+ console.warn('[cold] Failed to seed primary OURSECRET onboarding note:', err);
773
+ }
774
+ try {
775
+ ensureDontLookForVault(PRIMARY_VAULT_ID);
776
+ } catch (err) {
777
+ console.warn('[cold] Failed to seed primary DONTLOOK onboarding note:', err);
778
+ }
779
+ try {
780
+ ensureGettingStartedForVault(PRIMARY_VAULT_ID);
781
+ } catch (err) {
782
+ console.warn('[cold] Failed to seed primary GETTING_STARTED onboarding note:', err);
783
+ }
784
+
785
+ // Create default linked hierarchy once at setup time.
786
+ try {
787
+ ensureDefaultVaultHierarchy();
788
+ } catch (err) {
789
+ console.warn('[cold] Failed to create default linked vault hierarchy:', err);
790
+ }
791
+
792
+ return {
793
+ address: result.address,
794
+ tier: 'cold',
795
+ chain: 'all',
796
+ createdAt: new Date().toISOString(),
797
+ mnemonic: result.mnemonic,
798
+ };
799
+ }
800
+
801
+ export function exportSeed(): string | null {
802
+ if (!primaryVaultId) return null;
803
+ return getVaultMnemonic(primaryVaultId);
804
+ }
805
+
806
+ export function unlock(password: string): boolean {
807
+ ensureMigration();
808
+
809
+ if (!primaryVaultId) {
810
+ throw new Error('No cold wallet found');
811
+ }
812
+
813
+ return unlockVault(primaryVaultId, password);
814
+ }
815
+
816
+ /**
817
+ * Rotate the primary vault password by re-encrypting the stored mnemonic wrapper.
818
+ * Returns false when the current password is invalid.
819
+ */
820
+ export function rotatePrimaryVaultPassword(currentPassword: string, newPassword: string): boolean {
821
+ ensureMigration();
822
+
823
+ const vaultId = primaryVaultId || PRIMARY_VAULT_ID;
824
+ const filePath = getVaultFilePath(vaultId);
825
+ if (!fs.existsSync(filePath)) {
826
+ throw new Error('No cold wallet found');
827
+ }
828
+ if (newPassword.length < 8) {
829
+ throw new Error('Password must be at least 8 characters');
830
+ }
831
+
832
+ const vaultFile = readVaultFile(vaultId);
833
+ let mnemonic: string;
834
+ try {
835
+ mnemonic = decryptPrivateKey(vaultFile.encrypted, currentPassword);
836
+ } catch {
837
+ return false;
838
+ }
839
+
840
+ vaultFile.encrypted = encryptPrivateKey(mnemonic, newPassword);
841
+ writeVaultFile(vaultId, vaultFile);
842
+
843
+ if (primaryVaultPassword === currentPassword) {
844
+ primaryVaultPassword = newPassword;
845
+ }
846
+
847
+ return true;
848
+ }
849
+
850
+ /**
851
+ * Recover primary vault access by proving ownership of the seed phrase,
852
+ * then re-encrypting it with a new password and unlocking the vault.
853
+ */
854
+ export function recoverPrimaryVaultWithMnemonic(mnemonic: string, newPassword: string): boolean {
855
+ ensureMigration();
856
+
857
+ const vaultId = primaryVaultId || PRIMARY_VAULT_ID;
858
+ const filePath = getVaultFilePath(vaultId);
859
+ if (!fs.existsSync(filePath)) {
860
+ throw new Error('No cold wallet found');
861
+ }
862
+ if (newPassword.length < 8) {
863
+ throw new Error('Password must be at least 8 characters');
864
+ }
865
+
866
+ const normalizedMnemonic = mnemonic.trim().toLowerCase().split(/\s+/).join(' ');
867
+ if (!ethers.Mnemonic.isValidMnemonic(normalizedMnemonic)) {
868
+ return false;
869
+ }
870
+
871
+ const vaultFile = readVaultFile(vaultId);
872
+ const derived = ethers.HDNodeWallet.fromPhrase(normalizedMnemonic, undefined, COLD_PATH);
873
+ if (derived.address.toLowerCase() !== vaultFile.address.toLowerCase()) {
874
+ return false;
875
+ }
876
+
877
+ vaultFile.encrypted = encryptPrivateKey(normalizedMnemonic, newPassword);
878
+ writeVaultFile(vaultId, vaultFile);
879
+
880
+ const solanaKeypair = deriveSolanaColdKeypair(normalizedMnemonic);
881
+ vaultSessions.set(vaultId, {
882
+ id: vaultId,
883
+ mnemonic: normalizedMnemonic,
884
+ address: vaultFile.address,
885
+ solanaKeypair,
886
+ });
887
+ unlockCredentialVault(vaultId);
888
+
889
+ if (vaultId === (primaryVaultId || PRIMARY_VAULT_ID)) {
890
+ primaryVaultPassword = newPassword;
891
+ }
892
+
893
+ return true;
894
+ }
895
+
896
+ export function lock(): void {
897
+ lockAllVaults();
898
+ }
899
+
900
+ export function getColdWalletAddress(): string | null {
901
+ if (!primaryVaultId) {
902
+ ensureMigration();
903
+ if (!primaryVaultId) return null;
904
+ }
905
+ return getVaultAddress(primaryVaultId);
906
+ }
907
+
908
+ export function getMnemonic(): string | null {
909
+ if (!primaryVaultId) return null;
910
+ return getVaultMnemonic(primaryVaultId);
911
+ }
912
+
913
+ export async function signWithColdWallet(
914
+ transaction: ethers.TransactionRequest,
915
+ provider: ethers.Provider
916
+ ): Promise<string> {
917
+ if (!primaryVaultId) {
918
+ throw new Error('No primary vault configured');
919
+ }
920
+ return signWithVault(primaryVaultId, transaction, provider);
921
+ }
922
+
923
+ export function getColdWalletInfo(): WalletInfo | null {
924
+ ensureMigration();
925
+ if (!primaryVaultId) return null;
926
+
927
+ const filePath = getVaultFilePath(primaryVaultId);
928
+ if (!fs.existsSync(filePath)) return null;
929
+
930
+ const data: VaultFile = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
931
+
932
+ return {
933
+ address: data.address,
934
+ tier: 'cold',
935
+ chain: 'all',
936
+ createdAt: data.createdAt,
937
+ };
938
+ }
939
+
940
+ export function getSolanaColdAddress(): string | null {
941
+ if (!primaryVaultId) {
942
+ ensureMigration();
943
+ if (!primaryVaultId) return null;
944
+ }
945
+ return getVaultSolanaAddress(primaryVaultId);
946
+ }
947
+
948
+ export function getSolanaColdKeypair(): Keypair | null {
949
+ if (!primaryVaultId) return null;
950
+ return getVaultSolanaKeypair(primaryVaultId);
951
+ }
952
+
953
+ export function signSolanaColdTransaction(tx: Transaction): void {
954
+ if (!primaryVaultId) {
955
+ throw new Error('No primary vault configured');
956
+ }
957
+ signSolanaVaultTransaction(primaryVaultId, tx);
958
+ }
959
+
960
+ export function deleteColdWallet(): void {
961
+ if (primaryVaultId) {
962
+ deleteVault(primaryVaultId);
963
+ }
964
+ // Also remove legacy file if it exists
965
+ const legacyPath = getLegacyColdFilePath();
966
+ if (fs.existsSync(legacyPath)) {
967
+ fs.unlinkSync(legacyPath);
968
+ }
969
+ }
970
+
971
+ export function importColdWallet(mnemonic: string, password: string): WalletInfo {
972
+ ensureMigration();
973
+
974
+ if (hasColdWallet()) {
975
+ throw new Error('Cold wallet already exists. Delete it first if you want to import.');
976
+ }
977
+
978
+ const info = importVault(mnemonic, password, undefined, { seedOnboardingSecret: false });
979
+
980
+ // Make it the primary vault
981
+ const currentPath = getVaultFilePath(info.id);
982
+ const primaryPath = getVaultFilePath(PRIMARY_VAULT_ID);
983
+ fs.renameSync(currentPath, primaryPath);
984
+
985
+ // Update session key
986
+ const session = vaultSessions.get(info.id)!;
987
+ vaultSessions.delete(info.id);
988
+ vaultSessions.set(PRIMARY_VAULT_ID, { ...session, id: PRIMARY_VAULT_ID });
989
+ primaryVaultId = PRIMARY_VAULT_ID;
990
+ primaryVaultPassword = password;
991
+ unlockCredentialVault(PRIMARY_VAULT_ID);
992
+
993
+ // Persist primary mode metadata after rename.
994
+ try {
995
+ const primaryFile = readVaultFile(PRIMARY_VAULT_ID);
996
+ primaryFile.mode = 'primary';
997
+ delete primaryFile.linkedTo;
998
+ writeVaultFile(PRIMARY_VAULT_ID, primaryFile);
999
+ } catch (err) {
1000
+ console.warn('[cold] Failed to persist primary vault mode metadata:', err);
1001
+ }
1002
+
1003
+ // Seed primary onboarding secret after final vault ID is stable.
1004
+ try {
1005
+ ensureOurSecretForVault(PRIMARY_VAULT_ID);
1006
+ } catch (err) {
1007
+ console.warn('[cold] Failed to seed primary OURSECRET onboarding note:', err);
1008
+ }
1009
+ try {
1010
+ ensureDontLookForVault(PRIMARY_VAULT_ID);
1011
+ } catch (err) {
1012
+ console.warn('[cold] Failed to seed primary DONTLOOK onboarding note:', err);
1013
+ }
1014
+ try {
1015
+ ensureGettingStartedForVault(PRIMARY_VAULT_ID);
1016
+ } catch (err) {
1017
+ console.warn('[cold] Failed to seed primary GETTING_STARTED onboarding note:', err);
1018
+ }
1019
+
1020
+ // Create default linked hierarchy once at import time.
1021
+ try {
1022
+ ensureDefaultVaultHierarchy();
1023
+ } catch (err) {
1024
+ console.warn('[cold] Failed to create default linked vault hierarchy:', err);
1025
+ }
1026
+
1027
+ return {
1028
+ address: info.address,
1029
+ tier: 'cold',
1030
+ chain: 'all',
1031
+ createdAt: info.createdAt,
1032
+ };
1033
+ }
1034
+
1035
+ // Derive a hot wallet at a specific index
1036
+ // Hot wallets use path: m/44'/60'/1'/0/N
1037
+ export function deriveHotWallet(index: number): ethers.HDNodeWallet {
1038
+ if (!primaryVaultId) {
1039
+ throw new Error('No primary vault configured');
1040
+ }
1041
+ const mnemonic = getVaultMnemonic(primaryVaultId);
1042
+ if (!mnemonic) {
1043
+ throw new Error('Primary vault is locked. Unlock it first.');
1044
+ }
1045
+
1046
+ const hotPath = `m/44'/60'/1'/0/${index}`;
1047
+ return ethers.HDNodeWallet.fromPhrase(mnemonic, undefined, hotPath);
1048
+ }