gitspace 0.2.0-rc.1
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.
- package/.claude/settings.local.json +21 -0
- package/.gitspace/bundle.json +50 -0
- package/.gitspace/select/01-status.sh +40 -0
- package/.gitspace/setup/01-install-deps.sh +12 -0
- package/.gitspace/setup/02-typecheck.sh +16 -0
- package/AGENTS.md +439 -0
- package/CLAUDE.md +1 -0
- package/LICENSE +25 -0
- package/README.md +607 -0
- package/bin/gssh +62 -0
- package/bun.lock +647 -0
- package/docs/CONNECTION.md +623 -0
- package/docs/GATEWAY-WORKER.md +319 -0
- package/docs/GETTING-STARTED.md +448 -0
- package/docs/GITSPACE-PLATFORM.md +1819 -0
- package/docs/INFRASTRUCTURE.md +1347 -0
- package/docs/PROTOCOL.md +619 -0
- package/docs/QUICKSTART.md +174 -0
- package/docs/RELAY.md +327 -0
- package/docs/REMOTE-DESIGN.md +549 -0
- package/docs/ROADMAP.md +564 -0
- package/docs/SITE_DOCS_FIGMA_MAKE.md +1167 -0
- package/docs/STACK-DESIGN.md +588 -0
- package/docs/UNIFIED_ARCHITECTURE.md +292 -0
- package/experiments/pty-benchmark.ts +148 -0
- package/experiments/pty-latency.ts +100 -0
- package/experiments/router/client.ts +199 -0
- package/experiments/router/protocol.ts +74 -0
- package/experiments/router/router.ts +217 -0
- package/experiments/router/session.ts +180 -0
- package/experiments/router/test.ts +133 -0
- package/experiments/socket-bandwidth.ts +77 -0
- package/homebrew/gitspace.rb +45 -0
- package/landing-page/ATTRIBUTIONS.md +3 -0
- package/landing-page/README.md +11 -0
- package/landing-page/bun.lock +801 -0
- package/landing-page/guidelines/Guidelines.md +61 -0
- package/landing-page/index.html +37 -0
- package/landing-page/package.json +90 -0
- package/landing-page/postcss.config.mjs +15 -0
- package/landing-page/public/_redirects +1 -0
- package/landing-page/public/favicon.png +0 -0
- package/landing-page/src/app/App.tsx +53 -0
- package/landing-page/src/app/components/figma/ImageWithFallback.tsx +27 -0
- package/landing-page/src/app/components/ui/accordion.tsx +66 -0
- package/landing-page/src/app/components/ui/alert-dialog.tsx +157 -0
- package/landing-page/src/app/components/ui/alert.tsx +66 -0
- package/landing-page/src/app/components/ui/aspect-ratio.tsx +11 -0
- package/landing-page/src/app/components/ui/avatar.tsx +53 -0
- package/landing-page/src/app/components/ui/badge.tsx +46 -0
- package/landing-page/src/app/components/ui/breadcrumb.tsx +109 -0
- package/landing-page/src/app/components/ui/button.tsx +57 -0
- package/landing-page/src/app/components/ui/calendar.tsx +75 -0
- package/landing-page/src/app/components/ui/card.tsx +92 -0
- package/landing-page/src/app/components/ui/carousel.tsx +241 -0
- package/landing-page/src/app/components/ui/chart.tsx +353 -0
- package/landing-page/src/app/components/ui/checkbox.tsx +32 -0
- package/landing-page/src/app/components/ui/collapsible.tsx +33 -0
- package/landing-page/src/app/components/ui/command.tsx +177 -0
- package/landing-page/src/app/components/ui/context-menu.tsx +252 -0
- package/landing-page/src/app/components/ui/dialog.tsx +135 -0
- package/landing-page/src/app/components/ui/drawer.tsx +132 -0
- package/landing-page/src/app/components/ui/dropdown-menu.tsx +257 -0
- package/landing-page/src/app/components/ui/form.tsx +168 -0
- package/landing-page/src/app/components/ui/hover-card.tsx +44 -0
- package/landing-page/src/app/components/ui/input-otp.tsx +77 -0
- package/landing-page/src/app/components/ui/input.tsx +21 -0
- package/landing-page/src/app/components/ui/label.tsx +24 -0
- package/landing-page/src/app/components/ui/menubar.tsx +276 -0
- package/landing-page/src/app/components/ui/navigation-menu.tsx +168 -0
- package/landing-page/src/app/components/ui/pagination.tsx +127 -0
- package/landing-page/src/app/components/ui/popover.tsx +48 -0
- package/landing-page/src/app/components/ui/progress.tsx +31 -0
- package/landing-page/src/app/components/ui/radio-group.tsx +45 -0
- package/landing-page/src/app/components/ui/resizable.tsx +56 -0
- package/landing-page/src/app/components/ui/scroll-area.tsx +58 -0
- package/landing-page/src/app/components/ui/select.tsx +189 -0
- package/landing-page/src/app/components/ui/separator.tsx +28 -0
- package/landing-page/src/app/components/ui/sheet.tsx +139 -0
- package/landing-page/src/app/components/ui/sidebar.tsx +726 -0
- package/landing-page/src/app/components/ui/skeleton.tsx +13 -0
- package/landing-page/src/app/components/ui/slider.tsx +63 -0
- package/landing-page/src/app/components/ui/sonner.tsx +25 -0
- package/landing-page/src/app/components/ui/switch.tsx +31 -0
- package/landing-page/src/app/components/ui/table.tsx +116 -0
- package/landing-page/src/app/components/ui/tabs.tsx +66 -0
- package/landing-page/src/app/components/ui/textarea.tsx +18 -0
- package/landing-page/src/app/components/ui/toggle-group.tsx +73 -0
- package/landing-page/src/app/components/ui/toggle.tsx +47 -0
- package/landing-page/src/app/components/ui/tooltip.tsx +61 -0
- package/landing-page/src/app/components/ui/use-mobile.ts +21 -0
- package/landing-page/src/app/components/ui/utils.ts +6 -0
- package/landing-page/src/components/docs/DocsContent.tsx +718 -0
- package/landing-page/src/components/docs/DocsSidebar.tsx +84 -0
- package/landing-page/src/components/landing/CTA.tsx +59 -0
- package/landing-page/src/components/landing/Comparison.tsx +84 -0
- package/landing-page/src/components/landing/FaultyTerminal.tsx +424 -0
- package/landing-page/src/components/landing/Features.tsx +201 -0
- package/landing-page/src/components/landing/Hero.tsx +142 -0
- package/landing-page/src/components/landing/Pricing.tsx +140 -0
- package/landing-page/src/components/landing/Roadmap.tsx +86 -0
- package/landing-page/src/components/landing/Security.tsx +81 -0
- package/landing-page/src/components/landing/TerminalWindow.tsx +27 -0
- package/landing-page/src/components/landing/UseCases.tsx +55 -0
- package/landing-page/src/components/landing/Workflow.tsx +101 -0
- package/landing-page/src/components/layout/DashboardNavbar.tsx +37 -0
- package/landing-page/src/components/layout/Footer.tsx +55 -0
- package/landing-page/src/components/layout/LandingNavbar.tsx +82 -0
- package/landing-page/src/components/ui/badge.tsx +39 -0
- package/landing-page/src/components/ui/breadcrumb.tsx +115 -0
- package/landing-page/src/components/ui/button.tsx +57 -0
- package/landing-page/src/components/ui/card.tsx +79 -0
- package/landing-page/src/components/ui/mock-terminal.tsx +68 -0
- package/landing-page/src/components/ui/separator.tsx +28 -0
- package/landing-page/src/lib/utils.ts +6 -0
- package/landing-page/src/main.tsx +10 -0
- package/landing-page/src/pages/Dashboard.tsx +133 -0
- package/landing-page/src/pages/DocsPage.tsx +79 -0
- package/landing-page/src/pages/LandingPage.tsx +31 -0
- package/landing-page/src/pages/TerminalView.tsx +106 -0
- package/landing-page/src/styles/fonts.css +0 -0
- package/landing-page/src/styles/index.css +3 -0
- package/landing-page/src/styles/tailwind.css +4 -0
- package/landing-page/src/styles/theme.css +181 -0
- package/landing-page/vite.config.ts +19 -0
- package/npm/darwin-arm64/bin/gssh +0 -0
- package/npm/darwin-arm64/package.json +20 -0
- package/package.json +74 -0
- package/scripts/build.ts +284 -0
- package/scripts/release.ts +140 -0
- package/src/__tests__/test-utils.ts +298 -0
- package/src/commands/__tests__/serve-messages.test.ts +190 -0
- package/src/commands/access.ts +298 -0
- package/src/commands/add.ts +452 -0
- package/src/commands/auth.ts +364 -0
- package/src/commands/connect.ts +287 -0
- package/src/commands/directory.ts +16 -0
- package/src/commands/host.ts +396 -0
- package/src/commands/identity.ts +184 -0
- package/src/commands/list.ts +200 -0
- package/src/commands/relay.ts +315 -0
- package/src/commands/remove.ts +241 -0
- package/src/commands/serve.ts +1493 -0
- package/src/commands/share.ts +456 -0
- package/src/commands/status.ts +125 -0
- package/src/commands/switch.ts +353 -0
- package/src/commands/tmux.ts +317 -0
- package/src/core/__tests__/access.test.ts +240 -0
- package/src/core/access.ts +277 -0
- package/src/core/bundle.ts +342 -0
- package/src/core/config.ts +510 -0
- package/src/core/git.ts +317 -0
- package/src/core/github.ts +151 -0
- package/src/core/identity.ts +631 -0
- package/src/core/linear.ts +225 -0
- package/src/core/shell.ts +161 -0
- package/src/core/trusted-relays.ts +315 -0
- package/src/index.ts +821 -0
- package/src/lib/remote-session/index.ts +7 -0
- package/src/lib/remote-session/protocol.ts +267 -0
- package/src/lib/remote-session/session-handler.ts +581 -0
- package/src/lib/remote-session/workspace-scanner.ts +167 -0
- package/src/lib/tmux-lite/README.md +81 -0
- package/src/lib/tmux-lite/cli.ts +796 -0
- package/src/lib/tmux-lite/crypto/__tests__/helpers/handshake-runner.ts +349 -0
- package/src/lib/tmux-lite/crypto/__tests__/helpers/mock-relay.ts +291 -0
- package/src/lib/tmux-lite/crypto/__tests__/helpers/test-identities.ts +142 -0
- package/src/lib/tmux-lite/crypto/__tests__/integration/authorization.integration.test.ts +339 -0
- package/src/lib/tmux-lite/crypto/__tests__/integration/e2e-communication.integration.test.ts +477 -0
- package/src/lib/tmux-lite/crypto/__tests__/integration/error-handling.integration.test.ts +499 -0
- package/src/lib/tmux-lite/crypto/__tests__/integration/handshake.integration.test.ts +371 -0
- package/src/lib/tmux-lite/crypto/__tests__/integration/security.integration.test.ts +573 -0
- package/src/lib/tmux-lite/crypto/access-control.test.ts +512 -0
- package/src/lib/tmux-lite/crypto/access-control.ts +320 -0
- package/src/lib/tmux-lite/crypto/frames.test.ts +262 -0
- package/src/lib/tmux-lite/crypto/frames.ts +141 -0
- package/src/lib/tmux-lite/crypto/handshake.ts +894 -0
- package/src/lib/tmux-lite/crypto/identity.test.ts +220 -0
- package/src/lib/tmux-lite/crypto/identity.ts +286 -0
- package/src/lib/tmux-lite/crypto/index.ts +51 -0
- package/src/lib/tmux-lite/crypto/invites.test.ts +381 -0
- package/src/lib/tmux-lite/crypto/invites.ts +215 -0
- package/src/lib/tmux-lite/crypto/keyexchange.ts +435 -0
- package/src/lib/tmux-lite/crypto/keys.test.ts +58 -0
- package/src/lib/tmux-lite/crypto/keys.ts +47 -0
- package/src/lib/tmux-lite/crypto/secretbox.test.ts +169 -0
- package/src/lib/tmux-lite/crypto/secretbox.ts +124 -0
- package/src/lib/tmux-lite/handshake-handler.ts +451 -0
- package/src/lib/tmux-lite/protocol.test.ts +307 -0
- package/src/lib/tmux-lite/protocol.ts +266 -0
- package/src/lib/tmux-lite/relay-client.ts +506 -0
- package/src/lib/tmux-lite/server.ts +1250 -0
- package/src/lib/tmux-lite/shell-integration.sh +37 -0
- package/src/lib/tmux-lite/terminal-queries.test.ts +54 -0
- package/src/lib/tmux-lite/terminal-queries.ts +49 -0
- package/src/relay/__tests__/e2e-flow.test.ts +1284 -0
- package/src/relay/__tests__/helpers/auth.ts +354 -0
- package/src/relay/__tests__/helpers/ports.ts +51 -0
- package/src/relay/__tests__/protocol-validation.test.ts +265 -0
- package/src/relay/authorization.ts +303 -0
- package/src/relay/embedded-assets.generated.d.ts +15 -0
- package/src/relay/identity.ts +352 -0
- package/src/relay/index.ts +57 -0
- package/src/relay/pipes.test.ts +427 -0
- package/src/relay/pipes.ts +195 -0
- package/src/relay/protocol.ts +804 -0
- package/src/relay/registries.test.ts +437 -0
- package/src/relay/registries.ts +593 -0
- package/src/relay/server.test.ts +1323 -0
- package/src/relay/server.ts +1092 -0
- package/src/relay/signing.ts +238 -0
- package/src/relay/types.ts +69 -0
- package/src/serve/client-session-manager.ts +622 -0
- package/src/serve/daemon.ts +497 -0
- package/src/serve/pty-session.ts +236 -0
- package/src/serve/types.ts +169 -0
- package/src/shared/components/Flow.tsx +453 -0
- package/src/shared/components/Flow.tui.tsx +343 -0
- package/src/shared/components/Flow.web.tsx +442 -0
- package/src/shared/components/Inbox.tsx +446 -0
- package/src/shared/components/Inbox.tui.tsx +262 -0
- package/src/shared/components/Inbox.web.tsx +329 -0
- package/src/shared/components/MachineList.tsx +187 -0
- package/src/shared/components/MachineList.tui.tsx +161 -0
- package/src/shared/components/MachineList.web.tsx +210 -0
- package/src/shared/components/ProjectList.tsx +176 -0
- package/src/shared/components/ProjectList.tui.tsx +109 -0
- package/src/shared/components/ProjectList.web.tsx +143 -0
- package/src/shared/components/SpacesBrowser.tsx +332 -0
- package/src/shared/components/SpacesBrowser.tui.tsx +163 -0
- package/src/shared/components/SpacesBrowser.web.tsx +221 -0
- package/src/shared/components/index.ts +103 -0
- package/src/shared/hooks/index.ts +16 -0
- package/src/shared/hooks/useNavigation.ts +226 -0
- package/src/shared/index.ts +122 -0
- package/src/shared/providers/LocalMachineProvider.ts +425 -0
- package/src/shared/providers/MachineProvider.ts +165 -0
- package/src/shared/providers/RemoteMachineProvider.ts +444 -0
- package/src/shared/providers/index.ts +26 -0
- package/src/shared/types.ts +145 -0
- package/src/tui/adapters.ts +120 -0
- package/src/tui/app.tsx +1816 -0
- package/src/tui/components/Terminal.tsx +580 -0
- package/src/tui/hooks/index.ts +35 -0
- package/src/tui/hooks/useAppState.ts +314 -0
- package/src/tui/hooks/useDaemonStatus.ts +174 -0
- package/src/tui/hooks/useInboxTUI.ts +113 -0
- package/src/tui/hooks/useRemoteMachines.ts +209 -0
- package/src/tui/index.ts +24 -0
- package/src/tui/state.ts +299 -0
- package/src/tui/terminal-bracketed-paste.test.ts +45 -0
- package/src/tui/terminal-bracketed-paste.ts +47 -0
- package/src/types/bundle.ts +112 -0
- package/src/types/config.ts +89 -0
- package/src/types/errors.ts +206 -0
- package/src/types/identity.ts +284 -0
- package/src/types/workspace-fuzzy.ts +49 -0
- package/src/types/workspace.ts +151 -0
- package/src/utils/bun-socket-writer.ts +80 -0
- package/src/utils/deps.ts +127 -0
- package/src/utils/fuzzy-match.ts +125 -0
- package/src/utils/logger.ts +127 -0
- package/src/utils/markdown.ts +254 -0
- package/src/utils/onboarding.ts +229 -0
- package/src/utils/prompts.ts +114 -0
- package/src/utils/run-commands.ts +112 -0
- package/src/utils/run-scripts.ts +142 -0
- package/src/utils/sanitize.ts +98 -0
- package/src/utils/secrets.ts +122 -0
- package/src/utils/shell-escape.ts +40 -0
- package/src/utils/utf8.ts +79 -0
- package/src/utils/workspace-state.ts +47 -0
- package/src/web/README.md +73 -0
- package/src/web/bun.lock +575 -0
- package/src/web/eslint.config.js +23 -0
- package/src/web/index.html +16 -0
- package/src/web/package.json +37 -0
- package/src/web/public/vite.svg +1 -0
- package/src/web/src/App.tsx +604 -0
- package/src/web/src/assets/react.svg +1 -0
- package/src/web/src/components/Terminal.tsx +207 -0
- package/src/web/src/hooks/useRelayConnection.ts +224 -0
- package/src/web/src/hooks/useTerminal.ts +699 -0
- package/src/web/src/index.css +55 -0
- package/src/web/src/lib/crypto/__tests__/web-terminal.test.ts +1158 -0
- package/src/web/src/lib/crypto/frames.ts +205 -0
- package/src/web/src/lib/crypto/handshake.ts +396 -0
- package/src/web/src/lib/crypto/identity.ts +128 -0
- package/src/web/src/lib/crypto/keyexchange.ts +246 -0
- package/src/web/src/lib/crypto/relay-signing.ts +53 -0
- package/src/web/src/lib/invite.ts +58 -0
- package/src/web/src/lib/storage/identity-store.ts +94 -0
- package/src/web/src/main.tsx +10 -0
- package/src/web/src/types/identity.ts +45 -0
- package/src/web/tsconfig.app.json +28 -0
- package/src/web/tsconfig.json +7 -0
- package/src/web/tsconfig.node.json +26 -0
- package/src/web/vite.config.ts +31 -0
- package/todo-security.md +92 -0
- package/tsconfig.json +23 -0
- package/worker/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/12b7107e435bf1b9a8713a7f320472a63e543104d633d89a26f8d21f4e4ef182.sqlite +0 -0
- package/worker/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/12b7107e435bf1b9a8713a7f320472a63e543104d633d89a26f8d21f4e4ef182.sqlite-shm +0 -0
- package/worker/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/12b7107e435bf1b9a8713a7f320472a63e543104d633d89a26f8d21f4e4ef182.sqlite-wal +0 -0
- package/worker/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/1a1ac3db1ab86ecf712f90322868a9aabc2c7dc9fe2dfbe94f9b075096276b0f.sqlite +0 -0
- package/worker/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/1a1ac3db1ab86ecf712f90322868a9aabc2c7dc9fe2dfbe94f9b075096276b0f.sqlite-shm +0 -0
- package/worker/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/1a1ac3db1ab86ecf712f90322868a9aabc2c7dc9fe2dfbe94f9b075096276b0f.sqlite-wal +0 -0
- package/worker/bun.lock +237 -0
- package/worker/package.json +22 -0
- package/worker/schema.sql +96 -0
- package/worker/src/handlers/auth.ts +451 -0
- package/worker/src/handlers/subdomains.ts +376 -0
- package/worker/src/handlers/user.ts +98 -0
- package/worker/src/index.ts +70 -0
- package/worker/src/middleware/auth.ts +152 -0
- package/worker/src/services/cloudflare.ts +609 -0
- package/worker/src/types.ts +96 -0
- package/worker/tsconfig.json +15 -0
- package/worker/wrangler.toml +26 -0
|
@@ -0,0 +1,622 @@
|
|
|
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
|
+
}
|