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,593 @@
1
+ /**
2
+ * Relay registries for machine, invite, and authorization tracking
3
+ *
4
+ * In-memory storage for v1. State is lost on restart.
5
+ */
6
+
7
+ import type { ServerWebSocket } from "bun";
8
+ import type { WebSocketData } from "./types";
9
+
10
+ // ============================================================================
11
+ // Types
12
+ // ============================================================================
13
+
14
+ /** Registered machine information */
15
+ export interface MachineRegistration {
16
+ /** Machine identity ID */
17
+ machineId: string;
18
+ /** Account that registered this machine */
19
+ accountId: string;
20
+ /** Ed25519 signing public key (base64) */
21
+ signingKey: string;
22
+ /** X25519 key exchange public key (base64) */
23
+ keyExchangeKey: string;
24
+ /** Human-readable label */
25
+ label?: string;
26
+ /** WebSocket connection (null if offline) */
27
+ ws: ServerWebSocket<WebSocketData> | null;
28
+ /** When machine was registered */
29
+ registeredAt: number;
30
+ /** When machine last connected */
31
+ lastConnectedAt: number;
32
+ }
33
+
34
+ /** Registered invite information */
35
+ export interface InviteRegistration {
36
+ /** Invite ID (hash of token or random) */
37
+ inviteId: string;
38
+ /** Machine this invite is for */
39
+ machineId: string;
40
+ /** When invite expires */
41
+ expiresAt: number;
42
+ /** Max number of uses (null = unlimited) */
43
+ maxUses: number | null;
44
+ /** Number of times invite has been used */
45
+ usedCount: number;
46
+ /** When invite was registered */
47
+ registeredAt: number;
48
+ }
49
+
50
+ /** Client authorization for a machine */
51
+ export interface ClientAuthorization {
52
+ /** Client's identity ID */
53
+ clientIdentityId: string;
54
+ /** Client's signing public key (base64) */
55
+ signingKey: string;
56
+ /** Client's key exchange public key (base64) */
57
+ keyExchangeKey: string;
58
+ /** Access type: 'full' or 'session-invite' */
59
+ accessType: 'full' | 'session-invite';
60
+ /** For session-invite: specific session ID */
61
+ sessionId?: string;
62
+ /** When authorization was granted */
63
+ grantedAt: number;
64
+ }
65
+
66
+ // ============================================================================
67
+ // Machine Registry
68
+ // ============================================================================
69
+
70
+ /** Registered machines by machineId */
71
+ const machines = new Map<string, MachineRegistration>();
72
+
73
+ /** Result of machine registration attempt */
74
+ export type RegisterMachineResult =
75
+ | { success: true; registration: MachineRegistration }
76
+ | { success: false; error: string };
77
+
78
+ /**
79
+ * Register a machine
80
+ *
81
+ * Security: Re-registration requires matching accountId and signingKey
82
+ * to prevent machine takeover attacks.
83
+ */
84
+ export function registerMachine(
85
+ machineId: string,
86
+ accountId: string,
87
+ signingKey: string,
88
+ keyExchangeKey: string,
89
+ ws: ServerWebSocket<WebSocketData>,
90
+ label?: string
91
+ ): RegisterMachineResult {
92
+ const now = Date.now();
93
+
94
+ // Check if already registered
95
+ const existing = machines.get(machineId);
96
+ if (existing) {
97
+ // Security: Verify ownership - must be same account
98
+ if (existing.accountId !== accountId) {
99
+ return {
100
+ success: false,
101
+ error: "Machine already registered by different account",
102
+ };
103
+ }
104
+
105
+ // Security: Verify signing key matches - prevents key substitution attacks
106
+ if (existing.signingKey !== signingKey) {
107
+ return {
108
+ success: false,
109
+ error: "Signing key mismatch - machine identity has changed",
110
+ };
111
+ }
112
+
113
+ // Safe to update connection
114
+ existing.ws = ws;
115
+ existing.lastConnectedAt = now;
116
+ if (label) existing.label = label;
117
+ return { success: true, registration: existing };
118
+ }
119
+
120
+ const registration: MachineRegistration = {
121
+ machineId,
122
+ accountId,
123
+ signingKey,
124
+ keyExchangeKey,
125
+ label,
126
+ ws,
127
+ registeredAt: now,
128
+ lastConnectedAt: now,
129
+ };
130
+
131
+ machines.set(machineId, registration);
132
+ return { success: true, registration };
133
+ }
134
+
135
+ /**
136
+ * Get a registered machine
137
+ */
138
+ export function getMachine(machineId: string): MachineRegistration | undefined {
139
+ return machines.get(machineId);
140
+ }
141
+
142
+ /**
143
+ * Check if a machine is registered
144
+ */
145
+ export function hasMachine(machineId: string): boolean {
146
+ return machines.has(machineId);
147
+ }
148
+
149
+ /**
150
+ * Check if a machine is online (connected)
151
+ */
152
+ export function isMachineOnline(machineId: string): boolean {
153
+ const machine = machines.get(machineId);
154
+ return machine !== undefined && machine.ws !== null;
155
+ }
156
+
157
+ /**
158
+ * Update machine connection status
159
+ */
160
+ export function setMachineConnection(
161
+ machineId: string,
162
+ ws: ServerWebSocket<WebSocketData> | null
163
+ ): void {
164
+ const machine = machines.get(machineId);
165
+ if (machine) {
166
+ machine.ws = ws;
167
+ if (ws) {
168
+ machine.lastConnectedAt = Date.now();
169
+ }
170
+ }
171
+ }
172
+
173
+ /**
174
+ * Unregister a machine
175
+ */
176
+ export function unregisterMachine(machineId: string): boolean {
177
+ return machines.delete(machineId);
178
+ }
179
+
180
+ /**
181
+ * Get all registered machines
182
+ */
183
+ export function getAllMachines(): MachineRegistration[] {
184
+ return Array.from(machines.values());
185
+ }
186
+
187
+ // ============================================================================
188
+ // Invite Registry
189
+ // ============================================================================
190
+
191
+ /** Registered invites by inviteId */
192
+ const invites = new Map<string, InviteRegistration>();
193
+
194
+ /**
195
+ * Register an invite
196
+ */
197
+ export function registerInvite(
198
+ inviteId: string,
199
+ machineId: string,
200
+ expiresAt: number,
201
+ maxUses: number | null = null
202
+ ): InviteRegistration {
203
+ const registration: InviteRegistration = {
204
+ inviteId,
205
+ machineId,
206
+ expiresAt,
207
+ maxUses,
208
+ usedCount: 0,
209
+ registeredAt: Date.now(),
210
+ };
211
+
212
+ invites.set(inviteId, registration);
213
+ return registration;
214
+ }
215
+
216
+ /**
217
+ * Get an invite registration
218
+ */
219
+ export function getInvite(inviteId: string): InviteRegistration | undefined {
220
+ return invites.get(inviteId);
221
+ }
222
+
223
+ /**
224
+ * Check if an invite is valid (exists, not expired, not exhausted)
225
+ */
226
+ export function isInviteValid(inviteId: string): boolean {
227
+ const invite = invites.get(inviteId);
228
+ if (!invite) return false;
229
+ if (Date.now() > invite.expiresAt) return false;
230
+ if (invite.maxUses !== null && invite.usedCount >= invite.maxUses) return false;
231
+ return true;
232
+ }
233
+
234
+ /**
235
+ * Use an invite (increment use count)
236
+ */
237
+ export function useInvite(inviteId: string): boolean {
238
+ const invite = invites.get(inviteId);
239
+ if (!invite) return false;
240
+
241
+ invite.usedCount++;
242
+
243
+ // Remove if exhausted
244
+ if (invite.maxUses !== null && invite.usedCount >= invite.maxUses) {
245
+ invites.delete(inviteId);
246
+ }
247
+
248
+ return true;
249
+ }
250
+
251
+ /**
252
+ * Revoke an invite
253
+ */
254
+ export function revokeInvite(inviteId: string): boolean {
255
+ return invites.delete(inviteId);
256
+ }
257
+
258
+ /**
259
+ * Get invites for a machine
260
+ */
261
+ export function getInvitesForMachine(machineId: string): InviteRegistration[] {
262
+ return Array.from(invites.values()).filter(inv => inv.machineId === machineId);
263
+ }
264
+
265
+ /**
266
+ * Clean up expired invites
267
+ */
268
+ export function cleanupExpiredInvites(): number {
269
+ const now = Date.now();
270
+ let removed = 0;
271
+
272
+ for (const [id, invite] of invites) {
273
+ if (now > invite.expiresAt) {
274
+ invites.delete(id);
275
+ removed++;
276
+ }
277
+ }
278
+
279
+ return removed;
280
+ }
281
+
282
+ // ============================================================================
283
+ // Authorization Registry
284
+ // ============================================================================
285
+
286
+ /** Client authorizations by machineId */
287
+ const authorizations = new Map<string, Map<string, ClientAuthorization>>();
288
+
289
+ /**
290
+ * Authorize a client for a machine
291
+ */
292
+ export function authorizeClient(
293
+ machineId: string,
294
+ clientIdentityId: string,
295
+ signingKey: string,
296
+ keyExchangeKey: string,
297
+ accessType: 'full' | 'session-invite',
298
+ sessionId?: string
299
+ ): ClientAuthorization {
300
+ let machineAuths = authorizations.get(machineId);
301
+ if (!machineAuths) {
302
+ machineAuths = new Map();
303
+ authorizations.set(machineId, machineAuths);
304
+ }
305
+
306
+ const auth: ClientAuthorization = {
307
+ clientIdentityId,
308
+ signingKey,
309
+ keyExchangeKey,
310
+ accessType,
311
+ sessionId,
312
+ grantedAt: Date.now(),
313
+ };
314
+
315
+ machineAuths.set(clientIdentityId, auth);
316
+ return auth;
317
+ }
318
+
319
+ /**
320
+ * Check if a client is authorized for a machine
321
+ */
322
+ export function isClientAuthorized(machineId: string, clientIdentityId: string): boolean {
323
+ const machineAuths = authorizations.get(machineId);
324
+ return machineAuths?.has(clientIdentityId) ?? false;
325
+ }
326
+
327
+ /**
328
+ * Get client authorization for a machine
329
+ */
330
+ export function getClientAuthorization(
331
+ machineId: string,
332
+ clientIdentityId: string
333
+ ): ClientAuthorization | undefined {
334
+ return authorizations.get(machineId)?.get(clientIdentityId);
335
+ }
336
+
337
+ /**
338
+ * Revoke client authorization
339
+ */
340
+ export function revokeClientAuthorization(
341
+ machineId: string,
342
+ clientIdentityId: string
343
+ ): boolean {
344
+ const machineAuths = authorizations.get(machineId);
345
+ if (!machineAuths) return false;
346
+ return machineAuths.delete(clientIdentityId);
347
+ }
348
+
349
+ /**
350
+ * Get all clients authorized for a machine
351
+ */
352
+ export function getAuthorizedClients(machineId: string): ClientAuthorization[] {
353
+ const machineAuths = authorizations.get(machineId);
354
+ if (!machineAuths) return [];
355
+ return Array.from(machineAuths.values());
356
+ }
357
+
358
+ /**
359
+ * Get all machines a client is authorized for
360
+ */
361
+ export function getMachinesForClient(clientIdentityId: string): {
362
+ machineId: string;
363
+ machine: MachineRegistration | undefined;
364
+ authorization: ClientAuthorization;
365
+ }[] {
366
+ const results: {
367
+ machineId: string;
368
+ machine: MachineRegistration | undefined;
369
+ authorization: ClientAuthorization;
370
+ }[] = [];
371
+
372
+ for (const [machineId, machineAuths] of authorizations) {
373
+ const auth = machineAuths.get(clientIdentityId);
374
+ if (auth) {
375
+ results.push({
376
+ machineId,
377
+ machine: machines.get(machineId),
378
+ authorization: auth,
379
+ });
380
+ }
381
+ }
382
+
383
+ return results;
384
+ }
385
+
386
+ /**
387
+ * Get all registered machines with authorization status for a client
388
+ *
389
+ * Returns all machines (not just authorized ones) so clients can see
390
+ * what's available and understand they need authorization.
391
+ */
392
+ export function getAllMachinesWithAuthStatus(clientIdentityId: string): {
393
+ machineId: string;
394
+ machine: MachineRegistration;
395
+ isAuthorized: boolean;
396
+ accessType?: 'full' | 'session-invite';
397
+ sessionId?: string;
398
+ }[] {
399
+ const results: {
400
+ machineId: string;
401
+ machine: MachineRegistration;
402
+ isAuthorized: boolean;
403
+ accessType?: 'full' | 'session-invite';
404
+ sessionId?: string;
405
+ }[] = [];
406
+
407
+ for (const [machineId, machine] of machines) {
408
+ const machineAuths = authorizations.get(machineId);
409
+ const auth = machineAuths?.get(clientIdentityId);
410
+
411
+ results.push({
412
+ machineId,
413
+ machine,
414
+ isAuthorized: !!auth,
415
+ accessType: auth?.accessType,
416
+ sessionId: auth?.sessionId,
417
+ });
418
+ }
419
+
420
+ return results;
421
+ }
422
+
423
+ // ============================================================================
424
+ // Global Access List Registry
425
+ // ============================================================================
426
+
427
+ /** Global access entry (applies to all machines or specific machines) */
428
+ export interface GlobalAccessEntry {
429
+ /** Client identity ID */
430
+ clientIdentityId: string;
431
+ /** Client's signing public key (base64) */
432
+ signingKey: string;
433
+ /** Client's key exchange public key (base64) */
434
+ keyExchangeKey: string;
435
+ /** Human-readable label */
436
+ label?: string;
437
+ /** Access type: 'full' or 'session-invite' */
438
+ accessType: 'full' | 'session-invite';
439
+ /** For session-invite: specific session ID */
440
+ sessionId?: string;
441
+ /** When access was granted */
442
+ grantedAt: number;
443
+ /** Account that granted this access */
444
+ grantedBy: string;
445
+ /** If set, only applies to these machines (empty = all machines) */
446
+ machineIds?: string[];
447
+ }
448
+
449
+ /** Global access list - applies to all machines owned by an account */
450
+ const globalAccessList = new Map<string, GlobalAccessEntry[]>(); // accountId → entries
451
+
452
+ /**
453
+ * Get the global access list for an account
454
+ */
455
+ export function getGlobalAccessList(accountId: string): GlobalAccessEntry[] {
456
+ return globalAccessList.get(accountId) || [];
457
+ }
458
+
459
+ /**
460
+ * Get effective access list for a machine
461
+ * Combines global entries (for the account) with machine-specific overrides
462
+ */
463
+ export function getEffectiveAccessList(accountId: string, machineId: string): GlobalAccessEntry[] {
464
+ const entries = globalAccessList.get(accountId) || [];
465
+ return entries.filter(entry => {
466
+ // If no machineIds specified, applies to all
467
+ if (!entry.machineIds || entry.machineIds.length === 0) {
468
+ return true;
469
+ }
470
+ // Otherwise, check if this machine is in the list
471
+ return entry.machineIds.includes(machineId);
472
+ });
473
+ }
474
+
475
+ /**
476
+ * Add a global access entry
477
+ */
478
+ export function addGlobalAccess(
479
+ accountId: string,
480
+ entry: Omit<GlobalAccessEntry, 'grantedAt' | 'grantedBy'>
481
+ ): GlobalAccessEntry {
482
+ let entries = globalAccessList.get(accountId);
483
+ if (!entries) {
484
+ entries = [];
485
+ globalAccessList.set(accountId, entries);
486
+ }
487
+
488
+ // Check if already exists
489
+ const existingIndex = entries.findIndex(e => e.clientIdentityId === entry.clientIdentityId);
490
+
491
+ const fullEntry: GlobalAccessEntry = {
492
+ ...entry,
493
+ grantedAt: Date.now(),
494
+ grantedBy: accountId,
495
+ };
496
+
497
+ if (existingIndex >= 0) {
498
+ entries[existingIndex] = fullEntry;
499
+ } else {
500
+ entries.push(fullEntry);
501
+ }
502
+
503
+ return fullEntry;
504
+ }
505
+
506
+ /**
507
+ * Remove a global access entry
508
+ */
509
+ export function removeGlobalAccess(accountId: string, clientIdentityId: string): boolean {
510
+ const entries = globalAccessList.get(accountId);
511
+ if (!entries) return false;
512
+
513
+ const index = entries.findIndex(e => e.clientIdentityId === clientIdentityId);
514
+ if (index < 0) return false;
515
+
516
+ entries.splice(index, 1);
517
+ return true;
518
+ }
519
+
520
+ /**
521
+ * Broadcast access list update to all connected machines for an account
522
+ * @param accountId - Account that owns the machines
523
+ * @param added - New access entries
524
+ * @param removed - Removed client identity IDs
525
+ * @param signFn - Optional signing function for message authentication
526
+ */
527
+ export function broadcastAccessUpdate(
528
+ accountId: string,
529
+ added: GlobalAccessEntry[],
530
+ removed: string[],
531
+ signFn?: <T extends object>(msg: T) => T
532
+ ): void {
533
+ // Find all machines owned by this account
534
+ for (const machine of machines.values()) {
535
+ if (machine.accountId === accountId && machine.ws) {
536
+ const msg = {
537
+ type: 'access_update' as const,
538
+ added,
539
+ removed,
540
+ };
541
+ // Sign the message if signing function is provided
542
+ const signedMsg = signFn ? signFn(msg) : msg;
543
+ machine.ws.send(JSON.stringify(signedMsg));
544
+ }
545
+ }
546
+ }
547
+
548
+ /**
549
+ * Get all machines for an account
550
+ */
551
+ export function getMachinesForAccount(accountId: string): MachineRegistration[] {
552
+ return Array.from(machines.values()).filter(m => m.accountId === accountId);
553
+ }
554
+
555
+ // ============================================================================
556
+ // Stats
557
+ // ============================================================================
558
+
559
+ /**
560
+ * Get registry statistics
561
+ */
562
+ export function getRegistryStats(): {
563
+ machineCount: number;
564
+ onlineMachineCount: number;
565
+ inviteCount: number;
566
+ authorizationCount: number;
567
+ } {
568
+ let onlineMachineCount = 0;
569
+ for (const machine of machines.values()) {
570
+ if (machine.ws !== null) onlineMachineCount++;
571
+ }
572
+
573
+ let authorizationCount = 0;
574
+ for (const machineAuths of authorizations.values()) {
575
+ authorizationCount += machineAuths.size;
576
+ }
577
+
578
+ return {
579
+ machineCount: machines.size,
580
+ onlineMachineCount,
581
+ inviteCount: invites.size,
582
+ authorizationCount,
583
+ };
584
+ }
585
+
586
+ /**
587
+ * Clear all registries (for testing)
588
+ */
589
+ export function clearAllRegistries(): void {
590
+ machines.clear();
591
+ invites.clear();
592
+ authorizations.clear();
593
+ }