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,214 @@
1
+ 'use client';
2
+
3
+ import { useCallback, useEffect, useMemo, useState } from 'react';
4
+ import Link from 'next/link';
5
+ import { api, Api } from '@/lib/api';
6
+
7
+ type HealthFlagSummary = {
8
+ totalAnalyzed: number;
9
+ safe: number;
10
+ weak: number;
11
+ reused: number;
12
+ breached: number;
13
+ unknown: number;
14
+ lastScanAt: string;
15
+ };
16
+
17
+ type HealthRow = {
18
+ id: string;
19
+ name: string;
20
+ type: string;
21
+ vaultId: string;
22
+ health: {
23
+ status: string;
24
+ flags: { weak: boolean; reused: boolean; breached: boolean; unknown: boolean };
25
+ evidence: {
26
+ reuseCount: number;
27
+ breachCount: number | null;
28
+ weakReasons: string[];
29
+ };
30
+ engineVersion: string;
31
+ lastScannedAt: string | null;
32
+ };
33
+ };
34
+
35
+ type ScanStatus = 'idle' | 'queued' | 'running' | 'complete' | 'failed' | 'expired';
36
+
37
+ const STATUS_TONE: Record<string, string> = {
38
+ safe: 'text-emerald-700',
39
+ weak: 'text-amber-700',
40
+ reused: 'text-orange-700',
41
+ breached: 'text-red-700',
42
+ unknown: 'text-slate-600',
43
+ weak_reused: 'text-orange-700',
44
+ weak_breached: 'text-red-700',
45
+ reused_breached: 'text-red-700',
46
+ weak_reused_breached: 'text-red-700',
47
+ };
48
+
49
+ function fmtTime(input?: string | null): string {
50
+ if (!input) return '—';
51
+ const date = new Date(input);
52
+ if (Number.isNaN(date.getTime())) return '—';
53
+ return date.toLocaleString();
54
+ }
55
+
56
+ export function CredentialHealthDashboard() {
57
+ const [loading, setLoading] = useState(true);
58
+ const [error, setError] = useState<string | null>(null);
59
+ const [summary, setSummary] = useState<HealthFlagSummary | null>(null);
60
+ const [rows, setRows] = useState<HealthRow[]>([]);
61
+ const [scanId, setScanId] = useState<string | null>(null);
62
+ const [scanStatus, setScanStatus] = useState<ScanStatus>('idle');
63
+ const [scanError, setScanError] = useState<string | null>(null);
64
+
65
+ const load = useCallback(async () => {
66
+ setError(null);
67
+ try {
68
+ const [summaryRes, rowsRes] = await Promise.all([
69
+ api.get<{ success: boolean; summary: HealthFlagSummary }>(Api.Wallet, '/credentials/health/summary'),
70
+ api.get<{ success: boolean; credentials: HealthRow[] }>(Api.Wallet, '/credentials/health'),
71
+ ]);
72
+ setSummary(summaryRes.summary);
73
+ setRows(rowsRes.credentials || []);
74
+ } catch (err) {
75
+ setError((err as Error).message || 'Failed to load credential health');
76
+ } finally {
77
+ setLoading(false);
78
+ }
79
+ }, []);
80
+
81
+ useEffect(() => {
82
+ void load();
83
+ }, [load]);
84
+
85
+ useEffect(() => {
86
+ if (!scanId || (scanStatus !== 'queued' && scanStatus !== 'running')) return;
87
+
88
+ const timer = setInterval(async () => {
89
+ try {
90
+ const res = await api.get<{
91
+ success: boolean;
92
+ scan: { status: ScanStatus; error?: string };
93
+ }>(Api.Wallet, `/credentials/health/rescan/${scanId}`);
94
+ setScanStatus(res.scan.status);
95
+ setScanError(res.scan.error || null);
96
+ if (res.scan.status === 'complete') {
97
+ await load();
98
+ }
99
+ } catch (err) {
100
+ setScanStatus('failed');
101
+ setScanError((err as Error).message || 'Failed to read scan status');
102
+ }
103
+ }, 1000);
104
+
105
+ return () => clearInterval(timer);
106
+ }, [scanId, scanStatus, load]);
107
+
108
+ const startRescan = useCallback(async () => {
109
+ setScanError(null);
110
+ setScanStatus('queued');
111
+ try {
112
+ const res = await api.post<{ accepted: boolean; scanId: string }>(Api.Wallet, '/credentials/health/rescan');
113
+ setScanId(res.scanId);
114
+ } catch (err) {
115
+ setScanStatus('failed');
116
+ setScanError((err as Error).message || 'Failed to start rescan');
117
+ }
118
+ }, []);
119
+
120
+ const engineVersion = useMemo(() => rows.find((row) => row.health?.engineVersion)?.health.engineVersion, [rows]);
121
+
122
+ if (loading) {
123
+ return <div className="p-6 font-mono text-xs">LOADING_HEALTH...</div>;
124
+ }
125
+
126
+ if (error) {
127
+ return (
128
+ <div className="p-6 space-y-4">
129
+ <div className="font-mono text-xs text-red-700">{error}</div>
130
+ <button className="border px-3 py-2 font-mono text-xs" onClick={() => void load()}>RETRY</button>
131
+ </div>
132
+ );
133
+ }
134
+
135
+ return (
136
+ <div className="min-h-screen bg-[var(--color-background,#f4f4f5)] p-6">
137
+ <div className="max-w-6xl mx-auto space-y-4">
138
+ <div className="flex items-center justify-between gap-3">
139
+ <div>
140
+ <h1 className="font-mono text-lg">Credential Health</h1>
141
+ <p className="font-mono text-xs text-[var(--color-text-muted,#6b7280)]">
142
+ Last scan: {fmtTime(summary?.lastScanAt)} {engineVersion ? `· Engine v${engineVersion}` : ''}
143
+ </p>
144
+ </div>
145
+ <div className="flex items-center gap-2">
146
+ <Link href="/" className="border px-3 py-2 font-mono text-xs">VAULT</Link>
147
+ <button className="border px-3 py-2 font-mono text-xs" onClick={() => void startRescan()}>
148
+ RESCAN
149
+ </button>
150
+ </div>
151
+ </div>
152
+
153
+ {(scanStatus !== 'idle' || scanError) && (
154
+ <div className="border p-3 font-mono text-xs bg-white">
155
+ RESCAN_STATUS: {scanStatus.toUpperCase()}
156
+ {scanError ? ` · ${scanError}` : ''}
157
+ </div>
158
+ )}
159
+
160
+ {summary && summary.unknown > 0 && (
161
+ <div className="border border-amber-500/40 bg-amber-50 p-3 font-mono text-xs text-amber-800">
162
+ Some breach checks are unknown (network/rate-limit/disabled). Retry later or enable HEALTH_BREACH_CHECK.
163
+ </div>
164
+ )}
165
+
166
+ <div className="grid grid-cols-2 md:grid-cols-5 gap-2">
167
+ {[
168
+ ['WEAK', summary?.weak || 0],
169
+ ['REUSED', summary?.reused || 0],
170
+ ['BREACHED', summary?.breached || 0],
171
+ ['UNKNOWN', summary?.unknown || 0],
172
+ ['SAFE', summary?.safe || 0],
173
+ ].map(([label, count]) => (
174
+ <div key={label} className="border bg-white p-3">
175
+ <div className="font-mono text-[10px] text-[var(--color-text-muted,#6b7280)]">{label}</div>
176
+ <div className="font-mono text-xl">{count}</div>
177
+ </div>
178
+ ))}
179
+ </div>
180
+
181
+ <div className="border bg-white overflow-hidden">
182
+ <table className="w-full text-left font-mono text-xs">
183
+ <thead className="bg-[var(--color-background,#f4f4f5)]">
184
+ <tr>
185
+ <th className="p-2">Credential</th>
186
+ <th className="p-2">Status</th>
187
+ <th className="p-2">Evidence</th>
188
+ </tr>
189
+ </thead>
190
+ <tbody>
191
+ {rows.length === 0 ? (
192
+ <tr><td className="p-3" colSpan={3}>No scannable credentials found.</td></tr>
193
+ ) : rows.map((row) => (
194
+ <tr key={row.id} className="border-t">
195
+ <td className="p-2">
196
+ <div>{row.name}</div>
197
+ <div className="text-[10px] text-[var(--color-text-muted,#6b7280)]">{row.type} · {row.vaultId}</div>
198
+ </td>
199
+ <td className={`p-2 uppercase ${STATUS_TONE[row.health.status] || 'text-slate-700'}`}>
200
+ {row.health.status.replaceAll('_', ' ')}
201
+ </td>
202
+ <td className="p-2 text-[10px] text-[var(--color-text-muted,#6b7280)]">
203
+ reuse={row.health.evidence.reuseCount}; breaches={row.health.evidence.breachCount ?? 'unknown'};
204
+ weak={row.health.evidence.weakReasons.length > 0 ? row.health.evidence.weakReasons.join(',') : 'none'}
205
+ </td>
206
+ </tr>
207
+ ))}
208
+ </tbody>
209
+ </table>
210
+ </div>
211
+ </div>
212
+ </div>
213
+ );
214
+ }
@@ -0,0 +1,72 @@
1
+ import React from 'react';
2
+
3
+ interface IconProps {
4
+ size?: number;
5
+ className?: string;
6
+ }
7
+
8
+ export const BaseIcon: React.FC<IconProps> = ({ size = 16, className = '' }) => (
9
+ <svg width={size} height={size} viewBox="0 0 111 111" fill="none" className={className}>
10
+ <path d="M54.921 110.034C85.359 110.034 110.034 85.402 110.034 55.017C110.034 24.6319 85.359 0 54.921 0C26.0432 0 2.35281 22.1714 0 50.3923H72.8467V59.6416H0C2.35281 87.8625 26.0432 110.034 54.921 110.034Z" fill="currentColor"/>
11
+ </svg>
12
+ );
13
+
14
+ export const EthereumIcon: React.FC<IconProps> = ({ size = 16, className = '' }) => (
15
+ <svg width={size} height={size} viewBox="0 0 256 417" className={className}>
16
+ <path fill="currentColor" d="M127.961 0l-2.795 9.5v275.668l2.795 2.79 127.962-75.638z"/>
17
+ <path fill="currentColor" opacity="0.6" d="M127.962 0L0 212.32l127.962 75.639V154.158z"/>
18
+ <path fill="currentColor" d="M127.961 312.187l-1.575 1.92v98.199l1.575 4.6L256 236.587z"/>
19
+ <path fill="currentColor" opacity="0.6" d="M127.962 416.905v-104.72L0 236.585z"/>
20
+ <path fill="currentColor" opacity="0.8" d="M127.961 287.958l127.96-75.637-127.96-58.162z"/>
21
+ <path fill="currentColor" opacity="0.4" d="M0 212.32l127.96 75.638v-133.8z"/>
22
+ </svg>
23
+ );
24
+
25
+ export const ArbitrumIcon: React.FC<IconProps> = ({ size = 16, className = '' }) => (
26
+ <svg width={size} height={size} viewBox="0 0 256 256" className={className}>
27
+ <path fill="currentColor" d="M128 0C57.308 0 0 57.308 0 128s57.308 128 128 128 128-57.308 128-128S198.692 0 128 0zm70.56 186.624l-17.856-49.088L128 217.6l-52.704-80.064-17.856 49.088L128 38.4l70.56 148.224z"/>
28
+ </svg>
29
+ );
30
+
31
+ export const OptimismIcon: React.FC<IconProps> = ({ size = 16, className = '' }) => (
32
+ <svg width={size} height={size} viewBox="0 0 256 256" className={className}>
33
+ <circle cx="128" cy="128" r="128" fill="currentColor"/>
34
+ <path fill="white" d="M81.5 170.5c-11.3 0-20.2-3.2-26.7-9.5-6.5-6.4-9.8-15.3-9.8-26.8 0-14.5 4.2-26.2 12.5-35.2 8.4-9 19.5-13.5 33.3-13.5 11 0 19.7 3.1 26.2 9.4 6.5 6.2 9.7 14.9 9.7 26 0 14.7-4.2 26.6-12.5 35.7-8.3 9-19.4 13.5-33.3 13.5h.6v.4zm3.8-18c7.4 0 13.3-3.1 17.8-9.2 4.5-6.2 6.7-14.3 6.7-24.4 0-6.8-1.5-12-4.6-15.8-3-3.7-7.3-5.6-12.8-5.6-7.4 0-13.4 3.1-17.8 9.3-4.5 6.2-6.7 14.3-6.7 24.3 0 6.8 1.6 12 4.7 15.7 3.1 3.8 7.3 5.7 12.7 5.7z"/>
35
+ <path fill="white" d="M137.5 169V87.5h26.3c10.5 0 18.5 2.2 24 6.7 5.5 4.4 8.3 10.8 8.3 19.2 0 8.8-2.9 15.8-8.8 20.8-5.9 5-14.1 7.5-24.6 7.5h-7.8V169h-17.4zm17.4-43.8h5.3c5.2 0 9.1-1.1 11.8-3.4 2.7-2.3 4-5.5 4-9.8 0-3.8-1.2-6.6-3.6-8.6-2.4-2-6-3-10.8-3h-6.7v24.8z"/>
36
+ </svg>
37
+ );
38
+
39
+ export const PolygonIcon: React.FC<IconProps> = ({ size = 16, className = '' }) => (
40
+ <svg width={size} height={size} viewBox="0 0 256 256" className={className}>
41
+ <path fill="currentColor" d="M177.1 94.9c-3.5-2-7.8-2-11.3 0l-26.4 15.6-17.9 10.2-26.4 15.6c-3.5 2-7.8 2-11.3 0l-20.7-12.2c-3.5-2-5.7-5.8-5.7-9.8V91.8c0-3.9 2.1-7.7 5.7-9.8l20.5-11.9c3.5-2 7.8-2 11.3 0l20.5 11.9c3.5 2 5.7 5.8 5.7 9.8v15.6l17.9-10.4V81.1c0-3.9-2.1-7.7-5.7-9.8L95.7 47.8c-3.5-2-7.8-2-11.3 0L45.2 71.4c-3.6 2.1-5.7 5.9-5.7 9.8v47.1c0 3.9 2.1 7.7 5.7 9.8l38.8 22.5c3.5 2 7.8 2 11.3 0l26.4-15.4 17.9-10.4 26.4-15.4c3.5-2 7.8-2 11.3 0l20.5 11.9c3.5 2 5.7 5.8 5.7 9.8v22.5c0 3.9-2.1 7.7-5.7 9.8l-20.4 12c-3.5 2-7.8 2-11.3 0l-20.5-11.9c-3.5-2-5.7-5.8-5.7-9.8v-15.4l-17.9 10.4v15.8c0 3.9 2.1 7.7 5.7 9.8l38.8 22.5c3.5 2 7.8 2 11.3 0l38.8-22.5c3.5-2 5.7-5.8 5.7-9.8v-47.3c0-3.9-2.1-7.7-5.7-9.8l-39-22.3z"/>
42
+ </svg>
43
+ );
44
+
45
+ export const SolanaIcon: React.FC<IconProps> = ({ size = 16, className = '' }) => (
46
+ <svg width={size} height={size} viewBox="0 0 397 311" className={className}>
47
+ <path fill="currentColor" d="M64.6 237.9c2.4-2.4 5.7-3.8 9.2-3.8h317.4c5.8 0 8.7 7 4.6 11.1l-62.7 62.7c-2.4 2.4-5.7 3.8-9.2 3.8H6.5c-5.8 0-8.7-7-4.6-11.1l62.7-62.7z"/>
48
+ <path fill="currentColor" d="M64.6 3.8C67.1 1.4 70.4 0 73.8 0h317.4c5.8 0 8.7 7 4.6 11.1l-62.7 62.7c-2.4 2.4-5.7 3.8-9.2 3.8H6.5c-5.8 0-8.7-7-4.6-11.1L64.6 3.8z"/>
49
+ <path fill="currentColor" d="M333.1 120.1c-2.4-2.4-5.7-3.8-9.2-3.8H6.5c-5.8 0-8.7 7-4.6 11.1l62.7 62.7c2.4 2.4 5.7 3.8 9.2 3.8h317.4c5.8 0 8.7-7 4.6-11.1l-62.7-62.7z"/>
50
+ </svg>
51
+ );
52
+
53
+ export const DefaultChainIcon: React.FC<IconProps> = ({ size = 16, className = '' }) => (
54
+ <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" className={className}>
55
+ <circle cx="12" cy="12" r="10"/>
56
+ <path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/>
57
+ <path d="M2 12h20"/>
58
+ </svg>
59
+ );
60
+
61
+ export const getChainIcon = (chain: string): React.FC<IconProps> => {
62
+ const icons: Record<string, React.FC<IconProps>> = {
63
+ base: BaseIcon,
64
+ ethereum: EthereumIcon,
65
+ arbitrum: ArbitrumIcon,
66
+ optimism: OptimismIcon,
67
+ polygon: PolygonIcon,
68
+ solana: SolanaIcon,
69
+ 'solana-devnet': SolanaIcon,
70
+ };
71
+ return icons[chain.toLowerCase()] || DefaultChainIcon;
72
+ };
@@ -0,0 +1,369 @@
1
+ 'use client';
2
+
3
+ import React, { useEffect, useState, useMemo } from 'react';
4
+ import { Box, Plus, Loader2, Shield, Check, Download } from 'lucide-react';
5
+ import { Drawer, Button, Popover, TextInput } from '@/components/design-system';
6
+ import { APP_TYPES, type AppDefinition } from '@/lib/app-registry';
7
+ import type { AppManifest } from '@/lib/app-loader';
8
+ import { api, Api } from '@/lib/api';
9
+
10
+ interface AppStoreDrawerProps {
11
+ isOpen: boolean;
12
+ onClose: () => void;
13
+ onAddApp: (appType: string, config?: Record<string, unknown>) => void;
14
+ }
15
+
16
+ const CATEGORIES = [
17
+ { id: 'all', label: 'ALL' },
18
+ { id: 'builtin', label: 'BUILT-IN' },
19
+ { id: 'installed', label: 'INSTALLED' },
20
+ ];
21
+
22
+ export function AppStoreDrawer({ isOpen, onClose, onAddApp }: AppStoreDrawerProps) {
23
+ const [manifests, setManifests] = useState<AppManifest[]>([]);
24
+ const [loading, setLoading] = useState(false);
25
+ const [activeCategory, setActiveCategory] = useState('all');
26
+
27
+ // Install from URL state
28
+ const [installSource, setInstallSource] = useState('');
29
+ const [installing, setInstalling] = useState(false);
30
+ const [installError, setInstallError] = useState<string | null>(null);
31
+ const [installSuccess, setInstallSuccess] = useState<string | null>(null);
32
+
33
+ // Approval popover state
34
+ const [approvalApp, setApprovalApp] = useState<AppManifest | null>(null);
35
+ const [approvalAnchor, setApprovalAnchor] = useState<HTMLElement | null>(null);
36
+ const [approving, setApproving] = useState(false);
37
+ const [approvalError, setApprovalError] = useState<string | null>(null);
38
+
39
+ // Fetch third-party app manifests when drawer opens
40
+ useEffect(() => {
41
+ if (!isOpen) return;
42
+
43
+ setLoading(true);
44
+ api.get<{ success: boolean; manifests: AppManifest[] }>(Api.Workspace, '/apps/manifests')
45
+ .then(data => {
46
+ if (data.success) setManifests(data.manifests);
47
+ })
48
+ .catch(err => console.error('Failed to load app manifests:', err))
49
+ .finally(() => setLoading(false));
50
+ }, [isOpen]);
51
+
52
+ // Close approval popover when drawer closes
53
+ useEffect(() => {
54
+ if (!isOpen) {
55
+ setApprovalApp(null);
56
+ setApprovalAnchor(null);
57
+ }
58
+ }, [isOpen]);
59
+
60
+ const refreshManifests = () => {
61
+ api.get<{ success: boolean; manifests: AppManifest[] }>(Api.Workspace, '/apps/manifests')
62
+ .then(data => {
63
+ if (data.success) setManifests(data.manifests);
64
+ })
65
+ .catch(() => {});
66
+ };
67
+
68
+ const handleInstall = async () => {
69
+ if (!installSource.trim()) return;
70
+ setInstalling(true);
71
+ setInstallError(null);
72
+ setInstallSuccess(null);
73
+ try {
74
+ const result = await api.post<{ success: boolean; error?: string; app?: { id: string; name: string } }>(
75
+ Api.Workspace, '/apps/install', { source: installSource.trim() }
76
+ );
77
+ if (!result.success) {
78
+ setInstallError(result.error || 'Install failed');
79
+ return;
80
+ }
81
+ setInstallSuccess(`Installed "${result.app?.name || result.app?.id}"`);
82
+ setInstallSource('');
83
+ refreshManifests();
84
+ } catch (err) {
85
+ setInstallError((err as Error).message);
86
+ } finally {
87
+ setInstalling(false);
88
+ }
89
+ };
90
+
91
+ const handleApprove = async () => {
92
+ if (!approvalApp) return;
93
+ setApproving(true);
94
+ setApprovalError(null);
95
+ try {
96
+ const result = await api.post<{ success: boolean; error?: string }>(
97
+ Api.Wallet, `/apps/${approvalApp.id}/approve`
98
+ );
99
+ if (!result.success) {
100
+ setApprovalError(result.error || 'Approval failed');
101
+ return;
102
+ }
103
+ const appId = approvalApp.id;
104
+ const appName = approvalApp.name;
105
+ setApprovalApp(null);
106
+ setApprovalAnchor(null);
107
+ onAddApp(`installed:${appId}`, { appPath: appId, appName });
108
+ } catch (err) {
109
+ setApprovalError((err as Error).message);
110
+ } finally {
111
+ setApproving(false);
112
+ }
113
+ };
114
+
115
+ // Built-in apps as store cards
116
+ const builtinCards = useMemo(() => {
117
+ return Object.entries(APP_TYPES).map(([key, def]: [string, AppDefinition]) => ({
118
+ id: key,
119
+ name: def.title,
120
+ icon: def.icon,
121
+ description: def.description || `Built-in ${def.title.toLowerCase()} app`,
122
+ category: 'builtin' as const,
123
+ color: def.color,
124
+ permissions: [] as string[],
125
+ manifest: null as AppManifest | null,
126
+ onAdd: () => onAddApp(key),
127
+ }));
128
+ }, [onAddApp]);
129
+
130
+ // Installed (third-party) app cards
131
+ const installedCards = useMemo(() => {
132
+ return manifests.map(m => ({
133
+ id: `installed:${m.id}`,
134
+ name: m.name,
135
+ icon: Box,
136
+ description: m.description || `Installed app`,
137
+ category: 'installed' as const,
138
+ color: 'gray' as const,
139
+ permissions: Array.isArray(m.permissions) ? m.permissions : [],
140
+ manifest: m as AppManifest | null,
141
+ onAdd: () => onAddApp(`installed:${m.id}`, { appPath: m.id, appName: m.name }),
142
+ }));
143
+ }, [manifests, onAddApp]);
144
+
145
+ const allCards = useMemo(() => {
146
+ if (activeCategory === 'builtin') return builtinCards;
147
+ if (activeCategory === 'installed') return installedCards;
148
+ return [...builtinCards, ...installedCards];
149
+ }, [activeCategory, builtinCards, installedCards]);
150
+
151
+ return (
152
+ <Drawer
153
+ isOpen={isOpen}
154
+ onClose={onClose}
155
+ title="APP STORE"
156
+ subtitle="Add apps to workspace"
157
+ >
158
+ <div className="space-y-4">
159
+ {/* Category Filters */}
160
+ <div className="flex gap-1">
161
+ {CATEGORIES.map(cat => (
162
+ <button
163
+ key={cat.id}
164
+ onClick={() => setActiveCategory(cat.id)}
165
+ className={`px-2.5 py-1 font-mono text-[9px] tracking-widest border transition-colors ${
166
+ activeCategory === cat.id
167
+ ? 'bg-[var(--color-text,#0a0a0a)] text-[var(--color-accent,#ccff00)] border-[var(--color-text,#0a0a0a)]'
168
+ : 'bg-[var(--color-surface,#fff)] text-[var(--color-text-muted,#6b7280)] border-[var(--color-border,#d4d4d8)] hover:border-[var(--color-text,#0a0a0a)]'
169
+ }`}
170
+ >
171
+ {cat.label}
172
+ </button>
173
+ ))}
174
+ </div>
175
+
176
+ {/* Install from URL */}
177
+ {(activeCategory === 'all' || activeCategory === 'installed') && (
178
+ <div className="p-2.5 border border-[var(--color-border,#d4d4d8)] bg-[var(--color-surface,#fff)]">
179
+ <div className="font-mono text-[8px] text-[var(--color-text-muted,#6b7280)] mb-1.5 tracking-wider">
180
+ INSTALL FROM URL
181
+ </div>
182
+ <div className="flex gap-1.5">
183
+ <div className="flex-1">
184
+ <TextInput
185
+ value={installSource}
186
+ onChange={(e) => setInstallSource(e.target.value)}
187
+ placeholder="github.com/user/app or URL"
188
+ compact
189
+ onKeyDown={(e: React.KeyboardEvent) => {
190
+ if (e.key === 'Enter') handleInstall();
191
+ }}
192
+ disabled={installing}
193
+ />
194
+ </div>
195
+ <Button
196
+ variant="primary"
197
+ size="sm"
198
+ onClick={handleInstall}
199
+ loading={installing}
200
+ disabled={installing || !installSource.trim()}
201
+ icon={<Download size={10} />}
202
+ >
203
+ INSTALL
204
+ </Button>
205
+ </div>
206
+ {installError && (
207
+ <div className="font-mono text-[8px] text-[var(--color-warning,#ff4d00)] mt-1.5">
208
+ {installError}
209
+ </div>
210
+ )}
211
+ {installSuccess && (
212
+ <div className="font-mono text-[8px] text-[var(--color-success,#22c55e)] mt-1.5">
213
+ {installSuccess}
214
+ </div>
215
+ )}
216
+ </div>
217
+ )}
218
+
219
+ {/* Loading */}
220
+ {loading && (
221
+ <div className="py-8 flex items-center justify-center">
222
+ <Loader2 size={16} className="animate-spin text-[var(--color-text-muted,#6b7280)]" />
223
+ </div>
224
+ )}
225
+
226
+ {/* App Grid */}
227
+ {!loading && allCards.length === 0 && (
228
+ <div className="py-8 text-center border border-dashed border-[var(--color-border,#d4d4d8)]">
229
+ <Box size={20} className="mx-auto mb-2 text-[var(--color-text-faint,#9ca3af)]" />
230
+ <div className="font-mono text-[9px] text-[var(--color-text-muted,#6b7280)]">
231
+ {activeCategory === 'installed'
232
+ ? 'No apps installed. Drop app folders into apps/'
233
+ : 'No apps available'}
234
+ </div>
235
+ </div>
236
+ )}
237
+
238
+ {!loading && (
239
+ <div className="space-y-2">
240
+ {allCards.map(card => {
241
+ const IconComponent = card.icon;
242
+ const needsApproval = card.permissions.length > 0;
243
+
244
+ return (
245
+ <div
246
+ key={card.id}
247
+ className="p-3 border border-[var(--color-border,#d4d4d8)] bg-[var(--color-surface,#fff)] hover:border-[var(--color-border-focus,#0a0a0a)] transition-colors relative"
248
+ >
249
+ {/* Corner accents */}
250
+ <div className="absolute top-1 left-1 w-1.5 h-1.5 border-l border-t border-[var(--color-border-focus,#0a0a0a)] opacity-30" />
251
+ <div className="absolute top-1 right-1 w-1.5 h-1.5 border-r border-t border-[var(--color-border-focus,#0a0a0a)] opacity-30" />
252
+
253
+ <div className="flex items-start gap-3">
254
+ <div className="w-8 h-8 bg-[var(--color-background-alt,#f4f4f5)] flex items-center justify-center shrink-0">
255
+ <IconComponent size={14} className="text-[var(--color-text-muted,#6b7280)]" />
256
+ </div>
257
+ <div className="flex-1 min-w-0">
258
+ <div className="flex items-center gap-2">
259
+ <span className="font-mono text-[10px] font-bold text-[var(--color-text,#0a0a0a)]">
260
+ {card.name}
261
+ </span>
262
+ <span className={`font-mono text-[7px] px-1 py-0.5 ${
263
+ card.category === 'installed'
264
+ ? 'bg-[var(--color-accent,#ccff00)]/20 text-[var(--color-accent,#ccff00)]'
265
+ : 'bg-[var(--color-info,#0047ff)]/10 text-[var(--color-info,#0047ff)]'
266
+ }`}>
267
+ {card.category === 'installed' ? 'PLUGIN' : 'BUILT-IN'}
268
+ </span>
269
+ </div>
270
+ <div className="font-mono text-[8px] text-[var(--color-text-muted,#6b7280)] mt-0.5 leading-relaxed">
271
+ {card.description}
272
+ </div>
273
+ {needsApproval && (
274
+ <div className="flex items-center gap-1 mt-1">
275
+ <Shield size={8} className="text-[var(--color-warning,#ff4d00)]" />
276
+ <span className="font-mono text-[7px] text-[var(--color-warning,#ff4d00)]">
277
+ {card.permissions.length} PERMISSION{card.permissions.length > 1 ? 'S' : ''}
278
+ </span>
279
+ </div>
280
+ )}
281
+ </div>
282
+ <Button
283
+ variant={needsApproval ? 'secondary' : 'ghost'}
284
+ size="sm"
285
+ onClick={(e: React.MouseEvent<HTMLButtonElement>) => {
286
+ if (needsApproval && card.manifest) {
287
+ setApprovalApp(card.manifest);
288
+ setApprovalAnchor(e.currentTarget);
289
+ setApprovalError(null);
290
+ } else {
291
+ card.onAdd();
292
+ }
293
+ }}
294
+ icon={needsApproval ? <Shield size={10} /> : <Plus size={10} />}
295
+ >
296
+ {needsApproval ? 'APPROVE' : 'ADD'}
297
+ </Button>
298
+ </div>
299
+ </div>
300
+ );
301
+ })}
302
+ </div>
303
+ )}
304
+
305
+ {/* Approval Popover */}
306
+ <Popover
307
+ isOpen={!!approvalApp}
308
+ onClose={() => { setApprovalApp(null); setApprovalAnchor(null); }}
309
+ title="APP PERMISSIONS"
310
+ anchorEl={approvalAnchor}
311
+ anchor="right"
312
+ >
313
+ {approvalApp && (
314
+ <div className="space-y-3 min-w-[220px]">
315
+ <div>
316
+ <div className="font-mono text-[10px] font-bold text-[var(--color-text,#0a0a0a)]">
317
+ {approvalApp.name}
318
+ </div>
319
+ <div className="font-mono text-[8px] text-[var(--color-text-muted,#6b7280)] mt-0.5">
320
+ This app requests the following permissions:
321
+ </div>
322
+ </div>
323
+
324
+ <div className="space-y-1">
325
+ {(Array.isArray(approvalApp.permissions) ? approvalApp.permissions : []).map(perm => (
326
+ <div
327
+ key={perm}
328
+ className="flex items-center gap-1.5 px-2 py-1.5 bg-[var(--color-background-alt,#f4f4f5)]"
329
+ >
330
+ <Shield size={9} className="text-[var(--color-warning,#ff4d00)] shrink-0" />
331
+ <span className="font-mono text-[9px] text-[var(--color-text,#0a0a0a)]">{perm}</span>
332
+ </div>
333
+ ))}
334
+ </div>
335
+
336
+ {approvalError && (
337
+ <div className="font-mono text-[8px] text-[var(--color-warning,#ff4d00)] px-1">
338
+ {approvalError}
339
+ </div>
340
+ )}
341
+
342
+ <div className="flex gap-2 pt-1">
343
+ <Button
344
+ variant="primary"
345
+ size="sm"
346
+ onClick={handleApprove}
347
+ loading={approving}
348
+ disabled={approving}
349
+ icon={<Check size={10} />}
350
+ className="flex-1"
351
+ >
352
+ APPROVE
353
+ </Button>
354
+ <Button
355
+ variant="secondary"
356
+ size="sm"
357
+ onClick={() => { setApprovalApp(null); setApprovalAnchor(null); }}
358
+ disabled={approving}
359
+ >
360
+ CANCEL
361
+ </Button>
362
+ </div>
363
+ </div>
364
+ )}
365
+ </Popover>
366
+ </div>
367
+ </Drawer>
368
+ );
369
+ }
@@ -0,0 +1,21 @@
1
+ 'use client';
2
+
3
+ import React from 'react';
4
+
5
+ interface ContentAreaProps {
6
+ children: React.ReactNode;
7
+ }
8
+
9
+ export const ContentArea: React.FC<ContentAreaProps> = ({ children }) => {
10
+ return (
11
+ <div className="flex-1 bg-[#f4f4f5] overflow-y-auto relative">
12
+ {/* Subtle Grid Pattern */}
13
+ <div className="absolute inset-0 bg-[linear-gradient(to_right,#e5e5e5_1px,transparent_1px),linear-gradient(to_bottom,#e5e5e5_1px,transparent_1px)] bg-[size:3rem_3rem] opacity-30 pointer-events-none" />
14
+
15
+ {/* Content */}
16
+ <div className="relative z-10 p-6">
17
+ {children}
18
+ </div>
19
+ </div>
20
+ );
21
+ };