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,1272 @@
1
+ 'use client';
2
+
3
+ import React, { useCallback, useEffect, useMemo, useState } from 'react';
4
+ import { AlertTriangle, Check, Copy, KeyRound, Loader2, RefreshCw, ShieldAlert, ShieldCheck, Trash2 } from 'lucide-react';
5
+ import { Button, FilterDropdown, TextInput } from '@/components/design-system';
6
+ import { api, Api } from '@/lib/api';
7
+ import { decryptCredentialPayload, getVaultPublicKeyBase64 } from '@/lib/vault-crypto';
8
+ import type { AgentToken, HumanAction } from '@/hooks/useAgentActions';
9
+ import type { VaultInfo } from '@/components/vault/types';
10
+
11
+ const BUILTIN_PROFILE_OPTIONS = [
12
+ { id: 'strict', label: 'Strict', description: 'Agent vault only, read-only, strong field redaction. 15 min TTL.' },
13
+ { id: 'dev', label: 'Dev (recommended)', description: 'All vaults, read/write, moderate redaction. 1 hour TTL.' },
14
+ { id: 'admin', label: 'Admin (dangerous)', description: 'Full access, no redaction. Use with caution.' },
15
+ ] as const;
16
+
17
+ const TOKEN_PERMISSION_OPTIONS = [
18
+ { value: 'wallet:list', label: 'wallet:list' },
19
+ { value: 'wallet:create:hot', label: 'wallet:create:hot' },
20
+ { value: 'wallet:create:temp', label: 'wallet:create:temp' },
21
+ { value: 'wallet:rename', label: 'wallet:rename' },
22
+ { value: 'wallet:export', label: 'wallet:export' },
23
+ { value: 'wallet:tx:add', label: 'wallet:tx:add' },
24
+ { value: 'wallet:asset:add', label: 'wallet:asset:add' },
25
+ { value: 'wallet:asset:remove', label: 'wallet:asset:remove' },
26
+ { value: 'send:hot', label: 'send:hot' },
27
+ { value: 'send:temp', label: 'send:temp' },
28
+ { value: 'swap', label: 'swap' },
29
+ { value: 'fund', label: 'fund' },
30
+ { value: 'launch', label: 'launch' },
31
+ { value: 'apikey:get', label: 'apikey:get' },
32
+ { value: 'apikey:set', label: 'apikey:set' },
33
+ { value: 'workspace:modify', label: 'workspace:modify' },
34
+ { value: 'strategy:read', label: 'strategy:read' },
35
+ { value: 'strategy:manage', label: 'strategy:manage' },
36
+ { value: 'app:storage', label: 'app:storage' },
37
+ { value: 'app:storage:all', label: 'app:storage:all' },
38
+ { value: 'app:accesskey', label: 'app:accesskey' },
39
+ { value: 'action:create', label: 'action:create' },
40
+ { value: 'action:read', label: 'action:read' },
41
+ { value: 'action:resolve', label: 'action:resolve' },
42
+ { value: 'adapter:manage', label: 'adapter:manage' },
43
+ { value: 'addressbook:write', label: 'addressbook:write' },
44
+ { value: 'bookmark:write', label: 'bookmark:write' },
45
+ { value: 'secret:read', label: 'secret:read' },
46
+ { value: 'secret:write', label: 'secret:write' },
47
+ { value: 'totp:read', label: 'totp:read' },
48
+ { value: 'trade:all', label: 'trade:all' },
49
+ { value: 'wallet:write', label: 'wallet:write' },
50
+ { value: 'extension:*', label: 'extension:*' },
51
+ { value: 'admin:*', label: 'admin:* (dangerous)' },
52
+ ] as const;
53
+
54
+ const PROFILE_DESCRIPTIONS: Record<string, string> = {
55
+ strict: 'Read-only, agent vault only. Hidden: password, cvv, privateKey, seedPhrase, refresh_token. 15 min TTL, 50 reads max.',
56
+ dev: 'Read/write, all vaults. Hidden: cvv, seedPhrase, privateKey, refresh_token. 1 hour TTL, 500 reads max.',
57
+ admin: 'Full access, no field redaction. 1 hour TTL, unlimited reads.',
58
+ };
59
+
60
+ function describeProfileId(id: string): string {
61
+ return PROFILE_DESCRIPTIONS[id] || `Custom profile: ${id}`;
62
+ }
63
+
64
+ type IssueMode = 'profile' | 'permissions';
65
+
66
+ const ISSUE_MODE_OPTIONS: Array<{ value: IssueMode; label: string }> = [
67
+ { value: 'profile', label: 'Profile' },
68
+ { value: 'permissions', label: 'Permissions' },
69
+ ];
70
+
71
+ const PROFILE_STORAGE_KEY = 'aura:api-keys:profiles:v1';
72
+
73
+ const BUILTIN_PROFILE_TEMPLATES: Record<string, { ttlSeconds: number; maxReads?: number; scope: string[]; excludeFields: string[] }> = {
74
+ strict: { ttlSeconds: 900, maxReads: 50, scope: ['secret:read'], excludeFields: ['password', 'cvv', 'privateKey', 'seedPhrase', 'refresh_token'] },
75
+ dev: { ttlSeconds: 3600, maxReads: 500, scope: ['wallet:list', 'secret:read', 'secret:write', 'action:create', 'action:read', 'action:resolve'], excludeFields: ['cvv', 'seedPhrase', 'privateKey', 'refresh_token'] },
76
+ admin: { ttlSeconds: 3600, scope: ['admin:*'], excludeFields: [] },
77
+ };
78
+
79
+ interface ProfileOverrides {
80
+ ttlSeconds?: number;
81
+ maxReads?: number;
82
+ scope?: string[];
83
+ readScopes?: string[];
84
+ writeScopes?: string[];
85
+ excludeFields?: string[];
86
+ vaultReadScopes?: string[];
87
+ vaultWriteScopes?: string[];
88
+ }
89
+
90
+ interface LocalProfileDraft {
91
+ id: string;
92
+ name: string;
93
+ profile: string;
94
+ profileVersion: 'v1';
95
+ overrides: ProfileOverrides | null;
96
+ updatedAt: number;
97
+ }
98
+
99
+ interface PolicyPreviewResponse {
100
+ version: 'v1';
101
+ profile?: { id: string; version: string; displayName?: string };
102
+ effectivePolicy: {
103
+ permissions: string[];
104
+ credentialAccess: {
105
+ read: string[];
106
+ write: string[];
107
+ excludeFields: string[];
108
+ maxReads: number | null;
109
+ };
110
+ ttlSeconds: number;
111
+ maxReads: number | null;
112
+ rateBudget: {
113
+ state: 'none' | 'inherited' | 'explicit';
114
+ requests: number | null;
115
+ windowSeconds: number | null;
116
+ source: 'none' | 'profile' | 'override';
117
+ };
118
+ };
119
+ warnings: string[];
120
+ overrideDelta: string[];
121
+ denyExamples?: Array<{ code: string; message: string }>;
122
+ effectivePolicyHash: string;
123
+ }
124
+
125
+ interface IssueResponse {
126
+ success: boolean;
127
+ encryptedToken?: string;
128
+ warnings?: string[];
129
+ profile?: { id: string; version: string; displayName?: string };
130
+ }
131
+
132
+ interface ApiKeysConsoleProps {
133
+ requests: HumanAction[];
134
+ activeTokens: AgentToken[];
135
+ inactiveTokens: AgentToken[];
136
+ actionLoading: string | null;
137
+ onResolveAction: (id: string, approved: boolean) => Promise<{ success: boolean; message?: string }>;
138
+ onRevokeToken: (tokenHash: string) => Promise<boolean>;
139
+ vaults?: VaultInfo[];
140
+ }
141
+
142
+ function shortHash(value: string): string {
143
+ if (value.length <= 14) return value;
144
+ return `${value.slice(0, 8)}...${value.slice(-4)}`;
145
+ }
146
+
147
+ function generateSuggestedAgentId(): string {
148
+ const seed = Math.random().toString(36).slice(2, 8);
149
+ return `agent:local:${seed}`;
150
+ }
151
+
152
+ function normalizePreviewPayload(raw: unknown): PolicyPreviewResponse {
153
+ const data = (raw && typeof raw === 'object' ? raw : {}) as Record<string, unknown>;
154
+ const effectivePolicy = (data.effectivePolicy && typeof data.effectivePolicy === 'object'
155
+ ? data.effectivePolicy
156
+ : {}) as Record<string, unknown>;
157
+ const credentialAccess = (effectivePolicy.credentialAccess && typeof effectivePolicy.credentialAccess === 'object'
158
+ ? effectivePolicy.credentialAccess
159
+ : {}) as Record<string, unknown>;
160
+ const rateBudget = (effectivePolicy.rateBudget && typeof effectivePolicy.rateBudget === 'object'
161
+ ? effectivePolicy.rateBudget
162
+ : {}) as Record<string, unknown>;
163
+
164
+ return {
165
+ version: 'v1',
166
+ profile: data.profile && typeof data.profile === 'object'
167
+ ? data.profile as PolicyPreviewResponse['profile']
168
+ : undefined,
169
+ effectivePolicy: {
170
+ permissions: Array.isArray(effectivePolicy.permissions) ? effectivePolicy.permissions.filter((v): v is string => typeof v === 'string') : [],
171
+ credentialAccess: {
172
+ read: Array.isArray(credentialAccess.read) ? credentialAccess.read.filter((v): v is string => typeof v === 'string') : [],
173
+ write: Array.isArray(credentialAccess.write) ? credentialAccess.write.filter((v): v is string => typeof v === 'string') : [],
174
+ excludeFields: Array.isArray(credentialAccess.excludeFields) ? credentialAccess.excludeFields.filter((v): v is string => typeof v === 'string') : [],
175
+ maxReads: typeof credentialAccess.maxReads === 'number' ? credentialAccess.maxReads : null,
176
+ },
177
+ ttlSeconds: typeof effectivePolicy.ttlSeconds === 'number' ? effectivePolicy.ttlSeconds : 0,
178
+ maxReads: typeof effectivePolicy.maxReads === 'number' ? effectivePolicy.maxReads : null,
179
+ rateBudget: {
180
+ state: rateBudget.state === 'inherited' || rateBudget.state === 'explicit' ? rateBudget.state : 'none',
181
+ requests: typeof rateBudget.requests === 'number' ? rateBudget.requests : null,
182
+ windowSeconds: typeof rateBudget.windowSeconds === 'number' ? rateBudget.windowSeconds : null,
183
+ source: rateBudget.source === 'profile' || rateBudget.source === 'override' ? rateBudget.source : 'none',
184
+ },
185
+ },
186
+ warnings: Array.isArray(data.warnings) ? data.warnings.filter((v): v is string => typeof v === 'string') : [],
187
+ overrideDelta: Array.isArray(data.overrideDelta) ? data.overrideDelta.filter((v): v is string => typeof v === 'string') : [],
188
+ denyExamples: Array.isArray(data.denyExamples)
189
+ ? data.denyExamples
190
+ .filter((row): row is Record<string, unknown> => Boolean(row) && typeof row === 'object')
191
+ .map((row) => ({ code: String(row.code || 'UNKNOWN'), message: String(row.message || '') }))
192
+ : [],
193
+ effectivePolicyHash: typeof data.effectivePolicyHash === 'string' ? data.effectivePolicyHash : 'n/a',
194
+ };
195
+ }
196
+
197
+ function parseMetadata(raw?: string): Record<string, unknown> {
198
+ if (!raw) return {};
199
+ try {
200
+ const parsed = JSON.parse(raw) as unknown;
201
+ if (parsed && typeof parsed === 'object') return parsed as Record<string, unknown>;
202
+ return {};
203
+ } catch {
204
+ return {};
205
+ }
206
+ }
207
+
208
+ function parseCsv(value: string): string[] | undefined {
209
+ const parts = value
210
+ .split(',')
211
+ .map((part) => part.trim())
212
+ .filter(Boolean);
213
+ return parts.length > 0 ? parts : undefined;
214
+ }
215
+
216
+ function normalizeStringList(values: string[]): string[] {
217
+ return Array.from(
218
+ new Set(
219
+ values
220
+ .map((value) => value.trim().normalize('NFKC').toLowerCase())
221
+ .filter(Boolean),
222
+ ),
223
+ );
224
+ }
225
+
226
+ function buildOverrides(input: {
227
+ ttlSeconds: string;
228
+ maxReads: string;
229
+ scope: string[];
230
+ excludeFields: string;
231
+ }): { overrides: ProfileOverrides | null; error: string | null } {
232
+ const next: ProfileOverrides = {};
233
+
234
+ if (input.ttlSeconds.trim().length > 0) {
235
+ const ttl = Number(input.ttlSeconds.trim());
236
+ if (!Number.isFinite(ttl) || ttl <= 0) {
237
+ return { overrides: null, error: 'TTL override must be a positive number.' };
238
+ }
239
+ next.ttlSeconds = ttl;
240
+ }
241
+
242
+ if (input.maxReads.trim().length > 0) {
243
+ const maxReads = Number(input.maxReads.trim());
244
+ if (!Number.isFinite(maxReads) || maxReads <= 0) {
245
+ return { overrides: null, error: 'Max reads override must be a positive number.' };
246
+ }
247
+ next.maxReads = maxReads;
248
+ }
249
+
250
+ const scope = normalizeStringList(input.scope);
251
+ const excludeFields = parseCsv(input.excludeFields);
252
+
253
+ if (scope.length > 0) next.scope = scope;
254
+ if (excludeFields) next.excludeFields = excludeFields;
255
+
256
+ return { overrides: Object.keys(next).length > 0 ? next : null, error: null };
257
+ }
258
+
259
+ export const ApiKeysConsole: React.FC<ApiKeysConsoleProps> = ({
260
+ requests,
261
+ activeTokens,
262
+ inactiveTokens,
263
+ actionLoading,
264
+ onResolveAction,
265
+ onRevokeToken,
266
+ vaults = [],
267
+ }) => {
268
+ const [profiles, setProfiles] = useState<LocalProfileDraft[]>([]);
269
+ const [editingProfileId, setEditingProfileId] = useState<string | null>(null);
270
+
271
+ const [profileName, setProfileName] = useState('');
272
+ const [profileBase, setProfileBase] = useState<string>('dev');
273
+ const [profileTtlSeconds, setProfileTtlSeconds] = useState('');
274
+ const [profileMaxReads, setProfileMaxReads] = useState('');
275
+ const [profileScopes, setProfileScopes] = useState<string[]>([]);
276
+ const [profileScopeCandidate, setProfileScopeCandidate] = useState<string>('secret:read');
277
+ const [profileExcludeFields, setProfileExcludeFields] = useState('');
278
+ const [profileVaultScopes, setProfileVaultScopes] = useState<string[]>([]);
279
+
280
+ const [profileError, setProfileError] = useState<string | null>(null);
281
+ const [profileNotice, setProfileNotice] = useState<string | null>(null);
282
+
283
+ const [issueAgentId, setIssueAgentId] = useState<string>(() => generateSuggestedAgentId());
284
+ const [issueMode, setIssueMode] = useState<IssueMode>('profile');
285
+ const [issueProfileSource, setIssueProfileSource] = useState<string>('builtin:dev');
286
+ const [issuePermissionCandidate, setIssuePermissionCandidate] = useState<string>('secret:read');
287
+ const [issuePermissions, setIssuePermissions] = useState<string[]>(['secret:read', 'secret:write']);
288
+ const [issueVaultScopes, setIssueVaultScopes] = useState<string[]>([]);
289
+ const [issuing, setIssuing] = useState(false);
290
+ const [issueError, setIssueError] = useState<string | null>(null);
291
+ const [issuedToken, setIssuedToken] = useState<string | null>(null);
292
+ const [issuedMeta, setIssuedMeta] = useState<string | null>(null);
293
+ const [copied, setCopied] = useState(false);
294
+
295
+ const [previewLoading, setPreviewLoading] = useState(false);
296
+ const [previewError, setPreviewError] = useState<string | null>(null);
297
+ const [preview, setPreview] = useState<PolicyPreviewResponse | null>(null);
298
+
299
+ const [requestNotice, setRequestNotice] = useState<string | null>(null);
300
+
301
+ useEffect(() => {
302
+ try {
303
+ const raw = localStorage.getItem(PROFILE_STORAGE_KEY);
304
+ if (!raw) return;
305
+ const parsed = JSON.parse(raw) as LocalProfileDraft[];
306
+ if (!Array.isArray(parsed)) return;
307
+ const safe = parsed
308
+ .filter((entry) =>
309
+ entry
310
+ && typeof entry.id === 'string'
311
+ && typeof entry.name === 'string'
312
+ && typeof entry.profile === 'string'
313
+ && entry.profileVersion === 'v1'
314
+ && typeof entry.updatedAt === 'number'
315
+ )
316
+ .map((entry) => {
317
+ const scope = normalizeStringList([
318
+ ...(entry.overrides?.scope || []),
319
+ ...(entry.overrides?.readScopes || []),
320
+ ...(entry.overrides?.writeScopes || []),
321
+ ]);
322
+ const overrides = entry.overrides
323
+ ? {
324
+ ...entry.overrides,
325
+ ...(scope.length > 0 ? { scope } : {}),
326
+ readScopes: undefined,
327
+ writeScopes: undefined,
328
+ }
329
+ : null;
330
+ return {
331
+ ...entry,
332
+ overrides,
333
+ };
334
+ });
335
+ setProfiles(safe);
336
+ } catch {
337
+ setProfiles([]);
338
+ }
339
+ }, []);
340
+
341
+ useEffect(() => {
342
+ localStorage.setItem(PROFILE_STORAGE_KEY, JSON.stringify(profiles));
343
+ }, [profiles]);
344
+
345
+ const resetProfileForm = useCallback(() => {
346
+ setEditingProfileId(null);
347
+ setProfileName('');
348
+ setProfileBase('dev');
349
+ setProfileTtlSeconds('');
350
+ setProfileMaxReads('');
351
+ setProfileScopes([]);
352
+ setProfileScopeCandidate('secret:read');
353
+ setProfileExcludeFields('');
354
+ setProfileVaultScopes([]);
355
+ setProfileError(null);
356
+ setProfileNotice(null);
357
+ }, []);
358
+
359
+ const applyBaseTemplate = useCallback((baseProfile: string) => {
360
+ const template = BUILTIN_PROFILE_TEMPLATES[baseProfile];
361
+ if (!template) return;
362
+ setProfileTtlSeconds(String(template.ttlSeconds));
363
+ setProfileMaxReads(typeof template.maxReads === 'number' ? String(template.maxReads) : '');
364
+ setProfileScopes(template.scope);
365
+ setProfileScopeCandidate(template.scope[0] || 'secret:read');
366
+ setProfileExcludeFields(template.excludeFields.join(', '));
367
+ }, []);
368
+
369
+ const vaultScopeOptions = useMemo(() => {
370
+ const options: Array<{ value: string; label: string }> = [
371
+ { value: 'vault:*', label: 'All Vaults (vault:*)' },
372
+ ];
373
+ for (const vault of vaults) {
374
+ const name = vault.name || vault.id;
375
+ options.push({ value: `vault:${vault.id}`, label: `vault:${name}` });
376
+ }
377
+ return options;
378
+ }, [vaults]);
379
+
380
+ const pendingAuthRequests = useMemo(
381
+ () => requests.filter((request) => request.status === 'pending' && (request.type === 'auth' || request.type === 'permission_update')),
382
+ [requests],
383
+ );
384
+
385
+ const selectableProfileSources = useMemo(() => {
386
+ const builtin = BUILTIN_PROFILE_OPTIONS.map((option) => ({
387
+ value: `builtin:${option.id}`,
388
+ label: `${option.label} — ${option.description}`,
389
+ }));
390
+ const custom = profiles.map((profile) => ({
391
+ value: `draft:${profile.id}`,
392
+ label: `Custom · ${profile.name}`,
393
+ }));
394
+ return [...builtin, ...custom];
395
+ }, [profiles]);
396
+
397
+ useEffect(() => {
398
+ if (!selectableProfileSources.some((option) => option.value === issueProfileSource)) {
399
+ setIssueProfileSource('builtin:dev');
400
+ }
401
+ }, [issueProfileSource, selectableProfileSources]);
402
+
403
+ const resolveIssueProfile = useCallback((): { profile: string; profileVersion: string; profileOverrides?: ProfileOverrides } => {
404
+ if (issueProfileSource.startsWith('draft:')) {
405
+ const profileId = issueProfileSource.slice(6);
406
+ const draft = profiles.find((entry) => entry.id === profileId);
407
+ if (draft) {
408
+ const scope = normalizeStringList([
409
+ ...(draft.overrides?.scope || []),
410
+ ...(draft.overrides?.readScopes || []),
411
+ ...(draft.overrides?.writeScopes || []),
412
+ ]);
413
+ const profileOverrides = draft.overrides
414
+ ? {
415
+ ...draft.overrides,
416
+ ...(scope.length > 0 ? { scope } : {}),
417
+ }
418
+ : undefined;
419
+ return {
420
+ profile: draft.profile,
421
+ profileVersion: draft.profileVersion,
422
+ ...(profileOverrides ? { profileOverrides } : {}),
423
+ };
424
+ }
425
+ }
426
+ const profile = issueProfileSource.startsWith('builtin:') ? issueProfileSource.slice(8) : 'dev';
427
+ return { profile, profileVersion: 'v1' };
428
+ }, [issueProfileSource, profiles]);
429
+
430
+ const addIssuePermission = useCallback((permission: string) => {
431
+ const normalized = normalizeStringList([permission])[0];
432
+ if (!normalized) return;
433
+ setIssuePermissions((prev) => (prev.includes(normalized) ? prev : [...prev, normalized]));
434
+ }, []);
435
+
436
+ const removeIssuePermission = useCallback((permission: string) => {
437
+ setIssuePermissions((prev) => prev.filter((entry) => entry !== permission));
438
+ }, []);
439
+
440
+ const handleSaveProfile = useCallback(() => {
441
+ setProfileError(null);
442
+ setProfileNotice(null);
443
+ const trimmedName = profileName.trim();
444
+ if (!trimmedName) {
445
+ setProfileError('Profile name is required.');
446
+ return;
447
+ }
448
+
449
+ const { overrides: baseOverrides, error } = buildOverrides({
450
+ ttlSeconds: profileTtlSeconds,
451
+ maxReads: profileMaxReads,
452
+ scope: profileScopes,
453
+ excludeFields: profileExcludeFields,
454
+ });
455
+
456
+ if (error) {
457
+ setProfileError(error);
458
+ return;
459
+ }
460
+
461
+ const overrides: ProfileOverrides | null = profileVaultScopes.length > 0
462
+ ? { ...(baseOverrides || {}), readScopes: profileVaultScopes, writeScopes: profileVaultScopes }
463
+ : baseOverrides;
464
+
465
+ const existingNameConflict = profiles.some((profile) =>
466
+ profile.name.toLowerCase() === trimmedName.toLowerCase()
467
+ && profile.id !== editingProfileId
468
+ );
469
+ if (existingNameConflict) {
470
+ setProfileError('A profile with this name already exists.');
471
+ return;
472
+ }
473
+
474
+ const payload: LocalProfileDraft = {
475
+ id: editingProfileId || `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 7)}`,
476
+ name: trimmedName,
477
+ profile: profileBase,
478
+ profileVersion: 'v1',
479
+ overrides,
480
+ updatedAt: Date.now(),
481
+ };
482
+
483
+ setProfiles((prev) => {
484
+ if (editingProfileId) {
485
+ return prev.map((entry) => (entry.id === editingProfileId ? payload : entry));
486
+ }
487
+ return [payload, ...prev];
488
+ });
489
+ setProfileNotice(editingProfileId ? 'Profile updated.' : 'Profile created.');
490
+ if (!editingProfileId) {
491
+ setIssueProfileSource(`draft:${payload.id}`);
492
+ }
493
+ setEditingProfileId(payload.id);
494
+ }, [
495
+ editingProfileId,
496
+ profileBase,
497
+ profileExcludeFields,
498
+ profileMaxReads,
499
+ profileName,
500
+ profileScopes,
501
+ profileTtlSeconds,
502
+ profileVaultScopes,
503
+ profiles,
504
+ ]);
505
+
506
+ const handleEditProfile = useCallback((profile: LocalProfileDraft) => {
507
+ setEditingProfileId(profile.id);
508
+ setProfileName(profile.name);
509
+ setProfileBase(profile.profile);
510
+ setProfileTtlSeconds(profile.overrides?.ttlSeconds ? String(profile.overrides.ttlSeconds) : '');
511
+ setProfileMaxReads(profile.overrides?.maxReads ? String(profile.overrides.maxReads) : '');
512
+ const legacyScopes = [
513
+ ...(profile.overrides?.scope || []),
514
+ ...(profile.overrides?.readScopes || []),
515
+ ...(profile.overrides?.writeScopes || []),
516
+ ];
517
+ const normalizedScopes = normalizeStringList(legacyScopes);
518
+ setProfileScopes(normalizedScopes);
519
+ setProfileScopeCandidate(normalizedScopes[0] || 'secret:read');
520
+ setProfileExcludeFields((profile.overrides?.excludeFields || []).join(', '));
521
+ setProfileVaultScopes(profile.overrides?.readScopes || profile.overrides?.vaultReadScopes || []);
522
+ setProfileError(null);
523
+ setProfileNotice(null);
524
+ }, []);
525
+
526
+ const addProfileScope = useCallback((scope: string) => {
527
+ const normalized = normalizeStringList([scope])[0];
528
+ if (!normalized) return;
529
+ setProfileScopes((prev) => (prev.includes(normalized) ? prev : [...prev, normalized]));
530
+ }, []);
531
+
532
+ const removeProfileScope = useCallback((scope: string) => {
533
+ setProfileScopes((prev) => prev.filter((entry) => entry !== scope));
534
+ }, []);
535
+
536
+ const handleDeleteProfile = useCallback((id: string) => {
537
+ setProfiles((prev) => prev.filter((profile) => profile.id !== id));
538
+ if (editingProfileId === id) {
539
+ resetProfileForm();
540
+ }
541
+ if (issueProfileSource === `draft:${id}`) {
542
+ setIssueProfileSource('builtin:dev');
543
+ }
544
+ }, [editingProfileId, issueProfileSource, resetProfileForm]);
545
+
546
+ const handlePreview = useCallback(async () => {
547
+ setPreviewError(null);
548
+ setPreview(null);
549
+ if (issueMode === 'permissions') {
550
+ setPreviewError('Policy preview is currently available for profile-based issuance only.');
551
+ return;
552
+ }
553
+ setPreviewLoading(true);
554
+ try {
555
+ const selection = resolveIssueProfile();
556
+ const data = await api.post<PolicyPreviewResponse>(Api.Wallet, '/actions/token/preview', selection);
557
+ setPreview(normalizePreviewPayload(data));
558
+ } catch (error) {
559
+ setPreviewError(error instanceof Error ? error.message : 'Failed to preview profile policy.');
560
+ } finally {
561
+ setPreviewLoading(false);
562
+ }
563
+ }, [issueMode, resolveIssueProfile]);
564
+
565
+ const handleIssueApiKey = useCallback(async () => {
566
+ setIssueError(null);
567
+ setIssuedToken(null);
568
+ setIssuedMeta(null);
569
+ if (!issueAgentId.trim()) {
570
+ setIssueError('Agent ID is required.');
571
+ return;
572
+ }
573
+ if (issueMode === 'permissions' && issuePermissions.length === 0) {
574
+ setIssueError('Select at least one permission for manual issuance.');
575
+ return;
576
+ }
577
+ const pubkey = getVaultPublicKeyBase64();
578
+ if (!pubkey) {
579
+ setIssueError('Vault keypair is unavailable. Re-unlock and retry.');
580
+ return;
581
+ }
582
+
583
+ setIssuing(true);
584
+ try {
585
+ let result: IssueResponse;
586
+ if (issueMode === 'permissions') {
587
+ const credentialAccess = issueVaultScopes.length > 0
588
+ ? { read: issueVaultScopes, write: issueVaultScopes }
589
+ : undefined;
590
+ result = await api.post<IssueResponse>(Api.Wallet, '/actions/token', {
591
+ agentId: issueAgentId.trim(),
592
+ pubkey,
593
+ permissions: issuePermissions,
594
+ ...(credentialAccess ? { credentialAccess } : {}),
595
+ });
596
+ } else {
597
+ const profilePayload = resolveIssueProfile();
598
+ if (issueVaultScopes.length > 0) {
599
+ profilePayload.profileOverrides = {
600
+ ...(profilePayload.profileOverrides || {}),
601
+ readScopes: issueVaultScopes,
602
+ writeScopes: issueVaultScopes,
603
+ };
604
+ }
605
+ result = await api.post<IssueResponse>(Api.Wallet, '/actions/token', {
606
+ agentId: issueAgentId.trim(),
607
+ pubkey,
608
+ ...profilePayload,
609
+ });
610
+ }
611
+ if (!result.success || !result.encryptedToken) {
612
+ throw new Error('Token issuance failed.');
613
+ }
614
+ const token = await decryptCredentialPayload(result.encryptedToken);
615
+ setIssuedToken(token);
616
+ if (issueMode === 'permissions') {
617
+ setIssuedMeta(
618
+ `${issueAgentId.trim()} · permissions:${issuePermissions.join(',')}${result.warnings?.length ? ' · warnings present' : ''}`,
619
+ );
620
+ } else {
621
+ const selection = resolveIssueProfile();
622
+ setIssuedMeta(
623
+ `${issueAgentId.trim()} · ${selection.profile}@${selection.profileVersion}${result.warnings?.length ? ' · warnings present' : ''}`,
624
+ );
625
+ }
626
+ } catch (error) {
627
+ setIssueError(error instanceof Error ? error.message : 'Token issuance failed.');
628
+ } finally {
629
+ setIssuing(false);
630
+ }
631
+ }, [issueAgentId, issueMode, issuePermissions, issueVaultScopes, resolveIssueProfile]);
632
+
633
+ const handleCopyToken = useCallback(async () => {
634
+ if (!issuedToken || !navigator.clipboard) return;
635
+ await navigator.clipboard.writeText(issuedToken);
636
+ setCopied(true);
637
+ setTimeout(() => setCopied(false), 1500);
638
+ }, [issuedToken]);
639
+
640
+ const handleResolveRequest = useCallback(async (id: string, approved: boolean) => {
641
+ const result = await onResolveAction(id, approved);
642
+ if (result.success) {
643
+ setRequestNotice(approved ? `Approved request ${id.slice(0, 8)}.` : `Rejected request ${id.slice(0, 8)}.`);
644
+ return;
645
+ }
646
+ setRequestNotice(result.message || 'Failed to resolve request.');
647
+ }, [onResolveAction]);
648
+
649
+ const managedActiveTokens = useMemo(
650
+ () => activeTokens.filter((token) => !token.isAdmin),
651
+ [activeTokens],
652
+ );
653
+
654
+ return (
655
+ <div className="h-full overflow-y-auto px-5 py-4 font-mono">
656
+ <div className="mb-4 border border-[var(--color-border,#d4d4d8)] bg-[var(--color-surface,#fff)] p-3">
657
+ <div className="text-[10px] font-bold tracking-widest text-[var(--color-text,#0a0a0a)]">API KEYS</div>
658
+ <div className="mt-1 text-[9px] text-[var(--color-text-muted,#6b7280)] leading-relaxed">
659
+ Auth management surface for profile templates + permissions-based issuance + pending auth approvals.
660
+ </div>
661
+ </div>
662
+
663
+ <div className="grid grid-cols-1 gap-4 xl:grid-cols-2">
664
+ <section className="order-1 border border-[var(--color-border,#d4d4d8)] bg-[var(--color-surface,#fff)] p-3">
665
+ <div className="mb-2 text-[10px] font-bold tracking-widest text-[var(--color-text,#0a0a0a)]">ISSUE API KEY</div>
666
+ <div className="grid grid-cols-1 gap-2 md:grid-cols-2">
667
+ <div className="grid grid-cols-[minmax(0,1fr)_auto] items-end gap-1.5">
668
+ <TextInput
669
+ label="Agent ID"
670
+ value={issueAgentId}
671
+ onChange={(event) => setIssueAgentId(event.target.value)}
672
+ placeholder="agent:local:dev"
673
+ compact
674
+ />
675
+ <Button
676
+ variant="secondary"
677
+ size="sm"
678
+ onClick={() => setIssueAgentId(generateSuggestedAgentId())}
679
+ icon={<RefreshCw size={10} />}
680
+ title="Regenerate Agent ID"
681
+ className="mb-[1px]"
682
+ >
683
+ NEW ID
684
+ </Button>
685
+ </div>
686
+
687
+ <FilterDropdown
688
+ label="Issuance Mode"
689
+ options={ISSUE_MODE_OPTIONS}
690
+ value={issueMode}
691
+ onChange={(value) => setIssueMode(value as IssueMode)}
692
+ compact
693
+ />
694
+ </div>
695
+
696
+ {issueMode === 'profile' ? (
697
+ <div className="mt-2">
698
+ <FilterDropdown
699
+ label="Profile Source"
700
+ options={selectableProfileSources}
701
+ value={issueProfileSource}
702
+ onChange={setIssueProfileSource}
703
+ compact
704
+ />
705
+ {(() => {
706
+ const builtinId = issueProfileSource.startsWith('builtin:') ? issueProfileSource.slice(8) : null;
707
+ const desc = builtinId ? PROFILE_DESCRIPTIONS[builtinId] : null;
708
+ return desc ? (
709
+ <div className="mt-1 px-1 text-[8px] text-[var(--color-text-muted,#6b7280)]">{desc}</div>
710
+ ) : (
711
+ <div className="mt-1 px-1 text-[8px] text-[var(--color-text-faint,#9ca3af)]">
712
+ Profile mode resolves permissions/scopes/ttl from the selected profile.
713
+ </div>
714
+ );
715
+ })()}
716
+ </div>
717
+ ) : (
718
+ <div className="mt-2 space-y-1">
719
+ <div className="grid grid-cols-[minmax(0,1fr)_auto] items-end gap-1.5">
720
+ <FilterDropdown
721
+ label="Permissions"
722
+ options={TOKEN_PERMISSION_OPTIONS.map((option) => ({ value: option.value, label: option.label }))}
723
+ value={issuePermissionCandidate}
724
+ onChange={setIssuePermissionCandidate}
725
+ compact
726
+ />
727
+ <Button
728
+ variant="secondary"
729
+ size="sm"
730
+ onClick={() => addIssuePermission(issuePermissionCandidate)}
731
+ >
732
+ ADD
733
+ </Button>
734
+ </div>
735
+ {issuePermissions.length === 0 ? (
736
+ <div className="px-1 text-[8px] text-[var(--color-danger,#ef4444)]">
737
+ No permissions selected.
738
+ </div>
739
+ ) : (
740
+ <div className="flex flex-wrap gap-1">
741
+ {issuePermissions.map((permission) => (
742
+ <span
743
+ key={`perm-${permission}`}
744
+ className="inline-flex items-center gap-1 border border-[var(--color-border,#d4d4d8)] bg-[var(--color-background,#f4f4f5)] px-1.5 py-0.5 text-[8px] text-[var(--color-text,#0a0a0a)]"
745
+ >
746
+ {permission}
747
+ <Button
748
+ variant="ghost"
749
+ size="sm"
750
+ onClick={() => removeIssuePermission(permission)}
751
+ className="h-4 px-1 text-[8px]"
752
+ >
753
+ X
754
+ </Button>
755
+ </span>
756
+ ))}
757
+ </div>
758
+ )}
759
+ <div className="px-1 text-[8px] text-[var(--color-text-faint,#9ca3af)]">
760
+ Permissions mode issues exactly the selected permissions.
761
+ </div>
762
+ </div>
763
+ )}
764
+
765
+ {vaultScopeOptions.length > 1 && (
766
+ <div className="mt-2 space-y-1">
767
+ <div className="grid grid-cols-[minmax(0,1fr)_auto] items-end gap-1.5">
768
+ <FilterDropdown
769
+ label="Vault Scope"
770
+ options={vaultScopeOptions}
771
+ value={vaultScopeOptions[0]?.value || 'vault:*'}
772
+ onChange={(value) => {
773
+ if (!issueVaultScopes.includes(value)) {
774
+ setIssueVaultScopes((prev) => [...prev, value]);
775
+ }
776
+ }}
777
+ compact
778
+ />
779
+ <Button
780
+ variant="secondary"
781
+ size="sm"
782
+ onClick={() => setIssueVaultScopes([])}
783
+ >
784
+ CLEAR
785
+ </Button>
786
+ </div>
787
+ {issueVaultScopes.length === 0 ? (
788
+ <div className="px-1 text-[8px] text-[var(--color-text-faint,#9ca3af)]">
789
+ No vault restriction — uses profile/system defaults.
790
+ </div>
791
+ ) : (
792
+ <div className="flex flex-wrap gap-1">
793
+ {issueVaultScopes.map((scope) => (
794
+ <span
795
+ key={`vs-${scope}`}
796
+ className="inline-flex items-center gap-1 border border-[var(--color-border,#d4d4d8)] bg-[var(--color-background,#f4f4f5)] px-1.5 py-0.5 text-[8px] text-[var(--color-text,#0a0a0a)]"
797
+ >
798
+ {scope}
799
+ <Button
800
+ variant="ghost"
801
+ size="sm"
802
+ onClick={() => setIssueVaultScopes((prev) => prev.filter((s) => s !== scope))}
803
+ className="h-4 px-1 text-[8px]"
804
+ >
805
+ X
806
+ </Button>
807
+ </span>
808
+ ))}
809
+ </div>
810
+ )}
811
+ </div>
812
+ )}
813
+
814
+ <div className="mt-2 rounded border border-[var(--color-border,#d4d4d8)] bg-[var(--color-background,#f4f4f5)] px-2 py-2 text-[9px] text-[var(--color-text-muted,#6b7280)]">
815
+ Generate a key in one click. Preview is optional and only shows what permissions/scopes will be issued.
816
+ </div>
817
+
818
+ {issueError && (
819
+ <div className="mt-2 text-[9px] text-[var(--color-danger,#ef4444)]">{issueError}</div>
820
+ )}
821
+
822
+ <div className="mt-3 flex flex-wrap gap-2">
823
+ <Button
824
+ size="sm"
825
+ onClick={() => { void handleIssueApiKey(); }}
826
+ loading={issuing}
827
+ icon={!issuing ? <KeyRound size={11} /> : undefined}
828
+ >
829
+ ISSUE API KEY
830
+ </Button>
831
+ <Button
832
+ size="sm"
833
+ variant="secondary"
834
+ onClick={() => { void handlePreview(); }}
835
+ loading={previewLoading}
836
+ disabled={issueMode === 'permissions'}
837
+ icon={!previewLoading ? <RefreshCw size={11} /> : undefined}
838
+ >
839
+ PREVIEW POLICY
840
+ </Button>
841
+ </div>
842
+
843
+ {previewError && (
844
+ <div className="mt-2 text-[9px] text-[var(--color-danger,#ef4444)]">{previewError}</div>
845
+ )}
846
+
847
+ {preview && (
848
+ <div className="mt-3 rounded border border-[var(--color-border,#d4d4d8)] bg-[var(--color-background,#f4f4f5)] p-2">
849
+ <div className="text-[8px] tracking-widest text-[var(--color-text-faint,#9ca3af)]">
850
+ HASH {shortHash(preview.effectivePolicyHash)}
851
+ </div>
852
+ {preview.profile && (
853
+ <div className="mt-1 text-[8px] text-[var(--color-text-faint,#9ca3af)]">
854
+ profile: {preview.profile.id}@{preview.profile.version}
855
+ </div>
856
+ )}
857
+ <div className="mt-1 text-[9px] text-[var(--color-text-muted,#6b7280)]">
858
+ permissions: {preview.effectivePolicy.permissions.length} · ttl: {preview.effectivePolicy.ttlSeconds}s · max reads: {preview.effectivePolicy.maxReads ?? 'unlimited'}
859
+ </div>
860
+ <div className="mt-2 grid grid-cols-1 gap-1 text-[8px] text-[var(--color-text-muted,#6b7280)]">
861
+ <div>perms: {preview.effectivePolicy.permissions.join(', ') || '(none)'}</div>
862
+ <div>read scope: {preview.effectivePolicy.credentialAccess.read.join(', ') || '(none)'}</div>
863
+ <div>write scope: {preview.effectivePolicy.credentialAccess.write.join(', ') || '(none)'}</div>
864
+ <div>excluded fields: {preview.effectivePolicy.credentialAccess.excludeFields.join(', ') || '(none)'}</div>
865
+ <div>
866
+ rate budget: {preview.effectivePolicy.rateBudget.state} ({preview.effectivePolicy.rateBudget.requests ?? 'n/a'} / {preview.effectivePolicy.rateBudget.windowSeconds ?? 'n/a'}s, source={preview.effectivePolicy.rateBudget.source})
867
+ </div>
868
+ </div>
869
+
870
+ {preview.overrideDelta.length > 0 && (
871
+ <div className="mt-2 rounded border border-[var(--color-border,#d4d4d8)] bg-[var(--color-surface,#fff)] p-1.5 text-[8px] text-[var(--color-text-muted,#6b7280)]">
872
+ <div className="mb-1 tracking-widest text-[var(--color-text-faint,#9ca3af)]">OVERRIDE DELTA</div>
873
+ {preview.overrideDelta.join(' · ')}
874
+ </div>
875
+ )}
876
+
877
+ {preview.denyExamples && preview.denyExamples.length > 0 && (
878
+ <div className="mt-2 rounded border border-[var(--color-border,#d4d4d8)] bg-[var(--color-surface,#fff)] p-1.5 text-[8px] text-[var(--color-text-muted,#6b7280)]">
879
+ <div className="mb-1 tracking-widest text-[var(--color-text-faint,#9ca3af)]">EXPECTED DENY EXAMPLES</div>
880
+ <ul className="space-y-0.5">
881
+ {preview.denyExamples.map((deny) => (
882
+ <li key={`${deny.code}-${deny.message.slice(0, 12)}`}>• {deny.code}: {deny.message}</li>
883
+ ))}
884
+ </ul>
885
+ </div>
886
+ )}
887
+
888
+ {preview.warnings.length > 0 && (
889
+ <div className="mt-2 flex items-start gap-1.5 text-[8px] text-[var(--color-warning,#ff4d00)]">
890
+ <ShieldAlert size={10} className="mt-[1px]" />
891
+ <span>{preview.warnings.join(' | ')}</span>
892
+ </div>
893
+ )}
894
+ </div>
895
+ )}
896
+
897
+ {issuedToken && (
898
+ <div className="mt-3 rounded border border-[var(--color-border,#d4d4d8)] bg-[var(--color-surface,#fff)] p-2">
899
+ <div className="mb-1 flex items-center justify-between">
900
+ <div className="text-[8px] tracking-widest text-[var(--color-success,#16a34a)]">API KEY ISSUED</div>
901
+ <Button
902
+ variant="ghost"
903
+ size="sm"
904
+ onClick={() => { void handleCopyToken(); }}
905
+ icon={<Copy size={10} />}
906
+ >
907
+ {copied ? 'COPIED' : 'COPY'}
908
+ </Button>
909
+ </div>
910
+ {issuedMeta && (
911
+ <div className="mb-1 text-[8px] text-[var(--color-text-faint,#9ca3af)]">{issuedMeta}</div>
912
+ )}
913
+ <code className="block max-h-20 overflow-y-auto break-all bg-[var(--color-background,#f4f4f5)] px-2 py-1 text-[9px] text-[var(--color-text,#0a0a0a)]">
914
+ {issuedToken}
915
+ </code>
916
+ </div>
917
+ )}
918
+ </section>
919
+
920
+ <section className="order-2 border border-[var(--color-border,#d4d4d8)] bg-[var(--color-surface,#fff)] p-3">
921
+ <div className="mb-2 flex items-center justify-between">
922
+ <div className="text-[10px] font-bold tracking-widest text-[var(--color-text,#0a0a0a)]">PROFILE BUILDER</div>
923
+ {editingProfileId && (
924
+ <Button variant="ghost" size="sm" onClick={resetProfileForm}>
925
+ NEW
926
+ </Button>
927
+ )}
928
+ </div>
929
+
930
+ <div className="grid grid-cols-1 gap-2">
931
+ <TextInput
932
+ label="Profile Name"
933
+ value={profileName}
934
+ onChange={(event) => setProfileName(event.target.value)}
935
+ placeholder="e.g. CI deploy scoped"
936
+ compact
937
+ />
938
+
939
+ <FilterDropdown
940
+ label="Load from template"
941
+ options={BUILTIN_PROFILE_OPTIONS.map((profile) => ({ value: profile.id, label: `${profile.label} — ${profile.description}` }))}
942
+ value={profileBase}
943
+ onChange={(next) => {
944
+ setProfileBase(next);
945
+ applyBaseTemplate(next);
946
+ }}
947
+ compact
948
+ />
949
+ {profileBase && PROFILE_DESCRIPTIONS[profileBase] && (
950
+ <div className="px-1 text-[8px] text-[var(--color-text-muted,#6b7280)]">
951
+ {PROFILE_DESCRIPTIONS[profileBase]}
952
+ </div>
953
+ )}
954
+
955
+ <div className="grid grid-cols-2 gap-2">
956
+ <TextInput
957
+ label="TTL Override"
958
+ value={profileTtlSeconds}
959
+ onChange={(event) => setProfileTtlSeconds(event.target.value)}
960
+ placeholder="seconds"
961
+ compact
962
+ />
963
+ <TextInput
964
+ label="Max Reads"
965
+ value={profileMaxReads}
966
+ onChange={(event) => setProfileMaxReads(event.target.value)}
967
+ placeholder="count"
968
+ compact
969
+ />
970
+ </div>
971
+
972
+ <div className="space-y-1">
973
+ <div className="grid grid-cols-[minmax(0,1fr)_auto] items-end gap-1.5">
974
+ <FilterDropdown
975
+ label="Scope"
976
+ options={TOKEN_PERMISSION_OPTIONS.map((option) => ({ value: option.value, label: option.label }))}
977
+ value={profileScopeCandidate}
978
+ onChange={setProfileScopeCandidate}
979
+ compact
980
+ />
981
+ <Button
982
+ variant="secondary"
983
+ size="sm"
984
+ onClick={() => addProfileScope(profileScopeCandidate)}
985
+ >
986
+ ADD
987
+ </Button>
988
+ </div>
989
+ {profileScopes.length === 0 ? (
990
+ <div className="px-1 text-[8px] text-[var(--color-text-faint,#9ca3af)]">
991
+ Using base profile scope defaults.
992
+ </div>
993
+ ) : (
994
+ <div className="flex flex-wrap gap-1">
995
+ {profileScopes.map((scope) => (
996
+ <span
997
+ key={`scope-${scope}`}
998
+ className="inline-flex items-center gap-1 border border-[var(--color-border,#d4d4d8)] bg-[var(--color-background,#f4f4f5)] px-1.5 py-0.5 text-[8px] text-[var(--color-text,#0a0a0a)]"
999
+ >
1000
+ {scope}
1001
+ <Button
1002
+ variant="ghost"
1003
+ size="sm"
1004
+ onClick={() => removeProfileScope(scope)}
1005
+ className="h-4 px-1 text-[8px]"
1006
+ >
1007
+ X
1008
+ </Button>
1009
+ </span>
1010
+ ))}
1011
+ </div>
1012
+ )}
1013
+ </div>
1014
+
1015
+ <div className="px-1 text-[8px] text-[var(--color-text-faint,#9ca3af)]">
1016
+ Scope uses the same permission vocabulary as Issue API Key (for example: <code>secret:read</code>, <code>secret:write</code>, <code>wallet:create:hot</code>, <code>send:hot</code>).
1017
+ </div>
1018
+
1019
+ <TextInput
1020
+ label="Exclude Fields (CSV)"
1021
+ value={profileExcludeFields}
1022
+ onChange={(event) => setProfileExcludeFields(event.target.value)}
1023
+ placeholder="password, seedPhrase"
1024
+ compact
1025
+ />
1026
+
1027
+ {vaultScopeOptions.length > 1 && (
1028
+ <div className="space-y-1">
1029
+ <div className="grid grid-cols-[minmax(0,1fr)_auto] items-end gap-1.5">
1030
+ <FilterDropdown
1031
+ label="Vault Read/Write Scope"
1032
+ options={vaultScopeOptions}
1033
+ value={vaultScopeOptions[0]?.value || 'vault:*'}
1034
+ onChange={(value) => {
1035
+ if (!profileVaultScopes.includes(value)) {
1036
+ setProfileVaultScopes((prev) => [...prev, value]);
1037
+ }
1038
+ }}
1039
+ compact
1040
+ />
1041
+ <Button
1042
+ variant="secondary"
1043
+ size="sm"
1044
+ onClick={() => setProfileVaultScopes([])}
1045
+ >
1046
+ CLEAR
1047
+ </Button>
1048
+ </div>
1049
+ {profileVaultScopes.length === 0 ? (
1050
+ <div className="px-1 text-[8px] text-[var(--color-text-faint,#9ca3af)]">
1051
+ Using base profile vault defaults.
1052
+ </div>
1053
+ ) : (
1054
+ <div className="flex flex-wrap gap-1">
1055
+ {profileVaultScopes.map((scope) => (
1056
+ <span
1057
+ key={`pvs-${scope}`}
1058
+ className="inline-flex items-center gap-1 border border-[var(--color-border,#d4d4d8)] bg-[var(--color-background,#f4f4f5)] px-1.5 py-0.5 text-[8px] text-[var(--color-text,#0a0a0a)]"
1059
+ >
1060
+ {scope}
1061
+ <Button
1062
+ variant="ghost"
1063
+ size="sm"
1064
+ onClick={() => setProfileVaultScopes((prev) => prev.filter((s) => s !== scope))}
1065
+ className="h-4 px-1 text-[8px]"
1066
+ >
1067
+ X
1068
+ </Button>
1069
+ </span>
1070
+ ))}
1071
+ </div>
1072
+ )}
1073
+ <div className="px-1 text-[8px] text-[var(--color-text-faint,#9ca3af)]">
1074
+ Restricts which vaults this profile can read/write credentials from. Select specific vaults or leave empty for profile defaults.
1075
+ </div>
1076
+ </div>
1077
+ )}
1078
+ </div>
1079
+
1080
+ {profileError && (
1081
+ <div className="mt-2 text-[9px] text-[var(--color-danger,#ef4444)]">{profileError}</div>
1082
+ )}
1083
+ {profileNotice && (
1084
+ <div className="mt-2 text-[9px] text-[var(--color-success,#16a34a)]">{profileNotice}</div>
1085
+ )}
1086
+
1087
+ <div className="mt-3 flex gap-2">
1088
+ <Button size="sm" onClick={handleSaveProfile} icon={<ShieldCheck size={11} />}>
1089
+ {editingProfileId ? 'UPDATE PROFILE' : 'CREATE PROFILE'}
1090
+ </Button>
1091
+ {editingProfileId && (
1092
+ <Button variant="secondary" size="sm" onClick={resetProfileForm}>
1093
+ CANCEL
1094
+ </Button>
1095
+ )}
1096
+ </div>
1097
+
1098
+ <div className="mt-4 border-t border-[var(--color-border,#d4d4d8)] pt-3">
1099
+ <div className="mb-2 text-[9px] font-bold tracking-widest text-[var(--color-text-muted,#6b7280)]">
1100
+ SAVED PROFILES ({profiles.length})
1101
+ </div>
1102
+ {profiles.length === 0 ? (
1103
+ <div className="text-[9px] text-[var(--color-text-faint,#9ca3af)]">No custom profiles saved yet.</div>
1104
+ ) : (
1105
+ <div className="space-y-2">
1106
+ {profiles.map((profile) => (
1107
+ <div key={profile.id} className="flex items-center justify-between border border-[var(--color-border,#d4d4d8)] bg-[var(--color-background,#f4f4f5)] px-2 py-1.5">
1108
+ <Button
1109
+ variant="ghost"
1110
+ size="sm"
1111
+ onClick={() => handleEditProfile(profile)}
1112
+ className="h-auto min-h-0 w-full justify-start px-0 py-0 text-left hover:bg-transparent"
1113
+ >
1114
+ <span className="block">
1115
+ <span className="block text-[10px] text-[var(--color-text,#0a0a0a)]">{profile.name}</span>
1116
+ <span className="block text-[8px] text-[var(--color-text-faint,#9ca3af)]">{profile.profile}@{profile.profileVersion}</span>
1117
+ </span>
1118
+ </Button>
1119
+ <Button
1120
+ variant="ghost"
1121
+ size="sm"
1122
+ onClick={() => handleDeleteProfile(profile.id)}
1123
+ icon={<Trash2 size={10} />}
1124
+ >
1125
+ {''}
1126
+ </Button>
1127
+ </div>
1128
+ ))}
1129
+ </div>
1130
+ )}
1131
+ </div>
1132
+ </section>
1133
+ </div>
1134
+
1135
+ <div className="mt-4 grid grid-cols-1 gap-4 xl:grid-cols-2">
1136
+ <section className="border border-[var(--color-border,#d4d4d8)] bg-[var(--color-surface,#fff)] p-3">
1137
+ <div className="mb-2 text-[10px] font-bold tracking-widest text-[var(--color-text,#0a0a0a)]">PENDING AUTH REQUESTS</div>
1138
+ {requestNotice && (
1139
+ <div className="mb-2 text-[9px] text-[var(--color-text-muted,#6b7280)]">{requestNotice}</div>
1140
+ )}
1141
+ {pendingAuthRequests.length === 0 ? (
1142
+ <div className="text-[9px] text-[var(--color-text-faint,#9ca3af)]">No pending auth requests.</div>
1143
+ ) : (
1144
+ <div className="space-y-2">
1145
+ {pendingAuthRequests.map((request) => {
1146
+ const metadata = parseMetadata(request.metadata);
1147
+ const agentId = typeof metadata.agentId === 'string' ? metadata.agentId : 'unknown-agent';
1148
+ const profileObj = typeof metadata.profile === 'object' && metadata.profile && 'id' in metadata.profile
1149
+ ? metadata.profile as Record<string, unknown>
1150
+ : null;
1151
+ const profileLabel = profileObj
1152
+ ? `${String(profileObj.id)}@${String(profileObj.version || 'v1')}`
1153
+ : 'n/a';
1154
+ const profileDesc = profileObj ? describeProfileId(String(profileObj.id)) : null;
1155
+ const perms = Array.isArray(metadata.permissions) ? metadata.permissions as string[] : [];
1156
+ const resolving = actionLoading === `resolve-${request.id}`;
1157
+ return (
1158
+ <div key={request.id} className="rounded border border-[var(--color-border,#d4d4d8)] bg-[var(--color-background,#f4f4f5)] p-2">
1159
+ <div className="text-[9px] text-[var(--color-text,#0a0a0a)]">
1160
+ {agentId} · {request.type}
1161
+ </div>
1162
+ <div className="mt-1 text-[8px] text-[var(--color-text-faint,#9ca3af)]">
1163
+ {profileLabel} · {new Date(request.createdAt).toLocaleString()}
1164
+ </div>
1165
+ {profileDesc && (
1166
+ <div className="mt-1 text-[8px] text-[var(--color-text-muted,#6b7280)]">
1167
+ {profileDesc}
1168
+ </div>
1169
+ )}
1170
+ {perms.length > 0 && (
1171
+ <div className="mt-1 flex flex-wrap gap-1">
1172
+ {perms.slice(0, 6).map((p) => (
1173
+ <span key={p} className="border border-[var(--color-border,#d4d4d8)] px-1 py-0.5 text-[7px] text-[var(--color-text-muted,#6b7280)]">
1174
+ {String(p)}
1175
+ </span>
1176
+ ))}
1177
+ {perms.length > 6 && (
1178
+ <span className="px-1 py-0.5 text-[7px] text-[var(--color-text-faint,#9ca3af)]">
1179
+ +{perms.length - 6} more
1180
+ </span>
1181
+ )}
1182
+ </div>
1183
+ )}
1184
+ <div className="mt-2 flex gap-2">
1185
+ <Button
1186
+ size="sm"
1187
+ onClick={() => { void handleResolveRequest(request.id, true); }}
1188
+ loading={resolving}
1189
+ icon={!resolving ? <Check size={10} /> : undefined}
1190
+ >
1191
+ APPROVE
1192
+ </Button>
1193
+ <Button
1194
+ size="sm"
1195
+ variant="secondary"
1196
+ onClick={() => { void handleResolveRequest(request.id, false); }}
1197
+ disabled={resolving}
1198
+ icon={<AlertTriangle size={10} />}
1199
+ >
1200
+ REJECT
1201
+ </Button>
1202
+ </div>
1203
+ </div>
1204
+ );
1205
+ })}
1206
+ </div>
1207
+ )}
1208
+ </section>
1209
+
1210
+ <section className="border border-[var(--color-border,#d4d4d8)] bg-[var(--color-surface,#fff)] p-3">
1211
+ <div className="mb-2 text-[10px] font-bold tracking-widest text-[var(--color-text,#0a0a0a)]">ISSUED API KEYS</div>
1212
+
1213
+ <div className="mb-2 text-[8px] tracking-widest text-[var(--color-text-faint,#9ca3af)]">
1214
+ ACTIVE ({managedActiveTokens.length})
1215
+ </div>
1216
+ {managedActiveTokens.length === 0 ? (
1217
+ <div className="mb-3 text-[9px] text-[var(--color-text-faint,#9ca3af)]">No active non-admin tokens.</div>
1218
+ ) : (
1219
+ <div className="mb-3 space-y-2">
1220
+ {managedActiveTokens.map((token) => {
1221
+ const revoking = actionLoading === `revoke-${token.tokenHash}`;
1222
+ return (
1223
+ <div key={token.tokenHash} className="rounded border border-[var(--color-border,#d4d4d8)] bg-[var(--color-background,#f4f4f5)] p-2">
1224
+ <div className="flex items-center justify-between">
1225
+ <div className="text-[9px] text-[var(--color-text,#0a0a0a)]">{token.agentId}</div>
1226
+ <div className="text-[8px] text-[var(--color-text-faint,#9ca3af)]">{shortHash(token.tokenHash)}</div>
1227
+ </div>
1228
+ <div className="mt-1 text-[8px] text-[var(--color-text-faint,#9ca3af)]">
1229
+ perms: {token.permissions.length} · expires: {new Date(token.expiresAt).toLocaleString()}
1230
+ </div>
1231
+ <div className="mt-2">
1232
+ <Button
1233
+ size="sm"
1234
+ variant="secondary"
1235
+ onClick={() => { void onRevokeToken(token.tokenHash); }}
1236
+ disabled={revoking}
1237
+ icon={revoking ? <Loader2 size={10} className="animate-spin" /> : <Trash2 size={10} />}
1238
+ >
1239
+ REVOKE
1240
+ </Button>
1241
+ </div>
1242
+ </div>
1243
+ );
1244
+ })}
1245
+ </div>
1246
+ )}
1247
+
1248
+ <div className="mb-2 text-[8px] tracking-widest text-[var(--color-text-faint,#9ca3af)]">
1249
+ INACTIVE ({inactiveTokens.length})
1250
+ </div>
1251
+ {inactiveTokens.length === 0 ? (
1252
+ <div className="text-[9px] text-[var(--color-text-faint,#9ca3af)]">No inactive tokens.</div>
1253
+ ) : (
1254
+ <div className="space-y-1.5">
1255
+ {inactiveTokens.slice(0, 8).map((token) => (
1256
+ <div key={`${token.agentId}-${token.tokenHash}`} className="flex items-center justify-between rounded border border-[var(--color-border,#d4d4d8)] px-2 py-1 text-[8px] text-[var(--color-text-muted,#6b7280)]">
1257
+ <span>{token.agentId}</span>
1258
+ <span>{shortHash(token.tokenHash)}</span>
1259
+ </div>
1260
+ ))}
1261
+ {inactiveTokens.length > 8 && (
1262
+ <div className="text-[8px] text-[var(--color-text-faint,#9ca3af)]">
1263
+ +{inactiveTokens.length - 8} more inactive tokens
1264
+ </div>
1265
+ )}
1266
+ </div>
1267
+ )}
1268
+ </section>
1269
+ </div>
1270
+ </div>
1271
+ );
1272
+ };