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,298 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test Utilities for Type-Safe Mocking
|
|
3
|
+
*
|
|
4
|
+
* Provides type-safe mock factories and type guards for testing.
|
|
5
|
+
* Avoids the need for `as any` casts in test files.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type {
|
|
9
|
+
ProcessResult,
|
|
10
|
+
HandshakeMessage,
|
|
11
|
+
EstablishedSession,
|
|
12
|
+
} from "../lib/tmux-lite/handshake-handler.js";
|
|
13
|
+
|
|
14
|
+
// ============================================================================
|
|
15
|
+
// WebSocket Mocks
|
|
16
|
+
// ============================================================================
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* WebSocket data interface used by the relay server
|
|
20
|
+
*/
|
|
21
|
+
export interface WebSocketData {
|
|
22
|
+
machineId: string;
|
|
23
|
+
role: "machine" | "client";
|
|
24
|
+
connectionId: string;
|
|
25
|
+
accountId: string;
|
|
26
|
+
clientIdentityId?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Mock WebSocket configuration
|
|
31
|
+
*/
|
|
32
|
+
export interface MockWebSocketConfig {
|
|
33
|
+
data?: Partial<WebSocketData>;
|
|
34
|
+
sendMock?: (data: string) => void;
|
|
35
|
+
closeMock?: (code?: number, reason?: string) => void;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Type-safe mock WebSocket interface
|
|
40
|
+
*/
|
|
41
|
+
export interface MockWebSocket {
|
|
42
|
+
data: WebSocketData;
|
|
43
|
+
send: MockFn<[string], void>;
|
|
44
|
+
close: MockFn<[number?, string?], void>;
|
|
45
|
+
readyState: number;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Mock function type with call tracking
|
|
50
|
+
*/
|
|
51
|
+
interface MockFn<Args extends unknown[] = unknown[], Return = void> {
|
|
52
|
+
(...args: Args): Return;
|
|
53
|
+
calls: Args[];
|
|
54
|
+
callCount: number;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Create a mock function that tracks calls
|
|
59
|
+
*/
|
|
60
|
+
function createMockFn<Args extends unknown[] = unknown[], Return = void>(
|
|
61
|
+
impl?: (...args: Args) => Return
|
|
62
|
+
): MockFn<Args, Return> {
|
|
63
|
+
const calls: Args[] = [];
|
|
64
|
+
const fn = ((...args: Args) => {
|
|
65
|
+
calls.push(args);
|
|
66
|
+
return impl?.(...args) as Return;
|
|
67
|
+
}) as MockFn<Args, Return>;
|
|
68
|
+
fn.calls = calls;
|
|
69
|
+
Object.defineProperty(fn, "callCount", {
|
|
70
|
+
get() {
|
|
71
|
+
return calls.length;
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
return fn;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Create a type-safe mock WebSocket for testing
|
|
79
|
+
*
|
|
80
|
+
* @example
|
|
81
|
+
* const mockWs = createMockWebSocket({
|
|
82
|
+
* data: { machineId: "test-machine", role: "machine" },
|
|
83
|
+
* });
|
|
84
|
+
* expect(mockWs.send.callCount).toBe(0);
|
|
85
|
+
*/
|
|
86
|
+
export function createMockWebSocket(config: MockWebSocketConfig = {}): MockWebSocket {
|
|
87
|
+
return {
|
|
88
|
+
data: {
|
|
89
|
+
machineId: config.data?.machineId ?? "test-machine",
|
|
90
|
+
role: config.data?.role ?? "machine",
|
|
91
|
+
connectionId: config.data?.connectionId ?? "conn-1",
|
|
92
|
+
accountId: config.data?.accountId ?? "test-account",
|
|
93
|
+
clientIdentityId: config.data?.clientIdentityId,
|
|
94
|
+
},
|
|
95
|
+
send: createMockFn<[string], void>(config.sendMock),
|
|
96
|
+
close: createMockFn<[number?, string?], void>(config.closeMock),
|
|
97
|
+
readyState: 1, // OPEN
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Cast a mock WebSocket to any type for use with functions that expect
|
|
103
|
+
* specific WebSocket types (like ServerWebSocket<WebSocketData>).
|
|
104
|
+
*
|
|
105
|
+
* This is a deliberate type assertion for testing purposes - the mock
|
|
106
|
+
* implements the minimal interface needed for the tests.
|
|
107
|
+
*
|
|
108
|
+
* @example
|
|
109
|
+
* const mockWs = asMockWs(createMockWebSocket({ data: { machineId: "test" } }));
|
|
110
|
+
* registerMachine("id", "account", "key", "kxKey", mockWs);
|
|
111
|
+
*/
|
|
112
|
+
export function asMockWs<T>(mock: MockWebSocket): T {
|
|
113
|
+
return mock as unknown as T;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ============================================================================
|
|
117
|
+
// Handler Result Type Guards
|
|
118
|
+
// ============================================================================
|
|
119
|
+
|
|
120
|
+
// Re-export types from handshake-handler for convenience
|
|
121
|
+
export type { ProcessResult, HandshakeMessage, EstablishedSession };
|
|
122
|
+
|
|
123
|
+
/** Reply result from handshake processing */
|
|
124
|
+
export type ReplyResult = Extract<ProcessResult, { type: "reply" }>;
|
|
125
|
+
|
|
126
|
+
/** Established result from handshake processing */
|
|
127
|
+
export type EstablishedResult = Extract<ProcessResult, { type: "established" }>;
|
|
128
|
+
|
|
129
|
+
/** Error result from handshake processing */
|
|
130
|
+
export type ErrorResult = Extract<ProcessResult, { type: "error" }>;
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Type guard for reply results
|
|
134
|
+
*/
|
|
135
|
+
export function isReplyResult(result: ProcessResult): result is ReplyResult {
|
|
136
|
+
return result.type === "reply";
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Type guard for established results
|
|
141
|
+
*/
|
|
142
|
+
export function isEstablishedResult(result: ProcessResult): result is EstablishedResult {
|
|
143
|
+
return result.type === "established";
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Type guard for error results
|
|
148
|
+
*/
|
|
149
|
+
export function isErrorResult(result: ProcessResult): result is ErrorResult {
|
|
150
|
+
return result.type === "error";
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Extract message data from a reply result with proper typing
|
|
155
|
+
*/
|
|
156
|
+
export function getReplyData<T>(result: ProcessResult): T {
|
|
157
|
+
if (!isReplyResult(result)) {
|
|
158
|
+
throw new Error(`Expected reply result, got: ${result.type}`);
|
|
159
|
+
}
|
|
160
|
+
return result.message.data as T;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Get error reason from an error result
|
|
165
|
+
*/
|
|
166
|
+
export function getErrorReason(result: ProcessResult): string {
|
|
167
|
+
if (!isErrorResult(result)) {
|
|
168
|
+
throw new Error(`Expected error result, got: ${result.type}`);
|
|
169
|
+
}
|
|
170
|
+
return result.reason;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ============================================================================
|
|
174
|
+
// Object Utilities
|
|
175
|
+
// ============================================================================
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Create a copy of an object with specific properties omitted.
|
|
179
|
+
* Safer alternative to `delete (obj as any).prop`.
|
|
180
|
+
*
|
|
181
|
+
* @example
|
|
182
|
+
* const authWithoutSignature = omit(clientAuth, 'identitySignature');
|
|
183
|
+
*/
|
|
184
|
+
export function omit<T extends object, K extends keyof T>(
|
|
185
|
+
obj: T,
|
|
186
|
+
...keys: K[]
|
|
187
|
+
): Omit<T, K> {
|
|
188
|
+
const result = { ...obj };
|
|
189
|
+
for (const key of keys) {
|
|
190
|
+
delete result[key];
|
|
191
|
+
}
|
|
192
|
+
return result;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Create a copy of an object with only specific properties included.
|
|
197
|
+
*
|
|
198
|
+
* @example
|
|
199
|
+
* const minimalAuth = pick(clientAuth, 'version', 'identityKey');
|
|
200
|
+
*/
|
|
201
|
+
export function pick<T extends object, K extends keyof T>(
|
|
202
|
+
obj: T,
|
|
203
|
+
...keys: K[]
|
|
204
|
+
): Pick<T, K> {
|
|
205
|
+
const result = {} as Pick<T, K>;
|
|
206
|
+
for (const key of keys) {
|
|
207
|
+
if (key in obj) {
|
|
208
|
+
result[key] = obj[key];
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return result;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// ============================================================================
|
|
215
|
+
// Private Property Access for Testing
|
|
216
|
+
// ============================================================================
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Interface for accessing RelayClient internal state in tests.
|
|
220
|
+
* This should only be used for test verification, not production code.
|
|
221
|
+
*/
|
|
222
|
+
export interface RelayClientTestAccess {
|
|
223
|
+
readKey: Buffer | null;
|
|
224
|
+
writeKey: Buffer | null;
|
|
225
|
+
handshakeState: unknown;
|
|
226
|
+
sessionKeys: unknown;
|
|
227
|
+
peerIdentityId: string | null;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Get test access to RelayClient private properties.
|
|
232
|
+
* This uses a type assertion to access private fields for testing verification.
|
|
233
|
+
*
|
|
234
|
+
* @example
|
|
235
|
+
* const testAccess = getRelayClientTestAccess(client);
|
|
236
|
+
* expect(testAccess.writeKey).toBeDefined();
|
|
237
|
+
*/
|
|
238
|
+
export function getRelayClientTestAccess(client: object): RelayClientTestAccess {
|
|
239
|
+
// This is a deliberate type assertion for test purposes
|
|
240
|
+
// RelayClient has private properties that we need to verify in tests
|
|
241
|
+
const internal = client as {
|
|
242
|
+
readKey: Buffer | null;
|
|
243
|
+
writeKey: Buffer | null;
|
|
244
|
+
handshakeState: unknown;
|
|
245
|
+
sessionKeys: unknown;
|
|
246
|
+
peerIdentityId: string | null;
|
|
247
|
+
};
|
|
248
|
+
return {
|
|
249
|
+
readKey: internal.readKey,
|
|
250
|
+
writeKey: internal.writeKey,
|
|
251
|
+
handshakeState: internal.handshakeState,
|
|
252
|
+
sessionKeys: internal.sessionKeys,
|
|
253
|
+
peerIdentityId: internal.peerIdentityId,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// ============================================================================
|
|
258
|
+
// Assertion Helpers
|
|
259
|
+
// ============================================================================
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Assert that a value is defined (not null or undefined)
|
|
263
|
+
*/
|
|
264
|
+
export function assertDefined<T>(
|
|
265
|
+
value: T | null | undefined,
|
|
266
|
+
message = "Expected value to be defined"
|
|
267
|
+
): asserts value is T {
|
|
268
|
+
if (value === null || value === undefined) {
|
|
269
|
+
throw new Error(message);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Assert that a result is a reply type
|
|
275
|
+
*/
|
|
276
|
+
export function assertReply(result: ProcessResult): asserts result is ReplyResult {
|
|
277
|
+
if (!isReplyResult(result)) {
|
|
278
|
+
throw new Error(`Expected reply result, got: ${result.type}`);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Assert that a result is an established type
|
|
284
|
+
*/
|
|
285
|
+
export function assertEstablished(result: ProcessResult): asserts result is EstablishedResult {
|
|
286
|
+
if (!isEstablishedResult(result)) {
|
|
287
|
+
throw new Error(`Expected established result, got: ${result.type}`);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Assert that a result is an error type
|
|
293
|
+
*/
|
|
294
|
+
export function assertError(result: ProcessResult): asserts result is ErrorResult {
|
|
295
|
+
if (!isErrorResult(result)) {
|
|
296
|
+
throw new Error(`Expected error result, got: ${result.type}`);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Serve Message Handler Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests that the serve command properly handles all message types
|
|
5
|
+
* from the relay server. This ensures no "Unknown message type" errors.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, expect, test } from "bun:test";
|
|
9
|
+
import type { RelayToMachineMessage } from "../../relay/protocol";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* All message types that the relay can send to a machine.
|
|
13
|
+
* The serve command's message handler must handle all of these.
|
|
14
|
+
*/
|
|
15
|
+
const RELAY_TO_MACHINE_MESSAGE_TYPES: RelayToMachineMessage["type"][] = [
|
|
16
|
+
"relay_identity",
|
|
17
|
+
"challenge",
|
|
18
|
+
"registered",
|
|
19
|
+
"access_list",
|
|
20
|
+
"access_update",
|
|
21
|
+
"client_authorized",
|
|
22
|
+
"client_revoked",
|
|
23
|
+
"client_connected",
|
|
24
|
+
"client_disconnected",
|
|
25
|
+
"data",
|
|
26
|
+
"error",
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Message types that require explicit handling (not just acknowledgment).
|
|
31
|
+
*/
|
|
32
|
+
const CRITICAL_MESSAGE_TYPES = [
|
|
33
|
+
"relay_identity", // Must respond with register_machine
|
|
34
|
+
"registered", // Must sync access list
|
|
35
|
+
"client_connected", // Must set up session
|
|
36
|
+
"client_disconnected", // Must clean up session
|
|
37
|
+
"data", // Must route to session
|
|
38
|
+
"error", // Must log/handle error
|
|
39
|
+
"access_list", // Must update local ACL
|
|
40
|
+
"access_update", // Must update local ACL
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Message types that are acknowledgments (can be no-ops).
|
|
45
|
+
*/
|
|
46
|
+
const ACKNOWLEDGMENT_MESSAGE_TYPES = [
|
|
47
|
+
"client_authorized",
|
|
48
|
+
"client_revoked",
|
|
49
|
+
"challenge", // Only used if relay sends separate challenge (usually included in relay_identity)
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
describe("serve message handler coverage", () => {
|
|
53
|
+
test("documents all relay-to-machine message types", () => {
|
|
54
|
+
// This test documents the expected message types.
|
|
55
|
+
// If the protocol adds new types, this test should be updated.
|
|
56
|
+
expect(RELAY_TO_MACHINE_MESSAGE_TYPES).toHaveLength(11);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("critical message types are a subset of all types", () => {
|
|
60
|
+
for (const type of CRITICAL_MESSAGE_TYPES) {
|
|
61
|
+
expect(RELAY_TO_MACHINE_MESSAGE_TYPES).toContain(type as RelayToMachineMessage["type"]);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("acknowledgment message types are a subset of all types", () => {
|
|
66
|
+
for (const type of ACKNOWLEDGMENT_MESSAGE_TYPES) {
|
|
67
|
+
expect(RELAY_TO_MACHINE_MESSAGE_TYPES).toContain(type as RelayToMachineMessage["type"]);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("all message types are either critical or acknowledgment", () => {
|
|
72
|
+
const allCovered = new Set([
|
|
73
|
+
...CRITICAL_MESSAGE_TYPES,
|
|
74
|
+
...ACKNOWLEDGMENT_MESSAGE_TYPES,
|
|
75
|
+
]);
|
|
76
|
+
|
|
77
|
+
for (const type of RELAY_TO_MACHINE_MESSAGE_TYPES) {
|
|
78
|
+
expect(allCovered.has(type)).toBe(true);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe("message type handling requirements", () => {
|
|
84
|
+
/**
|
|
85
|
+
* These tests document what each message type requires.
|
|
86
|
+
* They serve as living documentation for the serve command implementation.
|
|
87
|
+
*/
|
|
88
|
+
|
|
89
|
+
test("relay_identity requires challenge-response authentication", () => {
|
|
90
|
+
const requirements = {
|
|
91
|
+
type: "relay_identity",
|
|
92
|
+
requiredFields: ["publicKey", "fingerprint", "challenge"],
|
|
93
|
+
expectedResponse: "register_machine with challengeResponse",
|
|
94
|
+
securityNote: "Must verify relay trust before responding",
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
expect(requirements.requiredFields).toContain("challenge");
|
|
98
|
+
expect(requirements.expectedResponse).toContain("challengeResponse");
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("registered triggers access list sync", () => {
|
|
102
|
+
const requirements = {
|
|
103
|
+
type: "registered",
|
|
104
|
+
requiredFields: ["machineId"],
|
|
105
|
+
sideEffects: [
|
|
106
|
+
"Sync all access entries to relay",
|
|
107
|
+
"Start access list file watcher",
|
|
108
|
+
"Set up access command handler",
|
|
109
|
+
],
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
expect(requirements.sideEffects.length).toBeGreaterThan(0);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("client_authorized is an acknowledgment", () => {
|
|
116
|
+
const requirements = {
|
|
117
|
+
type: "client_authorized",
|
|
118
|
+
requiredFields: ["clientIdentityId"],
|
|
119
|
+
sideEffects: [], // No action needed - authorization was already applied locally
|
|
120
|
+
note: "Sent by relay to confirm authorize_client was processed",
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
expect(requirements.sideEffects).toHaveLength(0);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("client_revoked is an acknowledgment", () => {
|
|
127
|
+
const requirements = {
|
|
128
|
+
type: "client_revoked",
|
|
129
|
+
requiredFields: ["clientIdentityId"],
|
|
130
|
+
sideEffects: [], // No action needed - revocation was already applied locally
|
|
131
|
+
note: "Sent by relay to confirm revoke_client was processed",
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
expect(requirements.sideEffects).toHaveLength(0);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("access_list requires full ACL replacement", () => {
|
|
138
|
+
const requirements = {
|
|
139
|
+
type: "access_list",
|
|
140
|
+
requiredFields: ["entries"],
|
|
141
|
+
sideEffects: [
|
|
142
|
+
"Update local access control list with all entries",
|
|
143
|
+
],
|
|
144
|
+
note: "Sent on reconnect to sync full state",
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
expect(requirements.requiredFields).toContain("entries");
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test("access_update requires incremental ACL update", () => {
|
|
151
|
+
const requirements = {
|
|
152
|
+
type: "access_update",
|
|
153
|
+
requiredFields: ["added", "removed"],
|
|
154
|
+
sideEffects: [
|
|
155
|
+
"Add new entries to local ACL",
|
|
156
|
+
"Remove revoked entries from local ACL",
|
|
157
|
+
],
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
expect(requirements.requiredFields).toContain("added");
|
|
161
|
+
expect(requirements.requiredFields).toContain("removed");
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
describe("error scenarios", () => {
|
|
166
|
+
test("unknown message types should log warning", () => {
|
|
167
|
+
// The serve command should log unknown message types
|
|
168
|
+
// rather than silently ignoring them
|
|
169
|
+
const unknownType = "some_future_message_type";
|
|
170
|
+
expect(RELAY_TO_MACHINE_MESSAGE_TYPES).not.toContain(unknownType);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test("malformed messages should not crash handler", () => {
|
|
174
|
+
// These are examples of malformed messages that the handler
|
|
175
|
+
// should gracefully reject without crashing
|
|
176
|
+
const malformedMessages = [
|
|
177
|
+
null,
|
|
178
|
+
undefined,
|
|
179
|
+
{},
|
|
180
|
+
{ type: null },
|
|
181
|
+
{ type: 123 },
|
|
182
|
+
"not an object",
|
|
183
|
+
{ type: "registered" }, // missing machineId
|
|
184
|
+
{ type: "data" }, // missing data field
|
|
185
|
+
];
|
|
186
|
+
|
|
187
|
+
// Each should be handled without throwing
|
|
188
|
+
expect(malformedMessages.length).toBe(8);
|
|
189
|
+
});
|
|
190
|
+
});
|