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,302 @@
1
+ import { WebSocketServer, WebSocket } from 'ws';
2
+ import type { IncomingMessage } from 'http';
3
+ import type { Duplex } from 'stream';
4
+ import type { WalletEvent, WorkspaceEvent, ThemeEvent } from './events';
5
+ import { WORKSPACE_EVENTS, THEME_EVENTS, APP_EVENTS } from './events';
6
+ import { handleWorkspaceMessage } from './workspace-handlers';
7
+ import { handleThemeMessage } from './theme-handlers';
8
+ import { validateToken, hasAnyPermission, TokenValidationResult } from './auth-client';
9
+ import { log as rootLog } from './pino';
10
+
11
+ const log = rootLog.child({ component: 'ws' });
12
+
13
+ // Singleton WebSocket server for Next.js
14
+ let wss: WebSocketServer | null = null;
15
+ const clients = new Set<WebSocket>();
16
+
17
+ // Heartbeat interval (30 seconds)
18
+ const HEARTBEAT_INTERVAL = 30000;
19
+ let heartbeatTimer: NodeJS.Timeout | null = null;
20
+
21
+ interface ExtendedWebSocket extends WebSocket {
22
+ isAlive?: boolean;
23
+ auth?: TokenValidationResult; // Auth info for this connection
24
+ }
25
+
26
+ // Workspace mutation events that require workspace:modify permission
27
+ const WORKSPACE_MUTATION_EVENTS = [
28
+ WORKSPACE_EVENTS.WORKSPACE_CREATED,
29
+ WORKSPACE_EVENTS.WORKSPACE_DELETED,
30
+ WORKSPACE_EVENTS.WORKSPACE_UPDATED,
31
+ WORKSPACE_EVENTS.APP_ADDED,
32
+ WORKSPACE_EVENTS.APP_REMOVED,
33
+ WORKSPACE_EVENTS.APP_UPDATED,
34
+ ];
35
+
36
+ /**
37
+ * Check if client has permission to perform workspace mutations
38
+ * Requires valid token with workspace:modify permission (or admin)
39
+ */
40
+ function canModifyWorkspace(ws: ExtendedWebSocket): boolean {
41
+ // No auth = deny (auth required for mutations)
42
+ if (!ws.auth) return false;
43
+
44
+ // Invalid token = deny
45
+ if (!ws.auth.valid) return false;
46
+
47
+ // Admin always allowed
48
+ if (ws.auth.isAdmin) return true;
49
+
50
+ // Check for workspace:modify permission
51
+ return hasAnyPermission(ws.auth, ['workspace:modify']);
52
+ }
53
+
54
+ /**
55
+ * Get or create the WebSocket server singleton
56
+ */
57
+ export function getWebSocketServer(): WebSocketServer {
58
+ if (!wss) {
59
+ wss = new WebSocketServer({ noServer: true });
60
+
61
+ wss.on('connection', (ws: ExtendedWebSocket) => {
62
+ ws.isAlive = true;
63
+ clients.add(ws);
64
+
65
+ log.debug({ clients: clients.size }, 'client connected');
66
+
67
+ ws.on('pong', () => {
68
+ ws.isAlive = true;
69
+ });
70
+
71
+ ws.on('message', async (data) => {
72
+ try {
73
+ const msg = JSON.parse(data.toString());
74
+
75
+ // Handle auth message — client sends token after connecting
76
+ if (msg.type === 'auth' && typeof msg.token === 'string') {
77
+ try {
78
+ const result = await validateToken(msg.token);
79
+ ws.auth = result;
80
+ if (result.valid) {
81
+ log.info({ agentId: result.payload?.agentId || 'admin' }, 'client authenticated');
82
+ } else {
83
+ log.warn({ error: result.error }, 'invalid token provided');
84
+ }
85
+ ws.send(JSON.stringify({ type: 'authenticated', valid: result.valid }));
86
+ } catch (err) {
87
+ log.error({ err }, 'token validation failed');
88
+ ws.send(JSON.stringify({ type: 'authenticated', valid: false }));
89
+ }
90
+ return;
91
+ }
92
+
93
+ const workspaceEventTypes = Object.values(WORKSPACE_EVENTS);
94
+ const themeEventTypes = Object.values(THEME_EVENTS);
95
+
96
+ // Check if this is a workspace event
97
+ if (workspaceEventTypes.includes(msg.type)) {
98
+ // Check permissions for mutation events
99
+ if (WORKSPACE_MUTATION_EVENTS.includes(msg.type)) {
100
+ if (!canModifyWorkspace(ws)) {
101
+ ws.send(JSON.stringify({
102
+ type: 'error',
103
+ error: 'Permission denied: workspace:modify required',
104
+ originalType: msg.type
105
+ }));
106
+ return;
107
+ }
108
+ }
109
+
110
+ const response = await handleWorkspaceMessage(msg as WorkspaceEvent);
111
+
112
+ // If there's a response (e.g., state:response), send it back to the requesting client
113
+ if (response) {
114
+ ws.send(JSON.stringify(response));
115
+ }
116
+
117
+ // For mutations, broadcast to all other clients
118
+ if (WORKSPACE_MUTATION_EVENTS.includes(msg.type)) {
119
+ broadcastExcept(msg, ws);
120
+ }
121
+ }
122
+ // Check if this is a theme event
123
+ else if (themeEventTypes.includes(msg.type)) {
124
+ // Theme changes also require workspace:modify
125
+ const themeMutations = [
126
+ THEME_EVENTS.THEME_MODE_CHANGED,
127
+ THEME_EVENTS.THEME_ACCENT_CHANGED,
128
+ THEME_EVENTS.THEME_UPDATED,
129
+ THEME_EVENTS.WORKSPACE_THEME_UPDATED
130
+ ];
131
+
132
+ if (themeMutations.includes(msg.type) && !canModifyWorkspace(ws)) {
133
+ ws.send(JSON.stringify({
134
+ type: 'error',
135
+ error: 'Permission denied: workspace:modify required',
136
+ originalType: msg.type
137
+ }));
138
+ return;
139
+ }
140
+
141
+ const response = await handleThemeMessage(msg as ThemeEvent);
142
+
143
+ // If there's a response (e.g., theme:response), send it back to the requesting client
144
+ if (response) {
145
+ ws.send(JSON.stringify(response));
146
+ }
147
+
148
+ // For mutations, broadcast to all other clients
149
+ if (themeMutations.includes(msg.type)) {
150
+ broadcastExcept(msg, ws);
151
+ }
152
+ }
153
+ // Check if this is a app event
154
+ else if (Object.values(APP_EVENTS).includes(msg.type)) {
155
+ // app:message — broadcast to all clients (agent picks it up)
156
+ if (msg.type === APP_EVENTS.APP_MESSAGE) {
157
+ broadcastExcept(msg, ws);
158
+ }
159
+ // app:response — broadcast to all clients (host bridge picks it up)
160
+ else if (msg.type === APP_EVENTS.APP_RESPONSE) {
161
+ broadcastExcept(msg, ws);
162
+ }
163
+ }
164
+ } catch (err) {
165
+ log.error({ err }, 'failed to handle message');
166
+ }
167
+ });
168
+
169
+ ws.on('close', () => {
170
+ clients.delete(ws);
171
+ log.debug({ clients: clients.size }, 'client disconnected');
172
+ });
173
+
174
+ ws.on('error', (err) => {
175
+ log.error({ err }, 'client error');
176
+ clients.delete(ws);
177
+ });
178
+
179
+ // Send initial connection confirmation
180
+ ws.send(JSON.stringify({
181
+ type: 'connected',
182
+ timestamp: Date.now(),
183
+ }));
184
+ });
185
+
186
+ // Start heartbeat if not already running
187
+ if (!heartbeatTimer) {
188
+ heartbeatTimer = setInterval(() => {
189
+ clients.forEach((ws) => {
190
+ const extWs = ws as ExtendedWebSocket;
191
+ if (extWs.isAlive === false) {
192
+ clients.delete(ws);
193
+ return ws.terminate();
194
+ }
195
+ extWs.isAlive = false;
196
+ ws.ping();
197
+ });
198
+ }, HEARTBEAT_INTERVAL);
199
+ }
200
+ }
201
+
202
+ return wss;
203
+ }
204
+
205
+ /**
206
+ * Handle WebSocket upgrade request
207
+ */
208
+ export function handleUpgrade(
209
+ request: IncomingMessage,
210
+ socket: Duplex,
211
+ head: Buffer
212
+ ): void {
213
+ const server = getWebSocketServer();
214
+ server.handleUpgrade(request, socket, head, (ws) => {
215
+ server.emit('connection', ws, request);
216
+ });
217
+ }
218
+
219
+ /**
220
+ * Broadcast an event to all connected clients
221
+ */
222
+ export function broadcast(event: WalletEvent | WorkspaceEvent): void {
223
+ const message = JSON.stringify(event);
224
+ let sent = 0;
225
+
226
+ clients.forEach((client) => {
227
+ if (client.readyState === WebSocket.OPEN) {
228
+ client.send(message);
229
+ sent++;
230
+ }
231
+ });
232
+
233
+ log.debug({ type: event.type, sent, total: clients.size }, 'broadcast');
234
+ }
235
+
236
+ /**
237
+ * Broadcast an event to all connected clients except one
238
+ */
239
+ export function broadcastExcept(event: WalletEvent | WorkspaceEvent, excludeClient: WebSocket): void {
240
+ const message = JSON.stringify(event);
241
+ let sent = 0;
242
+
243
+ clients.forEach((client) => {
244
+ if (client.readyState === WebSocket.OPEN && client !== excludeClient) {
245
+ client.send(message);
246
+ sent++;
247
+ }
248
+ });
249
+
250
+ log.debug({ type: event.type, sent, total: clients.size - 1 }, 'broadcast (excluding sender)');
251
+ }
252
+
253
+ /**
254
+ * Get number of connected clients
255
+ */
256
+ export function getClientCount(): number {
257
+ return clients.size;
258
+ }
259
+
260
+ /**
261
+ * Get connected clients with their auth info
262
+ */
263
+ export function getAuthenticatedClients(): Array<{
264
+ agentId: string;
265
+ isAdmin: boolean;
266
+ permissions: string[];
267
+ }> {
268
+ const result: Array<{ agentId: string; isAdmin: boolean; permissions: string[] }> = [];
269
+
270
+ clients.forEach((client) => {
271
+ const extWs = client as ExtendedWebSocket;
272
+ if (extWs.auth?.valid) {
273
+ result.push({
274
+ agentId: extWs.auth.payload?.agentId || 'admin',
275
+ isAdmin: extWs.auth.isAdmin || false,
276
+ permissions: extWs.auth.payload?.permissions || ['admin:*']
277
+ });
278
+ }
279
+ });
280
+
281
+ return result;
282
+ }
283
+
284
+ /**
285
+ * Cleanup WebSocket server (for testing/shutdown)
286
+ */
287
+ export function closeWebSocketServer(): void {
288
+ if (heartbeatTimer) {
289
+ clearInterval(heartbeatTimer);
290
+ heartbeatTimer = null;
291
+ }
292
+
293
+ clients.forEach((client) => {
294
+ client.close();
295
+ });
296
+ clients.clear();
297
+
298
+ if (wss) {
299
+ wss.close();
300
+ wss = null;
301
+ }
302
+ }
@@ -0,0 +1,79 @@
1
+ /**
2
+ * WebSocket server setup for Next.js
3
+ * Runs on a separate port since Next.js API routes don't support WebSocket upgrades
4
+ */
5
+
6
+ import { createServer } from 'http';
7
+ import { getWebSocketServer, broadcast, getClientCount } from './websocket-server';
8
+ import { ensureDefaultWorkspace } from './workspace-handlers';
9
+ import type { WalletEvent } from './events';
10
+ import { log as rootLog } from './pino';
11
+
12
+ const log = rootLog.child({ component: 'ws' });
13
+
14
+ const WS_PORT = parseInt(process.env.WS_PORT || '4748', 10);
15
+
16
+ let isSetup = false;
17
+ let httpServer: ReturnType<typeof createServer> | null = null;
18
+
19
+ export function setupWebSocketServer() {
20
+ if (isSetup) return;
21
+ isSetup = true;
22
+
23
+ // Create a simple HTTP server for WebSocket upgrades and broadcast endpoint
24
+ httpServer = createServer((req, res) => {
25
+ // Health check endpoint
26
+ if (req.url === '/health') {
27
+ res.writeHead(200, { 'Content-Type': 'application/json' });
28
+ res.end(JSON.stringify({ status: 'ok', websocket: true, clients: getClientCount() }));
29
+ return;
30
+ }
31
+
32
+ // Broadcast endpoint - receives events from Express and broadcasts to WebSocket clients
33
+ if (req.url === '/broadcast' && req.method === 'POST') {
34
+ let body = '';
35
+ req.on('data', (chunk) => {
36
+ body += chunk.toString();
37
+ });
38
+ req.on('end', () => {
39
+ try {
40
+ const event: WalletEvent = JSON.parse(body);
41
+ broadcast(event);
42
+ res.writeHead(200, { 'Content-Type': 'application/json' });
43
+ res.end(JSON.stringify({ success: true, type: event.type }));
44
+ } catch (err) {
45
+ log.error({ err }, 'failed to parse broadcast event');
46
+ res.writeHead(400, { 'Content-Type': 'application/json' });
47
+ res.end(JSON.stringify({ error: 'Invalid JSON' }));
48
+ }
49
+ });
50
+ return;
51
+ }
52
+
53
+ res.writeHead(404);
54
+ res.end();
55
+ });
56
+
57
+ const wss = getWebSocketServer();
58
+
59
+ // Handle WebSocket upgrade
60
+ httpServer.on('upgrade', (request, socket, head) => {
61
+ wss.handleUpgrade(request, socket, head, (ws) => {
62
+ wss.emit('connection', ws, request);
63
+ });
64
+ });
65
+
66
+ httpServer.listen(WS_PORT, '127.0.0.1', async () => {
67
+ log.info({ port: WS_PORT, url: `ws://127.0.0.1:${WS_PORT}` }, 'WebSocket server started');
68
+ // Ensure default workspace exists
69
+ try {
70
+ await ensureDefaultWorkspace();
71
+ } catch (err) {
72
+ log.error({ err }, 'failed to ensure default workspace');
73
+ }
74
+ });
75
+ }
76
+
77
+ export function getWebSocketPort(): number {
78
+ return WS_PORT;
79
+ }