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,1007 @@
1
+ /**
2
+ * aurawallet doctor — deterministic onboarding/runtime diagnostics
3
+ */
4
+
5
+ import fs from 'fs';
6
+ import path from 'path';
7
+ import net from 'net';
8
+ import os from 'os';
9
+ import { spawnSync } from 'child_process';
10
+ import { fetchJson, serverUrl, type SetupStatus } from '../lib/http';
11
+ import { parseAuraFile, type AuraMapping } from '../lib/aura-parser';
12
+ import { getErrorMessage } from '../../lib/error';
13
+ import { printBanner, checkBadge, printSection } from '../lib/theme';
14
+
15
+ export type CheckStatus = 'pass' | 'warn' | 'fail';
16
+ export type CheckSeverity = 'info' | 'low' | 'medium' | 'high' | 'critical';
17
+
18
+ export interface DoctorCheck {
19
+ id: string;
20
+ code: string;
21
+ severity: CheckSeverity;
22
+ status: CheckStatus;
23
+ finding: string;
24
+ evidence: string;
25
+ remediation: string;
26
+ }
27
+
28
+ interface DoctorResult {
29
+ ok: boolean;
30
+ mode: 'default' | 'strict';
31
+ summary: { pass: number; warn: number; fail: number };
32
+ checks: DoctorCheck[];
33
+ }
34
+
35
+ interface DoctorOptions {
36
+ json: boolean;
37
+ strict: boolean;
38
+ }
39
+
40
+ interface CredentialHealthSummary {
41
+ totalAnalyzed: number;
42
+ safe: number;
43
+ weak: number;
44
+ reused: number;
45
+ breached: number;
46
+ unknown: number;
47
+ lastScanAt: string | null;
48
+ }
49
+
50
+ interface TokenValidateResponse {
51
+ valid: boolean;
52
+ error?: string;
53
+ payload?: {
54
+ permissions?: string[];
55
+ };
56
+ }
57
+
58
+ interface AuthProbeState {
59
+ socketViable: boolean;
60
+ tokenPresent: boolean;
61
+ tokenShapeValid: boolean;
62
+ tokenMask: string;
63
+ tokenValidate?: TokenValidateResponse;
64
+ }
65
+
66
+ const EXIT = {
67
+ OK: 0,
68
+ FAIL: 1,
69
+ INTERNAL: 2,
70
+ ARGS: 3,
71
+ } as const;
72
+
73
+ const MAX_SECURITY_ENTRIES = 200;
74
+ const SECURITY_TIME_BUDGET_MS = 2000;
75
+ const MAX_AURA_MAPPINGS = 100;
76
+ const MAX_UNIQUE_CREDENTIAL_PROBES = 25;
77
+ const HTTP_TIMEOUT_MS = 1500;
78
+
79
+ function withTimeout<T>(promise: Promise<T>, timeoutMs: number, label: string): Promise<T> {
80
+ return new Promise<T>((resolve, reject) => {
81
+ const timer = setTimeout(() => reject(new Error(`${label}-timeout`)), timeoutMs);
82
+ promise
83
+ .then((value) => {
84
+ clearTimeout(timer);
85
+ resolve(value);
86
+ })
87
+ .catch((err) => {
88
+ clearTimeout(timer);
89
+ reject(err);
90
+ });
91
+ });
92
+ }
93
+
94
+ function parseArgs(argv: string[]): DoctorOptions {
95
+ const flags = new Set(argv);
96
+ for (const flag of flags) {
97
+ if (!['--json', '--strict', '--help', '-h'].includes(flag)) {
98
+ throw new Error(`Unknown flag: ${flag}`);
99
+ }
100
+ }
101
+
102
+ if (flags.has('--help') || flags.has('-h')) {
103
+ console.log('Usage: npx aurawallet doctor [--json] [--strict]');
104
+ process.exit(EXIT.OK);
105
+ }
106
+
107
+ return {
108
+ json: flags.has('--json'),
109
+ strict: flags.has('--strict'),
110
+ };
111
+ }
112
+
113
+ function maskToken(token?: string): string {
114
+ if (!token) return 'absent';
115
+ const trimmed = token.trim();
116
+ if (!trimmed) return 'present-empty';
117
+ return `tok_****${trimmed.slice(-4)}`;
118
+ }
119
+
120
+ function checkTokenShape(token?: string): boolean {
121
+ if (!token) return false;
122
+ return token.trim().length >= 16;
123
+ }
124
+
125
+ async function probeSocketViability(): Promise<{ viable: boolean; evidence: string }> {
126
+ const uid = process.getuid?.() ?? os.userInfo().uid;
127
+ const socketPath = `/tmp/aura-cli-${uid}.sock`;
128
+
129
+ let st: fs.Stats;
130
+ try {
131
+ st = fs.statSync(socketPath);
132
+ } catch {
133
+ return { viable: false, evidence: 'socket-missing' };
134
+ }
135
+
136
+ if (!st.isSocket()) return { viable: false, evidence: 'socket-path-not-unix-socket' };
137
+ if (st.uid !== uid) return { viable: false, evidence: 'socket-owner-mismatch' };
138
+ if ((st.mode & 0o777) > 0o600) return { viable: false, evidence: 'socket-perms-too-open' };
139
+
140
+ const connectOk = await new Promise<boolean>((resolve) => {
141
+ const client = net.createConnection(socketPath);
142
+ const timer = setTimeout(() => {
143
+ client.destroy();
144
+ resolve(false);
145
+ }, 500);
146
+
147
+ let done = false;
148
+ const finish = (value: boolean) => {
149
+ if (done) return;
150
+ done = true;
151
+ clearTimeout(timer);
152
+ client.destroy();
153
+ resolve(value);
154
+ };
155
+
156
+ client.once('connect', () => {
157
+ client.write('{"type":"ping"}\n');
158
+ });
159
+ client.once('data', (buf) => {
160
+ const msg = buf.toString('utf8');
161
+ finish(msg.includes('pong'));
162
+ });
163
+ client.once('error', () => finish(false));
164
+ client.once('close', () => finish(false));
165
+ });
166
+
167
+ return { viable: connectOk, evidence: connectOk ? 'socket-connect-ping-ok' : 'socket-connect-failed' };
168
+ }
169
+
170
+ function normalizeStatus(status: number): CheckStatus {
171
+ if (status >= 500) return 'fail';
172
+ if (status >= 400) return 'warn';
173
+ return 'pass';
174
+ }
175
+
176
+ export function evaluateCredentialHealthSeverity(summary: CredentialHealthSummary): {
177
+ status: CheckStatus;
178
+ severity: CheckSeverity;
179
+ code: string;
180
+ finding: string;
181
+ remediation: string;
182
+ evidence: string;
183
+ } {
184
+ if (summary.breached > 0) {
185
+ return {
186
+ status: 'fail',
187
+ severity: 'high',
188
+ code: 'AURA_DOCTOR_CREDENTIAL_HEALTH_BREACHED',
189
+ finding: 'Credential health check found breached credentials.',
190
+ remediation: 'Rotate breached credentials immediately and rerun scan',
191
+ evidence: `breached=${summary.breached},weak=${summary.weak},reused=${summary.reused},unknown=${summary.unknown}`,
192
+ };
193
+ }
194
+
195
+ if (summary.weak > 0 || summary.reused > 0 || summary.unknown > 0) {
196
+ const unknownRemediation = summary.unknown > 0
197
+ ? ' Retry health scan with HEALTH_BREACH_CHECK=true to resolve unknown breach status.'
198
+ : '';
199
+ return {
200
+ status: 'warn',
201
+ severity: 'medium',
202
+ code: summary.unknown > 0
203
+ ? 'AURA_DOCTOR_CREDENTIAL_HEALTH_WARN_UNKNOWN'
204
+ : 'AURA_DOCTOR_CREDENTIAL_HEALTH_WARN_RISK',
205
+ finding: 'Credential health check found weak/reused/unknown-risk credentials.',
206
+ remediation: `Fix weak/reused credentials and rerun scan.${unknownRemediation}`.trim(),
207
+ evidence: `breached=${summary.breached},weak=${summary.weak},reused=${summary.reused},unknown=${summary.unknown}`,
208
+ };
209
+ }
210
+
211
+ return {
212
+ status: 'pass',
213
+ severity: 'info',
214
+ code: 'AURA_DOCTOR_CREDENTIAL_HEALTH_PASS',
215
+ finding: 'Credential health check found no weak/reused/breached/unknown credentials.',
216
+ remediation: 'none',
217
+ evidence: `breached=0,weak=0,reused=0,unknown=0`,
218
+ };
219
+ }
220
+
221
+ async function fetchWithStatus(url: string, init?: RequestInit): Promise<{ ok: boolean; status: number; text: string }> {
222
+ const res = await withTimeout(fetch(url, init), HTTP_TIMEOUT_MS, 'http');
223
+ const text = await res.text();
224
+ return { ok: res.ok, status: res.status, text };
225
+ }
226
+
227
+ function findAuraFile(): string | null {
228
+ let dir = process.cwd();
229
+ while (true) {
230
+ const candidate = path.join(dir, '.aura');
231
+ if (fs.existsSync(candidate)) return candidate;
232
+ const parent = path.dirname(dir);
233
+ if (parent === dir) return null;
234
+ dir = parent;
235
+ }
236
+ }
237
+
238
+ function listLikelySecretFiles(cwd: string): { checked: number; matches: string[]; timedOut: boolean } {
239
+ const started = Date.now();
240
+ const queue: Array<{ dir: string; depth: number }> = [{ dir: cwd, depth: 0 }];
241
+ let checked = 0;
242
+ const matches: string[] = [];
243
+
244
+ while (queue.length > 0) {
245
+ if (Date.now() - started > SECURITY_TIME_BUDGET_MS) {
246
+ return { checked, matches, timedOut: true };
247
+ }
248
+ const { dir, depth } = queue.shift()!;
249
+
250
+ let entries: fs.Dirent[];
251
+ try {
252
+ entries = fs.readdirSync(dir, { withFileTypes: true });
253
+ } catch {
254
+ continue;
255
+ }
256
+
257
+ for (const entry of entries) {
258
+ checked += 1;
259
+ if (checked > MAX_SECURITY_ENTRIES) {
260
+ return { checked, matches, timedOut: false };
261
+ }
262
+
263
+ const full = path.join(dir, entry.name);
264
+ if (entry.isDirectory() && depth < 2 && entry.name !== 'node_modules' && !entry.name.startsWith('.git')) {
265
+ queue.push({ dir: full, depth: depth + 1 });
266
+ continue;
267
+ }
268
+
269
+ if (!entry.isFile()) continue;
270
+ if (/\.env($|\.)/i.test(entry.name) || /token/i.test(entry.name)) {
271
+ matches.push(path.relative(cwd, full));
272
+ }
273
+ }
274
+ }
275
+
276
+ return { checked, matches, timedOut: false };
277
+ }
278
+
279
+ async function runDoctor(options: DoctorOptions): Promise<DoctorResult> {
280
+ const checks: DoctorCheck[] = [];
281
+
282
+ const addCheck = (check: DoctorCheck) => checks.push(check);
283
+
284
+ // runtime.api.health
285
+ let apiHealthy = false;
286
+ try {
287
+ const health = await fetchWithStatus(`${serverUrl()}/health`);
288
+ apiHealthy = health.ok;
289
+ addCheck({
290
+ id: 'runtime.api.health',
291
+ code: health.ok ? 'AURA_DOCTOR_RUNTIME_API_HEALTHY' : 'AURA_DOCTOR_RUNTIME_API_UNREACHABLE',
292
+ severity: health.ok ? 'info' : 'critical',
293
+ status: health.ok ? 'pass' : 'fail',
294
+ finding: health.ok ? 'Aura API is reachable.' : 'Aura API is unreachable.',
295
+ evidence: health.ok ? `health-status-${health.status}` : `health-status-${health.status}`,
296
+ remediation: health.ok ? 'none' : 'Run: aura start',
297
+ });
298
+ } catch (err) {
299
+ addCheck({
300
+ id: 'runtime.api.health',
301
+ code: 'AURA_DOCTOR_RUNTIME_API_UNREACHABLE',
302
+ severity: 'critical',
303
+ status: 'fail',
304
+ finding: 'Aura API is unreachable.',
305
+ evidence: `health-error-${getErrorMessage(err)}`,
306
+ remediation: 'Run: aura start',
307
+ });
308
+ }
309
+
310
+ // runtime.api.setup + vault checks
311
+ let setup: SetupStatus | null = null;
312
+ try {
313
+ setup = await fetchJson<SetupStatus>('/setup');
314
+ addCheck({
315
+ id: 'runtime.api.setup',
316
+ code: 'AURA_DOCTOR_RUNTIME_SETUP_OK',
317
+ severity: 'info',
318
+ status: 'pass',
319
+ finding: 'Setup endpoint responded.',
320
+ evidence: 'setup-response-valid',
321
+ remediation: 'none',
322
+ });
323
+ } catch (err) {
324
+ addCheck({
325
+ id: 'runtime.api.setup',
326
+ code: 'AURA_DOCTOR_RUNTIME_SETUP_ERROR',
327
+ severity: 'high',
328
+ status: 'fail',
329
+ finding: 'Setup endpoint check failed.',
330
+ evidence: `setup-error-${getErrorMessage(err)}`,
331
+ remediation: 'Run: aura start',
332
+ });
333
+ }
334
+
335
+ // runtime.dashboard.reachability (never fail)
336
+ if (!apiHealthy) {
337
+ addCheck({
338
+ id: 'runtime.dashboard.reachability',
339
+ code: 'AURA_DOCTOR_DASHBOARD_SKIPPED_API_UNHEALTHY',
340
+ severity: 'low',
341
+ status: 'warn',
342
+ finding: 'Dashboard check skipped while API is unhealthy.',
343
+ evidence: 'api-unhealthy-dashboard-check-skipped',
344
+ remediation: 'Run: aura start',
345
+ });
346
+ } else {
347
+ try {
348
+ const dashboard = await withTimeout(fetch('http://localhost:4747/app'), 1500, 'dashboard');
349
+ const reachable = dashboard.ok || (dashboard.status >= 300 && dashboard.status < 400);
350
+ addCheck({
351
+ id: 'runtime.dashboard.reachability',
352
+ code: reachable ? 'AURA_DOCTOR_DASHBOARD_REACHABLE' : 'AURA_DOCTOR_DASHBOARD_UNREACHABLE',
353
+ severity: reachable ? 'info' : 'low',
354
+ status: reachable ? 'pass' : 'warn',
355
+ finding: reachable ? 'Dashboard endpoint is reachable.' : 'Dashboard endpoint is not reachable in current mode.',
356
+ evidence: reachable ? 'dashboard-reachable' : 'headless-or-dashboard-not-running',
357
+ remediation: reachable ? 'none' : 'If UI needed: aura start',
358
+ });
359
+ } catch {
360
+ addCheck({
361
+ id: 'runtime.dashboard.reachability',
362
+ code: 'AURA_DOCTOR_DASHBOARD_UNREACHABLE',
363
+ severity: 'low',
364
+ status: 'warn',
365
+ finding: 'Dashboard endpoint is not reachable in current mode.',
366
+ evidence: 'headless-or-dashboard-not-running',
367
+ remediation: 'If UI needed: aura start',
368
+ });
369
+ }
370
+ }
371
+
372
+ const hasWallet = !!setup?.hasWallet;
373
+ const unlocked = !!setup?.unlocked;
374
+ const hasAddress = !!setup?.address;
375
+
376
+ // vault.exists
377
+ addCheck({
378
+ id: 'vault.exists',
379
+ code: hasWallet ? 'AURA_DOCTOR_VAULT_EXISTS' : 'AURA_DOCTOR_VAULT_MISSING',
380
+ severity: hasWallet ? 'info' : 'high',
381
+ status: hasWallet ? 'pass' : 'fail',
382
+ finding: hasWallet ? 'Primary vault exists.' : 'No vault found.',
383
+ evidence: hasWallet ? 'hasWallet=true' : 'hasWallet=false',
384
+ remediation: hasWallet ? 'none' : 'Run: aura setup',
385
+ });
386
+
387
+ // vault.unlock_state
388
+ addCheck({
389
+ id: 'vault.unlock_state',
390
+ code: unlocked ? 'AURA_DOCTOR_VAULT_UNLOCKED' : 'AURA_DOCTOR_VAULT_LOCKED',
391
+ severity: unlocked ? 'info' : 'medium',
392
+ status: unlocked ? 'pass' : 'warn',
393
+ finding: unlocked ? 'Vault is unlocked.' : 'Vault is locked.',
394
+ evidence: `unlocked=${unlocked}`,
395
+ remediation: unlocked ? 'none' : 'Run: aura unlock',
396
+ });
397
+
398
+ // vault.primary.address tuple
399
+ let addressStatus: CheckStatus = 'pass';
400
+ let addressSeverity: CheckSeverity = 'info';
401
+ let addressEvidence = 'wallet-locked-address-not-required';
402
+ let addressCode = 'AURA_DOCTOR_VAULT_PRIMARY_ADDRESS_NA_LOCKED';
403
+ let addressFinding = 'Primary address is not required while locked.';
404
+ let addressRemediation = 'none';
405
+
406
+ if (!hasWallet) {
407
+ addressStatus = 'fail';
408
+ addressSeverity = 'high';
409
+ addressEvidence = 'no-wallet';
410
+ addressCode = 'AURA_DOCTOR_VAULT_NO_WALLET';
411
+ addressFinding = 'Primary address unavailable because no vault exists.';
412
+ addressRemediation = 'Run: aura setup';
413
+ } else if (unlocked && hasAddress) {
414
+ addressStatus = 'pass';
415
+ addressSeverity = 'info';
416
+ addressEvidence = 'primary-address-present';
417
+ addressCode = 'AURA_DOCTOR_VAULT_PRIMARY_ADDRESS_PRESENT';
418
+ addressFinding = 'Primary address is present.';
419
+ addressRemediation = 'none';
420
+ } else if (unlocked && !hasAddress) {
421
+ addressStatus = 'warn';
422
+ addressSeverity = 'medium';
423
+ addressEvidence = 'unlocked-without-primary-address';
424
+ addressCode = 'AURA_DOCTOR_VAULT_PRIMARY_ADDRESS_MISSING';
425
+ addressFinding = 'Vault is unlocked but primary address is missing.';
426
+ addressRemediation = 'Run: aura setup --repair';
427
+ }
428
+
429
+ addCheck({
430
+ id: 'vault.primary.address',
431
+ code: addressCode,
432
+ severity: addressSeverity,
433
+ status: addressStatus,
434
+ finding: addressFinding,
435
+ evidence: addressEvidence,
436
+ remediation: addressRemediation,
437
+ });
438
+
439
+ // auth.socket.path
440
+ const socketProbe = await probeSocketViability();
441
+ addCheck({
442
+ id: 'auth.socket.path',
443
+ code: socketProbe.viable ? 'AURA_DOCTOR_AUTH_SOCKET_VIABLE' : 'AURA_DOCTOR_AUTH_SOCKET_NOT_VIABLE',
444
+ severity: socketProbe.viable ? 'info' : 'low',
445
+ status: socketProbe.viable ? 'pass' : 'warn',
446
+ finding: socketProbe.viable ? 'Unix socket auth is viable.' : 'Unix socket auth is not viable.',
447
+ evidence: socketProbe.evidence,
448
+ remediation: socketProbe.viable ? 'none' : 'Run: aura start',
449
+ });
450
+
451
+ // auth.token.env
452
+ const envToken = process.env.AURA_TOKEN;
453
+ const tokenPresent = !!envToken;
454
+ const tokenShapeValid = checkTokenShape(envToken);
455
+ addCheck({
456
+ id: 'auth.token.env',
457
+ code: tokenPresent ? (tokenShapeValid ? 'AURA_DOCTOR_AUTH_TOKEN_PRESENT' : 'AURA_DOCTOR_AUTH_TOKEN_INVALID_SHAPE') : 'AURA_DOCTOR_AUTH_TOKEN_MISSING',
458
+ severity: tokenPresent ? (tokenShapeValid ? 'info' : 'medium') : 'low',
459
+ status: tokenPresent ? (tokenShapeValid ? 'pass' : 'warn') : 'warn',
460
+ finding: tokenPresent ? (tokenShapeValid ? 'AURA_TOKEN is present.' : 'AURA_TOKEN is present but malformed.') : 'AURA_TOKEN is not set.',
461
+ evidence: maskToken(envToken),
462
+ remediation: tokenPresent ? (tokenShapeValid ? 'none' : 'Export a valid AURA_TOKEN') : 'Export AURA_TOKEN or rely on socket auth',
463
+ });
464
+
465
+ // auth.mode.viability matrix
466
+ const tokenViable = tokenPresent && tokenShapeValid;
467
+ const authViable = socketProbe.viable || tokenViable;
468
+ addCheck({
469
+ id: 'auth.mode.viability',
470
+ code: authViable ? 'AURA_DOCTOR_AUTH_MODE_VIABLE' : 'AURA_DOCTOR_AUTH_MODE_UNAVAILABLE',
471
+ severity: authViable ? 'info' : 'critical',
472
+ status: authViable ? 'pass' : 'fail',
473
+ finding: authViable ? 'At least one auth bootstrap mode is viable.' : 'No viable auth bootstrap mode found.',
474
+ evidence: authViable
475
+ ? (socketProbe.viable && tokenViable ? 'socket-and-token-available' : socketProbe.viable ? 'socket-available' : 'token-available')
476
+ : 'no-viable-auth-bootstrap',
477
+ remediation: authViable ? 'none' : 'Start daemon for socket (aura start) or export AURA_TOKEN',
478
+ });
479
+
480
+ const authProbeState: AuthProbeState = {
481
+ socketViable: socketProbe.viable,
482
+ tokenPresent,
483
+ tokenShapeValid,
484
+ tokenMask: maskToken(envToken),
485
+ };
486
+
487
+ // token introspection (only if env token present + shape valid)
488
+ if (tokenViable) {
489
+ try {
490
+ const validate = await fetchJson<TokenValidateResponse>('/auth/validate', {
491
+ body: { token: envToken },
492
+ });
493
+ authProbeState.tokenValidate = validate;
494
+ } catch (err) {
495
+ authProbeState.tokenValidate = {
496
+ valid: false,
497
+ error: getErrorMessage(err),
498
+ };
499
+ }
500
+ }
501
+
502
+ // credential.list.readiness (read-only)
503
+ if (!tokenViable) {
504
+ addCheck({
505
+ id: 'credential.list.readiness',
506
+ code: 'AURA_DOCTOR_CREDENTIAL_LIST_TOKEN_UNAVAILABLE',
507
+ severity: 'medium',
508
+ status: 'warn',
509
+ finding: 'Credential list readiness requires explicit token audit mode.',
510
+ evidence: 'socket-mode-no-explicit-token',
511
+ remediation: 'Export AURA_TOKEN for explicit credential-read checks',
512
+ });
513
+ } else {
514
+ try {
515
+ const response = await fetchWithStatus(`${serverUrl()}/credentials`, {
516
+ headers: {
517
+ Authorization: `Bearer ${envToken}`,
518
+ },
519
+ });
520
+
521
+ const status = normalizeStatus(response.status);
522
+ const code = response.ok
523
+ ? 'AURA_DOCTOR_CREDENTIAL_LIST_READY'
524
+ : response.status === 401 || response.status === 403
525
+ ? 'AURA_DOCTOR_CREDENTIAL_LIST_UNAUTHORIZED'
526
+ : 'AURA_DOCTOR_CREDENTIAL_LIST_ERROR';
527
+
528
+ addCheck({
529
+ id: 'credential.list.readiness',
530
+ code,
531
+ severity: status === 'pass' ? 'info' : status === 'warn' ? 'medium' : 'high',
532
+ status,
533
+ finding: response.ok ? 'Credential list endpoint is readable.' : 'Credential list endpoint is not readable.',
534
+ evidence: `credentials-list-http-${response.status}`,
535
+ remediation: response.ok ? 'none' : 'Grant secret:read scope or refresh token',
536
+ });
537
+ } catch (err) {
538
+ addCheck({
539
+ id: 'credential.list.readiness',
540
+ code: 'AURA_DOCTOR_CREDENTIAL_LIST_ERROR',
541
+ severity: 'high',
542
+ status: 'fail',
543
+ finding: 'Credential list endpoint check failed.',
544
+ evidence: `credentials-list-error-${getErrorMessage(err)}`,
545
+ remediation: 'Verify Aura API health and token scope',
546
+ });
547
+ }
548
+ }
549
+
550
+ // credential.scope.sanity
551
+ if (!tokenViable) {
552
+ addCheck({
553
+ id: 'credential.scope.sanity',
554
+ code: 'AURA_DOCTOR_SCOPE_TOKEN_INTROSPECTION_UNAVAILABLE',
555
+ severity: 'low',
556
+ status: 'warn',
557
+ finding: 'Token scope introspection unavailable without explicit token.',
558
+ evidence: 'socket-mode-no-explicit-token',
559
+ remediation: 'Export AURA_TOKEN for explicit scope audit',
560
+ });
561
+ } else if (!authProbeState.tokenValidate?.valid) {
562
+ addCheck({
563
+ id: 'credential.scope.sanity',
564
+ code: 'AURA_DOCTOR_SCOPE_TOKEN_INVALID',
565
+ severity: 'high',
566
+ status: 'fail',
567
+ finding: 'Provided token failed validation.',
568
+ evidence: 'token-validation-failed',
569
+ remediation: 'Export a valid AURA_TOKEN with secret:read permissions',
570
+ });
571
+ } else {
572
+ const permissions = authProbeState.tokenValidate.payload?.permissions || [];
573
+ const hasSecretRead = permissions.includes('admin:*') || permissions.includes('secret:read');
574
+ addCheck({
575
+ id: 'credential.scope.sanity',
576
+ code: hasSecretRead ? 'AURA_DOCTOR_SCOPE_OK' : 'AURA_DOCTOR_SCOPE_MISSING_SECRET_READ',
577
+ severity: hasSecretRead ? 'info' : 'high',
578
+ status: hasSecretRead ? 'pass' : 'fail',
579
+ finding: hasSecretRead ? 'Token scope includes secret read access.' : 'Token scope is missing secret:read access.',
580
+ evidence: hasSecretRead ? 'required-scope-present' : 'required-scope-missing-secret-read',
581
+ remediation: hasSecretRead ? 'none' : 'Issue token with secret:read (or admin:*) permission',
582
+ });
583
+ }
584
+
585
+ // credential.health.summary
586
+ if (!tokenViable) {
587
+ addCheck({
588
+ id: 'credential.health.summary',
589
+ code: 'AURA_DOCTOR_CREDENTIAL_HEALTH_TOKEN_UNAVAILABLE',
590
+ severity: 'low',
591
+ status: 'warn',
592
+ finding: 'Credential health summary requires explicit token mode.',
593
+ evidence: 'socket-mode-no-explicit-token',
594
+ remediation: 'Export AURA_TOKEN for credential health summary check',
595
+ });
596
+ } else {
597
+ try {
598
+ const response = await fetchWithStatus(`${serverUrl()}/credentials/health/summary`, {
599
+ headers: { Authorization: `Bearer ${envToken}` },
600
+ });
601
+
602
+ if (!response.ok) {
603
+ addCheck({
604
+ id: 'credential.health.summary',
605
+ code: response.status === 401 || response.status === 403
606
+ ? 'AURA_DOCTOR_CREDENTIAL_HEALTH_UNAUTHORIZED'
607
+ : 'AURA_DOCTOR_CREDENTIAL_HEALTH_ERROR',
608
+ severity: response.status === 401 || response.status === 403 ? 'medium' : 'high',
609
+ status: normalizeStatus(response.status),
610
+ finding: 'Credential health summary endpoint is not readable.',
611
+ evidence: `credential-health-summary-http-${response.status}`,
612
+ remediation: 'Grant secret:read scope or refresh token',
613
+ });
614
+ } else {
615
+ const data = JSON.parse(response.text) as { summary?: CredentialHealthSummary };
616
+ if (!data.summary) {
617
+ addCheck({
618
+ id: 'credential.health.summary',
619
+ code: 'AURA_DOCTOR_CREDENTIAL_HEALTH_MALFORMED',
620
+ severity: 'high',
621
+ status: 'fail',
622
+ finding: 'Credential health summary payload is malformed.',
623
+ evidence: 'summary-missing',
624
+ remediation: 'Upgrade Aura server and rerun doctor',
625
+ });
626
+ } else {
627
+ const evaluated = evaluateCredentialHealthSeverity(data.summary);
628
+ addCheck({
629
+ id: 'credential.health.summary',
630
+ code: evaluated.code,
631
+ severity: evaluated.severity,
632
+ status: evaluated.status,
633
+ finding: evaluated.finding,
634
+ evidence: evaluated.evidence,
635
+ remediation: evaluated.remediation,
636
+ });
637
+ }
638
+ }
639
+ } catch (err) {
640
+ addCheck({
641
+ id: 'credential.health.summary',
642
+ code: 'AURA_DOCTOR_CREDENTIAL_HEALTH_ERROR',
643
+ severity: 'high',
644
+ status: 'fail',
645
+ finding: 'Credential health summary check failed.',
646
+ evidence: `credential-health-summary-error-${getErrorMessage(err)}`,
647
+ remediation: 'Verify Aura API health and token scope',
648
+ });
649
+ }
650
+ }
651
+
652
+ // .aura checks
653
+ const auraFile = findAuraFile();
654
+ if (!auraFile) {
655
+ addCheck({
656
+ id: 'aura_file.discovery',
657
+ code: 'AURA_DOCTOR_AURA_FILE_MISSING',
658
+ severity: 'low',
659
+ status: 'warn',
660
+ finding: 'No .aura file found in current/parent directories.',
661
+ evidence: 'aura-file-not-found',
662
+ remediation: 'Run: aura env init',
663
+ });
664
+
665
+ addCheck({
666
+ id: 'aura_file.parse',
667
+ code: 'AURA_DOCTOR_AURA_FILE_PARSE_SKIPPED',
668
+ severity: 'info',
669
+ status: 'pass',
670
+ finding: '.aura parse check skipped.',
671
+ evidence: 'no-aura-file',
672
+ remediation: 'none',
673
+ });
674
+
675
+ addCheck({
676
+ id: 'aura_file.mapping_resolution',
677
+ code: 'AURA_DOCTOR_AURA_MAPPING_SKIPPED',
678
+ severity: 'info',
679
+ status: 'pass',
680
+ finding: '.aura mapping resolution skipped.',
681
+ evidence: 'no-aura-file',
682
+ remediation: 'none',
683
+ });
684
+ } else {
685
+ addCheck({
686
+ id: 'aura_file.discovery',
687
+ code: 'AURA_DOCTOR_AURA_FILE_FOUND',
688
+ severity: 'info',
689
+ status: 'pass',
690
+ finding: '.aura file discovered.',
691
+ evidence: path.relative(process.cwd(), auraFile) || '.aura',
692
+ remediation: 'none',
693
+ });
694
+
695
+ let mappings: AuraMapping[] = [];
696
+ let parseOk = false;
697
+ try {
698
+ mappings = parseAuraFile(auraFile);
699
+ parseOk = true;
700
+ addCheck({
701
+ id: 'aura_file.parse',
702
+ code: 'AURA_DOCTOR_AURA_FILE_PARSE_OK',
703
+ severity: 'info',
704
+ status: 'pass',
705
+ finding: '.aura file parsed successfully.',
706
+ evidence: `mappings=${mappings.length}`,
707
+ remediation: 'none',
708
+ });
709
+ } catch (err) {
710
+ addCheck({
711
+ id: 'aura_file.parse',
712
+ code: 'AURA_DOCTOR_AURA_FILE_PARSE_FAILED',
713
+ severity: 'high',
714
+ status: 'fail',
715
+ finding: '.aura file parse failed.',
716
+ evidence: getErrorMessage(err),
717
+ remediation: 'Fix .aura syntax and rerun aura doctor',
718
+ });
719
+ }
720
+
721
+ if (!parseOk) {
722
+ addCheck({
723
+ id: 'aura_file.mapping_resolution',
724
+ code: 'AURA_DOCTOR_AURA_MAPPING_SKIPPED_PARSE_FAIL',
725
+ severity: 'info',
726
+ status: 'pass',
727
+ finding: '.aura mapping resolution skipped due to parse failure.',
728
+ evidence: 'parse-failed',
729
+ remediation: 'none',
730
+ });
731
+ } else if (!tokenViable) {
732
+ addCheck({
733
+ id: 'aura_file.mapping_resolution',
734
+ code: 'AURA_DOCTOR_AURA_MAPPING_TOKEN_UNAVAILABLE',
735
+ severity: 'medium',
736
+ status: 'warn',
737
+ finding: '.aura mapping resolution requires explicit token mode.',
738
+ evidence: 'socket-mode-no-explicit-token',
739
+ remediation: 'Export AURA_TOKEN for mapping resolution audit',
740
+ });
741
+ } else {
742
+ const cappedMappings = mappings.slice(0, MAX_AURA_MAPPINGS);
743
+ const uniqueCredentialNames = [...new Set(cappedMappings.map((m) => m.credentialName))].slice(0, MAX_UNIQUE_CREDENTIAL_PROBES);
744
+ const credIdByName = new Map<string, string>();
745
+ const failures = new Set<string>();
746
+
747
+ for (const credName of uniqueCredentialNames) {
748
+ try {
749
+ const resp = await fetchWithStatus(`${serverUrl()}/credentials?q=${encodeURIComponent(credName)}`, {
750
+ headers: { Authorization: `Bearer ${envToken}` },
751
+ });
752
+
753
+ if (!resp.ok) {
754
+ failures.add(`lookup:${credName}`);
755
+ continue;
756
+ }
757
+
758
+ const data = JSON.parse(resp.text) as { credentials?: Array<{ id: string; name: string }> };
759
+ const list = data.credentials || [];
760
+ const exact = list.find((c) => c.name.toLowerCase() === credName.toLowerCase()) || list[0];
761
+ if (!exact) failures.add(`missing-credential:${credName}`);
762
+ else credIdByName.set(credName, exact.id);
763
+ } catch {
764
+ failures.add(`lookup:${credName}`);
765
+ }
766
+ }
767
+
768
+ for (const mapping of cappedMappings) {
769
+ const id = credIdByName.get(mapping.credentialName);
770
+ if (!id) {
771
+ failures.add(`${mapping.envVar}->${mapping.credentialName}.${mapping.field}`);
772
+ continue;
773
+ }
774
+
775
+ try {
776
+ const resp = await fetchWithStatus(`${serverUrl()}/credentials/${id}/read`, {
777
+ method: 'POST',
778
+ headers: { Authorization: `Bearer ${envToken}` },
779
+ });
780
+
781
+ if (!resp.ok) {
782
+ failures.add(`${mapping.envVar}->${mapping.credentialName}.${mapping.field}`);
783
+ }
784
+ } catch {
785
+ failures.add(`${mapping.envVar}->${mapping.credentialName}.${mapping.field}`);
786
+ }
787
+ }
788
+
789
+ const failureList = [...failures];
790
+ const limitedEvidence = failureList.slice(0, 10);
791
+ const more = failureList.length > 10 ? ` (+${failureList.length - 10} more)` : '';
792
+
793
+ addCheck({
794
+ id: 'aura_file.mapping_resolution',
795
+ code: failureList.length === 0 ? 'AURA_DOCTOR_AURA_MAPPING_OK' : 'AURA_DOCTOR_AURA_MAPPING_FAILED',
796
+ severity: failureList.length === 0 ? 'info' : 'high',
797
+ status: failureList.length === 0 ? 'pass' : 'fail',
798
+ finding: failureList.length === 0 ? '.aura mappings are resolvable.' : 'One or more .aura mappings are not resolvable.',
799
+ evidence: failureList.length === 0 ? `checked=${cappedMappings.length}` : `${limitedEvidence.join(', ')}${more}`,
800
+ remediation: failureList.length === 0 ? 'none' : 'Fix missing credentials/fields or token scope',
801
+ });
802
+ }
803
+ }
804
+
805
+ // MCP checks (read-only)
806
+ const mcpHelp = spawnSync('npx', ['aurawallet', 'mcp', '--help'], {
807
+ encoding: 'utf8',
808
+ timeout: 1500,
809
+ });
810
+ const mcpAvailable = !mcpHelp.error && mcpHelp.status === 0;
811
+ addCheck({
812
+ id: 'mcp.command.available',
813
+ code: mcpAvailable ? 'AURA_DOCTOR_MCP_COMMAND_AVAILABLE' : 'AURA_DOCTOR_MCP_COMMAND_UNAVAILABLE',
814
+ severity: mcpAvailable ? 'info' : 'medium',
815
+ status: mcpAvailable ? 'pass' : 'warn',
816
+ finding: mcpAvailable ? 'MCP command is invokable.' : 'MCP command is not invokable.',
817
+ evidence: mcpAvailable ? 'mcp-help-ok' : `mcp-help-failed-${mcpHelp.status ?? 'error'}`,
818
+ remediation: mcpAvailable ? 'none' : 'Install dependencies and ensure npx aurawallet mcp is available',
819
+ });
820
+
821
+ const mcpConfigs = [
822
+ path.join(process.cwd(), '.vscode', 'mcp.json'),
823
+ path.join(os.homedir(), '.cursor', 'mcp.json'),
824
+ path.join(os.homedir(), '.windsurf', 'mcp.json'),
825
+ path.join(os.homedir(), 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json'),
826
+ ];
827
+
828
+ const existingConfigs = mcpConfigs.filter((p) => fs.existsSync(p));
829
+ let parseFailures = 0;
830
+ for (const file of existingConfigs) {
831
+ try {
832
+ JSON.parse(fs.readFileSync(file, 'utf8'));
833
+ } catch {
834
+ parseFailures += 1;
835
+ }
836
+ }
837
+
838
+ addCheck({
839
+ id: 'mcp.config.footprint',
840
+ code: parseFailures > 0 ? 'AURA_DOCTOR_MCP_CONFIG_INVALID' : existingConfigs.length > 0 ? 'AURA_DOCTOR_MCP_CONFIG_PRESENT' : 'AURA_DOCTOR_MCP_CONFIG_ABSENT',
841
+ severity: parseFailures > 0 ? 'medium' : existingConfigs.length > 0 ? 'info' : 'low',
842
+ status: parseFailures > 0 ? 'warn' : existingConfigs.length > 0 ? 'pass' : 'warn',
843
+ finding: parseFailures > 0
844
+ ? 'One or more MCP config files are invalid JSON.'
845
+ : existingConfigs.length > 0
846
+ ? 'MCP config footprint detected.'
847
+ : 'No MCP config footprint detected.',
848
+ evidence: parseFailures > 0
849
+ ? `invalid-json-count=${parseFailures}`
850
+ : existingConfigs.length > 0
851
+ ? `configs=${existingConfigs.length}`
852
+ : 'no-known-mcp-config-files',
853
+ remediation: parseFailures > 0
854
+ ? 'Fix malformed MCP config JSON files'
855
+ : existingConfigs.length > 0
856
+ ? 'none'
857
+ : 'Run: aura mcp --install',
858
+ });
859
+
860
+ addCheck({
861
+ id: 'mcp.auth.forecast',
862
+ code: authViable ? 'AURA_DOCTOR_MCP_AUTH_FORECAST_READY' : 'AURA_DOCTOR_MCP_AUTH_FORECAST_BLOCKED',
863
+ severity: authViable ? 'info' : 'high',
864
+ status: authViable ? 'pass' : 'fail',
865
+ finding: authViable ? 'MCP auth bootstrap appears ready.' : 'MCP auth bootstrap is blocked.',
866
+ evidence: authViable ? 'auth-bootstrap-viable' : 'no-viable-auth-bootstrap',
867
+ remediation: authViable ? 'none' : 'Start daemon socket or export AURA_TOKEN before MCP use',
868
+ });
869
+
870
+ // Extension check
871
+ addCheck({
872
+ id: 'extension.detectability.cli_mode',
873
+ code: 'AURA_DOCTOR_EXTENSION_NOT_DETECTABLE_CLI_MODE',
874
+ severity: 'low',
875
+ status: 'warn',
876
+ finding: 'Extension session state is not detectable in CLI mode.',
877
+ evidence: 'not-detectable-in-this-mode',
878
+ remediation: 'Use extension UI diagnostics for handshake details',
879
+ });
880
+
881
+ // Security checks
882
+ if (!tokenViable) {
883
+ addCheck({
884
+ id: 'security.token_scope.breadth',
885
+ code: 'AURA_DOCTOR_SECURITY_TOKEN_INTROSPECTION_UNAVAILABLE',
886
+ severity: 'low',
887
+ status: 'warn',
888
+ finding: 'Cannot evaluate token scope breadth without explicit token.',
889
+ evidence: 'socket-mode-no-explicit-token',
890
+ remediation: 'Export AURA_TOKEN for explicit scope audit',
891
+ });
892
+ } else {
893
+ const permissions = authProbeState.tokenValidate?.payload?.permissions || [];
894
+ const broad = permissions.some((p) => p === 'admin:*' || p === '*' || p.endsWith(':*'));
895
+ addCheck({
896
+ id: 'security.token_scope.breadth',
897
+ code: broad ? 'AURA_DOCTOR_SECURITY_SCOPE_BROAD' : 'AURA_DOCTOR_SECURITY_SCOPE_LEAST_PRIVILEGE',
898
+ severity: broad ? 'medium' : 'info',
899
+ status: broad ? 'warn' : 'pass',
900
+ finding: broad ? 'Token includes broad wildcard/admin scope.' : 'Token scope appears least-privilege oriented.',
901
+ evidence: broad ? 'broad-scope-detected' : 'no-broad-scope-detected',
902
+ remediation: broad ? 'Issue a narrower token for routine automation' : 'none',
903
+ });
904
+ }
905
+
906
+ const scan = listLikelySecretFiles(process.cwd());
907
+ addCheck({
908
+ id: 'security.plaintext_token.artifacts',
909
+ code: scan.timedOut
910
+ ? 'AURA_DOCTOR_SECURITY_SCAN_TIME_BUDGET_EXCEEDED'
911
+ : scan.matches.length > 0
912
+ ? 'AURA_DOCTOR_SECURITY_ARTIFACT_HINTS_FOUND'
913
+ : 'AURA_DOCTOR_SECURITY_ARTIFACT_HINTS_NONE',
914
+ severity: scan.timedOut ? 'low' : scan.matches.length > 0 ? 'medium' : 'info',
915
+ status: scan.timedOut ? 'warn' : scan.matches.length > 0 ? 'warn' : 'pass',
916
+ finding: scan.timedOut
917
+ ? 'Security artifact scan hit time budget.'
918
+ : scan.matches.length > 0
919
+ ? 'Potential plaintext secret artifacts detected.'
920
+ : 'No obvious plaintext secret artifacts detected.',
921
+ evidence: scan.timedOut
922
+ ? `scan-time-budget-exceeded checked=${scan.checked}`
923
+ : scan.matches.length > 0
924
+ ? `matches=${scan.matches.slice(0, 10).join(',')}${scan.matches.length > 10 ? ' (+more)' : ''}`
925
+ : `checked=${scan.checked}`,
926
+ remediation: scan.matches.length > 0
927
+ ? 'Move secrets to Aura vault and remove plaintext files'
928
+ : scan.timedOut
929
+ ? 'Rerun in a smaller working directory for complete scan'
930
+ : 'none',
931
+ });
932
+
933
+ const stalenessHours = process.env.AURA_TOKEN_ISSUED_AT
934
+ ? Math.floor((Date.now() - new Date(process.env.AURA_TOKEN_ISSUED_AT).getTime()) / (1000 * 60 * 60))
935
+ : null;
936
+ const stale = stalenessHours !== null && Number.isFinite(stalenessHours) && stalenessHours > 24;
937
+ addCheck({
938
+ id: 'security.auth_artifact.staleness',
939
+ code: stale ? 'AURA_DOCTOR_SECURITY_AUTH_ARTIFACT_STALE' : 'AURA_DOCTOR_SECURITY_AUTH_ARTIFACT_FRESH_OR_UNKNOWN',
940
+ severity: stale ? 'low' : 'info',
941
+ status: stale ? 'warn' : 'pass',
942
+ finding: stale ? 'Auth artifact appears stale.' : 'Auth artifact freshness is acceptable or unavailable.',
943
+ evidence: stale ? `token-age-hours=${stalenessHours}` : 'no-staleness-metadata',
944
+ remediation: stale ? 'Rotate token to reduce exposure window' : 'none',
945
+ });
946
+
947
+ const summary = checks.reduce(
948
+ (acc, check) => {
949
+ acc[check.status] += 1;
950
+ return acc;
951
+ },
952
+ { pass: 0, warn: 0, fail: 0 }
953
+ );
954
+
955
+ const hasBlocking = summary.fail > 0 || (options.strict && summary.warn > 0);
956
+
957
+ return {
958
+ ok: !hasBlocking,
959
+ mode: options.strict ? 'strict' : 'default',
960
+ summary,
961
+ checks,
962
+ };
963
+ }
964
+
965
+ function formatHuman(checks: DoctorCheck[]): string {
966
+ return checks
967
+ .map((c) => `${checkBadge(c.status)} ${c.id}\n ${c.finding}\n evidence: ${c.evidence}\n remediation: ${c.remediation}`)
968
+ .join('\n');
969
+ }
970
+
971
+ export function mapExitCode(result: DoctorResult): number {
972
+ return result.ok ? EXIT.OK : EXIT.FAIL;
973
+ }
974
+
975
+ async function main() {
976
+ try {
977
+ const options = parseArgs(process.argv.slice(2));
978
+ const result = await runDoctor(options);
979
+
980
+ if (options.json) {
981
+ console.log(JSON.stringify(result, null, 2));
982
+ } else {
983
+ printBanner('DOCTOR');
984
+ console.log(formatHuman(result.checks));
985
+ printSection('Summary');
986
+ console.log(` pass=${result.summary.pass} warn=${result.summary.warn} fail=${result.summary.fail}`);
987
+ if (options.strict) console.log('Mode: strict');
988
+ }
989
+
990
+ process.exit(mapExitCode(result));
991
+ } catch (err) {
992
+ const message = getErrorMessage(err);
993
+ if (message.toLowerCase().startsWith('unknown flag:')) {
994
+ console.error(message);
995
+ process.exit(EXIT.ARGS);
996
+ }
997
+
998
+ console.error(`Doctor internal error: ${message}`);
999
+ process.exit(EXIT.INTERNAL);
1000
+ }
1001
+ }
1002
+
1003
+ if (require.main === module) {
1004
+ main();
1005
+ }
1006
+
1007
+ export { runDoctor, parseArgs };