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,124 @@
1
+ /**
2
+ * Authenticated encryption using AES-256-GCM
3
+ *
4
+ * This is similar to NaCl secretbox but uses AES-256-GCM
5
+ * which is natively supported in Bun's node:crypto.
6
+ */
7
+
8
+ import {
9
+ createCipheriv,
10
+ createDecipheriv,
11
+ randomBytes,
12
+ } from "node:crypto";
13
+
14
+ /** Nonce/IV length in bytes (96-bit for AES-GCM) */
15
+ export const NONCE_LENGTH = 12;
16
+
17
+ /** Auth tag length in bytes */
18
+ export const AUTH_TAG_LENGTH = 16;
19
+
20
+ /** Algorithm name */
21
+ const ALGORITHM = "aes-256-gcm";
22
+
23
+ /**
24
+ * Generate a random nonce
25
+ */
26
+ export function generateNonce(): Buffer {
27
+ return randomBytes(NONCE_LENGTH);
28
+ }
29
+
30
+ /**
31
+ * Encrypt data using ChaCha20-Poly1305
32
+ *
33
+ * @param data - Plaintext data to encrypt
34
+ * @param key - 256-bit key (from deriveKey)
35
+ * @returns Object with nonce and ciphertext (includes auth tag)
36
+ */
37
+ export function encrypt(
38
+ data: Uint8Array | Buffer,
39
+ key: Uint8Array | Buffer
40
+ ): { nonce: Buffer; ciphertext: Buffer } {
41
+ const nonce = generateNonce();
42
+
43
+ const cipher = createCipheriv(ALGORITHM, key, nonce, {
44
+ authTagLength: AUTH_TAG_LENGTH,
45
+ });
46
+
47
+ const encrypted = Buffer.concat([cipher.update(data), cipher.final()]);
48
+ const authTag = cipher.getAuthTag();
49
+
50
+ // Append auth tag to ciphertext
51
+ const ciphertext = Buffer.concat([encrypted, authTag]);
52
+
53
+ return { nonce, ciphertext };
54
+ }
55
+
56
+ /**
57
+ * Decrypt data using ChaCha20-Poly1305
58
+ *
59
+ * @param ciphertext - Encrypted data (includes auth tag at end)
60
+ * @param nonce - Nonce used for encryption
61
+ * @param key - 256-bit key (same as used for encryption)
62
+ * @returns Decrypted plaintext, or null if authentication failed
63
+ */
64
+ export function decrypt(
65
+ ciphertext: Uint8Array | Buffer,
66
+ nonce: Uint8Array | Buffer,
67
+ key: Uint8Array | Buffer
68
+ ): Buffer | null {
69
+ try {
70
+ // Extract auth tag from end of ciphertext
71
+ const encrypted = ciphertext.slice(0, -AUTH_TAG_LENGTH);
72
+ const authTag = ciphertext.slice(-AUTH_TAG_LENGTH);
73
+
74
+ const decipher = createDecipheriv(ALGORITHM, key, nonce, {
75
+ authTagLength: AUTH_TAG_LENGTH,
76
+ });
77
+
78
+ decipher.setAuthTag(authTag);
79
+
80
+ const decrypted = Buffer.concat([
81
+ decipher.update(encrypted),
82
+ decipher.final(),
83
+ ]);
84
+
85
+ return decrypted;
86
+ } catch {
87
+ // Authentication failed or other error
88
+ return null;
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Encrypt data and return a single buffer with nonce prepended
94
+ *
95
+ * Format: nonce (12 bytes) || ciphertext || authTag (16 bytes)
96
+ */
97
+ export function seal(
98
+ data: Uint8Array | Buffer,
99
+ key: Uint8Array | Buffer
100
+ ): Buffer {
101
+ const { nonce, ciphertext } = encrypt(data, key);
102
+ return Buffer.concat([nonce, ciphertext]);
103
+ }
104
+
105
+ /**
106
+ * Decrypt data from a sealed buffer (nonce prepended)
107
+ *
108
+ * @param sealed - Buffer with format: nonce || ciphertext || authTag
109
+ * @param key - 256-bit key
110
+ * @returns Decrypted plaintext, or null if authentication failed
111
+ */
112
+ export function open(
113
+ sealed: Uint8Array | Buffer,
114
+ key: Uint8Array | Buffer
115
+ ): Buffer | null {
116
+ if (sealed.length < NONCE_LENGTH + AUTH_TAG_LENGTH) {
117
+ return null;
118
+ }
119
+
120
+ const nonce = sealed.slice(0, NONCE_LENGTH);
121
+ const ciphertext = sealed.slice(NONCE_LENGTH);
122
+
123
+ return decrypt(ciphertext, nonce, key);
124
+ }
@@ -0,0 +1,451 @@
1
+ /**
2
+ * Machine-side X3DH handshake handler
3
+ *
4
+ * This class manages X3DH handshakes for multiple concurrent client connections.
5
+ * It processes incoming handshake messages, validates clients via access lists
6
+ * or invite tokens, and returns established sessions on success.
7
+ *
8
+ * The relay server forwards raw bytes between clients and machines - the handshake
9
+ * is peer-to-peer between the CLIENT and MACHINE.
10
+ *
11
+ * Message flow:
12
+ * 1. ClientHello → Machine creates state, returns ServerHello
13
+ * 2. ClientAuth → Machine validates auth, returns ServerAuth with accept/reject
14
+ * 3. On accept → Returns established session with keys
15
+ *
16
+ * @module handshake-handler
17
+ */
18
+
19
+ import {
20
+ createServerState,
21
+ processClientHello,
22
+ createServerHello,
23
+ processClientAuth,
24
+ createServerAuth,
25
+ type X3DHServerState,
26
+ } from "./crypto/handshake.js";
27
+ import { AccessControlList } from "./crypto/access-control.js";
28
+ import { parseInviteToken, isInviteExpired } from "./crypto/invites.js";
29
+ import type {
30
+ Identity,
31
+ SessionKeys,
32
+ AccessType,
33
+ X3DHInitMessage,
34
+ X3DHAuthMessage,
35
+ } from "../../types/identity.js";
36
+
37
+ // ============================================================================
38
+ // Types
39
+ // ============================================================================
40
+
41
+ /** Configuration for HandshakeHandler */
42
+ export interface HandshakeHandlerConfig {
43
+ /** Machine's identity for authentication */
44
+ identity: Identity;
45
+ /** Access control list for authorized clients */
46
+ accessList: AccessControlList;
47
+ /**
48
+ * Optional custom invite validator
49
+ * Returns access type if valid, null if rejected
50
+ */
51
+ validateInvite?: (token: string) => Promise<{ accessType: AccessType; sessionId?: string } | null>;
52
+ /** Handshake timeout in milliseconds (default: 30000) */
53
+ handshakeTimeoutMs?: number;
54
+ }
55
+
56
+ /** Per-connection handshake state */
57
+ interface HandshakeContext {
58
+ /** X3DH server state */
59
+ state: X3DHServerState;
60
+ /** When handshake started (for timeout) */
61
+ startedAt: number;
62
+ /** Timeout handle for cleanup */
63
+ timeoutHandle?: ReturnType<typeof setTimeout>;
64
+ }
65
+
66
+ /** Handshake message envelope */
67
+ export interface HandshakeMessage {
68
+ type: "handshake";
69
+ phase: "client_hello" | "server_hello" | "client_auth" | "server_auth";
70
+ data: unknown;
71
+ }
72
+
73
+ /** Result of processing a handshake message */
74
+ export type ProcessResult =
75
+ | { type: "reply"; message: HandshakeMessage }
76
+ | { type: "established"; session: EstablishedSession; message: HandshakeMessage }
77
+ | { type: "error"; reason: string; close: boolean };
78
+
79
+ /** Established session after successful handshake */
80
+ export interface EstablishedSession {
81
+ /** Connection ID (maps to relay connection) */
82
+ connectionId: string;
83
+ /** Peer's identity ID */
84
+ peerIdentityId: string;
85
+ /** Granted access type */
86
+ accessType: AccessType;
87
+ /** Session ID for session-invite access */
88
+ sessionId?: string;
89
+ /** Derived session keys for encryption */
90
+ sessionKeys: SessionKeys;
91
+ /** When session was established (Unix ms) */
92
+ establishedAt: number;
93
+ }
94
+
95
+ // ============================================================================
96
+ // HandshakeHandler Class
97
+ // ============================================================================
98
+
99
+ /**
100
+ * Handles X3DH handshakes for multiple concurrent client connections
101
+ *
102
+ * @example
103
+ * ```typescript
104
+ * const handler = new HandshakeHandler({
105
+ * identity: machineIdentity,
106
+ * accessList: acl,
107
+ * });
108
+ *
109
+ * // On receiving a handshake message from client
110
+ * const result = await handler.processMessage(connectionId, message);
111
+ * if (result.type === "reply") {
112
+ * relay.send(connectionId, result.message);
113
+ * } else if (result.type === "established") {
114
+ * sessions.set(connectionId, result.session);
115
+ * } else if (result.type === "error") {
116
+ * console.error(result.reason);
117
+ * if (result.close) relay.close(connectionId);
118
+ * }
119
+ *
120
+ * // On client disconnect
121
+ * handler.cleanup(connectionId);
122
+ * ```
123
+ */
124
+ export class HandshakeHandler {
125
+ private config: HandshakeHandlerConfig;
126
+ private contexts: Map<string, HandshakeContext> = new Map();
127
+ private readonly defaultTimeoutMs = 30000;
128
+
129
+ /**
130
+ * Create a new HandshakeHandler
131
+ *
132
+ * @param config - Handler configuration
133
+ */
134
+ constructor(config: HandshakeHandlerConfig) {
135
+ this.config = config;
136
+ }
137
+
138
+ /**
139
+ * Process an incoming handshake message from a client
140
+ *
141
+ * @param connectionId - Unique identifier for this connection
142
+ * @param message - Handshake message to process
143
+ * @returns Processing result (reply, established, or error)
144
+ */
145
+ async processMessage(
146
+ connectionId: string,
147
+ message: HandshakeMessage
148
+ ): Promise<ProcessResult> {
149
+ try {
150
+ switch (message.phase) {
151
+ case "client_hello":
152
+ return this.handleClientHello(connectionId, message.data as X3DHInitMessage);
153
+
154
+ case "client_auth":
155
+ return await this.handleClientAuth(connectionId, message.data as X3DHAuthMessage);
156
+
157
+ default:
158
+ return {
159
+ type: "error",
160
+ reason: `Unexpected handshake phase: ${message.phase}`,
161
+ close: true,
162
+ };
163
+ }
164
+ } catch (error) {
165
+ return {
166
+ type: "error",
167
+ reason: `Handshake error: ${error instanceof Error ? error.message : String(error)}`,
168
+ close: true,
169
+ };
170
+ }
171
+ }
172
+
173
+ /**
174
+ * Handle ClientHello message (phase 1)
175
+ *
176
+ * Creates fresh server state and returns ServerHello
177
+ */
178
+ private handleClientHello(
179
+ connectionId: string,
180
+ clientHello: X3DHInitMessage
181
+ ): ProcessResult {
182
+ // Clean up any existing state for this connection
183
+ this.cleanup(connectionId);
184
+
185
+ // Create fresh server state
186
+ const serverState = createServerState(this.config.identity);
187
+
188
+ // Process ClientHello
189
+ const newState = processClientHello(serverState, clientHello);
190
+ if (!newState) {
191
+ return {
192
+ type: "error",
193
+ reason: "Invalid ClientHello",
194
+ close: true,
195
+ };
196
+ }
197
+
198
+ // Create ServerHello response
199
+ const { state: stateAfterHello, message: serverHello } = createServerHello(
200
+ newState,
201
+ this.config.identity
202
+ );
203
+
204
+ // Store context with timeout
205
+ const timeoutMs = this.config.handshakeTimeoutMs ?? this.defaultTimeoutMs;
206
+ const timeoutHandle = setTimeout(() => {
207
+ this.cleanup(connectionId);
208
+ }, timeoutMs);
209
+
210
+ this.contexts.set(connectionId, {
211
+ state: stateAfterHello,
212
+ startedAt: Date.now(),
213
+ timeoutHandle,
214
+ });
215
+
216
+ return {
217
+ type: "reply",
218
+ message: {
219
+ type: "handshake",
220
+ phase: "server_hello",
221
+ data: serverHello,
222
+ },
223
+ };
224
+ }
225
+
226
+ /**
227
+ * Handle ClientAuth message (phase 2)
228
+ *
229
+ * Validates client identity and authorization, returns ServerAuth
230
+ */
231
+ private async handleClientAuth(
232
+ connectionId: string,
233
+ clientAuth: X3DHAuthMessage
234
+ ): Promise<ProcessResult> {
235
+ // Get existing context
236
+ const context = this.contexts.get(connectionId);
237
+ if (!context) {
238
+ return {
239
+ type: "error",
240
+ reason: "No handshake in progress for this connection",
241
+ close: true,
242
+ };
243
+ }
244
+
245
+ // Clear timeout (we're completing the handshake)
246
+ if (context.timeoutHandle) {
247
+ clearTimeout(context.timeoutHandle);
248
+ }
249
+
250
+ // Process ClientAuth to get peer identity
251
+ const authResult = processClientAuth(
252
+ context.state,
253
+ clientAuth,
254
+ this.config.identity
255
+ );
256
+
257
+ if (!authResult) {
258
+ this.cleanup(connectionId);
259
+ return {
260
+ type: "error",
261
+ reason: "Invalid ClientAuth or identity proof",
262
+ close: true,
263
+ };
264
+ }
265
+
266
+ // Check authorization
267
+ const authCheck = await this.checkAuthorization(
268
+ authResult.peerIdentityId,
269
+ authResult.authorization,
270
+ authResult.clientIdentityKey,
271
+ authResult.clientKeyExchangeKey
272
+ );
273
+
274
+ // Create ServerAuth response
275
+ const { message: serverAuth, sessionKeys } = createServerAuth(
276
+ this.config.identity,
277
+ context.state,
278
+ authResult.clientIdentityKey,
279
+ authCheck
280
+ );
281
+
282
+ // Clean up handshake context
283
+ this.cleanup(connectionId);
284
+
285
+ // If rejected, send ServerAuth with rejection and close
286
+ if (authCheck.type === "rejected") {
287
+ return {
288
+ type: "reply",
289
+ message: {
290
+ type: "handshake",
291
+ phase: "server_auth",
292
+ data: serverAuth,
293
+ },
294
+ };
295
+ }
296
+
297
+ // Success! Return established session with ServerAuth message
298
+ const session: EstablishedSession = {
299
+ connectionId,
300
+ peerIdentityId: authResult.peerIdentityId,
301
+ accessType: authCheck.accessType,
302
+ sessionId: authCheck.sessionId,
303
+ sessionKeys,
304
+ establishedAt: Date.now(),
305
+ };
306
+
307
+ // Return established session with ServerAuth message
308
+ // The caller should send the ServerAuth reply then handle the established session
309
+ return {
310
+ type: "established",
311
+ session,
312
+ message: {
313
+ type: "handshake",
314
+ phase: "server_auth",
315
+ data: serverAuth,
316
+ },
317
+ };
318
+ }
319
+
320
+ /**
321
+ * Check client authorization via access list or invite token
322
+ */
323
+ private async checkAuthorization(
324
+ peerIdentityId: string,
325
+ authorization: X3DHAuthMessage["authorization"],
326
+ clientIdentityKey: Uint8Array,
327
+ clientKeyExchangeKey: Uint8Array
328
+ ): Promise<
329
+ | { type: "accepted"; accessType: AccessType; sessionId?: string }
330
+ | { type: "rejected"; reason: string }
331
+ > {
332
+ if (authorization.type === "access_list") {
333
+ // Check access list
334
+ const entry = this.config.accessList.getEntry(peerIdentityId);
335
+ if (!entry) {
336
+ return {
337
+ type: "rejected",
338
+ reason: "Not in access list",
339
+ };
340
+ }
341
+
342
+ return {
343
+ type: "accepted",
344
+ accessType: entry.accessType,
345
+ sessionId: entry.sessionId,
346
+ };
347
+ }
348
+
349
+ if (authorization.type === "invite") {
350
+ // Validate invite token
351
+ const token = parseInviteToken(authorization.inviteToken);
352
+
353
+ if (!token) {
354
+ return {
355
+ type: "rejected",
356
+ reason: "Invalid invite token",
357
+ };
358
+ }
359
+
360
+ if (isInviteExpired(token)) {
361
+ return {
362
+ type: "rejected",
363
+ reason: "Invite token expired",
364
+ };
365
+ }
366
+
367
+ // Verify token was issued by this machine
368
+ if (token.machineId !== this.config.identity.id) {
369
+ return {
370
+ type: "rejected",
371
+ reason: "Invite token not issued by this machine",
372
+ };
373
+ }
374
+
375
+ // Check custom validator if provided
376
+ if (this.config.validateInvite) {
377
+ const customResult = await this.config.validateInvite(
378
+ authorization.inviteToken
379
+ );
380
+ if (!customResult) {
381
+ return {
382
+ type: "rejected",
383
+ reason: "Invite rejected by custom validator",
384
+ };
385
+ }
386
+ return {
387
+ type: "accepted",
388
+ accessType: customResult.accessType,
389
+ sessionId: customResult.sessionId,
390
+ };
391
+ }
392
+
393
+ // Use access type from token
394
+ // Security: Only add to permanent access list if NOT a single-use invite
395
+ // Single-use invites grant access for this session only
396
+ if (!token.singleUse) {
397
+ this.config.accessList.addEntry(
398
+ {
399
+ id: peerIdentityId,
400
+ signingPublicKey: Buffer.from(clientIdentityKey).toString("base64"),
401
+ keyExchangePublicKey: Buffer.from(clientKeyExchangeKey).toString("base64"),
402
+ },
403
+ token.accessType,
404
+ token.sessionId
405
+ );
406
+ }
407
+
408
+ return {
409
+ type: "accepted",
410
+ accessType: token.accessType,
411
+ sessionId: token.sessionId,
412
+ };
413
+ }
414
+
415
+ return {
416
+ type: "rejected",
417
+ reason: "Unknown authorization type",
418
+ };
419
+ }
420
+
421
+ /**
422
+ * Clean up state for a disconnected client
423
+ *
424
+ * Call this when a client disconnects to free resources
425
+ *
426
+ * @param connectionId - Connection ID to clean up
427
+ */
428
+ cleanup(connectionId: string): void {
429
+ const context = this.contexts.get(connectionId);
430
+ if (context) {
431
+ if (context.timeoutHandle) {
432
+ clearTimeout(context.timeoutHandle);
433
+ }
434
+ this.contexts.delete(connectionId);
435
+ }
436
+ }
437
+
438
+ /**
439
+ * Get number of active handshakes
440
+ */
441
+ get activeHandshakes(): number {
442
+ return this.contexts.size;
443
+ }
444
+
445
+ /**
446
+ * Check if a connection has an active handshake
447
+ */
448
+ hasActiveHandshake(connectionId: string): boolean {
449
+ return this.contexts.has(connectionId);
450
+ }
451
+ }