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,506 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Relay client - WebSocket connection from client to relay
|
|
3
|
+
*
|
|
4
|
+
* Handles:
|
|
5
|
+
* - Connection to relay server
|
|
6
|
+
* - X3DH handshake for mutual authentication and key exchange
|
|
7
|
+
* - Reconnection with exponential backoff
|
|
8
|
+
* - Sending/receiving encrypted frames
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { createHash } from "crypto";
|
|
12
|
+
import {
|
|
13
|
+
createFrame,
|
|
14
|
+
openFrame,
|
|
15
|
+
MASTER_STREAM_ID,
|
|
16
|
+
} from "./crypto";
|
|
17
|
+
import {
|
|
18
|
+
createClientHello,
|
|
19
|
+
processServerHello,
|
|
20
|
+
createClientAuth,
|
|
21
|
+
processServerAuth,
|
|
22
|
+
type X3DHClientState,
|
|
23
|
+
} from "./crypto/handshake.js";
|
|
24
|
+
import type {
|
|
25
|
+
Identity,
|
|
26
|
+
SessionKeys,
|
|
27
|
+
AccessType,
|
|
28
|
+
X3DHResponseMessage,
|
|
29
|
+
X3DHAuthMessage,
|
|
30
|
+
X3DHResultMessage,
|
|
31
|
+
} from "../../types/identity.js";
|
|
32
|
+
import { signMessage, type SignatureBlock } from "../../relay/signing.js";
|
|
33
|
+
|
|
34
|
+
/** Relay client configuration (identity/X3DH handshake) */
|
|
35
|
+
export interface RelayClientConfig {
|
|
36
|
+
/** Relay WebSocket URL (e.g., wss://relay.example.com/ws) */
|
|
37
|
+
relayUrl: string;
|
|
38
|
+
/** Machine ID hint for relay routing */
|
|
39
|
+
machineId?: string;
|
|
40
|
+
/** Client's identity for authentication */
|
|
41
|
+
identity: Identity;
|
|
42
|
+
/** Invite token (if connecting via invite) */
|
|
43
|
+
inviteToken?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Connection state */
|
|
47
|
+
export type ConnectionState =
|
|
48
|
+
| "disconnected"
|
|
49
|
+
| "connecting"
|
|
50
|
+
| "handshaking"
|
|
51
|
+
| "connected"
|
|
52
|
+
| "reconnecting";
|
|
53
|
+
|
|
54
|
+
/** Relay client events */
|
|
55
|
+
export interface RelayClientEvents {
|
|
56
|
+
/** Called when connected to relay */
|
|
57
|
+
onConnect?: () => void;
|
|
58
|
+
/** Called when disconnected from relay */
|
|
59
|
+
onDisconnect?: (code: number, reason: string) => void;
|
|
60
|
+
/** Called when a message is received (already decrypted) */
|
|
61
|
+
onMessage?: (streamId: number, data: Buffer) => void;
|
|
62
|
+
/** Called on connection error */
|
|
63
|
+
onError?: (error: Error) => void;
|
|
64
|
+
/** Called when connection state changes */
|
|
65
|
+
onStateChange?: (state: ConnectionState) => void;
|
|
66
|
+
/** Called when handshake completes */
|
|
67
|
+
onHandshakeComplete?: (peerIdentityId: string, accessType: AccessType, sessionId?: string) => void;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Relay client for identity-based connections
|
|
72
|
+
*
|
|
73
|
+
* Uses X3DH handshake with Ed25519/X25519 keys.
|
|
74
|
+
*/
|
|
75
|
+
export class RelayClient {
|
|
76
|
+
private ws: WebSocket | null = null;
|
|
77
|
+
private config: RelayClientConfig;
|
|
78
|
+
private events: RelayClientEvents;
|
|
79
|
+
private state: ConnectionState = "disconnected";
|
|
80
|
+
private reconnectAttempts = 0;
|
|
81
|
+
private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
|
|
82
|
+
private readKey: Buffer | null = null;
|
|
83
|
+
private writeKey: Buffer | null = null;
|
|
84
|
+
|
|
85
|
+
// X3DH handshake state (identity mode)
|
|
86
|
+
private handshakeState: X3DHClientState | null = null;
|
|
87
|
+
private sessionKeys: SessionKeys | null = null;
|
|
88
|
+
private peerIdentityId: string | null = null;
|
|
89
|
+
private accessType: AccessType | null = null;
|
|
90
|
+
private sessionId: string | undefined = undefined;
|
|
91
|
+
|
|
92
|
+
/** Maximum reconnect attempts */
|
|
93
|
+
private readonly maxReconnectAttempts = 10;
|
|
94
|
+
/** Base reconnect delay in ms */
|
|
95
|
+
private readonly baseReconnectDelay = 1000;
|
|
96
|
+
/** Maximum reconnect delay in ms */
|
|
97
|
+
private readonly maxReconnectDelay = 30000;
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Create a new relay client
|
|
101
|
+
*
|
|
102
|
+
* @param config - Client configuration
|
|
103
|
+
* @param events - Event handlers
|
|
104
|
+
*/
|
|
105
|
+
constructor(config: RelayClientConfig, events: RelayClientEvents = {}) {
|
|
106
|
+
this.config = config;
|
|
107
|
+
this.events = events;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Get current connection state */
|
|
111
|
+
getState(): ConnectionState {
|
|
112
|
+
return this.state;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Check if connected */
|
|
116
|
+
isConnected(): boolean {
|
|
117
|
+
return this.state === "connected" && this.ws?.readyState === WebSocket.OPEN;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Get peer identity ID */
|
|
121
|
+
getPeerIdentityId(): string | null {
|
|
122
|
+
return this.peerIdentityId;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Get current access type */
|
|
126
|
+
getAccessType(): AccessType | null {
|
|
127
|
+
return this.accessType;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Get current session ID */
|
|
131
|
+
getSessionId(): string | undefined {
|
|
132
|
+
return this.sessionId;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Connect to the relay server
|
|
137
|
+
*/
|
|
138
|
+
async connect(): Promise<void> {
|
|
139
|
+
if (this.state === "connecting" || this.state === "connected" || this.state === "handshaking") {
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
this.setState("connecting");
|
|
144
|
+
this.doConnect();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Disconnect from the relay server
|
|
149
|
+
*/
|
|
150
|
+
disconnect(): void {
|
|
151
|
+
this.cancelReconnect();
|
|
152
|
+
if (this.ws) {
|
|
153
|
+
this.ws.close(1000, "Client disconnect");
|
|
154
|
+
this.ws = null;
|
|
155
|
+
}
|
|
156
|
+
this.setState("disconnected");
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Send data to all connected clients (via relay)
|
|
161
|
+
*
|
|
162
|
+
* @param data - Plaintext data to send (will be encrypted)
|
|
163
|
+
* @param streamId - Stream ID (default: master stream)
|
|
164
|
+
*/
|
|
165
|
+
send(data: Buffer | Uint8Array, streamId = MASTER_STREAM_ID): boolean {
|
|
166
|
+
if (!this.isConnected() || !this.writeKey) {
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
try {
|
|
171
|
+
const frame = createFrame(streamId, data, this.writeKey);
|
|
172
|
+
|
|
173
|
+
// Relay mode: wrap encrypted frame in JSON data message
|
|
174
|
+
this.ws!.send(JSON.stringify({
|
|
175
|
+
type: "data",
|
|
176
|
+
data: Buffer.from(frame).toString("base64"),
|
|
177
|
+
}));
|
|
178
|
+
return true;
|
|
179
|
+
} catch (e) {
|
|
180
|
+
console.error("[relay-client] Send error:", e);
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
private setState(state: ConnectionState): void {
|
|
186
|
+
if (this.state !== state) {
|
|
187
|
+
this.state = state;
|
|
188
|
+
this.events.onStateChange?.(state);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
private doConnect(): void {
|
|
193
|
+
const { relayUrl, machineId } = this.config;
|
|
194
|
+
|
|
195
|
+
// Build WebSocket URL
|
|
196
|
+
const url = new URL(relayUrl);
|
|
197
|
+
if (machineId) {
|
|
198
|
+
url.searchParams.set("m", machineId);
|
|
199
|
+
}
|
|
200
|
+
url.searchParams.set("role", "client");
|
|
201
|
+
|
|
202
|
+
try {
|
|
203
|
+
this.ws = new WebSocket(url.toString());
|
|
204
|
+
this.ws.binaryType = "arraybuffer";
|
|
205
|
+
|
|
206
|
+
this.ws.onopen = () => {
|
|
207
|
+
console.log("[relay-client] Connected to relay");
|
|
208
|
+
this.reconnectAttempts = 0;
|
|
209
|
+
|
|
210
|
+
// Need to send protocol message for routing
|
|
211
|
+
// Then wait for connection_established before starting handshake
|
|
212
|
+
if (this.config.inviteToken) {
|
|
213
|
+
// Client connecting via invite - send connect_with_invite
|
|
214
|
+
this.sendConnectWithInvite();
|
|
215
|
+
} else {
|
|
216
|
+
// Direct connection to machine - send connect_to_machine
|
|
217
|
+
this.sendConnectToMachine();
|
|
218
|
+
}
|
|
219
|
+
// Handshake will start when we receive connection_established
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
this.ws.onclose = (event) => {
|
|
223
|
+
console.log(
|
|
224
|
+
`[relay-client] Disconnected: ${event.code} ${event.reason}`
|
|
225
|
+
);
|
|
226
|
+
this.ws = null;
|
|
227
|
+
this.handshakeState = null;
|
|
228
|
+
this.events.onDisconnect?.(event.code, event.reason);
|
|
229
|
+
|
|
230
|
+
// Auto-reconnect unless explicitly disconnected
|
|
231
|
+
if (this.state !== "disconnected") {
|
|
232
|
+
this.scheduleReconnect();
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
this.ws.onerror = (event) => {
|
|
237
|
+
console.error("[relay-client] WebSocket error:", event);
|
|
238
|
+
this.events.onError?.(new Error("WebSocket error"));
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
this.ws.onmessage = (event) => {
|
|
242
|
+
this.handleMessage(event.data);
|
|
243
|
+
};
|
|
244
|
+
} catch (e) {
|
|
245
|
+
console.error("[relay-client] Connection error:", e);
|
|
246
|
+
this.events.onError?.(e as Error);
|
|
247
|
+
this.scheduleReconnect();
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Start X3DH handshake (identity mode)
|
|
253
|
+
*/
|
|
254
|
+
private startHandshake(): void {
|
|
255
|
+
if (!this.ws) {
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
console.log("[relay-client] Starting X3DH handshake");
|
|
260
|
+
|
|
261
|
+
// Create ClientHello
|
|
262
|
+
const { state, message } = createClientHello(this.config.machineId);
|
|
263
|
+
this.handshakeState = state;
|
|
264
|
+
|
|
265
|
+
// Send as JSON (handshake messages are not encrypted)
|
|
266
|
+
this.ws.send(JSON.stringify({ type: "handshake", phase: "client_hello", data: message }));
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
private signClientMessage<T extends object>(message: T): T & { signature: SignatureBlock } {
|
|
270
|
+
const privateKey = this.config.identity.signing.secretKey.slice(0, 32);
|
|
271
|
+
const publicKey = this.config.identity.signing.publicKey;
|
|
272
|
+
return signMessage(message, privateKey, publicKey);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Send connect_with_invite protocol message for relay routing
|
|
277
|
+
*/
|
|
278
|
+
private sendConnectWithInvite(): void {
|
|
279
|
+
if (!this.ws || !this.config.inviteToken) {
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Generate inviteId from token hash (same as share.ts)
|
|
284
|
+
const inviteId = createHash("sha256")
|
|
285
|
+
.update(this.config.inviteToken)
|
|
286
|
+
.digest("hex")
|
|
287
|
+
.substring(0, 16);
|
|
288
|
+
|
|
289
|
+
const clientIdentityId = this.config.identity.id;
|
|
290
|
+
|
|
291
|
+
console.log("[relay-client] Sending connect_with_invite");
|
|
292
|
+
const signed = this.signClientMessage({
|
|
293
|
+
type: "connect_with_invite",
|
|
294
|
+
inviteId,
|
|
295
|
+
clientIdentityId,
|
|
296
|
+
});
|
|
297
|
+
this.ws.send(JSON.stringify(signed));
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Send connect_to_machine protocol message for direct connection
|
|
302
|
+
*/
|
|
303
|
+
private sendConnectToMachine(): void {
|
|
304
|
+
if (!this.ws) {
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const clientIdentityId = this.config.identity.id;
|
|
309
|
+
const machineId = this.config.machineId;
|
|
310
|
+
|
|
311
|
+
console.log("[relay-client] Sending connect_to_machine");
|
|
312
|
+
const signed = this.signClientMessage({
|
|
313
|
+
type: "connect_to_machine",
|
|
314
|
+
machineId,
|
|
315
|
+
clientIdentityId,
|
|
316
|
+
});
|
|
317
|
+
this.ws.send(JSON.stringify(signed));
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
private handleMessage(data: ArrayBuffer | string): void {
|
|
321
|
+
try {
|
|
322
|
+
const jsonData = data instanceof ArrayBuffer
|
|
323
|
+
? new TextDecoder().decode(data)
|
|
324
|
+
: data;
|
|
325
|
+
|
|
326
|
+
const msg = JSON.parse(jsonData);
|
|
327
|
+
|
|
328
|
+
// Handle relay protocol messages
|
|
329
|
+
if (msg.type === "connection_established") {
|
|
330
|
+
console.log("[relay-client] Connection established to machine:", msg.machineId);
|
|
331
|
+
// Now start X3DH handshake - relay has set up the connection
|
|
332
|
+
this.setState("handshaking");
|
|
333
|
+
this.startHandshake();
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (msg.type === "error") {
|
|
338
|
+
console.error("[relay-client] Relay error:", msg.message);
|
|
339
|
+
this.events.onError?.(new Error(msg.message || "Relay error"));
|
|
340
|
+
this.disconnect();
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (msg.type === "handshake" && this.state === "handshaking") {
|
|
345
|
+
this.handleHandshakeMessage(msg);
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (msg.type === "data" && msg.data && this.state === "handshaking") {
|
|
350
|
+
// Handle handshake wrapped in data message from relay
|
|
351
|
+
try {
|
|
352
|
+
const decodedData = Buffer.from(msg.data, "base64").toString("utf-8");
|
|
353
|
+
const innerMsg = JSON.parse(decodedData);
|
|
354
|
+
if (innerMsg.type === "handshake") {
|
|
355
|
+
this.handleHandshakeMessage(innerMsg);
|
|
356
|
+
}
|
|
357
|
+
} catch {
|
|
358
|
+
// Ignore non-handshake data during handshaking
|
|
359
|
+
}
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
if (msg.type === "data" && msg.data && this.state === "connected" && this.readKey) {
|
|
364
|
+
const frameBuffer = Buffer.from(msg.data, "base64");
|
|
365
|
+
const result = openFrame(frameBuffer, this.readKey);
|
|
366
|
+
if (result) {
|
|
367
|
+
this.events.onMessage?.(result.streamId, result.data);
|
|
368
|
+
} else {
|
|
369
|
+
console.warn("[relay-client] Failed to decrypt frame");
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
} catch (e) {
|
|
373
|
+
console.error("[relay-client] Message handling error:", e);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Handle X3DH handshake messages
|
|
379
|
+
*/
|
|
380
|
+
private handleHandshakeMessage(msg: { type: string; phase: string; data: unknown }): void {
|
|
381
|
+
if (!this.handshakeState || !this.ws) {
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
try {
|
|
386
|
+
switch (msg.phase) {
|
|
387
|
+
case "server_hello": {
|
|
388
|
+
console.log("[relay-client] Received ServerHello");
|
|
389
|
+
|
|
390
|
+
// Process ServerHello
|
|
391
|
+
const serverHello = msg.data as X3DHResponseMessage;
|
|
392
|
+
const newState = processServerHello(this.handshakeState, serverHello);
|
|
393
|
+
|
|
394
|
+
if (!newState) {
|
|
395
|
+
console.error("[relay-client] Invalid ServerHello");
|
|
396
|
+
this.events.onError?.(new Error("Handshake failed: invalid ServerHello"));
|
|
397
|
+
this.disconnect();
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Create and send ClientAuth
|
|
402
|
+
const authorization: X3DHAuthMessage["authorization"] = this.config.inviteToken
|
|
403
|
+
? { type: "invite", inviteToken: this.config.inviteToken }
|
|
404
|
+
: { type: "access_list" };
|
|
405
|
+
|
|
406
|
+
const { state, message, sessionKeys } = createClientAuth(
|
|
407
|
+
newState,
|
|
408
|
+
this.config.identity,
|
|
409
|
+
authorization
|
|
410
|
+
);
|
|
411
|
+
|
|
412
|
+
this.handshakeState = state;
|
|
413
|
+
this.sessionKeys = sessionKeys;
|
|
414
|
+
|
|
415
|
+
console.log("[relay-client] Sending ClientAuth");
|
|
416
|
+
this.ws.send(JSON.stringify({ type: "handshake", phase: "client_auth", data: message }));
|
|
417
|
+
break;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
case "server_auth": {
|
|
421
|
+
console.log("[relay-client] Received ServerAuth");
|
|
422
|
+
|
|
423
|
+
if (!this.sessionKeys) {
|
|
424
|
+
console.error("[relay-client] Missing session keys");
|
|
425
|
+
this.disconnect();
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// Process ServerAuth
|
|
430
|
+
const serverAuth = msg.data as X3DHResultMessage;
|
|
431
|
+
const result = processServerAuth(this.handshakeState, serverAuth, this.sessionKeys);
|
|
432
|
+
|
|
433
|
+
if (!result) {
|
|
434
|
+
console.error("[relay-client] Handshake failed: invalid ServerAuth");
|
|
435
|
+
this.events.onError?.(new Error("Handshake failed: authentication rejected"));
|
|
436
|
+
this.disconnect();
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// Check if accepted
|
|
441
|
+
if (result.authResult.type === "rejected") {
|
|
442
|
+
console.error("[relay-client] Access denied:", result.authResult.reason);
|
|
443
|
+
this.events.onError?.(new Error(`Access denied: ${result.authResult.reason}`));
|
|
444
|
+
this.disconnect();
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// Handshake complete - set up encryption keys
|
|
449
|
+
this.peerIdentityId = result.peerIdentityId;
|
|
450
|
+
this.accessType = result.authResult.accessType;
|
|
451
|
+
this.sessionId = result.authResult.sessionId;
|
|
452
|
+
|
|
453
|
+
// Convert session keys to Buffers for frame encryption
|
|
454
|
+
this.writeKey = Buffer.from(result.sessionKeys.sendKey);
|
|
455
|
+
this.readKey = Buffer.from(result.sessionKeys.receiveKey);
|
|
456
|
+
|
|
457
|
+
console.log("[relay-client] Handshake complete, session established");
|
|
458
|
+
this.setState("connected");
|
|
459
|
+
this.events.onConnect?.();
|
|
460
|
+
this.events.onHandshakeComplete?.(this.peerIdentityId, this.accessType, this.sessionId);
|
|
461
|
+
break;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
default:
|
|
465
|
+
console.warn("[relay-client] Unknown handshake phase:", msg.phase);
|
|
466
|
+
}
|
|
467
|
+
} catch (e) {
|
|
468
|
+
console.error("[relay-client] Handshake error:", e);
|
|
469
|
+
this.events.onError?.(new Error(`Handshake error: ${e instanceof Error ? e.message : String(e)}`));
|
|
470
|
+
this.disconnect();
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
private scheduleReconnect(): void {
|
|
475
|
+
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
|
476
|
+
console.error("[relay-client] Max reconnect attempts reached");
|
|
477
|
+
this.setState("disconnected");
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
this.setState("reconnecting");
|
|
482
|
+
this.reconnectAttempts++;
|
|
483
|
+
|
|
484
|
+
// Exponential backoff with jitter
|
|
485
|
+
const delay = Math.min(
|
|
486
|
+
this.baseReconnectDelay * Math.pow(2, this.reconnectAttempts - 1) +
|
|
487
|
+
Math.random() * 1000,
|
|
488
|
+
this.maxReconnectDelay
|
|
489
|
+
);
|
|
490
|
+
|
|
491
|
+
console.log(
|
|
492
|
+
`[relay-client] Reconnecting in ${Math.round(delay)}ms (attempt ${this.reconnectAttempts})`
|
|
493
|
+
);
|
|
494
|
+
|
|
495
|
+
this.reconnectTimer = setTimeout(() => {
|
|
496
|
+
this.doConnect();
|
|
497
|
+
}, delay);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
private cancelReconnect(): void {
|
|
501
|
+
if (this.reconnectTimer) {
|
|
502
|
+
clearTimeout(this.reconnectTimer);
|
|
503
|
+
this.reconnectTimer = null;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
}
|