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,804 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Relay protocol message types and handlers
|
|
3
|
+
*
|
|
4
|
+
* Defines the WebSocket message format for machine-relay-client communication.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { SignatureBlock } from "./signing.js";
|
|
8
|
+
|
|
9
|
+
// ============================================================================
|
|
10
|
+
// Protocol Versioning
|
|
11
|
+
// ============================================================================
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Current protocol version
|
|
15
|
+
* - Version 1: Signatures optional (verify if present)
|
|
16
|
+
* - Version 2: Signatures required on security-critical messages
|
|
17
|
+
*/
|
|
18
|
+
export const PROTOCOL_VERSION = 2;
|
|
19
|
+
|
|
20
|
+
// ============================================================================
|
|
21
|
+
// Machine → Relay Messages
|
|
22
|
+
// ============================================================================
|
|
23
|
+
|
|
24
|
+
/** Machine registers itself with relay */
|
|
25
|
+
export interface RegisterMachineMessage {
|
|
26
|
+
type: "register_machine";
|
|
27
|
+
machineId: string;
|
|
28
|
+
signingKey: string;
|
|
29
|
+
keyExchangeKey: string;
|
|
30
|
+
label?: string;
|
|
31
|
+
/** Challenge response - signature of the nonce from relay_identity (base64) */
|
|
32
|
+
challengeResponse?: string;
|
|
33
|
+
/** Protocol version (for signature requirement negotiation) */
|
|
34
|
+
protocolVersion?: number;
|
|
35
|
+
/** Ed25519 signature of message */
|
|
36
|
+
signature?: SignatureBlock;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Machine registers an invite */
|
|
40
|
+
export interface RegisterInviteMessage {
|
|
41
|
+
type: "register_invite";
|
|
42
|
+
inviteId: string;
|
|
43
|
+
machineId: string;
|
|
44
|
+
expiresAt: number;
|
|
45
|
+
maxUses: number | null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Machine authorizes a client */
|
|
49
|
+
export interface AuthorizeClientMessage {
|
|
50
|
+
type: "authorize_client";
|
|
51
|
+
machineId: string;
|
|
52
|
+
clientIdentityId: string;
|
|
53
|
+
signingKey: string;
|
|
54
|
+
keyExchangeKey: string;
|
|
55
|
+
accessType: 'full' | 'session-invite';
|
|
56
|
+
sessionId?: string;
|
|
57
|
+
/** Ed25519 signature of message */
|
|
58
|
+
signature?: SignatureBlock;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Machine revokes client authorization */
|
|
62
|
+
export interface RevokeClientMessage {
|
|
63
|
+
type: "revoke_client";
|
|
64
|
+
machineId: string;
|
|
65
|
+
clientIdentityId: string;
|
|
66
|
+
/** Ed25519 signature of message */
|
|
67
|
+
signature?: SignatureBlock;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Machine sends data to a specific client */
|
|
71
|
+
export interface MachineDataMessage {
|
|
72
|
+
type: "data";
|
|
73
|
+
connectionId: string;
|
|
74
|
+
data: string; // base64 encoded
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Machine responds to identity challenge */
|
|
78
|
+
export interface ChallengeResponseMessage {
|
|
79
|
+
type: "challenge_response";
|
|
80
|
+
/** Signature of the challenge nonce (base64) */
|
|
81
|
+
signature: string;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Machine requests to add global access */
|
|
85
|
+
export interface AddGlobalAccessMessage {
|
|
86
|
+
type: "add_global_access";
|
|
87
|
+
clientIdentityId: string;
|
|
88
|
+
signingKey: string;
|
|
89
|
+
keyExchangeKey: string;
|
|
90
|
+
label?: string;
|
|
91
|
+
accessType: 'full' | 'session-invite';
|
|
92
|
+
sessionId?: string;
|
|
93
|
+
/** If set, only applies to specific machines */
|
|
94
|
+
machineIds?: string[];
|
|
95
|
+
/** Ed25519 signature of message */
|
|
96
|
+
signature?: SignatureBlock;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Machine requests to remove global access */
|
|
100
|
+
export interface RemoveGlobalAccessMessage {
|
|
101
|
+
type: "remove_global_access";
|
|
102
|
+
clientIdentityId: string;
|
|
103
|
+
/** Ed25519 signature of message */
|
|
104
|
+
signature?: SignatureBlock;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ============================================================================
|
|
108
|
+
// Client → Relay Messages
|
|
109
|
+
// ============================================================================
|
|
110
|
+
|
|
111
|
+
/** Client requests list of machines they can connect to */
|
|
112
|
+
export interface ListMachinesMessage {
|
|
113
|
+
type: "list_machines";
|
|
114
|
+
clientIdentityId: string;
|
|
115
|
+
/** Ed25519 signature of message */
|
|
116
|
+
signature: SignatureBlock;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Client connects using an invite */
|
|
120
|
+
export interface ConnectWithInviteMessage {
|
|
121
|
+
type: "connect_with_invite";
|
|
122
|
+
inviteId: string;
|
|
123
|
+
clientIdentityId: string;
|
|
124
|
+
/** Ed25519 signature of message */
|
|
125
|
+
signature: SignatureBlock;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Client connects to a specific machine (already authorized) */
|
|
129
|
+
export interface ConnectToMachineMessage {
|
|
130
|
+
type: "connect_to_machine";
|
|
131
|
+
machineId: string;
|
|
132
|
+
clientIdentityId: string;
|
|
133
|
+
/** Ed25519 signature of message */
|
|
134
|
+
signature: SignatureBlock;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Client sends data to machine */
|
|
138
|
+
export interface ClientDataMessage {
|
|
139
|
+
type: "data";
|
|
140
|
+
data: string; // base64 encoded
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Client sends handshake message to machine */
|
|
144
|
+
export interface ClientHandshakeMessage {
|
|
145
|
+
type: "handshake";
|
|
146
|
+
phase: "client_hello" | "client_auth";
|
|
147
|
+
data: unknown;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ============================================================================
|
|
151
|
+
// Relay → Machine Messages
|
|
152
|
+
// ============================================================================
|
|
153
|
+
|
|
154
|
+
/** Relay identity message - sent immediately on machine connect */
|
|
155
|
+
export interface RelayIdentityMessage {
|
|
156
|
+
type: "relay_identity";
|
|
157
|
+
/** Relay's Ed25519 signing public key (base64) */
|
|
158
|
+
publicKey: string;
|
|
159
|
+
/** Human-readable fingerprint (e.g., "Kx4f:2nB9:mP3q:vR8s") */
|
|
160
|
+
fingerprint: string;
|
|
161
|
+
/** Optional relay label */
|
|
162
|
+
label?: string;
|
|
163
|
+
/** Challenge nonce - machine must sign this to prove identity (base64) */
|
|
164
|
+
challenge: string;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Identity challenge from relay (machine must sign to prove key ownership) */
|
|
168
|
+
export interface ChallengeMessage {
|
|
169
|
+
type: "challenge";
|
|
170
|
+
/** Random nonce to sign (base64) */
|
|
171
|
+
nonce: string;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Registration confirmation */
|
|
175
|
+
export interface RegisteredMessage {
|
|
176
|
+
type: "registered";
|
|
177
|
+
machineId: string;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Access list sync from relay to machine */
|
|
181
|
+
export interface AccessListMessage {
|
|
182
|
+
type: "access_list";
|
|
183
|
+
entries: {
|
|
184
|
+
clientIdentityId: string;
|
|
185
|
+
signingKey: string;
|
|
186
|
+
keyExchangeKey: string;
|
|
187
|
+
label?: string;
|
|
188
|
+
accessType: 'full' | 'session-invite';
|
|
189
|
+
sessionId?: string;
|
|
190
|
+
grantedAt: number;
|
|
191
|
+
}[];
|
|
192
|
+
/** Protocol version */
|
|
193
|
+
protocolVersion?: number;
|
|
194
|
+
/** Ed25519 signature of message (signed by relay) */
|
|
195
|
+
signature?: SignatureBlock;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** Incremental access list update from relay to machine */
|
|
199
|
+
export interface AccessUpdateMessage {
|
|
200
|
+
type: "access_update";
|
|
201
|
+
added: {
|
|
202
|
+
clientIdentityId: string;
|
|
203
|
+
signingKey: string;
|
|
204
|
+
keyExchangeKey: string;
|
|
205
|
+
label?: string;
|
|
206
|
+
accessType: 'full' | 'session-invite';
|
|
207
|
+
sessionId?: string;
|
|
208
|
+
grantedAt: number;
|
|
209
|
+
}[];
|
|
210
|
+
removed: string[]; // clientIdentityIds to remove
|
|
211
|
+
/** Ed25519 signature of message (signed by relay) */
|
|
212
|
+
signature?: SignatureBlock;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** Client authorization confirmation */
|
|
216
|
+
export interface ClientAuthorizedMessage {
|
|
217
|
+
type: "client_authorized";
|
|
218
|
+
clientIdentityId: string;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** Client revocation confirmation */
|
|
222
|
+
export interface ClientRevokedMessage {
|
|
223
|
+
type: "client_revoked";
|
|
224
|
+
clientIdentityId: string;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** Client connected notification */
|
|
228
|
+
export interface ClientConnectedMessage {
|
|
229
|
+
type: "client_connected";
|
|
230
|
+
connectionId: string;
|
|
231
|
+
clientIdentityId?: string;
|
|
232
|
+
viaInvite?: string;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** Client disconnected notification */
|
|
236
|
+
export interface ClientDisconnectedMessage {
|
|
237
|
+
type: "client_disconnected";
|
|
238
|
+
connectionId: string;
|
|
239
|
+
reason: string;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** Data from client */
|
|
243
|
+
export interface DataFromClientMessage {
|
|
244
|
+
type: "data";
|
|
245
|
+
connectionId: string;
|
|
246
|
+
data: string; // base64 encoded
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// ============================================================================
|
|
250
|
+
// Relay → Client Messages
|
|
251
|
+
// ============================================================================
|
|
252
|
+
|
|
253
|
+
/** Machine list response */
|
|
254
|
+
export interface MachineListMessage {
|
|
255
|
+
type: "machine_list";
|
|
256
|
+
machines: {
|
|
257
|
+
machineId: string;
|
|
258
|
+
label?: string;
|
|
259
|
+
online: boolean;
|
|
260
|
+
isAuthorized: boolean;
|
|
261
|
+
accessType?: 'full' | 'session-invite';
|
|
262
|
+
sessionId?: string;
|
|
263
|
+
lastConnectedAt?: number;
|
|
264
|
+
}[];
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** Connection established */
|
|
268
|
+
export interface ConnectionEstablishedMessage {
|
|
269
|
+
type: "connection_established";
|
|
270
|
+
machineId: string;
|
|
271
|
+
connectionId: string;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/** Connection failed */
|
|
275
|
+
export interface ConnectionFailedMessage {
|
|
276
|
+
type: "connection_failed";
|
|
277
|
+
reason: string;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** Data from machine */
|
|
281
|
+
export interface DataFromMachineMessage {
|
|
282
|
+
type: "data";
|
|
283
|
+
data: string; // base64 encoded
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// ============================================================================
|
|
287
|
+
// Error Messages
|
|
288
|
+
// ============================================================================
|
|
289
|
+
|
|
290
|
+
/** Error response */
|
|
291
|
+
export interface ErrorMessage {
|
|
292
|
+
type: "error";
|
|
293
|
+
code: string;
|
|
294
|
+
message: string;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// ============================================================================
|
|
298
|
+
// Union Types
|
|
299
|
+
// ============================================================================
|
|
300
|
+
|
|
301
|
+
/** All messages from machine to relay */
|
|
302
|
+
export type MachineToRelayMessage =
|
|
303
|
+
| RegisterMachineMessage
|
|
304
|
+
| RegisterInviteMessage
|
|
305
|
+
| AuthorizeClientMessage
|
|
306
|
+
| RevokeClientMessage
|
|
307
|
+
| MachineDataMessage
|
|
308
|
+
| ChallengeResponseMessage
|
|
309
|
+
| AddGlobalAccessMessage
|
|
310
|
+
| RemoveGlobalAccessMessage;
|
|
311
|
+
|
|
312
|
+
/** All messages from client to relay */
|
|
313
|
+
export type ClientToRelayMessage =
|
|
314
|
+
| ListMachinesMessage
|
|
315
|
+
| ConnectWithInviteMessage
|
|
316
|
+
| ConnectToMachineMessage
|
|
317
|
+
| ClientDataMessage
|
|
318
|
+
| ClientHandshakeMessage;
|
|
319
|
+
|
|
320
|
+
/** All messages from relay to machine */
|
|
321
|
+
export type RelayToMachineMessage =
|
|
322
|
+
| RelayIdentityMessage
|
|
323
|
+
| ChallengeMessage
|
|
324
|
+
| RegisteredMessage
|
|
325
|
+
| AccessListMessage
|
|
326
|
+
| AccessUpdateMessage
|
|
327
|
+
| ClientAuthorizedMessage
|
|
328
|
+
| ClientRevokedMessage
|
|
329
|
+
| ClientConnectedMessage
|
|
330
|
+
| ClientDisconnectedMessage
|
|
331
|
+
| DataFromClientMessage
|
|
332
|
+
| ErrorMessage;
|
|
333
|
+
|
|
334
|
+
/** All messages from relay to client */
|
|
335
|
+
export type RelayToClientMessage =
|
|
336
|
+
| MachineListMessage
|
|
337
|
+
| ConnectionEstablishedMessage
|
|
338
|
+
| ConnectionFailedMessage
|
|
339
|
+
| DataFromMachineMessage
|
|
340
|
+
| ErrorMessage;
|
|
341
|
+
|
|
342
|
+
/** All protocol messages */
|
|
343
|
+
export type ProtocolMessage =
|
|
344
|
+
| MachineToRelayMessage
|
|
345
|
+
| ClientToRelayMessage
|
|
346
|
+
| RelayToMachineMessage
|
|
347
|
+
| RelayToClientMessage;
|
|
348
|
+
|
|
349
|
+
// ============================================================================
|
|
350
|
+
// Type Guards
|
|
351
|
+
// ============================================================================
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Type guard for MachineDataMessage (data message with connectionId from machine)
|
|
355
|
+
*/
|
|
356
|
+
export function isMachineDataMessage(msg: ProtocolMessage): msg is MachineDataMessage {
|
|
357
|
+
return msg.type === "data" && "connectionId" in msg && typeof (msg as MachineDataMessage).connectionId === "string";
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Type guard for ClientDataMessage (data message without connectionId)
|
|
362
|
+
*/
|
|
363
|
+
export function isClientDataMessage(msg: ProtocolMessage): msg is ClientDataMessage {
|
|
364
|
+
return msg.type === "data" && !("connectionId" in msg);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Type guard for ClientHandshakeMessage
|
|
369
|
+
*/
|
|
370
|
+
export function isClientHandshakeMessage(msg: ProtocolMessage): msg is ClientHandshakeMessage {
|
|
371
|
+
return msg.type === "handshake";
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// ============================================================================
|
|
375
|
+
// Parsing and Validation
|
|
376
|
+
// ============================================================================
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Maximum length for identifier strings
|
|
380
|
+
* Security: Prevents DoS via huge string allocations
|
|
381
|
+
*/
|
|
382
|
+
const MAX_ID_LENGTH = 256;
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Maximum length for label strings
|
|
386
|
+
*/
|
|
387
|
+
const MAX_LABEL_LENGTH = 256;
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Maximum total message size (1MB)
|
|
391
|
+
* Security: Prevents DoS via huge allocations
|
|
392
|
+
*/
|
|
393
|
+
const MAX_MESSAGE_SIZE = 1024 * 1024;
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Pattern for valid identifiers (alphanumeric, hyphens, underscores, dots, colons)
|
|
397
|
+
* Security: Prevents injection attacks via malicious identifier content
|
|
398
|
+
*
|
|
399
|
+
* This pattern allows:
|
|
400
|
+
* - machineId: e.g., "machine-1", "my_machine.local"
|
|
401
|
+
* - inviteId: e.g., "inv_abc123"
|
|
402
|
+
* - clientIdentityId: e.g., "client:xyz" or base64-like strings
|
|
403
|
+
* - connectionId: hex strings from randomBytes
|
|
404
|
+
*/
|
|
405
|
+
const VALID_ID_PATTERN = /^[a-zA-Z0-9\-_.:+=\/]+$/;
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Pattern for base64-encoded data
|
|
409
|
+
* Allows standard base64 (+/) and URL-safe base64 (-_)
|
|
410
|
+
*/
|
|
411
|
+
const VALID_BASE64_PATTERN = /^[a-zA-Z0-9+\/=\-_]+$/;
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Validate an identifier string
|
|
415
|
+
* Security: Prevents path traversal, injection, and DoS attacks
|
|
416
|
+
*/
|
|
417
|
+
export function isValidIdentifier(id: unknown): id is string {
|
|
418
|
+
if (typeof id !== "string") return false;
|
|
419
|
+
if (id.length === 0 || id.length > MAX_ID_LENGTH) return false;
|
|
420
|
+
return VALID_ID_PATTERN.test(id);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Validate a label string (more permissive than identifier)
|
|
425
|
+
*/
|
|
426
|
+
export function isValidLabel(label: unknown): label is string {
|
|
427
|
+
if (typeof label !== "string") return false;
|
|
428
|
+
if (label.length > MAX_LABEL_LENGTH) return false;
|
|
429
|
+
// Labels can contain spaces and more characters, but no control characters
|
|
430
|
+
// eslint-disable-next-line no-control-regex
|
|
431
|
+
return !/[\x00-\x1f\x7f]/.test(label);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Validate a base64-encoded string (for data payloads)
|
|
436
|
+
*/
|
|
437
|
+
export function isValidBase64(data: unknown): data is string {
|
|
438
|
+
if (typeof data !== "string") return false;
|
|
439
|
+
if (data.length === 0 || data.length > MAX_MESSAGE_SIZE) return false;
|
|
440
|
+
return VALID_BASE64_PATTERN.test(data);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Validate a signature block
|
|
445
|
+
*/
|
|
446
|
+
function isValidSignatureBlock(signature: unknown): signature is SignatureBlock {
|
|
447
|
+
if (!signature || typeof signature !== "object") return false;
|
|
448
|
+
const sig = signature as Record<string, unknown>;
|
|
449
|
+
if (!isValidBase64(sig.sig)) return false;
|
|
450
|
+
if (!isValidBase64(sig.pub)) return false;
|
|
451
|
+
if (typeof sig.ts !== "number") return false;
|
|
452
|
+
return true;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Validate a key string (signing key, key exchange key)
|
|
457
|
+
* Less strict than base64 - keys are validated cryptographically when used
|
|
458
|
+
*/
|
|
459
|
+
function isValidKeyString(key: unknown): key is string {
|
|
460
|
+
if (typeof key !== "string") return false;
|
|
461
|
+
if (key.length === 0 || key.length > MAX_ID_LENGTH * 4) return false;
|
|
462
|
+
// Allow any printable ASCII characters (no control characters)
|
|
463
|
+
// eslint-disable-next-line no-control-regex
|
|
464
|
+
return !/[\x00-\x1f\x7f]/.test(key);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Validate accessType value
|
|
469
|
+
*/
|
|
470
|
+
function isValidAccessType(accessType: unknown): accessType is 'full' | 'session-invite' {
|
|
471
|
+
return accessType === 'full' || accessType === 'session-invite';
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Validate specific message types and return properly typed result.
|
|
476
|
+
* Each case returns the specific message type after validation,
|
|
477
|
+
* eliminating the need for unsafe casts.
|
|
478
|
+
*/
|
|
479
|
+
function validateMessageFields(msg: Record<string, unknown>): ProtocolMessage | null {
|
|
480
|
+
switch (msg.type) {
|
|
481
|
+
case "register_machine": {
|
|
482
|
+
if (!isValidIdentifier(msg.machineId)) return null;
|
|
483
|
+
if (!isValidKeyString(msg.signingKey)) return null;
|
|
484
|
+
if (!isValidKeyString(msg.keyExchangeKey)) return null;
|
|
485
|
+
if (msg.label !== undefined && !isValidLabel(msg.label)) return null;
|
|
486
|
+
if (msg.challengeResponse !== undefined && !isValidBase64(msg.challengeResponse)) return null;
|
|
487
|
+
return {
|
|
488
|
+
type: "register_machine",
|
|
489
|
+
machineId: msg.machineId,
|
|
490
|
+
signingKey: msg.signingKey,
|
|
491
|
+
keyExchangeKey: msg.keyExchangeKey,
|
|
492
|
+
label: msg.label,
|
|
493
|
+
challengeResponse: msg.challengeResponse,
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
case "register_invite": {
|
|
498
|
+
if (!isValidIdentifier(msg.inviteId)) return null;
|
|
499
|
+
if (!isValidIdentifier(msg.machineId)) return null;
|
|
500
|
+
if (typeof msg.expiresAt !== "number") return null;
|
|
501
|
+
if (msg.maxUses !== null && msg.maxUses !== undefined && typeof msg.maxUses !== "number") return null;
|
|
502
|
+
return {
|
|
503
|
+
type: "register_invite",
|
|
504
|
+
inviteId: msg.inviteId,
|
|
505
|
+
machineId: msg.machineId,
|
|
506
|
+
expiresAt: msg.expiresAt,
|
|
507
|
+
maxUses: msg.maxUses ?? null,
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
case "authorize_client": {
|
|
512
|
+
if (!isValidIdentifier(msg.machineId)) return null;
|
|
513
|
+
if (!isValidIdentifier(msg.clientIdentityId)) return null;
|
|
514
|
+
if (!isValidKeyString(msg.signingKey)) return null;
|
|
515
|
+
if (!isValidKeyString(msg.keyExchangeKey)) return null;
|
|
516
|
+
if (!isValidAccessType(msg.accessType)) return null;
|
|
517
|
+
if (msg.sessionId !== undefined && !isValidIdentifier(msg.sessionId)) return null;
|
|
518
|
+
return {
|
|
519
|
+
type: "authorize_client",
|
|
520
|
+
machineId: msg.machineId,
|
|
521
|
+
clientIdentityId: msg.clientIdentityId,
|
|
522
|
+
signingKey: msg.signingKey,
|
|
523
|
+
keyExchangeKey: msg.keyExchangeKey,
|
|
524
|
+
accessType: msg.accessType,
|
|
525
|
+
sessionId: msg.sessionId,
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
case "revoke_client": {
|
|
530
|
+
if (!isValidIdentifier(msg.machineId)) return null;
|
|
531
|
+
if (!isValidIdentifier(msg.clientIdentityId)) return null;
|
|
532
|
+
return {
|
|
533
|
+
type: "revoke_client",
|
|
534
|
+
machineId: msg.machineId,
|
|
535
|
+
clientIdentityId: msg.clientIdentityId,
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
case "list_machines": {
|
|
540
|
+
if (!isValidIdentifier(msg.clientIdentityId)) return null;
|
|
541
|
+
if (!isValidSignatureBlock(msg.signature)) return null;
|
|
542
|
+
return {
|
|
543
|
+
type: "list_machines",
|
|
544
|
+
clientIdentityId: msg.clientIdentityId,
|
|
545
|
+
signature: msg.signature,
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
case "connect_with_invite": {
|
|
550
|
+
if (!isValidIdentifier(msg.inviteId)) return null;
|
|
551
|
+
if (!isValidIdentifier(msg.clientIdentityId)) return null;
|
|
552
|
+
if (!isValidSignatureBlock(msg.signature)) return null;
|
|
553
|
+
return {
|
|
554
|
+
type: "connect_with_invite",
|
|
555
|
+
inviteId: msg.inviteId,
|
|
556
|
+
clientIdentityId: msg.clientIdentityId,
|
|
557
|
+
signature: msg.signature,
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
case "connect_to_machine": {
|
|
562
|
+
if (!isValidIdentifier(msg.machineId)) return null;
|
|
563
|
+
if (!isValidIdentifier(msg.clientIdentityId)) return null;
|
|
564
|
+
if (!isValidSignatureBlock(msg.signature)) return null;
|
|
565
|
+
return {
|
|
566
|
+
type: "connect_to_machine",
|
|
567
|
+
machineId: msg.machineId,
|
|
568
|
+
clientIdentityId: msg.clientIdentityId,
|
|
569
|
+
signature: msg.signature,
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
case "data": {
|
|
574
|
+
if (msg.connectionId !== undefined && !isValidIdentifier(msg.connectionId)) return null;
|
|
575
|
+
if (!isValidBase64(msg.data)) return null;
|
|
576
|
+
// Return the appropriate data message type based on presence of connectionId
|
|
577
|
+
if (msg.connectionId !== undefined) {
|
|
578
|
+
return {
|
|
579
|
+
type: "data",
|
|
580
|
+
connectionId: msg.connectionId,
|
|
581
|
+
data: msg.data,
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
return {
|
|
585
|
+
type: "data",
|
|
586
|
+
data: msg.data,
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
case "handshake": {
|
|
591
|
+
if (msg.phase !== "client_hello" && msg.phase !== "client_auth") return null;
|
|
592
|
+
return {
|
|
593
|
+
type: "handshake",
|
|
594
|
+
phase: msg.phase,
|
|
595
|
+
data: msg.data,
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// Response messages (from relay) - construct typed responses
|
|
600
|
+
case "registered": {
|
|
601
|
+
if (!isValidIdentifier(msg.machineId)) return null;
|
|
602
|
+
return { type: "registered", machineId: msg.machineId };
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
case "client_authorized": {
|
|
606
|
+
if (!isValidIdentifier(msg.clientIdentityId)) return null;
|
|
607
|
+
return { type: "client_authorized", clientIdentityId: msg.clientIdentityId };
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
case "client_revoked": {
|
|
611
|
+
if (!isValidIdentifier(msg.clientIdentityId)) return null;
|
|
612
|
+
return { type: "client_revoked", clientIdentityId: msg.clientIdentityId };
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
case "client_connected": {
|
|
616
|
+
if (!isValidIdentifier(msg.connectionId)) return null;
|
|
617
|
+
return {
|
|
618
|
+
type: "client_connected",
|
|
619
|
+
connectionId: msg.connectionId,
|
|
620
|
+
clientIdentityId: typeof msg.clientIdentityId === "string" ? msg.clientIdentityId : undefined,
|
|
621
|
+
viaInvite: typeof msg.viaInvite === "string" ? msg.viaInvite : undefined,
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
case "client_disconnected": {
|
|
626
|
+
if (!isValidIdentifier(msg.connectionId)) return null;
|
|
627
|
+
if (typeof msg.reason !== "string") return null;
|
|
628
|
+
return {
|
|
629
|
+
type: "client_disconnected",
|
|
630
|
+
connectionId: msg.connectionId,
|
|
631
|
+
reason: msg.reason,
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
case "machine_list": {
|
|
636
|
+
if (!Array.isArray(msg.machines)) return null;
|
|
637
|
+
// Trust the machines array structure for relay-generated messages
|
|
638
|
+
return {
|
|
639
|
+
type: "machine_list",
|
|
640
|
+
machines: msg.machines as MachineListMessage["machines"],
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
case "connection_established": {
|
|
645
|
+
if (!isValidIdentifier(msg.machineId)) return null;
|
|
646
|
+
if (!isValidIdentifier(msg.connectionId)) return null;
|
|
647
|
+
return {
|
|
648
|
+
type: "connection_established",
|
|
649
|
+
machineId: msg.machineId,
|
|
650
|
+
connectionId: msg.connectionId,
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
case "connection_failed": {
|
|
655
|
+
if (typeof msg.reason !== "string") return null;
|
|
656
|
+
return {
|
|
657
|
+
type: "connection_failed",
|
|
658
|
+
reason: msg.reason,
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
case "error": {
|
|
663
|
+
if (typeof msg.code !== "string") return null;
|
|
664
|
+
if (typeof msg.message !== "string") return null;
|
|
665
|
+
return {
|
|
666
|
+
type: "error",
|
|
667
|
+
code: msg.code,
|
|
668
|
+
message: msg.message,
|
|
669
|
+
};
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
case "relay_identity": {
|
|
673
|
+
if (!isValidKeyString(msg.publicKey)) return null;
|
|
674
|
+
if (typeof msg.fingerprint !== "string") return null;
|
|
675
|
+
if (msg.label !== undefined && !isValidLabel(msg.label)) return null;
|
|
676
|
+
if (!isValidBase64(msg.challenge)) return null;
|
|
677
|
+
return {
|
|
678
|
+
type: "relay_identity",
|
|
679
|
+
publicKey: msg.publicKey,
|
|
680
|
+
fingerprint: msg.fingerprint,
|
|
681
|
+
label: msg.label,
|
|
682
|
+
challenge: msg.challenge,
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
case "challenge": {
|
|
687
|
+
if (!isValidBase64(msg.nonce)) return null;
|
|
688
|
+
return {
|
|
689
|
+
type: "challenge",
|
|
690
|
+
nonce: msg.nonce,
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
case "challenge_response": {
|
|
695
|
+
if (!isValidBase64(msg.signature)) return null;
|
|
696
|
+
return {
|
|
697
|
+
type: "challenge_response",
|
|
698
|
+
signature: msg.signature,
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
case "add_global_access": {
|
|
703
|
+
if (!isValidIdentifier(msg.clientIdentityId)) return null;
|
|
704
|
+
if (!isValidKeyString(msg.signingKey)) return null;
|
|
705
|
+
if (!isValidKeyString(msg.keyExchangeKey)) return null;
|
|
706
|
+
if (msg.label !== undefined && !isValidLabel(msg.label)) return null;
|
|
707
|
+
if (msg.accessType !== 'full' && msg.accessType !== 'session-invite') return null;
|
|
708
|
+
if (msg.sessionId !== undefined && !isValidIdentifier(msg.sessionId)) return null;
|
|
709
|
+
if (msg.machineIds !== undefined && !Array.isArray(msg.machineIds)) return null;
|
|
710
|
+
return {
|
|
711
|
+
type: "add_global_access",
|
|
712
|
+
clientIdentityId: msg.clientIdentityId,
|
|
713
|
+
signingKey: msg.signingKey,
|
|
714
|
+
keyExchangeKey: msg.keyExchangeKey,
|
|
715
|
+
label: msg.label,
|
|
716
|
+
accessType: msg.accessType,
|
|
717
|
+
sessionId: msg.sessionId,
|
|
718
|
+
machineIds: msg.machineIds,
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
case "remove_global_access": {
|
|
723
|
+
if (!isValidIdentifier(msg.clientIdentityId)) return null;
|
|
724
|
+
return {
|
|
725
|
+
type: "remove_global_access",
|
|
726
|
+
clientIdentityId: msg.clientIdentityId,
|
|
727
|
+
};
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
case "access_list": {
|
|
731
|
+
if (!Array.isArray(msg.entries)) return null;
|
|
732
|
+
return {
|
|
733
|
+
type: "access_list",
|
|
734
|
+
entries: msg.entries as AccessListMessage["entries"],
|
|
735
|
+
};
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
case "access_update": {
|
|
739
|
+
if (!Array.isArray(msg.added)) return null;
|
|
740
|
+
if (!Array.isArray(msg.removed)) return null;
|
|
741
|
+
return {
|
|
742
|
+
type: "access_update",
|
|
743
|
+
added: msg.added as AccessUpdateMessage["added"],
|
|
744
|
+
removed: msg.removed as string[],
|
|
745
|
+
};
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
default:
|
|
749
|
+
return null;
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
/**
|
|
754
|
+
* Parse a JSON message from WebSocket with security validation
|
|
755
|
+
*
|
|
756
|
+
* Security:
|
|
757
|
+
* - Limits message size to prevent DoS
|
|
758
|
+
* - Validates all identifier fields
|
|
759
|
+
* - Validates base64 data fields
|
|
760
|
+
*/
|
|
761
|
+
export function parseMessage(data: string | ArrayBuffer): ProtocolMessage | null {
|
|
762
|
+
try {
|
|
763
|
+
let jsonStr: string;
|
|
764
|
+
if (data instanceof ArrayBuffer) {
|
|
765
|
+
// Security: Check size before decoding
|
|
766
|
+
if (data.byteLength > MAX_MESSAGE_SIZE) {
|
|
767
|
+
return null;
|
|
768
|
+
}
|
|
769
|
+
jsonStr = new TextDecoder().decode(data);
|
|
770
|
+
} else {
|
|
771
|
+
// Security: Check size before parsing
|
|
772
|
+
if (data.length > MAX_MESSAGE_SIZE) {
|
|
773
|
+
return null;
|
|
774
|
+
}
|
|
775
|
+
jsonStr = data;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
const msg = JSON.parse(jsonStr);
|
|
779
|
+
|
|
780
|
+
// Basic validation - check type field exists
|
|
781
|
+
if (!msg || typeof msg.type !== "string") {
|
|
782
|
+
return null;
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
// Validate message-specific fields
|
|
786
|
+
return validateMessageFields(msg);
|
|
787
|
+
} catch {
|
|
788
|
+
return null;
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
/**
|
|
793
|
+
* Create an error message
|
|
794
|
+
*/
|
|
795
|
+
export function createErrorMessage(code: string, message: string): ErrorMessage {
|
|
796
|
+
return { type: "error", code, message };
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
/**
|
|
800
|
+
* Serialize a message for WebSocket send
|
|
801
|
+
*/
|
|
802
|
+
export function serializeMessage(msg: ProtocolMessage): string {
|
|
803
|
+
return JSON.stringify(msg);
|
|
804
|
+
}
|