auramaxx 1.0.0-alpha.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (363) hide show
  1. package/LICENSE +26 -0
  2. package/README.md +112 -0
  3. package/bin/aurawallet.js +121 -0
  4. package/docs/ADAPTERS.md +467 -0
  5. package/docs/API.md +2679 -0
  6. package/docs/APPS.md +198 -0
  7. package/docs/ARCHITECTURE.md +350 -0
  8. package/docs/AUTH.md +698 -0
  9. package/docs/BEST-PRACTICES.md +121 -0
  10. package/docs/CLI.md +61 -0
  11. package/docs/DEVELOPING-APPS.md +452 -0
  12. package/docs/EXTENSION.md +97 -0
  13. package/docs/JOBS.md +33 -0
  14. package/docs/MCP.md +76 -0
  15. package/docs/PROTOCOL.md +142 -0
  16. package/docs/SETUP.md +219 -0
  17. package/docs/WORKSPACE.md +672 -0
  18. package/docs/agent-auth.md +63 -0
  19. package/docs/aura-file.md +48 -0
  20. package/docs/credentials.md +53 -0
  21. package/docs/external/getting-started.md +65 -0
  22. package/docs/external/overview.md +45 -0
  23. package/docs/external/use-cases.md +48 -0
  24. package/docs/external/why-aura.md +35 -0
  25. package/docs/jobs/connect-agent.md +77 -0
  26. package/docs/jobs/migrate-from-dotenv.md +79 -0
  27. package/docs/jobs/recover-from-lockout.md +72 -0
  28. package/docs/jobs/secure-ci.md +63 -0
  29. package/docs/oauth2.md +42 -0
  30. package/docs/passkeys.md +60 -0
  31. package/docs/security.md +540 -0
  32. package/docs/specs/aura-open-protocol.md +61 -0
  33. package/docs/specs/aura-provider-plugin.md +24 -0
  34. package/docs/specs/aura-registry-model.md +31 -0
  35. package/docs/specs/fixtures/invalid-bad-key.aura +1 -0
  36. package/docs/specs/fixtures/invalid-bad-unicode-escape.aura +1 -0
  37. package/docs/specs/fixtures/invalid-duplicate-key.aura +2 -0
  38. package/docs/specs/fixtures/valid-basic.aura +4 -0
  39. package/docs/specs/fixtures/valid-provider-ref.aura +1 -0
  40. package/docs/specs/fixtures/valid-quoted-escapes.aura +2 -0
  41. package/docs/templates/RELEASE_NOTES_TEMPLATE.md +22 -0
  42. package/docs/totp.md +40 -0
  43. package/docs/wallet/AI.md +508 -0
  44. package/docs/wallet/DEVELOPING-STRATEGIES.md +713 -0
  45. package/docs/wallet/README.md +47 -0
  46. package/docs/wallet/STRATEGY.md +89 -0
  47. package/next.config.ts +21 -0
  48. package/package.json +151 -0
  49. package/postcss.config.mjs +8 -0
  50. package/prisma/migrations/20260214170000_baseline/migration.sql +511 -0
  51. package/prisma/migrations/20260216214537_add_passkey_model/migration.sql +18 -0
  52. package/prisma/migrations/20260217150500_add_credential_access_audit/migration.sql +31 -0
  53. package/prisma/migrations/migration_lock.toml +3 -0
  54. package/prisma/schema.prisma +447 -0
  55. package/public/logo-chevron.svg +31 -0
  56. package/public/logo-concentric.svg +31 -0
  57. package/public/logo-crosshatch.svg +39 -0
  58. package/public/logo-dashed.svg +39 -0
  59. package/public/logo-horizontal.svg +31 -0
  60. package/public/logo-m56.svg +64 -0
  61. package/public/logo.webp +0 -0
  62. package/scripts/add-app.js +245 -0
  63. package/scripts/init.sh +57 -0
  64. package/scripts/migrate-apikeys-to-credentials.ts +35 -0
  65. package/scripts/sandbox-agent-flow.sh +235 -0
  66. package/scripts/sandbox.sh +175 -0
  67. package/scripts/validate-job-docs.mjs +125 -0
  68. package/server/abi/SwapHelper.json +438 -0
  69. package/server/cli/approval.ts +447 -0
  70. package/server/cli/commands/app.ts +204 -0
  71. package/server/cli/commands/cron.ts +24 -0
  72. package/server/cli/commands/doctor.ts +1007 -0
  73. package/server/cli/commands/env.ts +456 -0
  74. package/server/cli/commands/init.ts +752 -0
  75. package/server/cli/commands/mcp.ts +125 -0
  76. package/server/cli/commands/restore.ts +314 -0
  77. package/server/cli/commands/shell-hook.ts +468 -0
  78. package/server/cli/commands/start.ts +62 -0
  79. package/server/cli/commands/status.ts +59 -0
  80. package/server/cli/commands/stop.ts +14 -0
  81. package/server/cli/commands/token.ts +180 -0
  82. package/server/cli/commands/unlock.ts +49 -0
  83. package/server/cli/commands/vault.ts +417 -0
  84. package/server/cli/index.ts +328 -0
  85. package/server/cli/lib/aura-parser.ts +64 -0
  86. package/server/cli/lib/credential-create.ts +74 -0
  87. package/server/cli/lib/credential-resolve.ts +254 -0
  88. package/server/cli/lib/dotenv-migrate.ts +116 -0
  89. package/server/cli/lib/dotenv-parser.ts +146 -0
  90. package/server/cli/lib/http.ts +91 -0
  91. package/server/cli/lib/init-steps.ts +76 -0
  92. package/server/cli/lib/local-agent-trust.ts +45 -0
  93. package/server/cli/lib/process.ts +136 -0
  94. package/server/cli/lib/prompt.ts +85 -0
  95. package/server/cli/lib/theme.ts +240 -0
  96. package/server/cli/socket.ts +570 -0
  97. package/server/cli/transport-client.ts +50 -0
  98. package/server/cron/index.ts +137 -0
  99. package/server/cron/job.ts +31 -0
  100. package/server/cron/jobs/balance-sync.ts +436 -0
  101. package/server/cron/jobs/incoming-scan.ts +506 -0
  102. package/server/cron/jobs/native-price.ts +70 -0
  103. package/server/cron/jobs/orphan-cleanup.ts +40 -0
  104. package/server/cron/jobs/strategy-runner.ts +175 -0
  105. package/server/cron/scheduler.ts +125 -0
  106. package/server/index.ts +406 -0
  107. package/server/lib/adapters/factory.ts +110 -0
  108. package/server/lib/adapters/index.ts +19 -0
  109. package/server/lib/adapters/router.ts +297 -0
  110. package/server/lib/adapters/telegram.ts +645 -0
  111. package/server/lib/adapters/types.ts +89 -0
  112. package/server/lib/adapters/webhook.ts +95 -0
  113. package/server/lib/address.ts +49 -0
  114. package/server/lib/agent-auth/contracts.ts +1194 -0
  115. package/server/lib/agent-profiles.ts +328 -0
  116. package/server/lib/ai.ts +285 -0
  117. package/server/lib/api-registry/contracts.ts +86 -0
  118. package/server/lib/api-registry/validation.ts +172 -0
  119. package/server/lib/apikey-migration.ts +189 -0
  120. package/server/lib/app-installer.ts +505 -0
  121. package/server/lib/app-tokens.ts +247 -0
  122. package/server/lib/auth.ts +314 -0
  123. package/server/lib/batch.ts +242 -0
  124. package/server/lib/cold.ts +874 -0
  125. package/server/lib/config.ts +381 -0
  126. package/server/lib/credential-access-audit.ts +85 -0
  127. package/server/lib/credential-access-policy.ts +110 -0
  128. package/server/lib/credential-health.ts +343 -0
  129. package/server/lib/credential-import.ts +487 -0
  130. package/server/lib/credential-scope.ts +87 -0
  131. package/server/lib/credential-shares.ts +190 -0
  132. package/server/lib/credential-transport.ts +342 -0
  133. package/server/lib/credential-vault.ts +77 -0
  134. package/server/lib/credentials.ts +333 -0
  135. package/server/lib/crypto.ts +8 -0
  136. package/server/lib/db.ts +15 -0
  137. package/server/lib/defaults.ts +366 -0
  138. package/server/lib/dex/index.ts +80 -0
  139. package/server/lib/dex/relay.ts +235 -0
  140. package/server/lib/dex/types.ts +59 -0
  141. package/server/lib/dex/uniswap.ts +370 -0
  142. package/server/lib/e2e-agent/artifacts.ts +36 -0
  143. package/server/lib/e2e-agent/contracts.ts +112 -0
  144. package/server/lib/e2e-agent/validation.ts +135 -0
  145. package/server/lib/encrypt.ts +128 -0
  146. package/server/lib/error.ts +20 -0
  147. package/server/lib/events.ts +205 -0
  148. package/server/lib/hot.ts +357 -0
  149. package/server/lib/key-fingerprint.ts +28 -0
  150. package/server/lib/logger.ts +331 -0
  151. package/server/lib/network.ts +137 -0
  152. package/server/lib/notifications.ts +219 -0
  153. package/server/lib/oauth2-refresh.ts +241 -0
  154. package/server/lib/oursecret.ts +54 -0
  155. package/server/lib/passkey-credential.ts +360 -0
  156. package/server/lib/passkey.ts +68 -0
  157. package/server/lib/permissions.ts +248 -0
  158. package/server/lib/pino.ts +24 -0
  159. package/server/lib/policy-preview.ts +138 -0
  160. package/server/lib/price.ts +338 -0
  161. package/server/lib/prices.ts +34 -0
  162. package/server/lib/project-scope.ts +239 -0
  163. package/server/lib/resolve-action.ts +427 -0
  164. package/server/lib/resolve.ts +36 -0
  165. package/server/lib/sessions.ts +632 -0
  166. package/server/lib/solana/connection.ts +26 -0
  167. package/server/lib/solana/jupiter.ts +128 -0
  168. package/server/lib/solana/transfer.ts +108 -0
  169. package/server/lib/solana/wallet.ts +136 -0
  170. package/server/lib/strategy/emits.ts +21 -0
  171. package/server/lib/strategy/engine.ts +1305 -0
  172. package/server/lib/strategy/executor.ts +115 -0
  173. package/server/lib/strategy/hook-context.ts +158 -0
  174. package/server/lib/strategy/hooks.ts +990 -0
  175. package/server/lib/strategy/index.ts +28 -0
  176. package/server/lib/strategy/installer.ts +305 -0
  177. package/server/lib/strategy/loader.ts +256 -0
  178. package/server/lib/strategy/message.ts +235 -0
  179. package/server/lib/strategy/repository.ts +218 -0
  180. package/server/lib/strategy/session-logger.ts +693 -0
  181. package/server/lib/strategy/sources.ts +288 -0
  182. package/server/lib/strategy/state.ts +189 -0
  183. package/server/lib/strategy/templates.ts +403 -0
  184. package/server/lib/strategy/tick.ts +404 -0
  185. package/server/lib/strategy/types.ts +230 -0
  186. package/server/lib/swap.ts +3 -0
  187. package/server/lib/temp.ts +86 -0
  188. package/server/lib/token-metadata.ts +86 -0
  189. package/server/lib/token-safety.ts +200 -0
  190. package/server/lib/token-search.ts +444 -0
  191. package/server/lib/totp.ts +194 -0
  192. package/server/lib/transactions.ts +123 -0
  193. package/server/lib/transport.ts +75 -0
  194. package/server/lib/txhistory/decoder.ts +262 -0
  195. package/server/lib/txhistory/enricher.ts +652 -0
  196. package/server/lib/txhistory/index.ts +391 -0
  197. package/server/lib/txhistory/signatures.ts +59 -0
  198. package/server/lib/verified-summary.ts +421 -0
  199. package/server/mcp/profile-policy.ts +30 -0
  200. package/server/mcp/server.ts +619 -0
  201. package/server/mcp/tools.ts +523 -0
  202. package/server/middleware/auth.ts +119 -0
  203. package/server/middleware/requestLogger.ts +84 -0
  204. package/server/routes/actions.ts +459 -0
  205. package/server/routes/adapters.ts +703 -0
  206. package/server/routes/addressbook.ts +113 -0
  207. package/server/routes/ai.ts +34 -0
  208. package/server/routes/apikeys.ts +295 -0
  209. package/server/routes/apps.ts +601 -0
  210. package/server/routes/auth.ts +457 -0
  211. package/server/routes/backup.ts +340 -0
  212. package/server/routes/batch.ts +270 -0
  213. package/server/routes/bookmarks.ts +162 -0
  214. package/server/routes/credential-shares.ts +198 -0
  215. package/server/routes/credential-vaults.ts +154 -0
  216. package/server/routes/credentials.ts +1290 -0
  217. package/server/routes/dashboard.ts +71 -0
  218. package/server/routes/defaults.ts +124 -0
  219. package/server/routes/fund.ts +229 -0
  220. package/server/routes/import.ts +352 -0
  221. package/server/routes/launch.ts +665 -0
  222. package/server/routes/lock.ts +54 -0
  223. package/server/routes/logs.ts +68 -0
  224. package/server/routes/nuke.ts +111 -0
  225. package/server/routes/passkey-credentials.ts +99 -0
  226. package/server/routes/passkey.ts +346 -0
  227. package/server/routes/portfolio.ts +217 -0
  228. package/server/routes/price.ts +63 -0
  229. package/server/routes/resolve.ts +31 -0
  230. package/server/routes/security.ts +45 -0
  231. package/server/routes/send-evm.ts +241 -0
  232. package/server/routes/send-solana.ts +281 -0
  233. package/server/routes/send.ts +178 -0
  234. package/server/routes/setup.ts +210 -0
  235. package/server/routes/strategy.ts +894 -0
  236. package/server/routes/swap-evm.ts +353 -0
  237. package/server/routes/swap-solana.ts +177 -0
  238. package/server/routes/swap.ts +356 -0
  239. package/server/routes/token.ts +247 -0
  240. package/server/routes/unlock.ts +403 -0
  241. package/server/routes/wallet-assets.ts +361 -0
  242. package/server/routes/wallet-transactions.ts +515 -0
  243. package/server/routes/wallet.ts +710 -0
  244. package/server/types.ts +146 -0
  245. package/skills/aurawallet/SKILL.md +739 -0
  246. package/skills/aurawallet-setup/SKILL.md +74 -0
  247. package/skills/security-review/SKILL.md +148 -0
  248. package/src/app/api/agent-requests/route.ts +30 -0
  249. package/src/app/api/apps/install/route.ts +126 -0
  250. package/src/app/api/apps/manifests/route.ts +16 -0
  251. package/src/app/api/apps/static/[...path]/route.ts +57 -0
  252. package/src/app/api/events/route.ts +92 -0
  253. package/src/app/api/page.tsx +212 -0
  254. package/src/app/api/workspace/[id]/apps/[wid]/route.ts +119 -0
  255. package/src/app/api/workspace/[id]/apps/route.ts +81 -0
  256. package/src/app/api/workspace/[id]/export/route.ts +67 -0
  257. package/src/app/api/workspace/[id]/route.ts +168 -0
  258. package/src/app/api/workspace/auth.ts +34 -0
  259. package/src/app/api/workspace/config/route.ts +106 -0
  260. package/src/app/api/workspace/import/route.ts +127 -0
  261. package/src/app/api/workspace/route.ts +116 -0
  262. package/src/app/app/page.tsx +2122 -0
  263. package/src/app/apple-icon.png +0 -0
  264. package/src/app/docs/page.tsx +178 -0
  265. package/src/app/favicon.ico +0 -0
  266. package/src/app/globals.css +572 -0
  267. package/src/app/health/page.tsx +5 -0
  268. package/src/app/hello/page.tsx +15 -0
  269. package/src/app/icon.png +0 -0
  270. package/src/app/layout.tsx +34 -0
  271. package/src/app/page.tsx +986 -0
  272. package/src/app/providers.tsx +90 -0
  273. package/src/app/share/[token]/page.tsx +295 -0
  274. package/src/components/ChainSelector.tsx +144 -0
  275. package/src/components/HumanActionBar.tsx +695 -0
  276. package/src/components/NotificationDrawer.tsx +129 -0
  277. package/src/components/apps/AgentKeysApp.tsx +490 -0
  278. package/src/components/apps/App.tsx +153 -0
  279. package/src/components/apps/AppGrid.tsx +15 -0
  280. package/src/components/apps/DetailedAddressDrawer.tsx +325 -0
  281. package/src/components/apps/DraggableApp.tsx +562 -0
  282. package/src/components/apps/IFrameApp.tsx +73 -0
  283. package/src/components/apps/LogsApp.tsx +360 -0
  284. package/src/components/apps/SendApp.tsx +394 -0
  285. package/src/components/apps/SetupWizardApp.tsx +1004 -0
  286. package/src/components/apps/SystemDefaultsApp.tsx +845 -0
  287. package/src/components/apps/ThirdPartyApp.tsx +428 -0
  288. package/src/components/apps/TokenApp.tsx +319 -0
  289. package/src/components/apps/TransactionsApp.tsx +438 -0
  290. package/src/components/apps/WalletDetailApp.tsx +1505 -0
  291. package/src/components/apps/index.ts +13 -0
  292. package/src/components/design-system/Button.tsx +53 -0
  293. package/src/components/design-system/ChainIndicator.tsx +65 -0
  294. package/src/components/design-system/ChainSelector.tsx +137 -0
  295. package/src/components/design-system/ConfirmationModal.tsx +106 -0
  296. package/src/components/design-system/ConfirmationPopover.tsx +81 -0
  297. package/src/components/design-system/Drawer.tsx +123 -0
  298. package/src/components/design-system/FilterDropdown.tsx +72 -0
  299. package/src/components/design-system/Modal.tsx +206 -0
  300. package/src/components/design-system/Popover.tsx +142 -0
  301. package/src/components/design-system/TextInput.tsx +85 -0
  302. package/src/components/design-system/Toggle.tsx +58 -0
  303. package/src/components/design-system/index.ts +11 -0
  304. package/src/components/docs/DocsThemeToggle.tsx +49 -0
  305. package/src/components/health/CredentialHealthDashboard.tsx +214 -0
  306. package/src/components/icons/ChainIcons.tsx +72 -0
  307. package/src/components/layout/AppStoreDrawer.tsx +369 -0
  308. package/src/components/layout/ContentArea.tsx +21 -0
  309. package/src/components/layout/TabBar.tsx +278 -0
  310. package/src/components/layout/WalletSidebar.tsx +1033 -0
  311. package/src/components/layout/index.ts +4 -0
  312. package/src/components/marketing/AuraWalletSpecOverlay.tsx +635 -0
  313. package/src/components/marketing/DeviceMorphExperience.tsx +216 -0
  314. package/src/components/vault/ApiKeysConsole.tsx +1080 -0
  315. package/src/components/vault/AuditConsole.tsx +584 -0
  316. package/src/components/vault/CredentialDetail.tsx +455 -0
  317. package/src/components/vault/CredentialEmpty.tsx +55 -0
  318. package/src/components/vault/CredentialField.tsx +361 -0
  319. package/src/components/vault/CredentialForm.tsx +1212 -0
  320. package/src/components/vault/CredentialList.tsx +165 -0
  321. package/src/components/vault/CredentialRow.tsx +97 -0
  322. package/src/components/vault/CredentialShareModal.tsx +178 -0
  323. package/src/components/vault/CredentialVault.tsx +754 -0
  324. package/src/components/vault/CredentialWalletWidget.tsx +103 -0
  325. package/src/components/vault/ImportCredentialsModal.tsx +515 -0
  326. package/src/components/vault/LargeTypeModal.tsx +64 -0
  327. package/src/components/vault/PasswordGenerator.tsx +224 -0
  328. package/src/components/vault/TOTPDisplay.tsx +123 -0
  329. package/src/components/vault/VaultSidebar.tsx +413 -0
  330. package/src/components/vault/types.ts +54 -0
  331. package/src/context/AuthContext.tsx +337 -0
  332. package/src/context/PriceContext.tsx +113 -0
  333. package/src/context/ThemeContext.tsx +164 -0
  334. package/src/context/WebSocketContext.tsx +269 -0
  335. package/src/context/WorkspaceContext.tsx +668 -0
  336. package/src/hooks/index.ts +3 -0
  337. package/src/hooks/useAgentActions.ts +368 -0
  338. package/src/hooks/useBalance.ts +103 -0
  339. package/src/hooks/useBalances.ts +129 -0
  340. package/src/instrumentation.ts +12 -0
  341. package/src/lib/api.ts +449 -0
  342. package/src/lib/app-loader.ts +148 -0
  343. package/src/lib/app-registry.ts +178 -0
  344. package/src/lib/app-sdk.ts +157 -0
  345. package/src/lib/audit-console-adapter.ts +151 -0
  346. package/src/lib/auth-client.ts +75 -0
  347. package/src/lib/config.ts +74 -0
  348. package/src/lib/crypto.ts +112 -0
  349. package/src/lib/db.ts +21 -0
  350. package/src/lib/docs.ts +390 -0
  351. package/src/lib/events.ts +361 -0
  352. package/src/lib/pino.ts +24 -0
  353. package/src/lib/theme-handlers.ts +168 -0
  354. package/src/lib/theme.ts +351 -0
  355. package/src/lib/tokenData.ts +378 -0
  356. package/src/lib/vault-crypto.ts +129 -0
  357. package/src/lib/websocket-server.ts +302 -0
  358. package/src/lib/websocket-setup.ts +79 -0
  359. package/src/lib/wordlist.ts +2050 -0
  360. package/src/lib/workspace-handlers.ts +285 -0
  361. package/start.sh +80 -0
  362. package/tailwind.config.ts +99 -0
  363. package/tsconfig.json +42 -0
@@ -0,0 +1,1080 @@
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
+
10
+ const BUILTIN_PROFILE_OPTIONS = [
11
+ { id: 'strict', label: 'Strict' },
12
+ { id: 'dev', label: 'Dev (recommended)' },
13
+ { id: 'admin', label: 'Admin (dangerous)' },
14
+ { id: 'observer', label: 'Observer' },
15
+ { id: 'deploy-bot', label: 'Deploy Bot' },
16
+ { id: 'rotation-bot', label: 'Rotation Bot' },
17
+ { id: 'trader', label: 'Trader' },
18
+ ] as const;
19
+
20
+ const CREDENTIAL_SCOPE_OPTIONS = [
21
+ { value: '*', label: 'All Credentials (*)' },
22
+ { value: 'vault:*', label: 'All Vaults (vault:*)' },
23
+ { value: 'tag:deploy', label: 'Tag · deploy' },
24
+ { value: 'tag:runtime', label: 'Tag · runtime' },
25
+ { value: 'tag:rotation', label: 'Tag · rotation' },
26
+ { value: 'tag:trading', label: 'Tag · trading' },
27
+ { value: 'tag:exchange', label: 'Tag · exchange' },
28
+ ] as const;
29
+
30
+ const TOKEN_PERMISSION_OPTIONS = [
31
+ { value: 'wallet:list', label: 'wallet:list' },
32
+ { value: 'wallet:create:hot', label: 'wallet:create:hot' },
33
+ { value: 'wallet:create:temp', label: 'wallet:create:temp' },
34
+ { value: 'wallet:rename', label: 'wallet:rename' },
35
+ { value: 'wallet:export', label: 'wallet:export' },
36
+ { value: 'wallet:tx:add', label: 'wallet:tx:add' },
37
+ { value: 'wallet:asset:add', label: 'wallet:asset:add' },
38
+ { value: 'wallet:asset:remove', label: 'wallet:asset:remove' },
39
+ { value: 'send:hot', label: 'send:hot' },
40
+ { value: 'send:temp', label: 'send:temp' },
41
+ { value: 'swap', label: 'swap' },
42
+ { value: 'fund', label: 'fund' },
43
+ { value: 'launch', label: 'launch' },
44
+ { value: 'apikey:get', label: 'apikey:get' },
45
+ { value: 'apikey:set', label: 'apikey:set' },
46
+ { value: 'workspace:modify', label: 'workspace:modify' },
47
+ { value: 'strategy:read', label: 'strategy:read' },
48
+ { value: 'strategy:manage', label: 'strategy:manage' },
49
+ { value: 'app:storage', label: 'app:storage' },
50
+ { value: 'app:storage:all', label: 'app:storage:all' },
51
+ { value: 'app:accesskey', label: 'app:accesskey' },
52
+ { value: 'action:create', label: 'action:create' },
53
+ { value: 'action:read', label: 'action:read' },
54
+ { value: 'action:resolve', label: 'action:resolve' },
55
+ { value: 'adapter:manage', label: 'adapter:manage' },
56
+ { value: 'addressbook:write', label: 'addressbook:write' },
57
+ { value: 'bookmark:write', label: 'bookmark:write' },
58
+ { value: 'secret:read', label: 'secret:read' },
59
+ { value: 'secret:write', label: 'secret:write' },
60
+ { value: 'totp:read', label: 'totp:read' },
61
+ { value: 'trade:all', label: 'trade:all' },
62
+ { value: 'wallet:write', label: 'wallet:write' },
63
+ { value: 'extension:*', label: 'extension:*' },
64
+ { value: 'admin:*', label: 'admin:* (dangerous)' },
65
+ ] as const;
66
+
67
+ const PROFILE_STORAGE_KEY = 'aura:api-keys:profiles:v1';
68
+
69
+ interface ProfileOverrides {
70
+ ttlSeconds?: number;
71
+ maxReads?: number;
72
+ readScopes?: string[];
73
+ writeScopes?: string[];
74
+ excludeFields?: string[];
75
+ }
76
+
77
+ interface LocalProfileDraft {
78
+ id: string;
79
+ name: string;
80
+ profile: string;
81
+ profileVersion: 'v1';
82
+ overrides: ProfileOverrides | null;
83
+ updatedAt: number;
84
+ }
85
+
86
+ interface PolicyPreviewResponse {
87
+ version: 'v1';
88
+ profile?: { id: string; version: string; displayName?: string };
89
+ effectivePolicy: {
90
+ permissions: string[];
91
+ credentialAccess: {
92
+ read: string[];
93
+ write: string[];
94
+ excludeFields: string[];
95
+ maxReads: number | null;
96
+ };
97
+ ttlSeconds: number;
98
+ maxReads: number | null;
99
+ rateBudget: {
100
+ state: 'none' | 'inherited' | 'explicit';
101
+ requests: number | null;
102
+ windowSeconds: number | null;
103
+ source: 'none' | 'profile' | 'override';
104
+ };
105
+ };
106
+ warnings: string[];
107
+ overrideDelta: string[];
108
+ denyExamples?: Array<{ code: string; message: string }>;
109
+ effectivePolicyHash: string;
110
+ }
111
+
112
+ interface IssueResponse {
113
+ success: boolean;
114
+ encryptedToken?: string;
115
+ warnings?: string[];
116
+ profile?: { id: string; version: string; displayName?: string };
117
+ }
118
+
119
+ interface ApiKeysConsoleProps {
120
+ requests: HumanAction[];
121
+ activeTokens: AgentToken[];
122
+ inactiveTokens: AgentToken[];
123
+ actionLoading: string | null;
124
+ onResolveAction: (id: string, approved: boolean) => Promise<{ success: boolean; message?: string }>;
125
+ onRevokeToken: (tokenHash: string) => Promise<boolean>;
126
+ }
127
+
128
+ function shortHash(value: string): string {
129
+ if (value.length <= 14) return value;
130
+ return `${value.slice(0, 8)}...${value.slice(-4)}`;
131
+ }
132
+
133
+ function generateSuggestedAgentId(): string {
134
+ const seed = Math.random().toString(36).slice(2, 8);
135
+ return `agent:local:${seed}`;
136
+ }
137
+
138
+ function normalizePreviewPayload(raw: unknown): PolicyPreviewResponse {
139
+ const data = (raw && typeof raw === 'object' ? raw : {}) as Record<string, unknown>;
140
+ const effectivePolicy = (data.effectivePolicy && typeof data.effectivePolicy === 'object'
141
+ ? data.effectivePolicy
142
+ : {}) as Record<string, unknown>;
143
+ const credentialAccess = (effectivePolicy.credentialAccess && typeof effectivePolicy.credentialAccess === 'object'
144
+ ? effectivePolicy.credentialAccess
145
+ : {}) as Record<string, unknown>;
146
+ const rateBudget = (effectivePolicy.rateBudget && typeof effectivePolicy.rateBudget === 'object'
147
+ ? effectivePolicy.rateBudget
148
+ : {}) as Record<string, unknown>;
149
+
150
+ return {
151
+ version: 'v1',
152
+ profile: data.profile && typeof data.profile === 'object'
153
+ ? data.profile as PolicyPreviewResponse['profile']
154
+ : undefined,
155
+ effectivePolicy: {
156
+ permissions: Array.isArray(effectivePolicy.permissions) ? effectivePolicy.permissions.filter((v): v is string => typeof v === 'string') : [],
157
+ credentialAccess: {
158
+ read: Array.isArray(credentialAccess.read) ? credentialAccess.read.filter((v): v is string => typeof v === 'string') : [],
159
+ write: Array.isArray(credentialAccess.write) ? credentialAccess.write.filter((v): v is string => typeof v === 'string') : [],
160
+ excludeFields: Array.isArray(credentialAccess.excludeFields) ? credentialAccess.excludeFields.filter((v): v is string => typeof v === 'string') : [],
161
+ maxReads: typeof credentialAccess.maxReads === 'number' ? credentialAccess.maxReads : null,
162
+ },
163
+ ttlSeconds: typeof effectivePolicy.ttlSeconds === 'number' ? effectivePolicy.ttlSeconds : 0,
164
+ maxReads: typeof effectivePolicy.maxReads === 'number' ? effectivePolicy.maxReads : null,
165
+ rateBudget: {
166
+ state: rateBudget.state === 'inherited' || rateBudget.state === 'explicit' ? rateBudget.state : 'none',
167
+ requests: typeof rateBudget.requests === 'number' ? rateBudget.requests : null,
168
+ windowSeconds: typeof rateBudget.windowSeconds === 'number' ? rateBudget.windowSeconds : null,
169
+ source: rateBudget.source === 'profile' || rateBudget.source === 'override' ? rateBudget.source : 'none',
170
+ },
171
+ },
172
+ warnings: Array.isArray(data.warnings) ? data.warnings.filter((v): v is string => typeof v === 'string') : [],
173
+ overrideDelta: Array.isArray(data.overrideDelta) ? data.overrideDelta.filter((v): v is string => typeof v === 'string') : [],
174
+ denyExamples: Array.isArray(data.denyExamples)
175
+ ? data.denyExamples
176
+ .filter((row): row is Record<string, unknown> => Boolean(row) && typeof row === 'object')
177
+ .map((row) => ({ code: String(row.code || 'UNKNOWN'), message: String(row.message || '') }))
178
+ : [],
179
+ effectivePolicyHash: typeof data.effectivePolicyHash === 'string' ? data.effectivePolicyHash : 'n/a',
180
+ };
181
+ }
182
+
183
+ function parseMetadata(raw?: string): Record<string, unknown> {
184
+ if (!raw) return {};
185
+ try {
186
+ const parsed = JSON.parse(raw) as unknown;
187
+ if (parsed && typeof parsed === 'object') return parsed as Record<string, unknown>;
188
+ return {};
189
+ } catch {
190
+ return {};
191
+ }
192
+ }
193
+
194
+ function parseCsv(value: string): string[] | undefined {
195
+ const parts = value
196
+ .split(',')
197
+ .map((part) => part.trim())
198
+ .filter(Boolean);
199
+ return parts.length > 0 ? parts : undefined;
200
+ }
201
+
202
+ function normalizeStringList(values: string[]): string[] {
203
+ return Array.from(
204
+ new Set(
205
+ values
206
+ .map((value) => value.trim().normalize('NFKC').toLowerCase())
207
+ .filter(Boolean),
208
+ ),
209
+ );
210
+ }
211
+
212
+ function buildOverrides(input: {
213
+ ttlSeconds: string;
214
+ maxReads: string;
215
+ readScopes: string[];
216
+ writeScopes: string[];
217
+ excludeFields: string;
218
+ }): { overrides: ProfileOverrides | null; error: string | null } {
219
+ const next: ProfileOverrides = {};
220
+
221
+ if (input.ttlSeconds.trim().length > 0) {
222
+ const ttl = Number(input.ttlSeconds.trim());
223
+ if (!Number.isFinite(ttl) || ttl <= 0) {
224
+ return { overrides: null, error: 'TTL override must be a positive number.' };
225
+ }
226
+ next.ttlSeconds = ttl;
227
+ }
228
+
229
+ if (input.maxReads.trim().length > 0) {
230
+ const maxReads = Number(input.maxReads.trim());
231
+ if (!Number.isFinite(maxReads) || maxReads <= 0) {
232
+ return { overrides: null, error: 'Max reads override must be a positive number.' };
233
+ }
234
+ next.maxReads = maxReads;
235
+ }
236
+
237
+ const readScopes = normalizeStringList(input.readScopes);
238
+ const writeScopes = normalizeStringList(input.writeScopes);
239
+ const excludeFields = parseCsv(input.excludeFields);
240
+
241
+ if (readScopes.length > 0) next.readScopes = readScopes;
242
+ if (writeScopes.length > 0) next.writeScopes = writeScopes;
243
+ if (excludeFields) next.excludeFields = excludeFields;
244
+
245
+ return { overrides: Object.keys(next).length > 0 ? next : null, error: null };
246
+ }
247
+
248
+ export const ApiKeysConsole: React.FC<ApiKeysConsoleProps> = ({
249
+ requests,
250
+ activeTokens,
251
+ inactiveTokens,
252
+ actionLoading,
253
+ onResolveAction,
254
+ onRevokeToken,
255
+ }) => {
256
+ const [profiles, setProfiles] = useState<LocalProfileDraft[]>([]);
257
+ const [editingProfileId, setEditingProfileId] = useState<string | null>(null);
258
+
259
+ const [profileName, setProfileName] = useState('');
260
+ const [profileBase, setProfileBase] = useState<string>('dev');
261
+ const [profileTtlSeconds, setProfileTtlSeconds] = useState('');
262
+ const [profileMaxReads, setProfileMaxReads] = useState('');
263
+ const [profileReadScopes, setProfileReadScopes] = useState<string[]>([]);
264
+ const [profileWriteScopes, setProfileWriteScopes] = useState<string[]>([]);
265
+ const [profileReadScopeCandidate, setProfileReadScopeCandidate] = useState<string>('vault:*');
266
+ const [profileWriteScopeCandidate, setProfileWriteScopeCandidate] = useState<string>('vault:*');
267
+ const [profileExcludeFields, setProfileExcludeFields] = useState('');
268
+
269
+ const [profileError, setProfileError] = useState<string | null>(null);
270
+ const [profileNotice, setProfileNotice] = useState<string | null>(null);
271
+
272
+ const [issueAgentId, setIssueAgentId] = useState<string>(() => generateSuggestedAgentId());
273
+ const [issueSource, setIssueSource] = useState<string>('builtin:dev');
274
+ const [issuePermissionCandidate, setIssuePermissionCandidate] = useState<string>('secret:read');
275
+ const [issuePermissions, setIssuePermissions] = useState<string[]>(['secret:read', 'secret:write']);
276
+ const [issuing, setIssuing] = useState(false);
277
+ const [issueError, setIssueError] = useState<string | null>(null);
278
+ const [issuedToken, setIssuedToken] = useState<string | null>(null);
279
+ const [issuedMeta, setIssuedMeta] = useState<string | null>(null);
280
+ const [copied, setCopied] = useState(false);
281
+
282
+ const [previewLoading, setPreviewLoading] = useState(false);
283
+ const [previewError, setPreviewError] = useState<string | null>(null);
284
+ const [preview, setPreview] = useState<PolicyPreviewResponse | null>(null);
285
+
286
+ const [requestNotice, setRequestNotice] = useState<string | null>(null);
287
+
288
+ useEffect(() => {
289
+ try {
290
+ const raw = localStorage.getItem(PROFILE_STORAGE_KEY);
291
+ if (!raw) return;
292
+ const parsed = JSON.parse(raw) as LocalProfileDraft[];
293
+ if (!Array.isArray(parsed)) return;
294
+ const safe = parsed.filter((entry) =>
295
+ entry
296
+ && typeof entry.id === 'string'
297
+ && typeof entry.name === 'string'
298
+ && typeof entry.profile === 'string'
299
+ && entry.profileVersion === 'v1'
300
+ && typeof entry.updatedAt === 'number'
301
+ );
302
+ setProfiles(safe);
303
+ } catch {
304
+ setProfiles([]);
305
+ }
306
+ }, []);
307
+
308
+ useEffect(() => {
309
+ localStorage.setItem(PROFILE_STORAGE_KEY, JSON.stringify(profiles));
310
+ }, [profiles]);
311
+
312
+ const resetProfileForm = useCallback(() => {
313
+ setEditingProfileId(null);
314
+ setProfileName('');
315
+ setProfileBase('dev');
316
+ setProfileTtlSeconds('');
317
+ setProfileMaxReads('');
318
+ setProfileReadScopes([]);
319
+ setProfileWriteScopes([]);
320
+ setProfileReadScopeCandidate('vault:*');
321
+ setProfileWriteScopeCandidate('vault:*');
322
+ setProfileExcludeFields('');
323
+ setProfileError(null);
324
+ setProfileNotice(null);
325
+ }, []);
326
+
327
+ const pendingAuthRequests = useMemo(
328
+ () => requests.filter((request) => request.status === 'pending' && (request.type === 'auth' || request.type === 'permission_update')),
329
+ [requests],
330
+ );
331
+
332
+ const selectableSources = useMemo(() => {
333
+ const manual = [{ value: 'manual', label: 'Manual · Permission Picker' }];
334
+ const builtin = BUILTIN_PROFILE_OPTIONS.map((option) => ({
335
+ value: `builtin:${option.id}`,
336
+ label: `Built-in · ${option.label}`,
337
+ }));
338
+ const custom = profiles.map((profile) => ({
339
+ value: `draft:${profile.id}`,
340
+ label: `Custom · ${profile.name}`,
341
+ }));
342
+ return [...manual, ...builtin, ...custom];
343
+ }, [profiles]);
344
+
345
+ useEffect(() => {
346
+ if (!selectableSources.some((option) => option.value === issueSource)) {
347
+ setIssueSource('builtin:dev');
348
+ }
349
+ }, [issueSource, selectableSources]);
350
+
351
+ const resolveIssueProfile = useCallback((): { profile: string; profileVersion: string; profileOverrides?: ProfileOverrides } => {
352
+ if (issueSource.startsWith('draft:')) {
353
+ const profileId = issueSource.slice(6);
354
+ const draft = profiles.find((entry) => entry.id === profileId);
355
+ if (draft) {
356
+ return {
357
+ profile: draft.profile,
358
+ profileVersion: draft.profileVersion,
359
+ ...(draft.overrides ? { profileOverrides: draft.overrides } : {}),
360
+ };
361
+ }
362
+ }
363
+ const profile = issueSource.startsWith('builtin:') ? issueSource.slice(8) : 'dev';
364
+ return { profile, profileVersion: 'v1' };
365
+ }, [issueSource, profiles]);
366
+
367
+ const addIssuePermission = useCallback((permission: string) => {
368
+ const normalized = normalizeStringList([permission])[0];
369
+ if (!normalized) return;
370
+ setIssuePermissions((prev) => (prev.includes(normalized) ? prev : [...prev, normalized]));
371
+ }, []);
372
+
373
+ const removeIssuePermission = useCallback((permission: string) => {
374
+ setIssuePermissions((prev) => prev.filter((entry) => entry !== permission));
375
+ }, []);
376
+
377
+ const handleSaveProfile = useCallback(() => {
378
+ setProfileError(null);
379
+ setProfileNotice(null);
380
+ const trimmedName = profileName.trim();
381
+ if (!trimmedName) {
382
+ setProfileError('Profile name is required.');
383
+ return;
384
+ }
385
+
386
+ const { overrides, error } = buildOverrides({
387
+ ttlSeconds: profileTtlSeconds,
388
+ maxReads: profileMaxReads,
389
+ readScopes: profileReadScopes,
390
+ writeScopes: profileWriteScopes,
391
+ excludeFields: profileExcludeFields,
392
+ });
393
+
394
+ if (error) {
395
+ setProfileError(error);
396
+ return;
397
+ }
398
+
399
+ const existingNameConflict = profiles.some((profile) =>
400
+ profile.name.toLowerCase() === trimmedName.toLowerCase()
401
+ && profile.id !== editingProfileId
402
+ );
403
+ if (existingNameConflict) {
404
+ setProfileError('A profile with this name already exists.');
405
+ return;
406
+ }
407
+
408
+ const payload: LocalProfileDraft = {
409
+ id: editingProfileId || `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 7)}`,
410
+ name: trimmedName,
411
+ profile: profileBase,
412
+ profileVersion: 'v1',
413
+ overrides,
414
+ updatedAt: Date.now(),
415
+ };
416
+
417
+ setProfiles((prev) => {
418
+ if (editingProfileId) {
419
+ return prev.map((entry) => (entry.id === editingProfileId ? payload : entry));
420
+ }
421
+ return [payload, ...prev];
422
+ });
423
+ setProfileNotice(editingProfileId ? 'Profile updated.' : 'Profile created.');
424
+ if (!editingProfileId) {
425
+ setIssueSource(`draft:${payload.id}`);
426
+ }
427
+ setEditingProfileId(payload.id);
428
+ }, [
429
+ editingProfileId,
430
+ profileBase,
431
+ profileExcludeFields,
432
+ profileMaxReads,
433
+ profileName,
434
+ profileReadScopes,
435
+ profileTtlSeconds,
436
+ profileWriteScopes,
437
+ profiles,
438
+ ]);
439
+
440
+ const handleEditProfile = useCallback((profile: LocalProfileDraft) => {
441
+ setEditingProfileId(profile.id);
442
+ setProfileName(profile.name);
443
+ setProfileBase(profile.profile);
444
+ setProfileTtlSeconds(profile.overrides?.ttlSeconds ? String(profile.overrides.ttlSeconds) : '');
445
+ setProfileMaxReads(profile.overrides?.maxReads ? String(profile.overrides.maxReads) : '');
446
+ setProfileReadScopes(profile.overrides?.readScopes || []);
447
+ setProfileWriteScopes(profile.overrides?.writeScopes || []);
448
+ setProfileReadScopeCandidate((profile.overrides?.readScopes || [])[0] || 'vault:*');
449
+ setProfileWriteScopeCandidate((profile.overrides?.writeScopes || [])[0] || 'vault:*');
450
+ setProfileExcludeFields((profile.overrides?.excludeFields || []).join(', '));
451
+ setProfileError(null);
452
+ setProfileNotice(null);
453
+ }, []);
454
+
455
+ const addProfileScope = useCallback((mode: 'read' | 'write', scope: string) => {
456
+ const normalized = normalizeStringList([scope])[0];
457
+ if (!normalized) return;
458
+ if (mode === 'read') {
459
+ setProfileReadScopes((prev) => (prev.includes(normalized) ? prev : [...prev, normalized]));
460
+ return;
461
+ }
462
+ setProfileWriteScopes((prev) => (prev.includes(normalized) ? prev : [...prev, normalized]));
463
+ }, []);
464
+
465
+ const removeProfileScope = useCallback((mode: 'read' | 'write', scope: string) => {
466
+ if (mode === 'read') {
467
+ setProfileReadScopes((prev) => prev.filter((entry) => entry !== scope));
468
+ return;
469
+ }
470
+ setProfileWriteScopes((prev) => prev.filter((entry) => entry !== scope));
471
+ }, []);
472
+
473
+ const handleDeleteProfile = useCallback((id: string) => {
474
+ setProfiles((prev) => prev.filter((profile) => profile.id !== id));
475
+ if (editingProfileId === id) {
476
+ resetProfileForm();
477
+ }
478
+ if (issueSource === `draft:${id}`) {
479
+ setIssueSource('builtin:dev');
480
+ }
481
+ }, [editingProfileId, issueSource, resetProfileForm]);
482
+
483
+ const handlePreview = useCallback(async () => {
484
+ setPreviewError(null);
485
+ setPreview(null);
486
+ if (issueSource === 'manual') {
487
+ setPreviewError('Policy preview is currently available for profile-based issuance only.');
488
+ return;
489
+ }
490
+ setPreviewLoading(true);
491
+ try {
492
+ const selection = resolveIssueProfile();
493
+ const data = await api.post<PolicyPreviewResponse>(Api.Wallet, '/actions/token/preview', selection);
494
+ setPreview(normalizePreviewPayload(data));
495
+ } catch (error) {
496
+ setPreviewError(error instanceof Error ? error.message : 'Failed to preview profile policy.');
497
+ } finally {
498
+ setPreviewLoading(false);
499
+ }
500
+ }, [issueSource, resolveIssueProfile]);
501
+
502
+ const handleIssueApiKey = useCallback(async () => {
503
+ setIssueError(null);
504
+ setIssuedToken(null);
505
+ setIssuedMeta(null);
506
+ if (!issueAgentId.trim()) {
507
+ setIssueError('Agent ID is required.');
508
+ return;
509
+ }
510
+ if (issueSource === 'manual' && issuePermissions.length === 0) {
511
+ setIssueError('Select at least one permission for manual issuance.');
512
+ return;
513
+ }
514
+ const pubkey = getVaultPublicKeyBase64();
515
+ if (!pubkey) {
516
+ setIssueError('Vault keypair is unavailable. Re-unlock and retry.');
517
+ return;
518
+ }
519
+
520
+ setIssuing(true);
521
+ try {
522
+ const result = issueSource === 'manual'
523
+ ? await api.post<IssueResponse>(Api.Wallet, '/actions/token', {
524
+ agentId: issueAgentId.trim(),
525
+ pubkey,
526
+ permissions: issuePermissions,
527
+ })
528
+ : await api.post<IssueResponse>(Api.Wallet, '/actions/token', {
529
+ agentId: issueAgentId.trim(),
530
+ pubkey,
531
+ ...resolveIssueProfile(),
532
+ });
533
+ if (!result.success || !result.encryptedToken) {
534
+ throw new Error('Token issuance failed.');
535
+ }
536
+ const token = await decryptCredentialPayload(result.encryptedToken);
537
+ setIssuedToken(token);
538
+ if (issueSource === 'manual') {
539
+ setIssuedMeta(
540
+ `${issueAgentId.trim()} · manual:${issuePermissions.join(',')}${result.warnings?.length ? ' · warnings present' : ''}`,
541
+ );
542
+ } else {
543
+ const selection = resolveIssueProfile();
544
+ setIssuedMeta(
545
+ `${issueAgentId.trim()} · ${selection.profile}@${selection.profileVersion}${result.warnings?.length ? ' · warnings present' : ''}`,
546
+ );
547
+ }
548
+ } catch (error) {
549
+ setIssueError(error instanceof Error ? error.message : 'Token issuance failed.');
550
+ } finally {
551
+ setIssuing(false);
552
+ }
553
+ }, [issueAgentId, issuePermissions, issueSource, resolveIssueProfile]);
554
+
555
+ const handleCopyToken = useCallback(async () => {
556
+ if (!issuedToken || !navigator.clipboard) return;
557
+ await navigator.clipboard.writeText(issuedToken);
558
+ setCopied(true);
559
+ setTimeout(() => setCopied(false), 1500);
560
+ }, [issuedToken]);
561
+
562
+ const handleResolveRequest = useCallback(async (id: string, approved: boolean) => {
563
+ const result = await onResolveAction(id, approved);
564
+ if (result.success) {
565
+ setRequestNotice(approved ? `Approved request ${id.slice(0, 8)}.` : `Rejected request ${id.slice(0, 8)}.`);
566
+ return;
567
+ }
568
+ setRequestNotice(result.message || 'Failed to resolve request.');
569
+ }, [onResolveAction]);
570
+
571
+ const managedActiveTokens = useMemo(
572
+ () => activeTokens.filter((token) => !token.isAdmin),
573
+ [activeTokens],
574
+ );
575
+
576
+ return (
577
+ <div className="h-full overflow-y-auto px-5 py-4 font-mono">
578
+ <div className="mb-4 border border-[var(--color-border,#d4d4d8)] bg-[var(--color-surface,#fff)] p-3">
579
+ <div className="text-[10px] font-bold tracking-widest text-[var(--color-text,#0a0a0a)]">API KEYS</div>
580
+ <div className="mt-1 text-[9px] text-[var(--color-text-muted,#6b7280)] leading-relaxed">
581
+ Auth management surface for profile templates + manual API key issuance + pending auth approvals.
582
+ </div>
583
+ </div>
584
+
585
+ <div className="grid grid-cols-1 gap-4 xl:grid-cols-2">
586
+ <section className="order-1 border border-[var(--color-border,#d4d4d8)] bg-[var(--color-surface,#fff)] p-3">
587
+ <div className="mb-2 text-[10px] font-bold tracking-widest text-[var(--color-text,#0a0a0a)]">ISSUE API KEY</div>
588
+ <div className="grid grid-cols-1 gap-2 md:grid-cols-2">
589
+ <div className="grid grid-cols-[minmax(0,1fr)_auto] items-end gap-1.5">
590
+ <TextInput
591
+ label="Agent ID"
592
+ value={issueAgentId}
593
+ onChange={(event) => setIssueAgentId(event.target.value)}
594
+ placeholder="agent:local:dev"
595
+ compact
596
+ />
597
+ <Button
598
+ variant="secondary"
599
+ size="sm"
600
+ onClick={() => setIssueAgentId(generateSuggestedAgentId())}
601
+ icon={<RefreshCw size={10} />}
602
+ title="Regenerate Agent ID"
603
+ className="mb-[1px]"
604
+ >
605
+ NEW ID
606
+ </Button>
607
+ </div>
608
+
609
+ <FilterDropdown
610
+ label="Profile Source"
611
+ options={selectableSources}
612
+ value={issueSource}
613
+ onChange={setIssueSource}
614
+ compact
615
+ />
616
+ </div>
617
+
618
+ <div className="mt-2 space-y-1">
619
+ <div className="grid grid-cols-[minmax(0,1fr)_auto] items-end gap-1.5">
620
+ <FilterDropdown
621
+ label="Permissions"
622
+ options={TOKEN_PERMISSION_OPTIONS.map((option) => ({ value: option.value, label: option.label }))}
623
+ value={issuePermissionCandidate}
624
+ onChange={setIssuePermissionCandidate}
625
+ compact
626
+ />
627
+ <Button
628
+ variant="secondary"
629
+ size="sm"
630
+ onClick={() => addIssuePermission(issuePermissionCandidate)}
631
+ >
632
+ ADD
633
+ </Button>
634
+ </div>
635
+ {issuePermissions.length === 0 ? (
636
+ <div className="px-1 text-[8px] text-[var(--color-danger,#ef4444)]">
637
+ No permissions selected.
638
+ </div>
639
+ ) : (
640
+ <div className="flex flex-wrap gap-1">
641
+ {issuePermissions.map((permission) => (
642
+ <span
643
+ key={`perm-${permission}`}
644
+ 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)]"
645
+ >
646
+ {permission}
647
+ <Button
648
+ variant="ghost"
649
+ size="sm"
650
+ onClick={() => removeIssuePermission(permission)}
651
+ className="h-4 px-1 text-[8px]"
652
+ >
653
+ X
654
+ </Button>
655
+ </span>
656
+ ))}
657
+ </div>
658
+ )}
659
+ <div className="px-1 text-[8px] text-[var(--color-text-faint,#9ca3af)]">
660
+ Selected permissions are used when Profile Source is <code>manual</code>.
661
+ </div>
662
+ </div>
663
+
664
+ <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)]">
665
+ Generate a key in one click. Preview is optional and only shows what permissions/scopes will be issued.
666
+ </div>
667
+
668
+ {issueError && (
669
+ <div className="mt-2 text-[9px] text-[var(--color-danger,#ef4444)]">{issueError}</div>
670
+ )}
671
+
672
+ <div className="mt-3 flex flex-wrap gap-2">
673
+ <Button
674
+ size="sm"
675
+ onClick={() => { void handleIssueApiKey(); }}
676
+ loading={issuing}
677
+ icon={!issuing ? <KeyRound size={11} /> : undefined}
678
+ >
679
+ ISSUE API KEY
680
+ </Button>
681
+ <Button
682
+ size="sm"
683
+ variant="secondary"
684
+ onClick={() => { void handlePreview(); }}
685
+ loading={previewLoading}
686
+ icon={!previewLoading ? <RefreshCw size={11} /> : undefined}
687
+ >
688
+ PREVIEW POLICY
689
+ </Button>
690
+ </div>
691
+
692
+ {previewError && (
693
+ <div className="mt-2 text-[9px] text-[var(--color-danger,#ef4444)]">{previewError}</div>
694
+ )}
695
+
696
+ {preview && (
697
+ <div className="mt-3 rounded border border-[var(--color-border,#d4d4d8)] bg-[var(--color-background,#f4f4f5)] p-2">
698
+ <div className="text-[8px] tracking-widest text-[var(--color-text-faint,#9ca3af)]">
699
+ HASH {shortHash(preview.effectivePolicyHash)}
700
+ </div>
701
+ {preview.profile && (
702
+ <div className="mt-1 text-[8px] text-[var(--color-text-faint,#9ca3af)]">
703
+ profile: {preview.profile.id}@{preview.profile.version}
704
+ </div>
705
+ )}
706
+ <div className="mt-1 text-[9px] text-[var(--color-text-muted,#6b7280)]">
707
+ permissions: {preview.effectivePolicy.permissions.length} · ttl: {preview.effectivePolicy.ttlSeconds}s · max reads: {preview.effectivePolicy.maxReads ?? 'unlimited'}
708
+ </div>
709
+ <div className="mt-2 grid grid-cols-1 gap-1 text-[8px] text-[var(--color-text-muted,#6b7280)]">
710
+ <div>perms: {preview.effectivePolicy.permissions.join(', ') || '(none)'}</div>
711
+ <div>read scope: {preview.effectivePolicy.credentialAccess.read.join(', ') || '(none)'}</div>
712
+ <div>write scope: {preview.effectivePolicy.credentialAccess.write.join(', ') || '(none)'}</div>
713
+ <div>excluded fields: {preview.effectivePolicy.credentialAccess.excludeFields.join(', ') || '(none)'}</div>
714
+ <div>
715
+ rate budget: {preview.effectivePolicy.rateBudget.state} ({preview.effectivePolicy.rateBudget.requests ?? 'n/a'} / {preview.effectivePolicy.rateBudget.windowSeconds ?? 'n/a'}s, source={preview.effectivePolicy.rateBudget.source})
716
+ </div>
717
+ </div>
718
+
719
+ {preview.overrideDelta.length > 0 && (
720
+ <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)]">
721
+ <div className="mb-1 tracking-widest text-[var(--color-text-faint,#9ca3af)]">OVERRIDE DELTA</div>
722
+ {preview.overrideDelta.join(' · ')}
723
+ </div>
724
+ )}
725
+
726
+ {preview.denyExamples && preview.denyExamples.length > 0 && (
727
+ <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)]">
728
+ <div className="mb-1 tracking-widest text-[var(--color-text-faint,#9ca3af)]">EXPECTED DENY EXAMPLES</div>
729
+ <ul className="space-y-0.5">
730
+ {preview.denyExamples.map((deny) => (
731
+ <li key={`${deny.code}-${deny.message.slice(0, 12)}`}>• {deny.code}: {deny.message}</li>
732
+ ))}
733
+ </ul>
734
+ </div>
735
+ )}
736
+
737
+ {preview.warnings.length > 0 && (
738
+ <div className="mt-2 flex items-start gap-1.5 text-[8px] text-[var(--color-warning,#ff4d00)]">
739
+ <ShieldAlert size={10} className="mt-[1px]" />
740
+ <span>{preview.warnings.join(' | ')}</span>
741
+ </div>
742
+ )}
743
+ </div>
744
+ )}
745
+
746
+ {issuedToken && (
747
+ <div className="mt-3 rounded border border-[var(--color-border,#d4d4d8)] bg-[var(--color-surface,#fff)] p-2">
748
+ <div className="mb-1 flex items-center justify-between">
749
+ <div className="text-[8px] tracking-widest text-[var(--color-success,#16a34a)]">API KEY ISSUED</div>
750
+ <Button
751
+ variant="ghost"
752
+ size="sm"
753
+ onClick={() => { void handleCopyToken(); }}
754
+ icon={<Copy size={10} />}
755
+ >
756
+ {copied ? 'COPIED' : 'COPY'}
757
+ </Button>
758
+ </div>
759
+ {issuedMeta && (
760
+ <div className="mb-1 text-[8px] text-[var(--color-text-faint,#9ca3af)]">{issuedMeta}</div>
761
+ )}
762
+ <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)]">
763
+ {issuedToken}
764
+ </code>
765
+ </div>
766
+ )}
767
+ </section>
768
+
769
+ <section className="order-2 border border-[var(--color-border,#d4d4d8)] bg-[var(--color-surface,#fff)] p-3">
770
+ <div className="mb-2 flex items-center justify-between">
771
+ <div className="text-[10px] font-bold tracking-widest text-[var(--color-text,#0a0a0a)]">PROFILE BUILDER</div>
772
+ {editingProfileId && (
773
+ <Button variant="ghost" size="sm" onClick={resetProfileForm}>
774
+ NEW
775
+ </Button>
776
+ )}
777
+ </div>
778
+
779
+ <div className="grid grid-cols-1 gap-2">
780
+ <TextInput
781
+ label="Profile Name"
782
+ value={profileName}
783
+ onChange={(event) => setProfileName(event.target.value)}
784
+ placeholder="e.g. CI deploy scoped"
785
+ compact
786
+ />
787
+
788
+ <FilterDropdown
789
+ label="Base Profile"
790
+ options={BUILTIN_PROFILE_OPTIONS.map((profile) => ({ value: profile.id, label: profile.label }))}
791
+ value={profileBase}
792
+ onChange={setProfileBase}
793
+ compact
794
+ />
795
+
796
+ <div className="grid grid-cols-2 gap-2">
797
+ <TextInput
798
+ label="TTL Override"
799
+ value={profileTtlSeconds}
800
+ onChange={(event) => setProfileTtlSeconds(event.target.value)}
801
+ placeholder="seconds"
802
+ compact
803
+ />
804
+ <TextInput
805
+ label="Max Reads"
806
+ value={profileMaxReads}
807
+ onChange={(event) => setProfileMaxReads(event.target.value)}
808
+ placeholder="count"
809
+ compact
810
+ />
811
+ </div>
812
+
813
+ <div className="space-y-1">
814
+ <div className="grid grid-cols-[minmax(0,1fr)_auto] items-end gap-1.5">
815
+ <FilterDropdown
816
+ label="Read Scopes"
817
+ options={CREDENTIAL_SCOPE_OPTIONS.map((option) => ({ value: option.value, label: option.label }))}
818
+ value={profileReadScopeCandidate}
819
+ onChange={setProfileReadScopeCandidate}
820
+ compact
821
+ />
822
+ <Button
823
+ variant="secondary"
824
+ size="sm"
825
+ onClick={() => addProfileScope('read', profileReadScopeCandidate)}
826
+ >
827
+ ADD
828
+ </Button>
829
+ </div>
830
+ {profileReadScopes.length === 0 ? (
831
+ <div className="px-1 text-[8px] text-[var(--color-text-faint,#9ca3af)]">
832
+ Using base profile read scopes.
833
+ </div>
834
+ ) : (
835
+ <div className="flex flex-wrap gap-1">
836
+ {profileReadScopes.map((scope) => (
837
+ <span
838
+ key={`read-${scope}`}
839
+ 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)]"
840
+ >
841
+ {scope}
842
+ <Button
843
+ variant="ghost"
844
+ size="sm"
845
+ onClick={() => removeProfileScope('read', scope)}
846
+ className="h-4 px-1 text-[8px]"
847
+ >
848
+ X
849
+ </Button>
850
+ </span>
851
+ ))}
852
+ </div>
853
+ )}
854
+ </div>
855
+
856
+ <div className="space-y-1">
857
+ <div className="grid grid-cols-[minmax(0,1fr)_auto] items-end gap-1.5">
858
+ <FilterDropdown
859
+ label="Write Scopes"
860
+ options={CREDENTIAL_SCOPE_OPTIONS.map((option) => ({ value: option.value, label: option.label }))}
861
+ value={profileWriteScopeCandidate}
862
+ onChange={setProfileWriteScopeCandidate}
863
+ compact
864
+ />
865
+ <Button
866
+ variant="secondary"
867
+ size="sm"
868
+ onClick={() => addProfileScope('write', profileWriteScopeCandidate)}
869
+ >
870
+ ADD
871
+ </Button>
872
+ </div>
873
+ {profileWriteScopes.length === 0 ? (
874
+ <div className="px-1 text-[8px] text-[var(--color-text-faint,#9ca3af)]">
875
+ Using base profile write scopes.
876
+ </div>
877
+ ) : (
878
+ <div className="flex flex-wrap gap-1">
879
+ {profileWriteScopes.map((scope) => (
880
+ <span
881
+ key={`write-${scope}`}
882
+ 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)]"
883
+ >
884
+ {scope}
885
+ <Button
886
+ variant="ghost"
887
+ size="sm"
888
+ onClick={() => removeProfileScope('write', scope)}
889
+ className="h-4 px-1 text-[8px]"
890
+ >
891
+ X
892
+ </Button>
893
+ </span>
894
+ ))}
895
+ </div>
896
+ )}
897
+ </div>
898
+
899
+ <div className="px-1 text-[8px] text-[var(--color-text-faint,#9ca3af)]">
900
+ Scope format: <code>*</code>, <code>vault:&lt;id&gt;</code>, <code>tag:&lt;name&gt;</code>, or exact credential ID.
901
+ </div>
902
+
903
+ <TextInput
904
+ label="Exclude Fields (CSV)"
905
+ value={profileExcludeFields}
906
+ onChange={(event) => setProfileExcludeFields(event.target.value)}
907
+ placeholder="password, seedPhrase"
908
+ compact
909
+ />
910
+ </div>
911
+
912
+ {profileError && (
913
+ <div className="mt-2 text-[9px] text-[var(--color-danger,#ef4444)]">{profileError}</div>
914
+ )}
915
+ {profileNotice && (
916
+ <div className="mt-2 text-[9px] text-[var(--color-success,#16a34a)]">{profileNotice}</div>
917
+ )}
918
+
919
+ <div className="mt-3 flex gap-2">
920
+ <Button size="sm" onClick={handleSaveProfile} icon={<ShieldCheck size={11} />}>
921
+ {editingProfileId ? 'UPDATE PROFILE' : 'CREATE PROFILE'}
922
+ </Button>
923
+ {editingProfileId && (
924
+ <Button variant="secondary" size="sm" onClick={resetProfileForm}>
925
+ CANCEL
926
+ </Button>
927
+ )}
928
+ </div>
929
+
930
+ <div className="mt-4 border-t border-[var(--color-border,#d4d4d8)] pt-3">
931
+ <div className="mb-2 text-[9px] font-bold tracking-widest text-[var(--color-text-muted,#6b7280)]">
932
+ SAVED PROFILES ({profiles.length})
933
+ </div>
934
+ {profiles.length === 0 ? (
935
+ <div className="text-[9px] text-[var(--color-text-faint,#9ca3af)]">No custom profiles saved yet.</div>
936
+ ) : (
937
+ <div className="space-y-2">
938
+ {profiles.map((profile) => (
939
+ <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">
940
+ <Button
941
+ variant="ghost"
942
+ size="sm"
943
+ onClick={() => handleEditProfile(profile)}
944
+ className="h-auto min-h-0 w-full justify-start px-0 py-0 text-left hover:bg-transparent"
945
+ >
946
+ <span className="block">
947
+ <span className="block text-[10px] text-[var(--color-text,#0a0a0a)]">{profile.name}</span>
948
+ <span className="block text-[8px] text-[var(--color-text-faint,#9ca3af)]">{profile.profile}@{profile.profileVersion}</span>
949
+ </span>
950
+ </Button>
951
+ <Button
952
+ variant="ghost"
953
+ size="sm"
954
+ onClick={() => handleDeleteProfile(profile.id)}
955
+ icon={<Trash2 size={10} />}
956
+ >
957
+ {''}
958
+ </Button>
959
+ </div>
960
+ ))}
961
+ </div>
962
+ )}
963
+ </div>
964
+ </section>
965
+ </div>
966
+
967
+ <div className="mt-4 grid grid-cols-1 gap-4 xl:grid-cols-2">
968
+ <section className="border border-[var(--color-border,#d4d4d8)] bg-[var(--color-surface,#fff)] p-3">
969
+ <div className="mb-2 text-[10px] font-bold tracking-widest text-[var(--color-text,#0a0a0a)]">PENDING AUTH REQUESTS</div>
970
+ {requestNotice && (
971
+ <div className="mb-2 text-[9px] text-[var(--color-text-muted,#6b7280)]">{requestNotice}</div>
972
+ )}
973
+ {pendingAuthRequests.length === 0 ? (
974
+ <div className="text-[9px] text-[var(--color-text-faint,#9ca3af)]">No pending auth requests.</div>
975
+ ) : (
976
+ <div className="space-y-2">
977
+ {pendingAuthRequests.map((request) => {
978
+ const metadata = parseMetadata(request.metadata);
979
+ const agentId = typeof metadata.agentId === 'string' ? metadata.agentId : 'unknown-agent';
980
+ const profile = typeof metadata.profile === 'object' && metadata.profile && 'id' in metadata.profile
981
+ ? `${String((metadata.profile as Record<string, unknown>).id)}@${String((metadata.profile as Record<string, unknown>).version || 'v1')}`
982
+ : 'n/a';
983
+ const resolving = actionLoading === `resolve-${request.id}`;
984
+ return (
985
+ <div key={request.id} className="rounded border border-[var(--color-border,#d4d4d8)] bg-[var(--color-background,#f4f4f5)] p-2">
986
+ <div className="text-[9px] text-[var(--color-text,#0a0a0a)]">
987
+ {agentId} · {request.type}
988
+ </div>
989
+ <div className="mt-1 text-[8px] text-[var(--color-text-faint,#9ca3af)]">
990
+ profile: {profile} · {new Date(request.createdAt).toLocaleString()}
991
+ </div>
992
+ <div className="mt-2 flex gap-2">
993
+ <Button
994
+ size="sm"
995
+ onClick={() => { void handleResolveRequest(request.id, true); }}
996
+ loading={resolving}
997
+ icon={!resolving ? <Check size={10} /> : undefined}
998
+ >
999
+ APPROVE
1000
+ </Button>
1001
+ <Button
1002
+ size="sm"
1003
+ variant="secondary"
1004
+ onClick={() => { void handleResolveRequest(request.id, false); }}
1005
+ disabled={resolving}
1006
+ icon={<AlertTriangle size={10} />}
1007
+ >
1008
+ REJECT
1009
+ </Button>
1010
+ </div>
1011
+ </div>
1012
+ );
1013
+ })}
1014
+ </div>
1015
+ )}
1016
+ </section>
1017
+
1018
+ <section className="border border-[var(--color-border,#d4d4d8)] bg-[var(--color-surface,#fff)] p-3">
1019
+ <div className="mb-2 text-[10px] font-bold tracking-widest text-[var(--color-text,#0a0a0a)]">ISSUED API KEYS</div>
1020
+
1021
+ <div className="mb-2 text-[8px] tracking-widest text-[var(--color-text-faint,#9ca3af)]">
1022
+ ACTIVE ({managedActiveTokens.length})
1023
+ </div>
1024
+ {managedActiveTokens.length === 0 ? (
1025
+ <div className="mb-3 text-[9px] text-[var(--color-text-faint,#9ca3af)]">No active non-admin tokens.</div>
1026
+ ) : (
1027
+ <div className="mb-3 space-y-2">
1028
+ {managedActiveTokens.map((token) => {
1029
+ const revoking = actionLoading === `revoke-${token.tokenHash}`;
1030
+ return (
1031
+ <div key={token.tokenHash} className="rounded border border-[var(--color-border,#d4d4d8)] bg-[var(--color-background,#f4f4f5)] p-2">
1032
+ <div className="flex items-center justify-between">
1033
+ <div className="text-[9px] text-[var(--color-text,#0a0a0a)]">{token.agentId}</div>
1034
+ <div className="text-[8px] text-[var(--color-text-faint,#9ca3af)]">{shortHash(token.tokenHash)}</div>
1035
+ </div>
1036
+ <div className="mt-1 text-[8px] text-[var(--color-text-faint,#9ca3af)]">
1037
+ perms: {token.permissions.length} · expires: {new Date(token.expiresAt).toLocaleString()}
1038
+ </div>
1039
+ <div className="mt-2">
1040
+ <Button
1041
+ size="sm"
1042
+ variant="secondary"
1043
+ onClick={() => { void onRevokeToken(token.tokenHash); }}
1044
+ disabled={revoking}
1045
+ icon={revoking ? <Loader2 size={10} className="animate-spin" /> : <Trash2 size={10} />}
1046
+ >
1047
+ REVOKE
1048
+ </Button>
1049
+ </div>
1050
+ </div>
1051
+ );
1052
+ })}
1053
+ </div>
1054
+ )}
1055
+
1056
+ <div className="mb-2 text-[8px] tracking-widest text-[var(--color-text-faint,#9ca3af)]">
1057
+ INACTIVE ({inactiveTokens.length})
1058
+ </div>
1059
+ {inactiveTokens.length === 0 ? (
1060
+ <div className="text-[9px] text-[var(--color-text-faint,#9ca3af)]">No inactive tokens.</div>
1061
+ ) : (
1062
+ <div className="space-y-1.5">
1063
+ {inactiveTokens.slice(0, 8).map((token) => (
1064
+ <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)]">
1065
+ <span>{token.agentId}</span>
1066
+ <span>{shortHash(token.tokenHash)}</span>
1067
+ </div>
1068
+ ))}
1069
+ {inactiveTokens.length > 8 && (
1070
+ <div className="text-[8px] text-[var(--color-text-faint,#9ca3af)]">
1071
+ +{inactiveTokens.length - 8} more inactive tokens
1072
+ </div>
1073
+ )}
1074
+ </div>
1075
+ )}
1076
+ </section>
1077
+ </div>
1078
+ </div>
1079
+ );
1080
+ };