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,499 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration tests for error handling during handshake
|
|
3
|
+
*
|
|
4
|
+
* Tests edge cases and error conditions.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, it, expect, beforeEach } from "bun:test";
|
|
8
|
+
import {
|
|
9
|
+
createClientHello,
|
|
10
|
+
processServerHello,
|
|
11
|
+
createClientAuth,
|
|
12
|
+
processServerAuth,
|
|
13
|
+
} from "../../handshake.js";
|
|
14
|
+
import {
|
|
15
|
+
HandshakeHandler,
|
|
16
|
+
type HandshakeMessage,
|
|
17
|
+
} from "../../../handshake-handler.js";
|
|
18
|
+
import { AccessControlList } from "../../access-control.js";
|
|
19
|
+
import { createInviteToken } from "../../invites.js";
|
|
20
|
+
import {
|
|
21
|
+
createTestIdentityPair,
|
|
22
|
+
createTestIdentity,
|
|
23
|
+
toPublicIdentity,
|
|
24
|
+
} from "../helpers/test-identities.js";
|
|
25
|
+
import {
|
|
26
|
+
createTamperFn,
|
|
27
|
+
createStaleTimestampTamperFn,
|
|
28
|
+
} from "../helpers/mock-relay.js";
|
|
29
|
+
import type { X3DHResponseMessage, X3DHInitMessage } from "../../../../../types/identity.js";
|
|
30
|
+
import {
|
|
31
|
+
isReplyResult,
|
|
32
|
+
isErrorResult,
|
|
33
|
+
getReplyData,
|
|
34
|
+
getErrorReason,
|
|
35
|
+
} from "../../../../../__tests__/test-utils.js";
|
|
36
|
+
|
|
37
|
+
describe("Error Handling Integration", () => {
|
|
38
|
+
describe("stale timestamps", () => {
|
|
39
|
+
it("should reject ClientHello with stale timestamp", async () => {
|
|
40
|
+
const { client, machine } = createTestIdentityPair();
|
|
41
|
+
const accessList = new AccessControlList();
|
|
42
|
+
accessList.addEntry(toPublicIdentity(client));
|
|
43
|
+
|
|
44
|
+
const handler = new HandshakeHandler({
|
|
45
|
+
identity: machine,
|
|
46
|
+
accessList,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// Create ClientHello with stale timestamp
|
|
50
|
+
const { message: clientHello } = createClientHello();
|
|
51
|
+
const staleClientHello: X3DHInitMessage = {
|
|
52
|
+
...clientHello,
|
|
53
|
+
timestamp: Date.now() - 10 * 60 * 1000, // 10 minutes ago
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const result = await handler.processMessage("conn-1", {
|
|
57
|
+
type: "handshake",
|
|
58
|
+
phase: "client_hello",
|
|
59
|
+
data: staleClientHello,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
expect(result.type).toBe("error");
|
|
63
|
+
expect(getErrorReason(result)).toContain("Invalid ClientHello");
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("should reject ClientHello with future timestamp", async () => {
|
|
67
|
+
const { client, machine } = createTestIdentityPair();
|
|
68
|
+
const accessList = new AccessControlList();
|
|
69
|
+
|
|
70
|
+
const handler = new HandshakeHandler({
|
|
71
|
+
identity: machine,
|
|
72
|
+
accessList,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// Create ClientHello with future timestamp
|
|
76
|
+
const { message: clientHello } = createClientHello();
|
|
77
|
+
const futureClientHello: X3DHInitMessage = {
|
|
78
|
+
...clientHello,
|
|
79
|
+
timestamp: Date.now() + 10 * 60 * 1000, // 10 minutes in future
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const result = await handler.processMessage("conn-1", {
|
|
83
|
+
type: "handshake",
|
|
84
|
+
phase: "client_hello",
|
|
85
|
+
data: futureClientHello,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
expect(result.type).toBe("error");
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe("invalid protocol version", () => {
|
|
93
|
+
it("should reject ClientHello with wrong version", async () => {
|
|
94
|
+
const { machine } = createTestIdentityPair();
|
|
95
|
+
const accessList = new AccessControlList();
|
|
96
|
+
|
|
97
|
+
const handler = new HandshakeHandler({
|
|
98
|
+
identity: machine,
|
|
99
|
+
accessList,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
const { message: clientHello } = createClientHello();
|
|
103
|
+
const wrongVersion = {
|
|
104
|
+
...clientHello,
|
|
105
|
+
version: 99,
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const result = await handler.processMessage("conn-1", {
|
|
109
|
+
type: "handshake",
|
|
110
|
+
phase: "client_hello",
|
|
111
|
+
data: wrongVersion,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
expect(result.type).toBe("error");
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("should reject ServerHello with wrong version (client side)", () => {
|
|
118
|
+
const { state, message: clientHello } = createClientHello();
|
|
119
|
+
|
|
120
|
+
// Create a fake ServerHello with wrong version
|
|
121
|
+
// Note: We use type assertion because we're deliberately testing invalid input
|
|
122
|
+
// that doesn't conform to the expected type (version 99 is not a valid version)
|
|
123
|
+
const wrongVersionResponse = {
|
|
124
|
+
version: 99,
|
|
125
|
+
identityKey: "fake",
|
|
126
|
+
keyExchangeKey: "fake",
|
|
127
|
+
ephemeralKey: "fake",
|
|
128
|
+
signedPreKey: "fake",
|
|
129
|
+
preKeySignature: "fake",
|
|
130
|
+
serverNonce: "fake",
|
|
131
|
+
timestamp: Date.now(),
|
|
132
|
+
} as unknown as X3DHResponseMessage;
|
|
133
|
+
|
|
134
|
+
const result = processServerHello(state, wrongVersionResponse);
|
|
135
|
+
expect(result).toBeNull();
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
describe("tampered signatures", () => {
|
|
140
|
+
it("should reject ServerHello with invalid pre-key signature", async () => {
|
|
141
|
+
const { client, machine } = createTestIdentityPair();
|
|
142
|
+
const accessList = new AccessControlList();
|
|
143
|
+
accessList.addEntry(toPublicIdentity(client));
|
|
144
|
+
|
|
145
|
+
const handler = new HandshakeHandler({
|
|
146
|
+
identity: machine,
|
|
147
|
+
accessList,
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// Start handshake
|
|
151
|
+
const { state, message: clientHello } = createClientHello();
|
|
152
|
+
|
|
153
|
+
const result1 = await handler.processMessage("conn-1", {
|
|
154
|
+
type: "handshake",
|
|
155
|
+
phase: "client_hello",
|
|
156
|
+
data: clientHello,
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
expect(result1.type).toBe("reply");
|
|
160
|
+
if (!isReplyResult(result1)) throw new Error("Expected reply");
|
|
161
|
+
const serverHello = getReplyData<X3DHResponseMessage>(result1);
|
|
162
|
+
|
|
163
|
+
// Tamper with the pre-key signature
|
|
164
|
+
const tamperedServerHello: X3DHResponseMessage = {
|
|
165
|
+
...serverHello,
|
|
166
|
+
preKeySignature: Buffer.from(
|
|
167
|
+
new Uint8Array(64).fill(0)
|
|
168
|
+
).toString("base64"),
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
// Client should reject
|
|
172
|
+
const clientState = processServerHello(state, tamperedServerHello);
|
|
173
|
+
expect(clientState).toBeNull();
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("should reject ClientAuth with invalid identity proof", async () => {
|
|
177
|
+
const { client, machine } = createTestIdentityPair();
|
|
178
|
+
const accessList = new AccessControlList();
|
|
179
|
+
accessList.addEntry(toPublicIdentity(client));
|
|
180
|
+
|
|
181
|
+
const handler = new HandshakeHandler({
|
|
182
|
+
identity: machine,
|
|
183
|
+
accessList,
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
// Complete ClientHello -> ServerHello
|
|
187
|
+
const { state: state1, message: clientHello } = createClientHello();
|
|
188
|
+
const result1 = await handler.processMessage("conn-1", {
|
|
189
|
+
type: "handshake",
|
|
190
|
+
phase: "client_hello",
|
|
191
|
+
data: clientHello,
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
if (!isReplyResult(result1)) throw new Error("Expected reply");
|
|
195
|
+
const serverHello = getReplyData<X3DHResponseMessage>(result1);
|
|
196
|
+
const state2 = processServerHello(state1, serverHello);
|
|
197
|
+
expect(state2).not.toBeNull();
|
|
198
|
+
|
|
199
|
+
// Create ClientAuth and tamper with identity proof
|
|
200
|
+
const { message: clientAuth } = createClientAuth(state2!, client, {
|
|
201
|
+
type: "access_list",
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
const tamperedClientAuth = {
|
|
205
|
+
...clientAuth,
|
|
206
|
+
identityProof: Buffer.from(new Uint8Array(32).fill(0)).toString(
|
|
207
|
+
"base64"
|
|
208
|
+
),
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
const result2 = await handler.processMessage("conn-1", {
|
|
212
|
+
type: "handshake",
|
|
213
|
+
phase: "client_auth",
|
|
214
|
+
data: tamperedClientAuth,
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
expect(result2.type).toBe("error");
|
|
218
|
+
expect(getErrorReason(result2)).toContain("Invalid ClientAuth");
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
describe("invalid key formats", () => {
|
|
223
|
+
it("should reject ClientHello with invalid ephemeral key", async () => {
|
|
224
|
+
const { machine } = createTestIdentityPair();
|
|
225
|
+
const accessList = new AccessControlList();
|
|
226
|
+
|
|
227
|
+
const handler = new HandshakeHandler({
|
|
228
|
+
identity: machine,
|
|
229
|
+
accessList,
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
const { message: clientHello } = createClientHello();
|
|
233
|
+
const invalidKey = {
|
|
234
|
+
...clientHello,
|
|
235
|
+
ephemeralKey: "not-valid-base64!!!",
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
const result = await handler.processMessage("conn-1", {
|
|
239
|
+
type: "handshake",
|
|
240
|
+
phase: "client_hello",
|
|
241
|
+
data: invalidKey,
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
expect(result.type).toBe("error");
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it("should reject ClientHello with all-zero ephemeral key", async () => {
|
|
248
|
+
const { machine } = createTestIdentityPair();
|
|
249
|
+
const accessList = new AccessControlList();
|
|
250
|
+
|
|
251
|
+
const handler = new HandshakeHandler({
|
|
252
|
+
identity: machine,
|
|
253
|
+
accessList,
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
const { message: clientHello } = createClientHello();
|
|
257
|
+
const zeroKey = {
|
|
258
|
+
...clientHello,
|
|
259
|
+
ephemeralKey: Buffer.from(new Uint8Array(32).fill(0)).toString(
|
|
260
|
+
"base64"
|
|
261
|
+
),
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
const result = await handler.processMessage("conn-1", {
|
|
265
|
+
type: "handshake",
|
|
266
|
+
phase: "client_hello",
|
|
267
|
+
data: zeroKey,
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
expect(result.type).toBe("error");
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
describe("wrong identity", () => {
|
|
275
|
+
it("should reject ClientAuth with wrong identity key", async () => {
|
|
276
|
+
const { client, machine } = createTestIdentityPair();
|
|
277
|
+
const imposter = createTestIdentity("Imposter");
|
|
278
|
+
const accessList = new AccessControlList();
|
|
279
|
+
accessList.addEntry(toPublicIdentity(client)); // Only real client authorized
|
|
280
|
+
|
|
281
|
+
const handler = new HandshakeHandler({
|
|
282
|
+
identity: machine,
|
|
283
|
+
accessList,
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
// Start handshake
|
|
287
|
+
const { state: state1, message: clientHello } = createClientHello();
|
|
288
|
+
const result1 = await handler.processMessage("conn-1", {
|
|
289
|
+
type: "handshake",
|
|
290
|
+
phase: "client_hello",
|
|
291
|
+
data: clientHello,
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
if (!isReplyResult(result1)) throw new Error("Expected reply");
|
|
295
|
+
const serverHello = getReplyData<X3DHResponseMessage>(result1);
|
|
296
|
+
const state2 = processServerHello(state1, serverHello);
|
|
297
|
+
expect(state2).not.toBeNull();
|
|
298
|
+
|
|
299
|
+
// Imposter tries to use client's handshake state with their own identity
|
|
300
|
+
const { message: imposterAuth } = createClientAuth(state2!, imposter, {
|
|
301
|
+
type: "access_list",
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
const result2 = await handler.processMessage("conn-1", {
|
|
305
|
+
type: "handshake",
|
|
306
|
+
phase: "client_auth",
|
|
307
|
+
data: imposterAuth,
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
// Should fail - either invalid proof or not in access list
|
|
311
|
+
if (result2.type === "error") {
|
|
312
|
+
expect(result2.reason).toBeDefined();
|
|
313
|
+
} else if (result2.type === "established") {
|
|
314
|
+
// If it somehow gets to authorization, imposter shouldn't be authorized
|
|
315
|
+
expect(result2.session.peerIdentityId).toBe(imposter.id);
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
describe("out-of-order messages", () => {
|
|
321
|
+
it("should reject ClientAuth before ClientHello", async () => {
|
|
322
|
+
const { client, machine } = createTestIdentityPair();
|
|
323
|
+
const accessList = new AccessControlList();
|
|
324
|
+
|
|
325
|
+
const handler = new HandshakeHandler({
|
|
326
|
+
identity: machine,
|
|
327
|
+
accessList,
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
// Try to send ClientAuth without first sending ClientHello
|
|
331
|
+
const { state, message: clientHello } = createClientHello();
|
|
332
|
+
|
|
333
|
+
// Manually advance state to create a ClientAuth
|
|
334
|
+
// This requires faking the server response
|
|
335
|
+
// For simplicity, just test that handler rejects unknown connection
|
|
336
|
+
const result = await handler.processMessage("conn-1", {
|
|
337
|
+
type: "handshake",
|
|
338
|
+
phase: "client_auth",
|
|
339
|
+
data: {
|
|
340
|
+
version: 1,
|
|
341
|
+
identityKey: "fake",
|
|
342
|
+
keyExchangeKey: "fake",
|
|
343
|
+
identityProof: "fake",
|
|
344
|
+
authorization: { type: "access_list" },
|
|
345
|
+
},
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
expect(result.type).toBe("error");
|
|
349
|
+
expect(getErrorReason(result)).toContain(
|
|
350
|
+
"No handshake in progress"
|
|
351
|
+
);
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
it("should reject duplicate ClientHello", async () => {
|
|
355
|
+
const { machine } = createTestIdentityPair();
|
|
356
|
+
const accessList = new AccessControlList();
|
|
357
|
+
|
|
358
|
+
const handler = new HandshakeHandler({
|
|
359
|
+
identity: machine,
|
|
360
|
+
accessList,
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
// Send first ClientHello
|
|
364
|
+
const { message: clientHello1 } = createClientHello();
|
|
365
|
+
const result1 = await handler.processMessage("conn-1", {
|
|
366
|
+
type: "handshake",
|
|
367
|
+
phase: "client_hello",
|
|
368
|
+
data: clientHello1,
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
expect(result1.type).toBe("reply");
|
|
372
|
+
|
|
373
|
+
// Send second ClientHello on same connection
|
|
374
|
+
const { message: clientHello2 } = createClientHello();
|
|
375
|
+
const result2 = await handler.processMessage("conn-1", {
|
|
376
|
+
type: "handshake",
|
|
377
|
+
phase: "client_hello",
|
|
378
|
+
data: clientHello2,
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
// Should restart handshake (replaces state)
|
|
382
|
+
expect(result2.type).toBe("reply");
|
|
383
|
+
expect(handler.activeHandshakes).toBe(1);
|
|
384
|
+
});
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
describe("handshake timeout", () => {
|
|
388
|
+
it("should clean up incomplete handshakes after timeout", async () => {
|
|
389
|
+
const { machine } = createTestIdentityPair();
|
|
390
|
+
const accessList = new AccessControlList();
|
|
391
|
+
|
|
392
|
+
const handler = new HandshakeHandler({
|
|
393
|
+
identity: machine,
|
|
394
|
+
accessList,
|
|
395
|
+
handshakeTimeoutMs: 100, // 100ms timeout
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
// Start handshake but don't complete
|
|
399
|
+
const { message: clientHello } = createClientHello();
|
|
400
|
+
await handler.processMessage("conn-1", {
|
|
401
|
+
type: "handshake",
|
|
402
|
+
phase: "client_hello",
|
|
403
|
+
data: clientHello,
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
expect(handler.hasActiveHandshake("conn-1")).toBe(true);
|
|
407
|
+
|
|
408
|
+
// Wait for timeout
|
|
409
|
+
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
410
|
+
|
|
411
|
+
// Handshake should be cleaned up
|
|
412
|
+
expect(handler.hasActiveHandshake("conn-1")).toBe(false);
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
it("should not timeout completed handshakes", async () => {
|
|
416
|
+
const { client, machine } = createTestIdentityPair();
|
|
417
|
+
const accessList = new AccessControlList();
|
|
418
|
+
accessList.addEntry(toPublicIdentity(client));
|
|
419
|
+
|
|
420
|
+
const handler = new HandshakeHandler({
|
|
421
|
+
identity: machine,
|
|
422
|
+
accessList,
|
|
423
|
+
handshakeTimeoutMs: 100,
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
// Complete handshake quickly
|
|
427
|
+
const { state: state1, message: clientHello } = createClientHello();
|
|
428
|
+
const result1 = await handler.processMessage("conn-1", {
|
|
429
|
+
type: "handshake",
|
|
430
|
+
phase: "client_hello",
|
|
431
|
+
data: clientHello,
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
if (!isReplyResult(result1)) throw new Error("Expected reply");
|
|
435
|
+
const serverHello = getReplyData<X3DHResponseMessage>(result1);
|
|
436
|
+
const state2 = processServerHello(state1, serverHello);
|
|
437
|
+
|
|
438
|
+
const { message: clientAuth } = createClientAuth(state2!, client, {
|
|
439
|
+
type: "access_list",
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
const result2 = await handler.processMessage("conn-1", {
|
|
443
|
+
type: "handshake",
|
|
444
|
+
phase: "client_auth",
|
|
445
|
+
data: clientAuth,
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
expect(result2.type).toBe("established");
|
|
449
|
+
|
|
450
|
+
// Wait past timeout
|
|
451
|
+
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
452
|
+
|
|
453
|
+
// No crash - completed handshakes are cleaned up immediately
|
|
454
|
+
expect(handler.activeHandshakes).toBe(0);
|
|
455
|
+
});
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
describe("unexpected message phases", () => {
|
|
459
|
+
it("should reject unknown handshake phase", async () => {
|
|
460
|
+
const { machine } = createTestIdentityPair();
|
|
461
|
+
const accessList = new AccessControlList();
|
|
462
|
+
|
|
463
|
+
const handler = new HandshakeHandler({
|
|
464
|
+
identity: machine,
|
|
465
|
+
accessList,
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
// Note: We use type assertion here because we're deliberately testing
|
|
469
|
+
// that invalid phase values are properly rejected
|
|
470
|
+
const result = await handler.processMessage("conn-1", {
|
|
471
|
+
type: "handshake",
|
|
472
|
+
phase: "unknown_phase" as "client_hello",
|
|
473
|
+
data: {},
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
expect(result.type).toBe("error");
|
|
477
|
+
expect(getErrorReason(result)).toContain("Unexpected handshake phase");
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
it("should reject server-side phases sent by client", async () => {
|
|
481
|
+
const { machine } = createTestIdentityPair();
|
|
482
|
+
const accessList = new AccessControlList();
|
|
483
|
+
|
|
484
|
+
const handler = new HandshakeHandler({
|
|
485
|
+
identity: machine,
|
|
486
|
+
accessList,
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
// Client shouldn't send server_hello
|
|
490
|
+
const result = await handler.processMessage("conn-1", {
|
|
491
|
+
type: "handshake",
|
|
492
|
+
phase: "server_hello",
|
|
493
|
+
data: {},
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
expect(result.type).toBe("error");
|
|
497
|
+
});
|
|
498
|
+
});
|
|
499
|
+
});
|