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.
Files changed (318) hide show
  1. package/.claude/settings.local.json +21 -0
  2. package/.gitspace/bundle.json +50 -0
  3. package/.gitspace/select/01-status.sh +40 -0
  4. package/.gitspace/setup/01-install-deps.sh +12 -0
  5. package/.gitspace/setup/02-typecheck.sh +16 -0
  6. package/AGENTS.md +439 -0
  7. package/CLAUDE.md +1 -0
  8. package/LICENSE +25 -0
  9. package/README.md +607 -0
  10. package/bin/gssh +62 -0
  11. package/bun.lock +647 -0
  12. package/docs/CONNECTION.md +623 -0
  13. package/docs/GATEWAY-WORKER.md +319 -0
  14. package/docs/GETTING-STARTED.md +448 -0
  15. package/docs/GITSPACE-PLATFORM.md +1819 -0
  16. package/docs/INFRASTRUCTURE.md +1347 -0
  17. package/docs/PROTOCOL.md +619 -0
  18. package/docs/QUICKSTART.md +174 -0
  19. package/docs/RELAY.md +327 -0
  20. package/docs/REMOTE-DESIGN.md +549 -0
  21. package/docs/ROADMAP.md +564 -0
  22. package/docs/SITE_DOCS_FIGMA_MAKE.md +1167 -0
  23. package/docs/STACK-DESIGN.md +588 -0
  24. package/docs/UNIFIED_ARCHITECTURE.md +292 -0
  25. package/experiments/pty-benchmark.ts +148 -0
  26. package/experiments/pty-latency.ts +100 -0
  27. package/experiments/router/client.ts +199 -0
  28. package/experiments/router/protocol.ts +74 -0
  29. package/experiments/router/router.ts +217 -0
  30. package/experiments/router/session.ts +180 -0
  31. package/experiments/router/test.ts +133 -0
  32. package/experiments/socket-bandwidth.ts +77 -0
  33. package/homebrew/gitspace.rb +45 -0
  34. package/landing-page/ATTRIBUTIONS.md +3 -0
  35. package/landing-page/README.md +11 -0
  36. package/landing-page/bun.lock +801 -0
  37. package/landing-page/guidelines/Guidelines.md +61 -0
  38. package/landing-page/index.html +37 -0
  39. package/landing-page/package.json +90 -0
  40. package/landing-page/postcss.config.mjs +15 -0
  41. package/landing-page/public/_redirects +1 -0
  42. package/landing-page/public/favicon.png +0 -0
  43. package/landing-page/src/app/App.tsx +53 -0
  44. package/landing-page/src/app/components/figma/ImageWithFallback.tsx +27 -0
  45. package/landing-page/src/app/components/ui/accordion.tsx +66 -0
  46. package/landing-page/src/app/components/ui/alert-dialog.tsx +157 -0
  47. package/landing-page/src/app/components/ui/alert.tsx +66 -0
  48. package/landing-page/src/app/components/ui/aspect-ratio.tsx +11 -0
  49. package/landing-page/src/app/components/ui/avatar.tsx +53 -0
  50. package/landing-page/src/app/components/ui/badge.tsx +46 -0
  51. package/landing-page/src/app/components/ui/breadcrumb.tsx +109 -0
  52. package/landing-page/src/app/components/ui/button.tsx +57 -0
  53. package/landing-page/src/app/components/ui/calendar.tsx +75 -0
  54. package/landing-page/src/app/components/ui/card.tsx +92 -0
  55. package/landing-page/src/app/components/ui/carousel.tsx +241 -0
  56. package/landing-page/src/app/components/ui/chart.tsx +353 -0
  57. package/landing-page/src/app/components/ui/checkbox.tsx +32 -0
  58. package/landing-page/src/app/components/ui/collapsible.tsx +33 -0
  59. package/landing-page/src/app/components/ui/command.tsx +177 -0
  60. package/landing-page/src/app/components/ui/context-menu.tsx +252 -0
  61. package/landing-page/src/app/components/ui/dialog.tsx +135 -0
  62. package/landing-page/src/app/components/ui/drawer.tsx +132 -0
  63. package/landing-page/src/app/components/ui/dropdown-menu.tsx +257 -0
  64. package/landing-page/src/app/components/ui/form.tsx +168 -0
  65. package/landing-page/src/app/components/ui/hover-card.tsx +44 -0
  66. package/landing-page/src/app/components/ui/input-otp.tsx +77 -0
  67. package/landing-page/src/app/components/ui/input.tsx +21 -0
  68. package/landing-page/src/app/components/ui/label.tsx +24 -0
  69. package/landing-page/src/app/components/ui/menubar.tsx +276 -0
  70. package/landing-page/src/app/components/ui/navigation-menu.tsx +168 -0
  71. package/landing-page/src/app/components/ui/pagination.tsx +127 -0
  72. package/landing-page/src/app/components/ui/popover.tsx +48 -0
  73. package/landing-page/src/app/components/ui/progress.tsx +31 -0
  74. package/landing-page/src/app/components/ui/radio-group.tsx +45 -0
  75. package/landing-page/src/app/components/ui/resizable.tsx +56 -0
  76. package/landing-page/src/app/components/ui/scroll-area.tsx +58 -0
  77. package/landing-page/src/app/components/ui/select.tsx +189 -0
  78. package/landing-page/src/app/components/ui/separator.tsx +28 -0
  79. package/landing-page/src/app/components/ui/sheet.tsx +139 -0
  80. package/landing-page/src/app/components/ui/sidebar.tsx +726 -0
  81. package/landing-page/src/app/components/ui/skeleton.tsx +13 -0
  82. package/landing-page/src/app/components/ui/slider.tsx +63 -0
  83. package/landing-page/src/app/components/ui/sonner.tsx +25 -0
  84. package/landing-page/src/app/components/ui/switch.tsx +31 -0
  85. package/landing-page/src/app/components/ui/table.tsx +116 -0
  86. package/landing-page/src/app/components/ui/tabs.tsx +66 -0
  87. package/landing-page/src/app/components/ui/textarea.tsx +18 -0
  88. package/landing-page/src/app/components/ui/toggle-group.tsx +73 -0
  89. package/landing-page/src/app/components/ui/toggle.tsx +47 -0
  90. package/landing-page/src/app/components/ui/tooltip.tsx +61 -0
  91. package/landing-page/src/app/components/ui/use-mobile.ts +21 -0
  92. package/landing-page/src/app/components/ui/utils.ts +6 -0
  93. package/landing-page/src/components/docs/DocsContent.tsx +718 -0
  94. package/landing-page/src/components/docs/DocsSidebar.tsx +84 -0
  95. package/landing-page/src/components/landing/CTA.tsx +59 -0
  96. package/landing-page/src/components/landing/Comparison.tsx +84 -0
  97. package/landing-page/src/components/landing/FaultyTerminal.tsx +424 -0
  98. package/landing-page/src/components/landing/Features.tsx +201 -0
  99. package/landing-page/src/components/landing/Hero.tsx +142 -0
  100. package/landing-page/src/components/landing/Pricing.tsx +140 -0
  101. package/landing-page/src/components/landing/Roadmap.tsx +86 -0
  102. package/landing-page/src/components/landing/Security.tsx +81 -0
  103. package/landing-page/src/components/landing/TerminalWindow.tsx +27 -0
  104. package/landing-page/src/components/landing/UseCases.tsx +55 -0
  105. package/landing-page/src/components/landing/Workflow.tsx +101 -0
  106. package/landing-page/src/components/layout/DashboardNavbar.tsx +37 -0
  107. package/landing-page/src/components/layout/Footer.tsx +55 -0
  108. package/landing-page/src/components/layout/LandingNavbar.tsx +82 -0
  109. package/landing-page/src/components/ui/badge.tsx +39 -0
  110. package/landing-page/src/components/ui/breadcrumb.tsx +115 -0
  111. package/landing-page/src/components/ui/button.tsx +57 -0
  112. package/landing-page/src/components/ui/card.tsx +79 -0
  113. package/landing-page/src/components/ui/mock-terminal.tsx +68 -0
  114. package/landing-page/src/components/ui/separator.tsx +28 -0
  115. package/landing-page/src/lib/utils.ts +6 -0
  116. package/landing-page/src/main.tsx +10 -0
  117. package/landing-page/src/pages/Dashboard.tsx +133 -0
  118. package/landing-page/src/pages/DocsPage.tsx +79 -0
  119. package/landing-page/src/pages/LandingPage.tsx +31 -0
  120. package/landing-page/src/pages/TerminalView.tsx +106 -0
  121. package/landing-page/src/styles/fonts.css +0 -0
  122. package/landing-page/src/styles/index.css +3 -0
  123. package/landing-page/src/styles/tailwind.css +4 -0
  124. package/landing-page/src/styles/theme.css +181 -0
  125. package/landing-page/vite.config.ts +19 -0
  126. package/npm/darwin-arm64/bin/gssh +0 -0
  127. package/npm/darwin-arm64/package.json +20 -0
  128. package/package.json +74 -0
  129. package/scripts/build.ts +284 -0
  130. package/scripts/release.ts +140 -0
  131. package/src/__tests__/test-utils.ts +298 -0
  132. package/src/commands/__tests__/serve-messages.test.ts +190 -0
  133. package/src/commands/access.ts +298 -0
  134. package/src/commands/add.ts +452 -0
  135. package/src/commands/auth.ts +364 -0
  136. package/src/commands/connect.ts +287 -0
  137. package/src/commands/directory.ts +16 -0
  138. package/src/commands/host.ts +396 -0
  139. package/src/commands/identity.ts +184 -0
  140. package/src/commands/list.ts +200 -0
  141. package/src/commands/relay.ts +315 -0
  142. package/src/commands/remove.ts +241 -0
  143. package/src/commands/serve.ts +1493 -0
  144. package/src/commands/share.ts +456 -0
  145. package/src/commands/status.ts +125 -0
  146. package/src/commands/switch.ts +353 -0
  147. package/src/commands/tmux.ts +317 -0
  148. package/src/core/__tests__/access.test.ts +240 -0
  149. package/src/core/access.ts +277 -0
  150. package/src/core/bundle.ts +342 -0
  151. package/src/core/config.ts +510 -0
  152. package/src/core/git.ts +317 -0
  153. package/src/core/github.ts +151 -0
  154. package/src/core/identity.ts +631 -0
  155. package/src/core/linear.ts +225 -0
  156. package/src/core/shell.ts +161 -0
  157. package/src/core/trusted-relays.ts +315 -0
  158. package/src/index.ts +821 -0
  159. package/src/lib/remote-session/index.ts +7 -0
  160. package/src/lib/remote-session/protocol.ts +267 -0
  161. package/src/lib/remote-session/session-handler.ts +581 -0
  162. package/src/lib/remote-session/workspace-scanner.ts +167 -0
  163. package/src/lib/tmux-lite/README.md +81 -0
  164. package/src/lib/tmux-lite/cli.ts +796 -0
  165. package/src/lib/tmux-lite/crypto/__tests__/helpers/handshake-runner.ts +349 -0
  166. package/src/lib/tmux-lite/crypto/__tests__/helpers/mock-relay.ts +291 -0
  167. package/src/lib/tmux-lite/crypto/__tests__/helpers/test-identities.ts +142 -0
  168. package/src/lib/tmux-lite/crypto/__tests__/integration/authorization.integration.test.ts +339 -0
  169. package/src/lib/tmux-lite/crypto/__tests__/integration/e2e-communication.integration.test.ts +477 -0
  170. package/src/lib/tmux-lite/crypto/__tests__/integration/error-handling.integration.test.ts +499 -0
  171. package/src/lib/tmux-lite/crypto/__tests__/integration/handshake.integration.test.ts +371 -0
  172. package/src/lib/tmux-lite/crypto/__tests__/integration/security.integration.test.ts +573 -0
  173. package/src/lib/tmux-lite/crypto/access-control.test.ts +512 -0
  174. package/src/lib/tmux-lite/crypto/access-control.ts +320 -0
  175. package/src/lib/tmux-lite/crypto/frames.test.ts +262 -0
  176. package/src/lib/tmux-lite/crypto/frames.ts +141 -0
  177. package/src/lib/tmux-lite/crypto/handshake.ts +894 -0
  178. package/src/lib/tmux-lite/crypto/identity.test.ts +220 -0
  179. package/src/lib/tmux-lite/crypto/identity.ts +286 -0
  180. package/src/lib/tmux-lite/crypto/index.ts +51 -0
  181. package/src/lib/tmux-lite/crypto/invites.test.ts +381 -0
  182. package/src/lib/tmux-lite/crypto/invites.ts +215 -0
  183. package/src/lib/tmux-lite/crypto/keyexchange.ts +435 -0
  184. package/src/lib/tmux-lite/crypto/keys.test.ts +58 -0
  185. package/src/lib/tmux-lite/crypto/keys.ts +47 -0
  186. package/src/lib/tmux-lite/crypto/secretbox.test.ts +169 -0
  187. package/src/lib/tmux-lite/crypto/secretbox.ts +124 -0
  188. package/src/lib/tmux-lite/handshake-handler.ts +451 -0
  189. package/src/lib/tmux-lite/protocol.test.ts +307 -0
  190. package/src/lib/tmux-lite/protocol.ts +266 -0
  191. package/src/lib/tmux-lite/relay-client.ts +506 -0
  192. package/src/lib/tmux-lite/server.ts +1250 -0
  193. package/src/lib/tmux-lite/shell-integration.sh +37 -0
  194. package/src/lib/tmux-lite/terminal-queries.test.ts +54 -0
  195. package/src/lib/tmux-lite/terminal-queries.ts +49 -0
  196. package/src/relay/__tests__/e2e-flow.test.ts +1284 -0
  197. package/src/relay/__tests__/helpers/auth.ts +354 -0
  198. package/src/relay/__tests__/helpers/ports.ts +51 -0
  199. package/src/relay/__tests__/protocol-validation.test.ts +265 -0
  200. package/src/relay/authorization.ts +303 -0
  201. package/src/relay/embedded-assets.generated.d.ts +15 -0
  202. package/src/relay/identity.ts +352 -0
  203. package/src/relay/index.ts +57 -0
  204. package/src/relay/pipes.test.ts +427 -0
  205. package/src/relay/pipes.ts +195 -0
  206. package/src/relay/protocol.ts +804 -0
  207. package/src/relay/registries.test.ts +437 -0
  208. package/src/relay/registries.ts +593 -0
  209. package/src/relay/server.test.ts +1323 -0
  210. package/src/relay/server.ts +1092 -0
  211. package/src/relay/signing.ts +238 -0
  212. package/src/relay/types.ts +69 -0
  213. package/src/serve/client-session-manager.ts +622 -0
  214. package/src/serve/daemon.ts +497 -0
  215. package/src/serve/pty-session.ts +236 -0
  216. package/src/serve/types.ts +169 -0
  217. package/src/shared/components/Flow.tsx +453 -0
  218. package/src/shared/components/Flow.tui.tsx +343 -0
  219. package/src/shared/components/Flow.web.tsx +442 -0
  220. package/src/shared/components/Inbox.tsx +446 -0
  221. package/src/shared/components/Inbox.tui.tsx +262 -0
  222. package/src/shared/components/Inbox.web.tsx +329 -0
  223. package/src/shared/components/MachineList.tsx +187 -0
  224. package/src/shared/components/MachineList.tui.tsx +161 -0
  225. package/src/shared/components/MachineList.web.tsx +210 -0
  226. package/src/shared/components/ProjectList.tsx +176 -0
  227. package/src/shared/components/ProjectList.tui.tsx +109 -0
  228. package/src/shared/components/ProjectList.web.tsx +143 -0
  229. package/src/shared/components/SpacesBrowser.tsx +332 -0
  230. package/src/shared/components/SpacesBrowser.tui.tsx +163 -0
  231. package/src/shared/components/SpacesBrowser.web.tsx +221 -0
  232. package/src/shared/components/index.ts +103 -0
  233. package/src/shared/hooks/index.ts +16 -0
  234. package/src/shared/hooks/useNavigation.ts +226 -0
  235. package/src/shared/index.ts +122 -0
  236. package/src/shared/providers/LocalMachineProvider.ts +425 -0
  237. package/src/shared/providers/MachineProvider.ts +165 -0
  238. package/src/shared/providers/RemoteMachineProvider.ts +444 -0
  239. package/src/shared/providers/index.ts +26 -0
  240. package/src/shared/types.ts +145 -0
  241. package/src/tui/adapters.ts +120 -0
  242. package/src/tui/app.tsx +1816 -0
  243. package/src/tui/components/Terminal.tsx +580 -0
  244. package/src/tui/hooks/index.ts +35 -0
  245. package/src/tui/hooks/useAppState.ts +314 -0
  246. package/src/tui/hooks/useDaemonStatus.ts +174 -0
  247. package/src/tui/hooks/useInboxTUI.ts +113 -0
  248. package/src/tui/hooks/useRemoteMachines.ts +209 -0
  249. package/src/tui/index.ts +24 -0
  250. package/src/tui/state.ts +299 -0
  251. package/src/tui/terminal-bracketed-paste.test.ts +45 -0
  252. package/src/tui/terminal-bracketed-paste.ts +47 -0
  253. package/src/types/bundle.ts +112 -0
  254. package/src/types/config.ts +89 -0
  255. package/src/types/errors.ts +206 -0
  256. package/src/types/identity.ts +284 -0
  257. package/src/types/workspace-fuzzy.ts +49 -0
  258. package/src/types/workspace.ts +151 -0
  259. package/src/utils/bun-socket-writer.ts +80 -0
  260. package/src/utils/deps.ts +127 -0
  261. package/src/utils/fuzzy-match.ts +125 -0
  262. package/src/utils/logger.ts +127 -0
  263. package/src/utils/markdown.ts +254 -0
  264. package/src/utils/onboarding.ts +229 -0
  265. package/src/utils/prompts.ts +114 -0
  266. package/src/utils/run-commands.ts +112 -0
  267. package/src/utils/run-scripts.ts +142 -0
  268. package/src/utils/sanitize.ts +98 -0
  269. package/src/utils/secrets.ts +122 -0
  270. package/src/utils/shell-escape.ts +40 -0
  271. package/src/utils/utf8.ts +79 -0
  272. package/src/utils/workspace-state.ts +47 -0
  273. package/src/web/README.md +73 -0
  274. package/src/web/bun.lock +575 -0
  275. package/src/web/eslint.config.js +23 -0
  276. package/src/web/index.html +16 -0
  277. package/src/web/package.json +37 -0
  278. package/src/web/public/vite.svg +1 -0
  279. package/src/web/src/App.tsx +604 -0
  280. package/src/web/src/assets/react.svg +1 -0
  281. package/src/web/src/components/Terminal.tsx +207 -0
  282. package/src/web/src/hooks/useRelayConnection.ts +224 -0
  283. package/src/web/src/hooks/useTerminal.ts +699 -0
  284. package/src/web/src/index.css +55 -0
  285. package/src/web/src/lib/crypto/__tests__/web-terminal.test.ts +1158 -0
  286. package/src/web/src/lib/crypto/frames.ts +205 -0
  287. package/src/web/src/lib/crypto/handshake.ts +396 -0
  288. package/src/web/src/lib/crypto/identity.ts +128 -0
  289. package/src/web/src/lib/crypto/keyexchange.ts +246 -0
  290. package/src/web/src/lib/crypto/relay-signing.ts +53 -0
  291. package/src/web/src/lib/invite.ts +58 -0
  292. package/src/web/src/lib/storage/identity-store.ts +94 -0
  293. package/src/web/src/main.tsx +10 -0
  294. package/src/web/src/types/identity.ts +45 -0
  295. package/src/web/tsconfig.app.json +28 -0
  296. package/src/web/tsconfig.json +7 -0
  297. package/src/web/tsconfig.node.json +26 -0
  298. package/src/web/vite.config.ts +31 -0
  299. package/todo-security.md +92 -0
  300. package/tsconfig.json +23 -0
  301. package/worker/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/12b7107e435bf1b9a8713a7f320472a63e543104d633d89a26f8d21f4e4ef182.sqlite +0 -0
  302. package/worker/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/12b7107e435bf1b9a8713a7f320472a63e543104d633d89a26f8d21f4e4ef182.sqlite-shm +0 -0
  303. package/worker/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/12b7107e435bf1b9a8713a7f320472a63e543104d633d89a26f8d21f4e4ef182.sqlite-wal +0 -0
  304. package/worker/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/1a1ac3db1ab86ecf712f90322868a9aabc2c7dc9fe2dfbe94f9b075096276b0f.sqlite +0 -0
  305. package/worker/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/1a1ac3db1ab86ecf712f90322868a9aabc2c7dc9fe2dfbe94f9b075096276b0f.sqlite-shm +0 -0
  306. package/worker/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/1a1ac3db1ab86ecf712f90322868a9aabc2c7dc9fe2dfbe94f9b075096276b0f.sqlite-wal +0 -0
  307. package/worker/bun.lock +237 -0
  308. package/worker/package.json +22 -0
  309. package/worker/schema.sql +96 -0
  310. package/worker/src/handlers/auth.ts +451 -0
  311. package/worker/src/handlers/subdomains.ts +376 -0
  312. package/worker/src/handlers/user.ts +98 -0
  313. package/worker/src/index.ts +70 -0
  314. package/worker/src/middleware/auth.ts +152 -0
  315. package/worker/src/services/cloudflare.ts +609 -0
  316. package/worker/src/types.ts +96 -0
  317. package/worker/tsconfig.json +15 -0
  318. 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
+ });