gitspace 0.2.0-rc.2 → 0.2.0-rc.21

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 (316) hide show
  1. package/README.md +68 -38
  2. package/package.json +36 -25
  3. package/.claude/settings.local.json +0 -21
  4. package/.gitspace/bundle.json +0 -50
  5. package/.gitspace/select/01-status.sh +0 -40
  6. package/.gitspace/setup/01-install-deps.sh +0 -12
  7. package/.gitspace/setup/02-typecheck.sh +0 -16
  8. package/AGENTS.md +0 -439
  9. package/CLAUDE.md +0 -1
  10. package/bun.lock +0 -647
  11. package/docs/CONNECTION.md +0 -623
  12. package/docs/GATEWAY-WORKER.md +0 -319
  13. package/docs/GETTING-STARTED.md +0 -448
  14. package/docs/GITSPACE-PLATFORM.md +0 -1819
  15. package/docs/INFRASTRUCTURE.md +0 -1347
  16. package/docs/PROTOCOL.md +0 -619
  17. package/docs/QUICKSTART.md +0 -174
  18. package/docs/RELAY.md +0 -327
  19. package/docs/REMOTE-DESIGN.md +0 -549
  20. package/docs/ROADMAP.md +0 -564
  21. package/docs/SITE_DOCS_FIGMA_MAKE.md +0 -1167
  22. package/docs/STACK-DESIGN.md +0 -588
  23. package/docs/UNIFIED_ARCHITECTURE.md +0 -292
  24. package/experiments/pty-benchmark.ts +0 -148
  25. package/experiments/pty-latency.ts +0 -100
  26. package/experiments/router/client.ts +0 -199
  27. package/experiments/router/protocol.ts +0 -74
  28. package/experiments/router/router.ts +0 -217
  29. package/experiments/router/session.ts +0 -180
  30. package/experiments/router/test.ts +0 -133
  31. package/experiments/socket-bandwidth.ts +0 -77
  32. package/homebrew/gitspace.rb +0 -45
  33. package/landing-page/ATTRIBUTIONS.md +0 -3
  34. package/landing-page/README.md +0 -11
  35. package/landing-page/bun.lock +0 -801
  36. package/landing-page/guidelines/Guidelines.md +0 -61
  37. package/landing-page/index.html +0 -37
  38. package/landing-page/package.json +0 -90
  39. package/landing-page/postcss.config.mjs +0 -15
  40. package/landing-page/public/_redirects +0 -1
  41. package/landing-page/public/favicon.png +0 -0
  42. package/landing-page/src/app/App.tsx +0 -53
  43. package/landing-page/src/app/components/figma/ImageWithFallback.tsx +0 -27
  44. package/landing-page/src/app/components/ui/accordion.tsx +0 -66
  45. package/landing-page/src/app/components/ui/alert-dialog.tsx +0 -157
  46. package/landing-page/src/app/components/ui/alert.tsx +0 -66
  47. package/landing-page/src/app/components/ui/aspect-ratio.tsx +0 -11
  48. package/landing-page/src/app/components/ui/avatar.tsx +0 -53
  49. package/landing-page/src/app/components/ui/badge.tsx +0 -46
  50. package/landing-page/src/app/components/ui/breadcrumb.tsx +0 -109
  51. package/landing-page/src/app/components/ui/button.tsx +0 -57
  52. package/landing-page/src/app/components/ui/calendar.tsx +0 -75
  53. package/landing-page/src/app/components/ui/card.tsx +0 -92
  54. package/landing-page/src/app/components/ui/carousel.tsx +0 -241
  55. package/landing-page/src/app/components/ui/chart.tsx +0 -353
  56. package/landing-page/src/app/components/ui/checkbox.tsx +0 -32
  57. package/landing-page/src/app/components/ui/collapsible.tsx +0 -33
  58. package/landing-page/src/app/components/ui/command.tsx +0 -177
  59. package/landing-page/src/app/components/ui/context-menu.tsx +0 -252
  60. package/landing-page/src/app/components/ui/dialog.tsx +0 -135
  61. package/landing-page/src/app/components/ui/drawer.tsx +0 -132
  62. package/landing-page/src/app/components/ui/dropdown-menu.tsx +0 -257
  63. package/landing-page/src/app/components/ui/form.tsx +0 -168
  64. package/landing-page/src/app/components/ui/hover-card.tsx +0 -44
  65. package/landing-page/src/app/components/ui/input-otp.tsx +0 -77
  66. package/landing-page/src/app/components/ui/input.tsx +0 -21
  67. package/landing-page/src/app/components/ui/label.tsx +0 -24
  68. package/landing-page/src/app/components/ui/menubar.tsx +0 -276
  69. package/landing-page/src/app/components/ui/navigation-menu.tsx +0 -168
  70. package/landing-page/src/app/components/ui/pagination.tsx +0 -127
  71. package/landing-page/src/app/components/ui/popover.tsx +0 -48
  72. package/landing-page/src/app/components/ui/progress.tsx +0 -31
  73. package/landing-page/src/app/components/ui/radio-group.tsx +0 -45
  74. package/landing-page/src/app/components/ui/resizable.tsx +0 -56
  75. package/landing-page/src/app/components/ui/scroll-area.tsx +0 -58
  76. package/landing-page/src/app/components/ui/select.tsx +0 -189
  77. package/landing-page/src/app/components/ui/separator.tsx +0 -28
  78. package/landing-page/src/app/components/ui/sheet.tsx +0 -139
  79. package/landing-page/src/app/components/ui/sidebar.tsx +0 -726
  80. package/landing-page/src/app/components/ui/skeleton.tsx +0 -13
  81. package/landing-page/src/app/components/ui/slider.tsx +0 -63
  82. package/landing-page/src/app/components/ui/sonner.tsx +0 -25
  83. package/landing-page/src/app/components/ui/switch.tsx +0 -31
  84. package/landing-page/src/app/components/ui/table.tsx +0 -116
  85. package/landing-page/src/app/components/ui/tabs.tsx +0 -66
  86. package/landing-page/src/app/components/ui/textarea.tsx +0 -18
  87. package/landing-page/src/app/components/ui/toggle-group.tsx +0 -73
  88. package/landing-page/src/app/components/ui/toggle.tsx +0 -47
  89. package/landing-page/src/app/components/ui/tooltip.tsx +0 -61
  90. package/landing-page/src/app/components/ui/use-mobile.ts +0 -21
  91. package/landing-page/src/app/components/ui/utils.ts +0 -6
  92. package/landing-page/src/components/docs/DocsContent.tsx +0 -718
  93. package/landing-page/src/components/docs/DocsSidebar.tsx +0 -84
  94. package/landing-page/src/components/landing/CTA.tsx +0 -59
  95. package/landing-page/src/components/landing/Comparison.tsx +0 -84
  96. package/landing-page/src/components/landing/FaultyTerminal.tsx +0 -424
  97. package/landing-page/src/components/landing/Features.tsx +0 -201
  98. package/landing-page/src/components/landing/Hero.tsx +0 -142
  99. package/landing-page/src/components/landing/Pricing.tsx +0 -140
  100. package/landing-page/src/components/landing/Roadmap.tsx +0 -86
  101. package/landing-page/src/components/landing/Security.tsx +0 -81
  102. package/landing-page/src/components/landing/TerminalWindow.tsx +0 -27
  103. package/landing-page/src/components/landing/UseCases.tsx +0 -55
  104. package/landing-page/src/components/landing/Workflow.tsx +0 -101
  105. package/landing-page/src/components/layout/DashboardNavbar.tsx +0 -37
  106. package/landing-page/src/components/layout/Footer.tsx +0 -55
  107. package/landing-page/src/components/layout/LandingNavbar.tsx +0 -82
  108. package/landing-page/src/components/ui/badge.tsx +0 -39
  109. package/landing-page/src/components/ui/breadcrumb.tsx +0 -115
  110. package/landing-page/src/components/ui/button.tsx +0 -57
  111. package/landing-page/src/components/ui/card.tsx +0 -79
  112. package/landing-page/src/components/ui/mock-terminal.tsx +0 -68
  113. package/landing-page/src/components/ui/separator.tsx +0 -28
  114. package/landing-page/src/lib/utils.ts +0 -6
  115. package/landing-page/src/main.tsx +0 -10
  116. package/landing-page/src/pages/Dashboard.tsx +0 -133
  117. package/landing-page/src/pages/DocsPage.tsx +0 -79
  118. package/landing-page/src/pages/LandingPage.tsx +0 -31
  119. package/landing-page/src/pages/TerminalView.tsx +0 -106
  120. package/landing-page/src/styles/fonts.css +0 -0
  121. package/landing-page/src/styles/index.css +0 -3
  122. package/landing-page/src/styles/tailwind.css +0 -4
  123. package/landing-page/src/styles/theme.css +0 -181
  124. package/landing-page/vite.config.ts +0 -19
  125. package/npm/darwin-arm64/bin/gssh +0 -0
  126. package/npm/darwin-arm64/package.json +0 -20
  127. package/scripts/build.ts +0 -298
  128. package/scripts/release.ts +0 -140
  129. package/src/__tests__/test-utils.ts +0 -298
  130. package/src/commands/__tests__/serve-messages.test.ts +0 -190
  131. package/src/commands/access.ts +0 -298
  132. package/src/commands/add.ts +0 -452
  133. package/src/commands/auth.ts +0 -364
  134. package/src/commands/connect.ts +0 -287
  135. package/src/commands/directory.ts +0 -16
  136. package/src/commands/host.ts +0 -396
  137. package/src/commands/identity.ts +0 -184
  138. package/src/commands/list.ts +0 -200
  139. package/src/commands/relay.ts +0 -315
  140. package/src/commands/remove.ts +0 -241
  141. package/src/commands/serve.ts +0 -1493
  142. package/src/commands/share.ts +0 -456
  143. package/src/commands/status.ts +0 -125
  144. package/src/commands/switch.ts +0 -353
  145. package/src/commands/tmux.ts +0 -317
  146. package/src/core/__tests__/access.test.ts +0 -240
  147. package/src/core/access.ts +0 -277
  148. package/src/core/bundle.ts +0 -342
  149. package/src/core/config.ts +0 -510
  150. package/src/core/git.ts +0 -317
  151. package/src/core/github.ts +0 -151
  152. package/src/core/identity.ts +0 -631
  153. package/src/core/linear.ts +0 -225
  154. package/src/core/shell.ts +0 -161
  155. package/src/core/trusted-relays.ts +0 -315
  156. package/src/index.ts +0 -810
  157. package/src/lib/remote-session/index.ts +0 -7
  158. package/src/lib/remote-session/protocol.ts +0 -267
  159. package/src/lib/remote-session/session-handler.ts +0 -581
  160. package/src/lib/remote-session/workspace-scanner.ts +0 -167
  161. package/src/lib/tmux-lite/README.md +0 -81
  162. package/src/lib/tmux-lite/cli.ts +0 -796
  163. package/src/lib/tmux-lite/crypto/__tests__/helpers/handshake-runner.ts +0 -349
  164. package/src/lib/tmux-lite/crypto/__tests__/helpers/mock-relay.ts +0 -291
  165. package/src/lib/tmux-lite/crypto/__tests__/helpers/test-identities.ts +0 -142
  166. package/src/lib/tmux-lite/crypto/__tests__/integration/authorization.integration.test.ts +0 -339
  167. package/src/lib/tmux-lite/crypto/__tests__/integration/e2e-communication.integration.test.ts +0 -477
  168. package/src/lib/tmux-lite/crypto/__tests__/integration/error-handling.integration.test.ts +0 -499
  169. package/src/lib/tmux-lite/crypto/__tests__/integration/handshake.integration.test.ts +0 -371
  170. package/src/lib/tmux-lite/crypto/__tests__/integration/security.integration.test.ts +0 -573
  171. package/src/lib/tmux-lite/crypto/access-control.test.ts +0 -512
  172. package/src/lib/tmux-lite/crypto/access-control.ts +0 -320
  173. package/src/lib/tmux-lite/crypto/frames.test.ts +0 -262
  174. package/src/lib/tmux-lite/crypto/frames.ts +0 -141
  175. package/src/lib/tmux-lite/crypto/handshake.ts +0 -894
  176. package/src/lib/tmux-lite/crypto/identity.test.ts +0 -220
  177. package/src/lib/tmux-lite/crypto/identity.ts +0 -286
  178. package/src/lib/tmux-lite/crypto/index.ts +0 -51
  179. package/src/lib/tmux-lite/crypto/invites.test.ts +0 -381
  180. package/src/lib/tmux-lite/crypto/invites.ts +0 -215
  181. package/src/lib/tmux-lite/crypto/keyexchange.ts +0 -435
  182. package/src/lib/tmux-lite/crypto/keys.test.ts +0 -58
  183. package/src/lib/tmux-lite/crypto/keys.ts +0 -47
  184. package/src/lib/tmux-lite/crypto/secretbox.test.ts +0 -169
  185. package/src/lib/tmux-lite/crypto/secretbox.ts +0 -124
  186. package/src/lib/tmux-lite/handshake-handler.ts +0 -451
  187. package/src/lib/tmux-lite/protocol.test.ts +0 -307
  188. package/src/lib/tmux-lite/protocol.ts +0 -266
  189. package/src/lib/tmux-lite/relay-client.ts +0 -506
  190. package/src/lib/tmux-lite/server.ts +0 -1250
  191. package/src/lib/tmux-lite/shell-integration.sh +0 -37
  192. package/src/lib/tmux-lite/terminal-queries.test.ts +0 -54
  193. package/src/lib/tmux-lite/terminal-queries.ts +0 -49
  194. package/src/relay/__tests__/e2e-flow.test.ts +0 -1284
  195. package/src/relay/__tests__/helpers/auth.ts +0 -354
  196. package/src/relay/__tests__/helpers/ports.ts +0 -51
  197. package/src/relay/__tests__/protocol-validation.test.ts +0 -265
  198. package/src/relay/authorization.ts +0 -303
  199. package/src/relay/embedded-assets.generated.d.ts +0 -15
  200. package/src/relay/identity.ts +0 -352
  201. package/src/relay/index.ts +0 -57
  202. package/src/relay/pipes.test.ts +0 -427
  203. package/src/relay/pipes.ts +0 -195
  204. package/src/relay/protocol.ts +0 -804
  205. package/src/relay/registries.test.ts +0 -437
  206. package/src/relay/registries.ts +0 -593
  207. package/src/relay/server.test.ts +0 -1323
  208. package/src/relay/server.ts +0 -1092
  209. package/src/relay/signing.ts +0 -238
  210. package/src/relay/types.ts +0 -69
  211. package/src/serve/client-session-manager.ts +0 -622
  212. package/src/serve/daemon.ts +0 -497
  213. package/src/serve/pty-session.ts +0 -236
  214. package/src/serve/types.ts +0 -169
  215. package/src/shared/components/Flow.tsx +0 -453
  216. package/src/shared/components/Flow.tui.tsx +0 -343
  217. package/src/shared/components/Flow.web.tsx +0 -442
  218. package/src/shared/components/Inbox.tsx +0 -446
  219. package/src/shared/components/Inbox.tui.tsx +0 -262
  220. package/src/shared/components/Inbox.web.tsx +0 -329
  221. package/src/shared/components/MachineList.tsx +0 -187
  222. package/src/shared/components/MachineList.tui.tsx +0 -161
  223. package/src/shared/components/MachineList.web.tsx +0 -210
  224. package/src/shared/components/ProjectList.tsx +0 -176
  225. package/src/shared/components/ProjectList.tui.tsx +0 -109
  226. package/src/shared/components/ProjectList.web.tsx +0 -143
  227. package/src/shared/components/SpacesBrowser.tsx +0 -332
  228. package/src/shared/components/SpacesBrowser.tui.tsx +0 -163
  229. package/src/shared/components/SpacesBrowser.web.tsx +0 -221
  230. package/src/shared/components/index.ts +0 -103
  231. package/src/shared/hooks/index.ts +0 -16
  232. package/src/shared/hooks/useNavigation.ts +0 -226
  233. package/src/shared/index.ts +0 -122
  234. package/src/shared/providers/LocalMachineProvider.ts +0 -425
  235. package/src/shared/providers/MachineProvider.ts +0 -165
  236. package/src/shared/providers/RemoteMachineProvider.ts +0 -444
  237. package/src/shared/providers/index.ts +0 -26
  238. package/src/shared/types.ts +0 -145
  239. package/src/tui/adapters.ts +0 -120
  240. package/src/tui/app.tsx +0 -1816
  241. package/src/tui/components/Terminal.tsx +0 -580
  242. package/src/tui/hooks/index.ts +0 -35
  243. package/src/tui/hooks/useAppState.ts +0 -314
  244. package/src/tui/hooks/useDaemonStatus.ts +0 -174
  245. package/src/tui/hooks/useInboxTUI.ts +0 -113
  246. package/src/tui/hooks/useRemoteMachines.ts +0 -209
  247. package/src/tui/index.ts +0 -24
  248. package/src/tui/state.ts +0 -299
  249. package/src/tui/terminal-bracketed-paste.test.ts +0 -45
  250. package/src/tui/terminal-bracketed-paste.ts +0 -47
  251. package/src/types/bundle.ts +0 -112
  252. package/src/types/config.ts +0 -89
  253. package/src/types/errors.ts +0 -206
  254. package/src/types/identity.ts +0 -284
  255. package/src/types/workspace-fuzzy.ts +0 -49
  256. package/src/types/workspace.ts +0 -151
  257. package/src/utils/bun-socket-writer.ts +0 -80
  258. package/src/utils/deps.ts +0 -127
  259. package/src/utils/fuzzy-match.ts +0 -125
  260. package/src/utils/logger.ts +0 -127
  261. package/src/utils/markdown.ts +0 -254
  262. package/src/utils/onboarding.ts +0 -229
  263. package/src/utils/prompts.ts +0 -114
  264. package/src/utils/run-commands.ts +0 -112
  265. package/src/utils/run-scripts.ts +0 -142
  266. package/src/utils/sanitize.ts +0 -98
  267. package/src/utils/secrets.ts +0 -122
  268. package/src/utils/shell-escape.ts +0 -40
  269. package/src/utils/utf8.ts +0 -79
  270. package/src/utils/workspace-state.ts +0 -47
  271. package/src/web/README.md +0 -73
  272. package/src/web/bun.lock +0 -575
  273. package/src/web/eslint.config.js +0 -23
  274. package/src/web/index.html +0 -16
  275. package/src/web/package.json +0 -37
  276. package/src/web/public/vite.svg +0 -1
  277. package/src/web/src/App.tsx +0 -604
  278. package/src/web/src/assets/react.svg +0 -1
  279. package/src/web/src/components/Terminal.tsx +0 -207
  280. package/src/web/src/hooks/useRelayConnection.ts +0 -224
  281. package/src/web/src/hooks/useTerminal.ts +0 -699
  282. package/src/web/src/index.css +0 -55
  283. package/src/web/src/lib/crypto/__tests__/web-terminal.test.ts +0 -1158
  284. package/src/web/src/lib/crypto/frames.ts +0 -205
  285. package/src/web/src/lib/crypto/handshake.ts +0 -396
  286. package/src/web/src/lib/crypto/identity.ts +0 -128
  287. package/src/web/src/lib/crypto/keyexchange.ts +0 -246
  288. package/src/web/src/lib/crypto/relay-signing.ts +0 -53
  289. package/src/web/src/lib/invite.ts +0 -58
  290. package/src/web/src/lib/storage/identity-store.ts +0 -94
  291. package/src/web/src/main.tsx +0 -10
  292. package/src/web/src/types/identity.ts +0 -45
  293. package/src/web/tsconfig.app.json +0 -28
  294. package/src/web/tsconfig.json +0 -7
  295. package/src/web/tsconfig.node.json +0 -26
  296. package/src/web/vite.config.ts +0 -31
  297. package/todo-security.md +0 -92
  298. package/tsconfig.json +0 -23
  299. package/worker/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/12b7107e435bf1b9a8713a7f320472a63e543104d633d89a26f8d21f4e4ef182.sqlite +0 -0
  300. package/worker/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/12b7107e435bf1b9a8713a7f320472a63e543104d633d89a26f8d21f4e4ef182.sqlite-shm +0 -0
  301. package/worker/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/12b7107e435bf1b9a8713a7f320472a63e543104d633d89a26f8d21f4e4ef182.sqlite-wal +0 -0
  302. package/worker/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/1a1ac3db1ab86ecf712f90322868a9aabc2c7dc9fe2dfbe94f9b075096276b0f.sqlite +0 -0
  303. package/worker/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/1a1ac3db1ab86ecf712f90322868a9aabc2c7dc9fe2dfbe94f9b075096276b0f.sqlite-shm +0 -0
  304. package/worker/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/1a1ac3db1ab86ecf712f90322868a9aabc2c7dc9fe2dfbe94f9b075096276b0f.sqlite-wal +0 -0
  305. package/worker/bun.lock +0 -237
  306. package/worker/package.json +0 -22
  307. package/worker/schema.sql +0 -96
  308. package/worker/src/handlers/auth.ts +0 -451
  309. package/worker/src/handlers/subdomains.ts +0 -376
  310. package/worker/src/handlers/user.ts +0 -98
  311. package/worker/src/index.ts +0 -70
  312. package/worker/src/middleware/auth.ts +0 -152
  313. package/worker/src/services/cloudflare.ts +0 -609
  314. package/worker/src/types.ts +0 -96
  315. package/worker/tsconfig.json +0 -15
  316. package/worker/wrangler.toml +0 -26
@@ -1,622 +0,0 @@
1
- /**
2
- * Client session manager for the serve daemon
3
- *
4
- * Manages multiple concurrent client connections:
5
- * - Routes handshake messages to HandshakeHandler
6
- * - After handshake, enters "browsing" mode for workspace/session listing
7
- * - Spawns PTY sessions when client attaches to a session
8
- * - Routes encrypted frames between clients and PTY sessions
9
- * - Handles disconnect cleanup
10
- */
11
-
12
- import { HandshakeHandler, type HandshakeMessage, type EstablishedSession } from "../lib/tmux-lite/handshake-handler.js";
13
- import { PTYSession } from "./pty-session.js";
14
- import { createFrame, openFrame, MASTER_STREAM_ID } from "../lib/tmux-lite/crypto/frames.js";
15
- import { encodeControl, encodePTY, parseFrames, decodeControl, FrameType, type SessionEvent } from "../lib/tmux-lite/protocol.js";
16
- import { RemoteSessionHandler, type RemoteClientSession } from "../lib/remote-session/index.js";
17
- import { STREAM_ID, canWrite, type ServeOptions, type ClientSession, type ServeEventHandler, type HandshakeMessageEnvelope } from "./types.js";
18
- import { createBufferedSocketWriter } from "../utils/bun-socket-writer.js";
19
-
20
- // ============================================================================
21
- // ClientSessionManager Class
22
- // ============================================================================
23
-
24
- /**
25
- * Manages client sessions for the serve daemon
26
- *
27
- * @example
28
- * ```typescript
29
- * const manager = new ClientSessionManager({
30
- * relay: "wss://relay.example.com",
31
- * identity: machineIdentity,
32
- * accessList: acl,
33
- * });
34
- *
35
- * manager.onEvent((event) => {
36
- * if (event.type === "client_authenticated") {
37
- * console.log(`Client ${event.identityId} connected`);
38
- * }
39
- * });
40
- *
41
- * // Handle incoming message
42
- * const response = await manager.handleMessage(connectionId, data);
43
- * if (response) {
44
- * relay.send(connectionId, response);
45
- * }
46
- * ```
47
- */
48
- export class ClientSessionManager {
49
- private sessions: Map<string, ClientSession> = new Map();
50
- private handshakeHandler: HandshakeHandler;
51
- private remoteSessionHandler: RemoteSessionHandler;
52
- private options: ServeOptions;
53
- private eventHandler: ServeEventHandler | null = null;
54
-
55
- constructor(options: ServeOptions) {
56
- this.options = options;
57
- this.handshakeHandler = new HandshakeHandler({
58
- identity: options.identity,
59
- accessList: options.accessList,
60
- handshakeTimeoutMs: options.handshakeTimeoutMs,
61
- });
62
- this.remoteSessionHandler = new RemoteSessionHandler();
63
- }
64
-
65
- private writeToTmuxSocket(session: ClientSession, frame: Buffer): void {
66
- if (session.tmuxSocketWriter) {
67
- session.tmuxSocketWriter.write(frame);
68
- return;
69
- }
70
- session.tmuxSocket?.write(frame);
71
- }
72
-
73
- /**
74
- * Initialize async resources (like tmux-lite connection)
75
- */
76
- async initialize(): Promise<void> {
77
- await this.remoteSessionHandler.initialize();
78
- }
79
-
80
- /**
81
- * Set event handler for session events
82
- */
83
- onEvent(handler: ServeEventHandler): void {
84
- this.eventHandler = handler;
85
- }
86
-
87
- /**
88
- * Emit an event
89
- */
90
- private emit(event: Parameters<ServeEventHandler>[0]): void {
91
- this.eventHandler?.(event);
92
- }
93
-
94
- /**
95
- * Get number of active sessions
96
- */
97
- get activeSessionCount(): number {
98
- return this.sessions.size;
99
- }
100
-
101
- /**
102
- * Get number of established sessions (post-handshake: browsing or attached)
103
- */
104
- get establishedSessionCount(): number {
105
- let count = 0;
106
- for (const session of this.sessions.values()) {
107
- if (session.state === "browsing" || session.state === "attached") count++;
108
- }
109
- return count;
110
- }
111
-
112
- /**
113
- * Get session by connection ID
114
- */
115
- getSession(connectionId: string): ClientSession | undefined {
116
- return this.sessions.get(connectionId);
117
- }
118
-
119
- /**
120
- * Get all sessions
121
- */
122
- getAllSessions(): ClientSession[] {
123
- return Array.from(this.sessions.values());
124
- }
125
-
126
- /**
127
- * Handle a new client connection
128
- */
129
- handleConnect(connectionId: string): void {
130
- // Create new session in handshaking state
131
- const session: ClientSession = {
132
- connectionId,
133
- state: "handshaking",
134
- handshakeStartedAt: Date.now(),
135
- };
136
- this.sessions.set(connectionId, session);
137
-
138
- this.emit({ type: "client_connected", connectionId });
139
- }
140
-
141
- /**
142
- * Handle incoming message from a client
143
- *
144
- * Routes to handshake handler or PTY session based on state.
145
- *
146
- * @param connectionId - Client connection ID
147
- * @param data - Raw message data
148
- * @returns Response to send back (if any)
149
- */
150
- async handleMessage(
151
- connectionId: string,
152
- data: Uint8Array
153
- ): Promise<Uint8Array | null> {
154
- let session = this.sessions.get(connectionId);
155
-
156
- // New connection - create session
157
- if (!session) {
158
- this.handleConnect(connectionId);
159
- session = this.sessions.get(connectionId)!;
160
- }
161
-
162
- // Handle based on session state
163
- if (session.state === "handshaking") {
164
- return this.handleHandshakeMessage(connectionId, session, data);
165
- }
166
-
167
- if (session.state === "browsing") {
168
- // Handle browse commands (list_workspaces, list_sessions, attach_session, etc.)
169
- return this.handleBrowseMessage(connectionId, session, data);
170
- }
171
-
172
- if (session.state === "attached" && session.tmuxSocket) {
173
- // Decrypt and route to tmux-lite session based on stream ID
174
- return this.handleAttachedMessage(connectionId, session, data);
175
- }
176
-
177
- if (session.state === "attached" && session.ptySession) {
178
- // Legacy: Forward encrypted data to PTY
179
- session.ptySession.write(Buffer.from(data));
180
- return null;
181
- }
182
-
183
- // Invalid state
184
- console.warn(`[session-manager] Message in invalid state: ${session.state}`);
185
- return null;
186
- }
187
-
188
- /**
189
- * Handle message in attached state - route to tmux-lite session based on stream ID
190
- */
191
- private async handleAttachedMessage(
192
- connectionId: string,
193
- session: ClientSession,
194
- data: Uint8Array
195
- ): Promise<Uint8Array | null> {
196
- if (!session.sessionKeys || !session.tmuxSocket) {
197
- console.error("[session-manager] handleAttachedMessage: missing sessionKeys or tmuxSocket");
198
- return null;
199
- }
200
-
201
- try {
202
- // Decrypt the frame
203
- const result = openFrame(data, session.sessionKeys.receiveKey);
204
- if (!result) {
205
- console.error("[session-manager] Failed to decrypt attached frame");
206
- return null;
207
- }
208
-
209
- // Debug: console.log(`[session-manager] Attached message: streamId=${result.streamId}, dataLen=${result.data.length}`);
210
-
211
- if (result.streamId === STREAM_ID.CONTROL) {
212
- // Control message (resize, detach) - parse and encode for tmux-lite protocol
213
- const msg = JSON.parse(new TextDecoder().decode(result.data));
214
- console.log(`[session-manager] Control message: ${msg.type}`);
215
-
216
- if (msg.type === "detach") {
217
- // Handle detach specially - close tmux socket and send response to client
218
- // Store socket reference and clear it BEFORE ending to prevent close callback
219
- // from triggering handleDisconnect
220
- const socket = session.tmuxSocket;
221
- const writer = session.tmuxSocketWriter;
222
- session.tmuxSocket = undefined;
223
- session.tmuxSocketWriter = undefined;
224
- session.state = "browsing";
225
- session.attachedSessionId = undefined;
226
- session.sessionSocketPath = undefined;
227
- session.waitingForResize = undefined;
228
- session.frameBuffer = undefined;
229
-
230
- // Now send detach and close the socket (using framed protocol)
231
- {
232
- const frame = encodeControl(msg);
233
- if (writer) writer.write(frame);
234
- else socket.write(frame);
235
- }
236
- socket.end();
237
-
238
- // Send detached response to client
239
- const detachedMsg = JSON.stringify({ type: "detached" });
240
- const detachedData = new TextEncoder().encode(detachedMsg);
241
- const frame = createFrame(STREAM_ID.DATA, detachedData, session.sessionKeys.sendKey);
242
- console.log("[session-manager] Sent detached response, returning to browsing mode");
243
- return frame;
244
- }
245
-
246
- if (msg.type === "resize" && session.waitingForResize) {
247
- // First resize - send attach-init with actual dimensions
248
- console.log(`[session-manager] First resize: ${msg.cols}x${msg.rows} - sending attach-init`);
249
- session.waitingForResize = false;
250
- this.writeToTmuxSocket(session, encodeControl({ type: "attach-init", cols: msg.cols, rows: msg.rows, clientType: "web" }));
251
- return null; // attach-init handles the resize
252
- }
253
-
254
- // Other control messages (resize after init) - encode for tmux-lite and send
255
- this.writeToTmuxSocket(session, encodeControl(msg));
256
- } else {
257
- // Raw PTY input (STREAM_ID.DATA) - send directly to socket
258
- // Security: Check write permission before forwarding input
259
- if (!canWrite(session.accessType)) {
260
- console.warn(`[session-manager] Read-only client ${connectionId} attempted PTY write - denied`);
261
- return null; // Silently drop input from read-only clients
262
- }
263
-
264
- // Only forward if we've sent attach-init (waitingForResize is false)
265
- if (!session.waitingForResize) {
266
- // Wrap PTY data in a frame for the framed protocol
267
- this.writeToTmuxSocket(session, encodePTY(result.data));
268
- } else {
269
- console.warn("[session-manager] Ignoring PTY data before attach-init");
270
- }
271
- }
272
-
273
- return null;
274
- } catch (e) {
275
- console.error("[session-manager] Error handling attached message:", e);
276
- return null;
277
- }
278
- }
279
-
280
- /**
281
- * Handle browse message (encrypted command in browsing state)
282
- */
283
- private async handleBrowseMessage(
284
- connectionId: string,
285
- session: ClientSession,
286
- data: Uint8Array
287
- ): Promise<Uint8Array | null> {
288
- if (!session.sessionKeys) {
289
- console.error("[session-manager] No session keys for browse message");
290
- return null;
291
- }
292
-
293
- // Create RemoteClientSession adapter for the handler
294
- const remoteSession: RemoteClientSession = {
295
- connectionId,
296
- state: "browsing",
297
- sessionKeys: session.sessionKeys,
298
- accessType: session.accessType,
299
- grantedSessionId: session.sessionId,
300
- };
301
-
302
- // Create send callback that captures the raw encrypted response
303
- // Don't wrap in JSON here - serve.ts handles the relay envelope
304
- let responseData: Uint8Array | null = null;
305
- const sendResponse = (encryptedFrame: Uint8Array) => {
306
- responseData = encryptedFrame;
307
- };
308
-
309
- // Handle the message through RemoteSessionHandler
310
- await this.remoteSessionHandler.handleMessage(remoteSession, data, sendResponse);
311
-
312
- // Check if we're now attached (after attach_session command)
313
- if (remoteSession.state === "attached" && remoteSession.attachedSessionId) {
314
- session.state = "attached";
315
- session.attachedSessionId = remoteSession.attachedSessionId;
316
- session.sessionSocketPath = remoteSession.sessionSocketPath;
317
-
318
- // Connect to tmux-lite session socket for PTY I/O
319
- await this.attachToTmuxLiteSession(connectionId, session);
320
- }
321
-
322
- return responseData;
323
- }
324
-
325
- /**
326
- * Handle handshake message
327
- */
328
- private async handleHandshakeMessage(
329
- connectionId: string,
330
- session: ClientSession,
331
- data: Uint8Array
332
- ): Promise<Uint8Array | null> {
333
- try {
334
- // Parse as JSON handshake message
335
- const jsonStr = new TextDecoder().decode(data);
336
- const envelope = JSON.parse(jsonStr) as HandshakeMessageEnvelope;
337
-
338
- if (envelope.type !== "handshake") {
339
- console.warn(`[session-manager] Expected handshake, got: ${envelope.type}`);
340
- return null;
341
- }
342
-
343
- // Process through HandshakeHandler
344
- const result = await this.handshakeHandler.processMessage(connectionId, envelope as HandshakeMessage);
345
-
346
- switch (result.type) {
347
- case "reply": {
348
- // Send reply back to client
349
- return new TextEncoder().encode(JSON.stringify(result.message));
350
- }
351
-
352
- case "established": {
353
- // Handshake complete - spawn PTY and send ServerAuth
354
- return this.handleHandshakeEstablished(connectionId, session, result.session, result.message);
355
- }
356
-
357
- case "error": {
358
- console.error(`[session-manager] Handshake error: ${result.reason}`);
359
- this.emit({ type: "error", connectionId, error: new Error(result.reason) });
360
-
361
- if (result.close) {
362
- this.handleDisconnect(connectionId, result.reason);
363
- }
364
- return null;
365
- }
366
- }
367
- } catch (e) {
368
- console.error("[session-manager] Handshake message parse error:", e);
369
- this.emit({
370
- type: "error",
371
- connectionId,
372
- error: new Error(`Invalid handshake message: ${e instanceof Error ? e.message : String(e)}`),
373
- });
374
- return null;
375
- }
376
- }
377
-
378
- /**
379
- * Handle successful handshake - enter browsing mode
380
- */
381
- private handleHandshakeEstablished(
382
- connectionId: string,
383
- session: ClientSession,
384
- established: EstablishedSession,
385
- serverAuthMessage: HandshakeMessage
386
- ): Uint8Array | null {
387
- // Update session state - enter browsing mode (not spawning PTY yet)
388
- session.state = "browsing";
389
- session.sessionKeys = established.sessionKeys;
390
- session.accessType = established.accessType;
391
- session.sessionId = established.sessionId;
392
- session.peerIdentityId = established.peerIdentityId;
393
-
394
- // Emit event
395
- this.emit({
396
- type: "client_authenticated",
397
- connectionId,
398
- identityId: established.peerIdentityId,
399
- accessType: established.accessType,
400
- sessionId: established.sessionId,
401
- });
402
-
403
- // Client can now send list_workspaces, list_sessions, attach_session commands
404
- // PTY will be spawned when attach_session is received
405
-
406
- // Return ServerAuth message from HandshakeHandler
407
- return new TextEncoder().encode(JSON.stringify(serverAuthMessage));
408
- }
409
-
410
- /**
411
- * Spawn PTY session for an established connection
412
- */
413
- private spawnPTYSession(connectionId: string, session: ClientSession): void {
414
- if (!session.sessionKeys) {
415
- console.error("[session-manager] Cannot spawn PTY: no session keys");
416
- return;
417
- }
418
-
419
- // Callback to send encrypted data to client
420
- const sendToClient = this.createSendCallback(connectionId);
421
-
422
- session.ptySession = new PTYSession({
423
- shell: this.options.shell,
424
- env: {
425
- ...this.options.env,
426
- SPACES_PEER_ID: session.peerIdentityId ?? "",
427
- },
428
- sessionKeys: session.sessionKeys,
429
- onData: (encrypted) => {
430
- sendToClient(encrypted);
431
- },
432
- onClose: (exitCode) => {
433
- console.log(`[session-manager] PTY exited: ${exitCode}`);
434
- this.handleDisconnect(connectionId, `PTY exited with code ${exitCode}`);
435
- },
436
- });
437
-
438
- console.log(`[session-manager] PTY spawned for ${connectionId} (pid: ${session.ptySession.pid})`);
439
- }
440
-
441
- /**
442
- * Attach to a tmux-lite session socket for PTY I/O
443
- * This is the proper way to connect - through the existing tmux-lite session
444
- */
445
- private async attachToTmuxLiteSession(connectionId: string, session: ClientSession): Promise<void> {
446
- if (!session.sessionKeys || !session.sessionSocketPath) {
447
- console.error("[session-manager] Cannot attach: missing session keys or socket path");
448
- return;
449
- }
450
-
451
- const sendToClient = this.createSendCallback(connectionId);
452
-
453
- try {
454
- // Connect to tmux-lite session socket
455
- const socket = await Bun.connect({
456
- unix: session.sessionSocketPath,
457
- socket: {
458
- drain: () => {
459
- session.tmuxSocketWriter?.flush();
460
- },
461
- data: (sock, data) => {
462
- if (!session.sessionKeys) return;
463
-
464
- // Accumulate in frame buffer (for handling partial frames)
465
- const prev = session.frameBuffer || Buffer.alloc(0);
466
- const buf = Buffer.concat([prev, Buffer.from(data)]);
467
-
468
- // Parse frames from the accumulated buffer
469
- let frames;
470
- let remaining;
471
- try {
472
- const result = parseFrames(buf);
473
- frames = result.frames;
474
- remaining = result.remaining;
475
- } catch (err) {
476
- // Protocol error - likely desync or corrupted data
477
- const msg = err instanceof Error ? err.message : 'Frame parse error';
478
- console.error(`[session-manager] Frame parse error: ${msg}`);
479
- this.handleDisconnect(connectionId, `Frame parse error: ${msg}`);
480
- return;
481
- }
482
- // Copy remaining bytes - subarray references can become invalid when Bun reuses buffers
483
- session.frameBuffer = Buffer.from(remaining);
484
-
485
- for (const frame of frames) {
486
- if (frame.type === FrameType.CONTROL) {
487
- // Decode and handle control events
488
- const event = decodeControl(frame.payload) as SessionEvent;
489
-
490
- if (event.type === "exited") {
491
- console.log(`[session-manager] Session exited: ${event.code}`);
492
- // Send exit notification to client
493
- const exitMsg = JSON.stringify({ type: "session_exited", sessionId: session.attachedSessionId, exitCode: event.code });
494
- const exitData = new TextEncoder().encode(exitMsg);
495
- const encFrame = createFrame(STREAM_ID.DATA, exitData, session.sessionKeys.sendKey);
496
- sendToClient(Buffer.from(encFrame));
497
- this.handleDisconnect(connectionId, `Session exited with code ${event.code}`);
498
- return;
499
- } else if (event.type === "kicked") {
500
- console.log("[session-manager] Session kicked");
501
- this.handleDisconnect(connectionId, "Session kicked");
502
- return;
503
- }
504
- // Ignore attach-ready and attached - handled by client
505
- } else if (frame.type === FrameType.PTY) {
506
- // Forward PTY data to web client
507
- const encFrame = createFrame(STREAM_ID.DATA, frame.payload, session.sessionKeys.sendKey);
508
- sendToClient(Buffer.from(encFrame));
509
- }
510
- }
511
- },
512
-
513
- close: () => {
514
- // Check if this was a voluntary detach (tmuxSocket already cleared)
515
- // vs an unexpected close
516
- if (session.tmuxSocket) {
517
- console.log("[session-manager] tmux-lite socket closed unexpectedly");
518
- this.handleDisconnect(connectionId, "Session closed");
519
- } else {
520
- console.log("[session-manager] tmux-lite socket closed (detached)");
521
- }
522
- },
523
-
524
- error: (_, e) => {
525
- console.error("[session-manager] tmux-lite socket error:", e);
526
- this.handleDisconnect(connectionId, e.message);
527
- },
528
- }
529
- });
530
-
531
- // Store socket reference
532
- session.tmuxSocket = socket;
533
- session.tmuxSocketWriter = createBufferedSocketWriter(socket);
534
-
535
- // Don't send attach-init yet - wait for the first resize from client
536
- // This ensures tmux-lite receives the actual terminal dimensions
537
- session.waitingForResize = true;
538
-
539
- console.log(`[session-manager] Connected to tmux-lite session: ${session.sessionSocketPath} (waiting for resize)`);
540
- } catch (e) {
541
- console.error("[session-manager] Failed to connect to tmux-lite session:", e);
542
- this.handleDisconnect(connectionId, "Failed to connect to session");
543
- }
544
- }
545
-
546
- /**
547
- * Create a callback to send data to a specific client
548
- *
549
- * This is set by the serve command to route through the relay.
550
- */
551
- private sendCallbacks: Map<string, (data: Buffer) => void> = new Map();
552
-
553
- /**
554
- * Register a send callback for a connection
555
- */
556
- setSendCallback(connectionId: string, callback: (data: Buffer) => void): void {
557
- this.sendCallbacks.set(connectionId, callback);
558
- }
559
-
560
- /**
561
- * Create send callback for a connection
562
- */
563
- private createSendCallback(connectionId: string): (data: Buffer) => void {
564
- return (data: Buffer) => {
565
- const callback = this.sendCallbacks.get(connectionId);
566
- if (callback) {
567
- callback(data);
568
- } else {
569
- console.warn(`[session-manager] No send callback for ${connectionId}`);
570
- }
571
- };
572
- }
573
-
574
- /**
575
- * Handle client disconnect
576
- */
577
- handleDisconnect(connectionId: string, reason: string = "disconnected"): void {
578
- const session = this.sessions.get(connectionId);
579
- if (!session) return;
580
-
581
- // Close tmux-lite socket if active
582
- if (session.tmuxSocket) {
583
- try {
584
- // Send detach message before closing (using framed protocol)
585
- this.writeToTmuxSocket(session, encodeControl({ type: "detach" }));
586
- session.tmuxSocket.end();
587
- } catch {
588
- // Socket may already be closed
589
- }
590
- session.tmuxSocket = undefined;
591
- session.tmuxSocketWriter = undefined;
592
- session.frameBuffer = undefined;
593
- }
594
-
595
- // Close PTY if active (legacy)
596
- if (session.ptySession && !session.ptySession.isClosed) {
597
- session.ptySession.close();
598
- }
599
-
600
- // Cleanup handshake state
601
- this.handshakeHandler.cleanup(connectionId);
602
-
603
- // Remove send callback
604
- this.sendCallbacks.delete(connectionId);
605
-
606
- // Remove session
607
- session.state = "closed";
608
- this.sessions.delete(connectionId);
609
-
610
- this.emit({ type: "client_disconnected", connectionId, reason });
611
- }
612
-
613
- /**
614
- * Clean up all sessions
615
- */
616
- async cleanup(): Promise<void> {
617
- for (const [connectionId] of this.sessions) {
618
- this.handleDisconnect(connectionId, "server shutdown");
619
- }
620
- await this.remoteSessionHandler.cleanup();
621
- }
622
- }