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,631 @@
1
+ /**
2
+ * Identity file operations for managing keypairs, access lists, and machine identity
3
+ *
4
+ * This module handles persistent storage of cryptographic identities and access control:
5
+ * - Encrypted keypair storage (password-protected)
6
+ * - Access list management (authorized public keys)
7
+ * - Machine identity configuration
8
+ *
9
+ * Directory structure:
10
+ * ~/gitspace/.identity/
11
+ * ├── keypair.json # Encrypted identity keypair
12
+ * ├── access-list.json # Allowed public keys
13
+ * └── machine.json # Machine registration info
14
+ *
15
+ * @module identity
16
+ */
17
+
18
+ import {
19
+ existsSync,
20
+ mkdirSync,
21
+ readFileSync,
22
+ writeFileSync,
23
+ } from 'node:fs';
24
+ import { join } from 'node:path';
25
+ import type {
26
+ Identity,
27
+ PublicIdentity,
28
+ AccessEntry,
29
+ AccessType,
30
+ MachineIdentity,
31
+ } from '../types/identity.js';
32
+ import {
33
+ generateIdentity,
34
+ serializeIdentity,
35
+ deserializeIdentity,
36
+ getPublicIdentity,
37
+ } from '../lib/tmux-lite/crypto/identity.js';
38
+ import { DEFAULT_ACCESS_TYPE } from '../lib/tmux-lite/crypto/access-control.js';
39
+ import { seal, open } from '../lib/tmux-lite/crypto/secretbox.js';
40
+ import { deriveKey, generateSalt } from '../lib/tmux-lite/crypto/keys.js';
41
+ import { getSpacesDir } from './config.js';
42
+ import {
43
+ SpacesError,
44
+ NoIdentityError,
45
+ InvalidPasswordError,
46
+ IdentityExistsError,
47
+ } from '../types/errors.js';
48
+
49
+ // ============================================================================
50
+ // Storage Format Types
51
+ // ============================================================================
52
+
53
+ /**
54
+ * Encrypted keypair storage format
55
+ */
56
+ interface EncryptedKeypairStorage {
57
+ version: 1;
58
+ id: string;
59
+ label?: string;
60
+ createdAt: number;
61
+ signingPublicKey: string;
62
+ keyExchangePublicKey: string;
63
+ /** base64-encoded encrypted secrets (nonce prepended) */
64
+ encryptedSecrets: string;
65
+ /** base64-encoded salt for key derivation */
66
+ salt: string;
67
+ }
68
+
69
+ /**
70
+ * Decrypted secrets structure
71
+ */
72
+ interface DecryptedSecrets {
73
+ signingSecretKey: string;
74
+ keyExchangePrivateKey: string;
75
+ }
76
+
77
+ // ============================================================================
78
+ // Directory Paths
79
+ // ============================================================================
80
+
81
+ /**
82
+ * Get the identity directory path
83
+ *
84
+ * @returns Path to ~/gitspace/.identity/
85
+ */
86
+ export function getIdentityDir(): string {
87
+ return join(getSpacesDir(), '.identity');
88
+ }
89
+
90
+ /**
91
+ * Get the keypair file path
92
+ *
93
+ * @returns Path to keypair.json
94
+ */
95
+ export function getKeypairPath(): string {
96
+ return join(getIdentityDir(), 'keypair.json');
97
+ }
98
+
99
+ /**
100
+ * Get the access list file path
101
+ *
102
+ * @returns Path to access-list.json
103
+ */
104
+ export function getAccessListPath(): string {
105
+ return join(getIdentityDir(), 'access-list.json');
106
+ }
107
+
108
+ /**
109
+ * Get the machine identity file path
110
+ *
111
+ * @returns Path to machine.json
112
+ */
113
+ export function getMachineIdentityPath(): string {
114
+ return join(getIdentityDir(), 'machine.json');
115
+ }
116
+
117
+ /**
118
+ * Ensure identity directory exists
119
+ */
120
+ function ensureIdentityDir(): void {
121
+ const identityDir = getIdentityDir();
122
+ if (!existsSync(identityDir)) {
123
+ mkdirSync(identityDir, { recursive: true, mode: 0o700 });
124
+ }
125
+ }
126
+
127
+ // ============================================================================
128
+ // Keypair Management
129
+ // ============================================================================
130
+
131
+ /**
132
+ * Generate a new identity and save it to disk (encrypted)
133
+ *
134
+ * Creates a new Ed25519 + X25519 keypair, encrypts the secret keys with
135
+ * a password-derived key, and saves to keypair.json.
136
+ *
137
+ * @param password - Password to encrypt the keypair
138
+ * @param label - Optional human-readable label
139
+ * @param force - If true, overwrite existing keypair
140
+ * @returns Public identity information
141
+ * @throws {IdentityExistsError} If keypair exists and force is false
142
+ */
143
+ export async function generateAndSaveKeypair(
144
+ password: string,
145
+ label?: string,
146
+ force: boolean = false
147
+ ): Promise<PublicIdentity> {
148
+ // Check if keypair already exists
149
+ if (keypairExists() && !force) {
150
+ throw new IdentityExistsError();
151
+ }
152
+
153
+ ensureIdentityDir();
154
+
155
+ // Generate new identity
156
+ const identity = generateIdentity(label);
157
+
158
+ // Serialize identity to get base64 strings
159
+ const serialized = serializeIdentity(identity);
160
+
161
+ // Create secrets object to encrypt
162
+ const secrets: DecryptedSecrets = {
163
+ signingSecretKey: serialized.signingSecretKey,
164
+ keyExchangePrivateKey: serialized.keyExchangePrivateKey,
165
+ };
166
+
167
+ // Generate salt and derive encryption key from password
168
+ const salt = generateSalt();
169
+ const encryptionKey = await deriveKey(password, salt);
170
+
171
+ // Encrypt secrets
172
+ const secretsJson = JSON.stringify(secrets);
173
+ const encryptedSecrets = seal(Buffer.from(secretsJson, 'utf-8'), encryptionKey);
174
+
175
+ // Create storage format
176
+ const storage: EncryptedKeypairStorage = {
177
+ version: 1,
178
+ id: identity.id,
179
+ label: identity.label,
180
+ createdAt: identity.createdAt,
181
+ signingPublicKey: serialized.signingPublicKey,
182
+ keyExchangePublicKey: serialized.keyExchangePublicKey,
183
+ encryptedSecrets: encryptedSecrets.toString('base64'),
184
+ salt: salt.toString('base64'),
185
+ };
186
+
187
+ // Write to disk
188
+ try {
189
+ writeFileSync(getKeypairPath(), JSON.stringify(storage, null, 2), {
190
+ encoding: 'utf-8',
191
+ mode: 0o600, // Owner read/write only
192
+ });
193
+ } catch (error) {
194
+ throw new SpacesError(
195
+ `Failed to save keypair: ${
196
+ error instanceof Error ? error.message : 'Unknown error'
197
+ }`,
198
+ 'SYSTEM_ERROR',
199
+ 2
200
+ );
201
+ }
202
+
203
+ return getPublicIdentity(identity);
204
+ }
205
+
206
+ /**
207
+ * Load and decrypt the keypair from disk
208
+ *
209
+ * Reads the encrypted keypair, derives the decryption key from the password,
210
+ * and returns the full identity with secret keys.
211
+ *
212
+ * @param password - Password to decrypt the keypair
213
+ * @returns Complete identity with secret keys
214
+ * @throws {NoIdentityError} If keypair doesn't exist
215
+ * @throws {InvalidPasswordError} If password is incorrect
216
+ */
217
+ export async function loadKeypair(password: string): Promise<Identity> {
218
+ if (!keypairExists()) {
219
+ throw new NoIdentityError();
220
+ }
221
+
222
+ // Read storage file
223
+ let storage: EncryptedKeypairStorage;
224
+ try {
225
+ const content = readFileSync(getKeypairPath(), 'utf-8');
226
+ storage = JSON.parse(content) as EncryptedKeypairStorage;
227
+ } catch (error) {
228
+ throw new SpacesError(
229
+ `Failed to read keypair: ${
230
+ error instanceof Error ? error.message : 'Unknown error'
231
+ }`,
232
+ 'SYSTEM_ERROR',
233
+ 2
234
+ );
235
+ }
236
+
237
+ // Derive decryption key from password
238
+ const salt = Buffer.from(storage.salt, 'base64');
239
+ const decryptionKey = await deriveKey(password, salt);
240
+
241
+ // Decrypt secrets
242
+ const encryptedSecrets = Buffer.from(storage.encryptedSecrets, 'base64');
243
+ const decryptedSecretsBuffer = open(encryptedSecrets, decryptionKey);
244
+
245
+ if (!decryptedSecretsBuffer) {
246
+ throw new InvalidPasswordError();
247
+ }
248
+
249
+ // Parse secrets
250
+ let secrets: DecryptedSecrets;
251
+ try {
252
+ secrets = JSON.parse(decryptedSecretsBuffer.toString('utf-8')) as DecryptedSecrets;
253
+ } catch (error) {
254
+ throw new SpacesError(
255
+ 'Failed to parse decrypted secrets',
256
+ 'SYSTEM_ERROR',
257
+ 2
258
+ );
259
+ }
260
+
261
+ // Reconstruct stored identity format
262
+ const storedIdentity = {
263
+ id: storage.id,
264
+ label: storage.label,
265
+ createdAt: storage.createdAt,
266
+ signingPublicKey: storage.signingPublicKey,
267
+ keyExchangePublicKey: storage.keyExchangePublicKey,
268
+ signingSecretKey: secrets.signingSecretKey,
269
+ keyExchangePrivateKey: secrets.keyExchangePrivateKey,
270
+ };
271
+
272
+ // Deserialize to Identity format
273
+ return deserializeIdentity(storedIdentity);
274
+ }
275
+
276
+ /**
277
+ * Check if a keypair exists on disk
278
+ *
279
+ * @returns True if keypair.json exists
280
+ */
281
+ export function keypairExists(): boolean {
282
+ return existsSync(getKeypairPath());
283
+ }
284
+
285
+ /**
286
+ * Get the public identity without requiring password
287
+ *
288
+ * Reads only the public keys from the stored keypair file.
289
+ * This is safe to call without authentication.
290
+ *
291
+ * @returns Public identity if keypair exists, null otherwise
292
+ */
293
+ export function getPublicKeyWithoutPassword(): PublicIdentity | null {
294
+ if (!keypairExists()) {
295
+ return null;
296
+ }
297
+
298
+ try {
299
+ const content = readFileSync(getKeypairPath(), 'utf-8');
300
+ const storage = JSON.parse(content) as EncryptedKeypairStorage;
301
+
302
+ return {
303
+ id: storage.id,
304
+ signingPublicKey: storage.signingPublicKey,
305
+ keyExchangePublicKey: storage.keyExchangePublicKey,
306
+ label: storage.label,
307
+ };
308
+ } catch (error) {
309
+ throw new SpacesError(
310
+ `Failed to read public key: ${
311
+ error instanceof Error ? error.message : 'Unknown error'
312
+ }`,
313
+ 'SYSTEM_ERROR',
314
+ 2
315
+ );
316
+ }
317
+ }
318
+
319
+ // ============================================================================
320
+ // Access List Management
321
+ // ============================================================================
322
+
323
+ /**
324
+ * Read the access list from disk
325
+ *
326
+ * Returns an empty array if the file doesn't exist.
327
+ *
328
+ * @returns Array of access entries
329
+ */
330
+ export function readAccessList(): AccessEntry[] {
331
+ const accessListPath = getAccessListPath();
332
+
333
+ if (!existsSync(accessListPath)) {
334
+ return [];
335
+ }
336
+
337
+ try {
338
+ const content = readFileSync(accessListPath, 'utf-8');
339
+ return JSON.parse(content) as AccessEntry[];
340
+ } catch (error) {
341
+ throw new SpacesError(
342
+ `Failed to read access list: ${
343
+ error instanceof Error ? error.message : 'Unknown error'
344
+ }`,
345
+ 'SYSTEM_ERROR',
346
+ 2
347
+ );
348
+ }
349
+ }
350
+
351
+ /**
352
+ * Write the access list to disk
353
+ *
354
+ * Creates the identity directory if it doesn't exist.
355
+ *
356
+ * @param entries - Array of access entries to write
357
+ */
358
+ export function writeAccessList(entries: AccessEntry[]): void {
359
+ ensureIdentityDir();
360
+
361
+ try {
362
+ writeFileSync(
363
+ getAccessListPath(),
364
+ JSON.stringify(entries, null, 2),
365
+ {
366
+ encoding: 'utf-8',
367
+ mode: 0o600, // Owner read/write only
368
+ }
369
+ );
370
+ } catch (error) {
371
+ throw new SpacesError(
372
+ `Failed to write access list: ${
373
+ error instanceof Error ? error.message : 'Unknown error'
374
+ }`,
375
+ 'SYSTEM_ERROR',
376
+ 2
377
+ );
378
+ }
379
+ }
380
+
381
+ /**
382
+ * Add a new identity to the access list
383
+ *
384
+ * Creates a new access entry with the given access type.
385
+ * If the identity already exists, it will be replaced.
386
+ *
387
+ * @param publicIdentity - Public identity to add
388
+ * @param label - Optional label override (uses identity.label if not provided)
389
+ * @param accessType - Access type to grant (defaults to 'full')
390
+ * @param sessionId - Optional session ID for session-invite access
391
+ * @returns The created access entry
392
+ */
393
+ export function addAccess(
394
+ publicIdentity: PublicIdentity,
395
+ label?: string,
396
+ accessType: AccessType = DEFAULT_ACCESS_TYPE,
397
+ sessionId?: string
398
+ ): AccessEntry {
399
+ const entries = readAccessList();
400
+
401
+ // Create new entry
402
+ const newEntry: AccessEntry = {
403
+ identityId: publicIdentity.id,
404
+ signingPublicKey: publicIdentity.signingPublicKey,
405
+ keyExchangePublicKey: publicIdentity.keyExchangePublicKey,
406
+ label: label || publicIdentity.label,
407
+ grantedAt: Date.now(),
408
+ accessType,
409
+ sessionId,
410
+ };
411
+
412
+ // Remove existing entry with same ID (if any)
413
+ const filteredEntries = entries.filter(
414
+ (e) => e.identityId !== publicIdentity.id
415
+ );
416
+
417
+ // Add new entry
418
+ filteredEntries.push(newEntry);
419
+
420
+ // Write back to disk
421
+ writeAccessList(filteredEntries);
422
+
423
+ return newEntry;
424
+ }
425
+
426
+ /**
427
+ * Remove an identity from the access list
428
+ *
429
+ * Searches by identity ID or label (case-insensitive).
430
+ *
431
+ * @param identityIdOrLabel - Identity ID or label to remove
432
+ * @returns True if an entry was removed, false if not found
433
+ */
434
+ export function removeAccess(identityIdOrLabel: string): boolean {
435
+ const entries = readAccessList();
436
+ const searchLower = identityIdOrLabel.toLowerCase();
437
+
438
+ const filteredEntries = entries.filter((e) => {
439
+ const matchesId = e.identityId.toLowerCase() === searchLower;
440
+ const matchesLabel = e.label?.toLowerCase() === searchLower;
441
+ return !matchesId && !matchesLabel;
442
+ });
443
+
444
+ // Check if anything was removed
445
+ if (filteredEntries.length === entries.length) {
446
+ return false;
447
+ }
448
+
449
+ writeAccessList(filteredEntries);
450
+ return true;
451
+ }
452
+
453
+ /**
454
+ * Get an access entry by identity ID or label
455
+ *
456
+ * Searches by identity ID or label (case-insensitive).
457
+ *
458
+ * @param identityIdOrLabel - Identity ID or label to search for
459
+ * @returns Access entry if found, undefined otherwise
460
+ */
461
+ export function getAccessEntry(identityIdOrLabel: string): AccessEntry | undefined {
462
+ const entries = readAccessList();
463
+ const searchLower = identityIdOrLabel.toLowerCase();
464
+
465
+ return entries.find((e) => {
466
+ const matchesId = e.identityId.toLowerCase() === searchLower;
467
+ const matchesLabel = e.label?.toLowerCase() === searchLower;
468
+ return matchesId || matchesLabel;
469
+ });
470
+ }
471
+
472
+ // ============================================================================
473
+ // Machine Identity Management
474
+ // ============================================================================
475
+
476
+ /**
477
+ * Read machine identity configuration from disk
478
+ *
479
+ * @returns Machine identity if exists, null otherwise
480
+ */
481
+ export function readMachineIdentity(): MachineIdentity | null {
482
+ const machineIdentityPath = getMachineIdentityPath();
483
+
484
+ if (!existsSync(machineIdentityPath)) {
485
+ return null;
486
+ }
487
+
488
+ try {
489
+ const content = readFileSync(machineIdentityPath, 'utf-8');
490
+ return JSON.parse(content) as MachineIdentity;
491
+ } catch (error) {
492
+ throw new SpacesError(
493
+ `Failed to read machine identity: ${
494
+ error instanceof Error ? error.message : 'Unknown error'
495
+ }`,
496
+ 'SYSTEM_ERROR',
497
+ 2
498
+ );
499
+ }
500
+ }
501
+
502
+ /**
503
+ * Write machine identity configuration to disk
504
+ *
505
+ * Creates the identity directory if it doesn't exist.
506
+ *
507
+ * @param identity - Machine identity to write
508
+ */
509
+ export function writeMachineIdentity(identity: MachineIdentity): void {
510
+ ensureIdentityDir();
511
+
512
+ try {
513
+ writeFileSync(
514
+ getMachineIdentityPath(),
515
+ JSON.stringify(identity, null, 2),
516
+ {
517
+ encoding: 'utf-8',
518
+ mode: 0o600, // Owner read/write only
519
+ }
520
+ );
521
+ } catch (error) {
522
+ throw new SpacesError(
523
+ `Failed to write machine identity: ${
524
+ error instanceof Error ? error.message : 'Unknown error'
525
+ }`,
526
+ 'SYSTEM_ERROR',
527
+ 2
528
+ );
529
+ }
530
+ }
531
+
532
+ // ============================================================================
533
+ // Relay Configuration Management
534
+ // ============================================================================
535
+
536
+ /**
537
+ * Relay configuration for coordination between serve/share/access commands
538
+ *
539
+ * Note: Authentication is now done via challenge-response (no JWT tokens)
540
+ */
541
+ export interface RelayConfig {
542
+ /** Relay WebSocket URL */
543
+ relayUrl: string;
544
+ /** Machine ID registered with relay */
545
+ machineId: string;
546
+ /** When this config was saved */
547
+ savedAt: number;
548
+ }
549
+
550
+ /**
551
+ * Get the relay config file path
552
+ *
553
+ * @returns Path to relay.json
554
+ */
555
+ export function getRelayConfigPath(): string {
556
+ return join(getIdentityDir(), 'relay.json');
557
+ }
558
+
559
+ /**
560
+ * Read relay configuration from disk
561
+ *
562
+ * @returns Relay config if exists, null otherwise
563
+ */
564
+ export function readRelayConfig(): RelayConfig | null {
565
+ const relayConfigPath = getRelayConfigPath();
566
+
567
+ if (!existsSync(relayConfigPath)) {
568
+ return null;
569
+ }
570
+
571
+ try {
572
+ const content = readFileSync(relayConfigPath, 'utf-8');
573
+ return JSON.parse(content) as RelayConfig;
574
+ } catch (error) {
575
+ throw new SpacesError(
576
+ `Failed to read relay config: ${
577
+ error instanceof Error ? error.message : 'Unknown error'
578
+ }`,
579
+ 'SYSTEM_ERROR',
580
+ 2
581
+ );
582
+ }
583
+ }
584
+
585
+ /**
586
+ * Write relay configuration to disk
587
+ *
588
+ * Creates the identity directory if it doesn't exist.
589
+ *
590
+ * @param config - Relay configuration to write
591
+ */
592
+ export function writeRelayConfig(config: RelayConfig): void {
593
+ ensureIdentityDir();
594
+
595
+ try {
596
+ writeFileSync(
597
+ getRelayConfigPath(),
598
+ JSON.stringify(config, null, 2),
599
+ {
600
+ encoding: 'utf-8',
601
+ mode: 0o600, // Owner read/write only
602
+ }
603
+ );
604
+ } catch (error) {
605
+ throw new SpacesError(
606
+ `Failed to write relay config: ${
607
+ error instanceof Error ? error.message : 'Unknown error'
608
+ }`,
609
+ 'SYSTEM_ERROR',
610
+ 2
611
+ );
612
+ }
613
+ }
614
+
615
+ /**
616
+ * Clear relay configuration
617
+ *
618
+ * Removes the relay config file if it exists.
619
+ */
620
+ export function clearRelayConfig(): void {
621
+ const relayConfigPath = getRelayConfigPath();
622
+
623
+ if (existsSync(relayConfigPath)) {
624
+ try {
625
+ const { unlinkSync } = require('node:fs');
626
+ unlinkSync(relayConfigPath);
627
+ } catch (error) {
628
+ // Ignore errors when clearing
629
+ }
630
+ }
631
+ }