gitspace 0.2.0-rc.2 → 0.2.0-rc.21

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