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,703 @@
1
+ import { Router, Request, Response } from 'express';
2
+ import { createHmac, randomBytes, timingSafeEqual } from 'crypto';
3
+ import { prisma } from '../lib/db';
4
+ import { requireWalletAuth } from '../middleware/auth';
5
+ import { requirePermission } from '../lib/permissions';
6
+ import { ApprovalRouter, loadAdaptersFromDb } from '../lib/adapters';
7
+ import { SERVER_PORT } from '../lib/config';
8
+ import { validateExternalUrl } from '../lib/network';
9
+ import { logger } from '../lib/logger';
10
+ import { getErrorMessage } from '../lib/error';
11
+
12
+ /** Reference to the live approval router (set via setApprovalRouter) */
13
+ let approvalRouter: ApprovalRouter | null = null;
14
+
15
+ /** In-memory nonce store for Telegram chat ID auto-detection */
16
+ interface SetupNonce {
17
+ botToken: string;
18
+ botUsername: string;
19
+ expiresAt: number;
20
+ /** getUpdates offset — skip all updates before this ID */
21
+ offset?: number;
22
+ }
23
+ const telegramSetupNonces = new Map<string, SetupNonce>();
24
+
25
+ /** Exported for testing */
26
+ export { telegramSetupNonces };
27
+
28
+ /** Called from server/index.ts to share the approval router reference */
29
+ export function setApprovalRouter(router: ApprovalRouter | null): void {
30
+ approvalRouter = router;
31
+ }
32
+
33
+ /** Called from server/index.ts to read current router */
34
+ export function getApprovalRouter(): ApprovalRouter | null {
35
+ return approvalRouter;
36
+ }
37
+
38
+ const router = Router();
39
+
40
+ // GET /adapters — List configured adapters
41
+ router.get('/', requireWalletAuth, requirePermission('adapter:manage'), async (_req: Request, res: Response) => {
42
+ try {
43
+ const appConfig = await prisma.appConfig.findUnique({
44
+ where: { id: 'global' },
45
+ });
46
+
47
+ let config: { enabled: boolean; chat?: { defaultApp?: string }; adapters: Array<{ type: string; enabled: boolean; config: Record<string, unknown>; chat?: { enabled?: boolean } }> } = {
48
+ enabled: false,
49
+ adapters: [],
50
+ };
51
+
52
+ if (appConfig?.adapterConfig) {
53
+ try {
54
+ config = JSON.parse(appConfig.adapterConfig);
55
+ } catch {
56
+ // Invalid JSON, return default
57
+ }
58
+ }
59
+
60
+ // Check which adapter types have secrets stored
61
+ const secretKeys = await prisma.apiKey.findMany({
62
+ where: { service: { startsWith: 'adapter:' }, isActive: true },
63
+ select: { service: true, name: true },
64
+ });
65
+
66
+ const secretsByType: Record<string, string[]> = {};
67
+ for (const key of secretKeys) {
68
+ const adapterType = key.service.replace('adapter:', '');
69
+ if (!secretsByType[adapterType]) secretsByType[adapterType] = [];
70
+ secretsByType[adapterType].push(key.name);
71
+ }
72
+
73
+ // Annotate adapters with secret status
74
+ const adapters = (config.adapters || []).map((a) => ({
75
+ type: a.type,
76
+ enabled: a.enabled,
77
+ config: a.config,
78
+ chat: a.chat,
79
+ hasSecrets: (secretsByType[a.type] || []).length > 0,
80
+ secretKeys: secretsByType[a.type] || [],
81
+ }));
82
+
83
+ res.json({
84
+ success: true,
85
+ enabled: config.enabled,
86
+ chat: config.chat,
87
+ adapters,
88
+ running: approvalRouter !== null,
89
+ });
90
+ } catch (error) {
91
+ const message = getErrorMessage(error);
92
+ res.status(500).json({ error: message });
93
+ }
94
+ });
95
+
96
+ // POST /adapters — Save adapter config
97
+ router.post('/', requireWalletAuth, requirePermission('adapter:manage'), async (req: Request, res: Response) => {
98
+ try {
99
+ const { type, enabled, config, chat: chatConfig } = req.body;
100
+
101
+ if (!type || typeof type !== 'string') {
102
+ res.status(400).json({ error: 'type is required' });
103
+ return;
104
+ }
105
+
106
+ if (typeof enabled !== 'boolean') {
107
+ res.status(400).json({ error: 'enabled must be a boolean' });
108
+ return;
109
+ }
110
+
111
+ // Read current config
112
+ const appConfig = await prisma.appConfig.findUnique({
113
+ where: { id: 'global' },
114
+ });
115
+
116
+ let current: { enabled: boolean; chat?: { defaultApp?: string }; adapters: Array<{ type: string; enabled: boolean; config: Record<string, unknown>; chat?: { enabled?: boolean } }> } = {
117
+ enabled: true,
118
+ adapters: [],
119
+ };
120
+
121
+ if (appConfig?.adapterConfig) {
122
+ try {
123
+ current = JSON.parse(appConfig.adapterConfig);
124
+ } catch {
125
+ // Reset on invalid JSON
126
+ }
127
+ }
128
+
129
+ // Upsert the adapter entry
130
+ const idx = current.adapters.findIndex((a) => a.type === type);
131
+ const entry: { type: string; enabled: boolean; config: Record<string, unknown>; chat?: { enabled?: boolean } } = { type, enabled, config: config || {} };
132
+ if (chatConfig && typeof chatConfig === 'object') {
133
+ entry.chat = chatConfig;
134
+ }
135
+
136
+ if (idx >= 0) {
137
+ current.adapters[idx] = entry;
138
+ } else {
139
+ current.adapters.push(entry);
140
+ }
141
+
142
+ // If any adapter is being enabled, enable the system
143
+ if (enabled) {
144
+ current.enabled = true;
145
+ }
146
+
147
+ await prisma.appConfig.upsert({
148
+ where: { id: 'global' },
149
+ update: { adapterConfig: JSON.stringify(current) },
150
+ create: { id: 'global', adapterConfig: JSON.stringify(current) },
151
+ });
152
+
153
+ const action = idx >= 0 ? 'updated' : 'created';
154
+ logger.adapterChanged(action, type);
155
+
156
+ res.json({ success: true, adapter: entry });
157
+ } catch (error) {
158
+ const message = getErrorMessage(error);
159
+ res.status(500).json({ error: message });
160
+ }
161
+ });
162
+
163
+ // DELETE /adapters/:type — Remove adapter config
164
+ router.delete('/:type', requireWalletAuth, requirePermission('adapter:manage'), async (req: Request, res: Response) => {
165
+ try {
166
+ const { type } = req.params;
167
+
168
+ const appConfig = await prisma.appConfig.findUnique({
169
+ where: { id: 'global' },
170
+ });
171
+
172
+ if (!appConfig?.adapterConfig) {
173
+ res.status(404).json({ error: 'No adapter config found' });
174
+ return;
175
+ }
176
+
177
+ let current: { enabled: boolean; adapters: Array<{ type: string; enabled: boolean; config: Record<string, unknown> }> };
178
+ try {
179
+ current = JSON.parse(appConfig.adapterConfig);
180
+ } catch {
181
+ res.status(500).json({ error: 'Invalid adapter config in database' });
182
+ return;
183
+ }
184
+
185
+ const idx = current.adapters.findIndex((a) => a.type === type);
186
+ if (idx < 0) {
187
+ res.status(404).json({ error: `Adapter type '${type}' not found` });
188
+ return;
189
+ }
190
+
191
+ current.adapters.splice(idx, 1);
192
+
193
+ // If no adapters remain, disable the system
194
+ if (current.adapters.length === 0) {
195
+ current.enabled = false;
196
+ }
197
+
198
+ await prisma.appConfig.update({
199
+ where: { id: 'global' },
200
+ data: { adapterConfig: JSON.stringify(current) },
201
+ });
202
+
203
+ logger.adapterChanged('deleted', type);
204
+
205
+ res.json({ success: true, message: `Adapter '${type}' removed` });
206
+ } catch (error) {
207
+ const message = getErrorMessage(error);
208
+ res.status(500).json({ error: message });
209
+ }
210
+ });
211
+
212
+ // POST /adapters/test — Send a test message through a configured adapter
213
+ router.post('/test', requireWalletAuth, requirePermission('adapter:manage'), async (req: Request, res: Response) => {
214
+ try {
215
+ const { type } = req.body;
216
+
217
+ if (!type || typeof type !== 'string') {
218
+ res.status(400).json({ error: 'type is required' });
219
+ return;
220
+ }
221
+
222
+ // 5s timeout for external calls
223
+ const controller = new AbortController();
224
+ const timeout = setTimeout(() => controller.abort(), 5000);
225
+
226
+ try {
227
+ switch (type) {
228
+ case 'telegram': {
229
+ // Read bot token from DB
230
+ const tokenKey = await prisma.apiKey.findFirst({
231
+ where: { service: 'adapter:telegram', name: 'botToken', isActive: true },
232
+ });
233
+ if (!tokenKey) {
234
+ console.warn('[adapters] telegram test: bot token not found in ApiKey table');
235
+ res.status(400).json({ error: 'Telegram bot token not configured' });
236
+ return;
237
+ }
238
+
239
+ // Read chat ID from adapter config
240
+ const appConfig = await prisma.appConfig.findUnique({ where: { id: 'global' } });
241
+ let chatId: string | undefined;
242
+ if (appConfig?.adapterConfig) {
243
+ try {
244
+ const config = JSON.parse(appConfig.adapterConfig);
245
+ const telegramAdapter = config.adapters?.find((a: { type: string }) => a.type === 'telegram');
246
+ chatId = telegramAdapter?.config?.chatId ? String(telegramAdapter.config.chatId) : undefined;
247
+ } catch { /* ignore parse errors */ }
248
+ }
249
+
250
+ if (!chatId) {
251
+ console.warn('[adapters] telegram test: chatId not found in adapter config');
252
+ res.status(400).json({ error: 'Telegram chat ID not configured. Reconfigure the adapter with a chat ID.' });
253
+ return;
254
+ }
255
+
256
+ const resp = await fetch(`https://api.telegram.org/bot${tokenKey.key}/sendMessage`, {
257
+ method: 'POST',
258
+ headers: { 'Content-Type': 'application/json' },
259
+ body: JSON.stringify({
260
+ chat_id: chatId,
261
+ text: '<b>AuraWallet</b>\n\nAdapter chat is now active. You can send messages here and the AI will respond.',
262
+ parse_mode: 'HTML',
263
+ }),
264
+ signal: controller.signal,
265
+ });
266
+
267
+ const data = await resp.json() as { ok?: boolean; description?: string };
268
+ if (data.ok) {
269
+ res.json({ success: true });
270
+ } else {
271
+ console.warn('[adapters] telegram test: API call failed:', data.description);
272
+ res.json({ success: false, error: data.description || 'Failed to send test message' });
273
+ }
274
+ break;
275
+ }
276
+
277
+ case 'webhook': {
278
+ // Read webhook URL from adapter config
279
+ const appConfig2 = await prisma.appConfig.findUnique({ where: { id: 'global' } });
280
+ let webhookUrl: string | undefined;
281
+ if (appConfig2?.adapterConfig) {
282
+ try {
283
+ const config = JSON.parse(appConfig2.adapterConfig);
284
+ const webhookAdapter = config.adapters?.find((a: { type: string }) => a.type === 'webhook');
285
+ webhookUrl = webhookAdapter?.config?.url;
286
+ } catch { /* ignore parse errors */ }
287
+ }
288
+
289
+ if (!webhookUrl) {
290
+ res.status(404).json({ error: 'Webhook URL not configured' });
291
+ return;
292
+ }
293
+
294
+ // SSRF protection: validate webhook URL before sending test
295
+ try {
296
+ await validateExternalUrl(webhookUrl);
297
+ } catch (err) {
298
+ const msg = getErrorMessage(err);
299
+ res.status(403).json({ error: msg });
300
+ return;
301
+ }
302
+
303
+ const resp = await fetch(webhookUrl, {
304
+ method: 'POST',
305
+ headers: { 'Content-Type': 'application/json' },
306
+ body: JSON.stringify({
307
+ type: 'test',
308
+ data: {},
309
+ timestamp: Date.now(),
310
+ }),
311
+ signal: controller.signal,
312
+ });
313
+
314
+ if (resp.ok) {
315
+ res.json({ success: true });
316
+ } else {
317
+ res.json({ success: false, error: `Webhook returned ${resp.status}` });
318
+ }
319
+ break;
320
+ }
321
+
322
+ default:
323
+ res.status(400).json({ error: `Unknown adapter type: ${type}` });
324
+ return;
325
+ }
326
+ } catch (err) {
327
+ if (err instanceof Error && err.name === 'AbortError') {
328
+ res.json({ success: false, error: 'Test timed out' });
329
+ } else {
330
+ throw err;
331
+ }
332
+ } finally {
333
+ clearTimeout(timeout);
334
+ }
335
+ } catch (error) {
336
+ const message = getErrorMessage(error);
337
+ res.status(500).json({ error: message });
338
+ }
339
+ });
340
+
341
+ // POST /adapters/chat — Update top-level chat config (defaultApp)
342
+ router.post('/chat', requireWalletAuth, requirePermission('adapter:manage'), async (req: Request, res: Response) => {
343
+ try {
344
+ const { defaultApp } = req.body;
345
+
346
+ const appConfig = await prisma.appConfig.findUnique({
347
+ where: { id: 'global' },
348
+ });
349
+
350
+ let current: { enabled: boolean; chat?: { defaultApp?: string }; adapters: unknown[] } = {
351
+ enabled: true,
352
+ adapters: [],
353
+ };
354
+
355
+ if (appConfig?.adapterConfig) {
356
+ try {
357
+ current = JSON.parse(appConfig.adapterConfig);
358
+ } catch {
359
+ // Reset on invalid JSON
360
+ }
361
+ }
362
+
363
+ current.chat = {
364
+ ...current.chat,
365
+ defaultApp: defaultApp || undefined,
366
+ };
367
+
368
+ await prisma.appConfig.upsert({
369
+ where: { id: 'global' },
370
+ update: { adapterConfig: JSON.stringify(current) },
371
+ create: { id: 'global', adapterConfig: JSON.stringify(current) },
372
+ });
373
+
374
+ // Clear the app cache in the router
375
+ if (approvalRouter) {
376
+ approvalRouter.clearAppCache();
377
+ }
378
+
379
+ res.json({ success: true, chat: current.chat });
380
+ } catch (error) {
381
+ const message = getErrorMessage(error);
382
+ res.status(500).json({ error: message });
383
+ }
384
+ });
385
+
386
+ // POST /adapters/:type/message — Inbound chat from external adapter (HMAC-authenticated)
387
+ router.post('/:type/message', async (req: Request, res: Response) => {
388
+ try {
389
+ const { type } = req.params;
390
+ const { text, senderId } = req.body;
391
+
392
+ if (!text || typeof text !== 'string') {
393
+ res.status(400).json({ error: 'text is required' });
394
+ return;
395
+ }
396
+
397
+ if (!senderId || typeof senderId !== 'string') {
398
+ res.status(400).json({ error: 'senderId is required' });
399
+ return;
400
+ }
401
+
402
+ // HMAC authentication: look up the adapter's secret
403
+ const secretKey = await prisma.apiKey.findFirst({
404
+ where: { service: `adapter:${type}`, name: 'secret', isActive: true },
405
+ });
406
+
407
+ if (secretKey) {
408
+ // Validate HMAC signature
409
+ const signature = req.headers['x-signature-256'] as string | undefined;
410
+ if (!signature) {
411
+ res.status(401).json({ error: 'Missing X-Signature-256 header' });
412
+ return;
413
+ }
414
+
415
+ const rawBody = JSON.stringify(req.body);
416
+ const expected = `sha256=${createHmac('sha256', secretKey.key).update(rawBody).digest('hex')}`;
417
+
418
+ try {
419
+ const sigBuf = Buffer.from(signature);
420
+ const expectedBuf = Buffer.from(expected);
421
+ if (sigBuf.length !== expectedBuf.length || !timingSafeEqual(sigBuf, expectedBuf)) {
422
+ res.status(401).json({ error: 'Invalid signature' });
423
+ return;
424
+ }
425
+ } catch {
426
+ res.status(401).json({ error: 'Invalid signature' });
427
+ return;
428
+ }
429
+ }
430
+
431
+ if (!approvalRouter) {
432
+ res.status(503).json({ error: 'Adapter router not running' });
433
+ return;
434
+ }
435
+
436
+ const appId = await approvalRouter.resolveApp(req.body.targetApp);
437
+ if (!appId) {
438
+ res.status(400).json({ error: 'No target app configured. Set chat.defaultApp in adapter config.' });
439
+ return;
440
+ }
441
+
442
+ const result = await approvalRouter.sendMessage(appId, text, undefined, type);
443
+ res.json({
444
+ success: !result.error,
445
+ reply: result.reply,
446
+ error: result.error,
447
+ });
448
+ } catch (error) {
449
+ const message = getErrorMessage(error);
450
+ res.status(500).json({ error: message });
451
+ }
452
+ });
453
+
454
+ // POST /adapters/telegram/setup-link — Generate deep link for auto-detecting chat ID
455
+ router.post('/telegram/setup-link', requireWalletAuth, requirePermission('adapter:manage'), async (req: Request, res: Response) => {
456
+ try {
457
+ // Accept bot token from body (pre-save flow) or read from DB
458
+ let botToken = req.body.botToken as string | undefined;
459
+ if (!botToken) {
460
+ const tokenKey = await prisma.apiKey.findFirst({
461
+ where: { service: 'adapter:telegram', name: 'botToken', isActive: true },
462
+ });
463
+ if (!tokenKey) {
464
+ res.status(400).json({ error: 'Bot token not provided and not saved. Pass botToken in body or save it first.' });
465
+ return;
466
+ }
467
+ botToken = tokenKey.key;
468
+ }
469
+
470
+ // Validate bot token via getMe
471
+ const controller = new AbortController();
472
+ const timeout = setTimeout(() => controller.abort(), 5000);
473
+ try {
474
+ const meResp = await fetch(`https://api.telegram.org/bot${botToken}/getMe`, {
475
+ signal: controller.signal,
476
+ });
477
+ const meData = await meResp.json() as { ok?: boolean; result?: { username?: string }; description?: string };
478
+ if (!meData.ok) {
479
+ res.status(400).json({ error: meData.description || 'Invalid bot token' });
480
+ return;
481
+ }
482
+
483
+ const botUsername = meData.result?.username || '';
484
+
485
+ // Generate nonce
486
+ const nonce = randomBytes(12).toString('base64url');
487
+
488
+ // Lazy cleanup of expired nonces
489
+ const now = Date.now();
490
+ for (const [key, val] of telegramSetupNonces) {
491
+ if (val.expiresAt < now) telegramSetupNonces.delete(key);
492
+ }
493
+
494
+ // Store nonce with 120s TTL
495
+ telegramSetupNonces.set(nonce, {
496
+ botToken,
497
+ botUsername,
498
+ expiresAt: now + 120_000,
499
+ });
500
+
501
+ // Stop the running approval router so its Telegram polling doesn't
502
+ // consume the /start message before detect-chat can see it.
503
+ // It will be restarted after detection completes (via /adapters/restart).
504
+ if (approvalRouter) {
505
+ await approvalRouter.stop();
506
+ approvalRouter = null;
507
+ }
508
+
509
+ // Delete webhook to ensure getUpdates polling works
510
+ await fetch(`https://api.telegram.org/bot${botToken}/deleteWebhook`, {
511
+ signal: controller.signal,
512
+ }).catch(() => { /* non-fatal */ });
513
+
514
+ // Flush stale updates so detect-chat only sees new messages.
515
+ // Call getUpdates with offset=-1 to get the last update, then
516
+ // confirm it so subsequent polls start fresh.
517
+ let nextOffset = 0;
518
+ try {
519
+ const flushResp = await fetch(
520
+ `https://api.telegram.org/bot${botToken}/getUpdates?offset=-1&timeout=0`,
521
+ { signal: controller.signal },
522
+ );
523
+ const flushData = await flushResp.json() as { ok?: boolean; result?: Array<{ update_id: number }> };
524
+ if (flushData.ok && flushData.result?.length) {
525
+ const lastId = flushData.result[flushData.result.length - 1].update_id;
526
+ // Confirm the last update so it's removed from the queue
527
+ await fetch(
528
+ `https://api.telegram.org/bot${botToken}/getUpdates?offset=${lastId + 1}&timeout=0`,
529
+ { signal: controller.signal },
530
+ );
531
+ nextOffset = lastId + 1;
532
+ }
533
+ } catch { /* non-fatal — detect-chat will still work, just may see stale updates */ }
534
+
535
+ // Store offset in nonce so detect-chat can skip old updates
536
+ const nonceEntry = telegramSetupNonces.get(nonce);
537
+ if (nonceEntry) nonceEntry.offset = nextOffset;
538
+
539
+ res.json({
540
+ success: true,
541
+ link: `https://t.me/${botUsername}?start=${nonce}`,
542
+ setupToken: nonce,
543
+ botUsername,
544
+ });
545
+ } finally {
546
+ clearTimeout(timeout);
547
+ }
548
+ } catch (error) {
549
+ const message = getErrorMessage(error);
550
+ res.status(500).json({ error: message });
551
+ }
552
+ });
553
+
554
+ // POST /adapters/telegram/detect-chat — Poll for /start message to auto-detect chat ID
555
+ router.post('/telegram/detect-chat', requireWalletAuth, requirePermission('adapter:manage'), async (req: Request, res: Response) => {
556
+ try {
557
+ const { setupToken } = req.body;
558
+ if (!setupToken || typeof setupToken !== 'string') {
559
+ res.status(400).json({ error: 'setupToken is required' });
560
+ return;
561
+ }
562
+
563
+ const nonceEntry = telegramSetupNonces.get(setupToken);
564
+ if (!nonceEntry) {
565
+ res.status(400).json({ error: 'Invalid or expired setup token' });
566
+ return;
567
+ }
568
+
569
+ if (nonceEntry.expiresAt < Date.now()) {
570
+ telegramSetupNonces.delete(setupToken);
571
+ res.status(400).json({ error: 'Setup token expired' });
572
+ return;
573
+ }
574
+
575
+ const { botToken } = nonceEntry;
576
+
577
+ // Long-poll getUpdates (25s timeout)
578
+ const controller = new AbortController();
579
+ const timeout = setTimeout(() => controller.abort(), 30_000);
580
+ try {
581
+ const offsetParam = nonceEntry.offset ? `&offset=${nonceEntry.offset}` : '';
582
+ const updatesResp = await fetch(
583
+ `https://api.telegram.org/bot${botToken}/getUpdates?timeout=25&allowed_updates=${encodeURIComponent(JSON.stringify(['message']))}${offsetParam}`,
584
+ { signal: controller.signal },
585
+ );
586
+ const updatesData = await updatesResp.json() as {
587
+ ok?: boolean;
588
+ result?: Array<{
589
+ update_id: number;
590
+ message?: {
591
+ text?: string;
592
+ chat?: { id: number; first_name?: string; username?: string };
593
+ };
594
+ }>;
595
+ };
596
+
597
+ if (!updatesData.ok || !updatesData.result) {
598
+ res.json({ chatId: null, timeout: true });
599
+ return;
600
+ }
601
+
602
+ // Look for /start message
603
+ let detectedUpdate: (typeof updatesData.result)[number] | null = null;
604
+ let verified = false;
605
+
606
+ for (const update of updatesData.result) {
607
+ const text = update.message?.text || '';
608
+ if (text === `/start ${setupToken}`) {
609
+ detectedUpdate = update;
610
+ verified = true;
611
+ break;
612
+ }
613
+ if (text === '/start' || text.startsWith('/start ')) {
614
+ detectedUpdate = update;
615
+ verified = false;
616
+ // Keep looking for exact nonce match
617
+ }
618
+ }
619
+
620
+ if (!detectedUpdate || !detectedUpdate.message?.chat) {
621
+ // Advance offset past any updates we just saw so the next
622
+ // poll attempt doesn't re-process them
623
+ if (updatesData.result.length > 0) {
624
+ const maxId = updatesData.result[updatesData.result.length - 1].update_id;
625
+ nonceEntry.offset = maxId + 1;
626
+ await fetch(
627
+ `https://api.telegram.org/bot${botToken}/getUpdates?offset=${maxId + 1}&timeout=0`,
628
+ { signal: controller.signal },
629
+ ).catch(() => { /* non-fatal */ });
630
+ }
631
+ res.json({ chatId: null, timeout: true });
632
+ return;
633
+ }
634
+
635
+ const chat = detectedUpdate.message.chat;
636
+ const chatId = String(chat.id);
637
+
638
+ // Confirm the update by calling getUpdates with offset past it
639
+ await fetch(
640
+ `https://api.telegram.org/bot${botToken}/getUpdates?offset=${detectedUpdate.update_id + 1}&timeout=0`,
641
+ { signal: controller.signal },
642
+ ).catch(() => { /* non-fatal */ });
643
+
644
+ // Clean up nonce
645
+ telegramSetupNonces.delete(setupToken);
646
+
647
+ res.json({
648
+ chatId,
649
+ firstName: chat.first_name || null,
650
+ username: chat.username || null,
651
+ verified,
652
+ });
653
+ } finally {
654
+ clearTimeout(timeout);
655
+ }
656
+ } catch (error) {
657
+ if (error instanceof Error && error.name === 'AbortError') {
658
+ res.json({ chatId: null, timeout: true });
659
+ } else {
660
+ const message = getErrorMessage(error);
661
+ res.status(500).json({ error: message });
662
+ }
663
+ }
664
+ });
665
+
666
+ // POST /adapters/restart — Restart approval router with current DB config
667
+ router.post('/restart', requireWalletAuth, requirePermission('adapter:manage'), async (_req: Request, res: Response) => {
668
+ try {
669
+ // Stop existing router
670
+ if (approvalRouter) {
671
+ await approvalRouter.stop();
672
+ approvalRouter = null;
673
+ }
674
+
675
+ // Load adapters from DB
676
+ const adapters = await loadAdaptersFromDb();
677
+
678
+ if (adapters.length === 0) {
679
+ res.json({ success: true, message: 'No adapters configured, router stopped', running: false });
680
+ return;
681
+ }
682
+
683
+ const newRouter = new ApprovalRouter(`http://127.0.0.1:${SERVER_PORT}`);
684
+ for (const adapter of adapters) {
685
+ newRouter.registerAdapter(adapter);
686
+ }
687
+
688
+ await newRouter.start();
689
+ approvalRouter = newRouter;
690
+
691
+ res.json({
692
+ success: true,
693
+ message: `Approval router started with ${adapters.length} adapter(s)`,
694
+ running: true,
695
+ adapterCount: adapters.length,
696
+ });
697
+ } catch (error) {
698
+ const message = getErrorMessage(error);
699
+ res.status(500).json({ error: message });
700
+ }
701
+ });
702
+
703
+ export default router;