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,298 @@
1
+ /**
2
+ * Access control command implementations
3
+ * Handles 'gssh access add', 'gssh access list', and 'gssh access remove'
4
+ */
5
+
6
+ import chalk from 'chalk';
7
+ import { logger } from '../utils/logger.js';
8
+ import { promptInput, promptConfirm } from '../utils/prompts.js';
9
+ import {
10
+ readAccessList,
11
+ addAccess,
12
+ removeAccess,
13
+ getAccessEntry,
14
+ parsePublicKey,
15
+ formatFingerprint,
16
+ formatAccessType,
17
+ } from '../core/access.js';
18
+ import type { AccessEntry } from '../types/identity.js';
19
+ import { SpacesError } from '../types/errors.js';
20
+ import {
21
+ isServeRunning,
22
+ sendAddAccessCommand,
23
+ sendRemoveAccessCommand,
24
+ } from '../serve/daemon.js';
25
+
26
+ /**
27
+ * Sync access change to relay via serve daemon
28
+ * Falls back to local-only if daemon not running
29
+ *
30
+ * @param action - 'add' or 'remove'
31
+ * @param entry - Access entry being modified
32
+ * @returns true if synced to relay, false if local-only
33
+ */
34
+ async function syncToRelay(
35
+ action: 'add' | 'remove',
36
+ entry: AccessEntry
37
+ ): Promise<boolean> {
38
+ if (!isServeRunning()) {
39
+ logger.dim('(serve daemon not running - saved locally only)');
40
+ return false;
41
+ }
42
+
43
+ try {
44
+ if (action === 'add') {
45
+ const result = await sendAddAccessCommand({
46
+ clientIdentityId: entry.identityId,
47
+ signingKey: entry.signingPublicKey,
48
+ keyExchangeKey: entry.keyExchangePublicKey || '',
49
+ label: entry.label,
50
+ accessType: entry.accessType,
51
+ sessionId: entry.sessionId,
52
+ });
53
+
54
+ if (result.success) {
55
+ logger.dim('(synced to relay)');
56
+ return true;
57
+ } else {
58
+ logger.dim(`(relay sync failed: ${result.error})`);
59
+ return false;
60
+ }
61
+ } else {
62
+ const result = await sendRemoveAccessCommand(entry.identityId);
63
+
64
+ if (result.success) {
65
+ logger.dim('(synced to relay)');
66
+ return true;
67
+ } else {
68
+ logger.dim(`(relay sync failed: ${result.error})`);
69
+ return false;
70
+ }
71
+ }
72
+ } catch (err) {
73
+ logger.dim(`(relay sync error: ${err instanceof Error ? err.message : 'unknown'})`);
74
+ return false;
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Add a new access key (grants full access)
80
+ *
81
+ * @param pubkey - Public key string (gssh-pub:SIGNING:KEYEXCHANGE or just SIGNING)
82
+ * @param options - Command options
83
+ */
84
+ export async function addAccessKey(
85
+ pubkey: string,
86
+ options: {
87
+ label?: string;
88
+ }
89
+ ): Promise<void> {
90
+ // Parse the public key
91
+ let publicIdentity;
92
+ try {
93
+ publicIdentity = parsePublicKey(pubkey);
94
+ } catch (error) {
95
+ if (error instanceof SpacesError) {
96
+ throw error;
97
+ }
98
+ throw new SpacesError(
99
+ `Invalid public key format: ${error instanceof Error ? error.message : String(error)}`,
100
+ 'USER_ERROR',
101
+ 1
102
+ );
103
+ }
104
+
105
+ // Check if key exchange key is missing (only signing key was provided)
106
+ if (!publicIdentity.keyExchangePublicKey) {
107
+ logger.warning('Only signing key provided. Key exchange key is required for full functionality.');
108
+ const keyExchangeKey = await promptInput(
109
+ 'Enter key exchange public key (base64, or press Enter to skip):'
110
+ );
111
+ if (keyExchangeKey && keyExchangeKey.trim()) {
112
+ publicIdentity.keyExchangePublicKey = keyExchangeKey.trim();
113
+ } else {
114
+ logger.warning('Proceeding without key exchange key. Encrypted connections will not be possible.');
115
+ }
116
+ }
117
+
118
+ // Get label if not provided
119
+ let label = options.label;
120
+ if (!label) {
121
+ const input = await promptInput('Enter a label for this key (optional):');
122
+ label = input || undefined;
123
+ }
124
+
125
+ // Check if this identity already exists
126
+ const existing = getAccessEntry(publicIdentity.id);
127
+ if (existing) {
128
+ logger.warning(`Identity ${formatFingerprint(publicIdentity.id)} already exists with label "${existing.label}"`);
129
+ const replace = await promptConfirm('Replace existing entry?', false);
130
+ if (!replace) {
131
+ logger.info('Cancelled');
132
+ return;
133
+ }
134
+ }
135
+
136
+ // Add to access list with full access
137
+ const entry = addAccess(publicIdentity, label, 'full');
138
+
139
+ logger.success('Access key added');
140
+ logger.log(` ID: ${chalk.cyan(entry.identityId)}`);
141
+ logger.log(` Fingerprint: ${chalk.dim(formatFingerprint(entry.identityId))}`);
142
+ if (entry.label) {
143
+ logger.log(` Label: ${chalk.yellow(entry.label)}`);
144
+ }
145
+ logger.log(` Access: ${formatAccessType(entry.accessType)}`);
146
+
147
+ // Sync to relay (if serve daemon running)
148
+ await syncToRelay('add', entry);
149
+ }
150
+
151
+ /**
152
+ * List all access keys
153
+ *
154
+ * @param options - Command options
155
+ */
156
+ export async function listAccessKeys(
157
+ options: {
158
+ json?: boolean;
159
+ } = {}
160
+ ): Promise<void> {
161
+ const entries = readAccessList();
162
+
163
+ if (entries.length === 0) {
164
+ if (options.json) {
165
+ console.log(JSON.stringify([], null, 2));
166
+ return;
167
+ }
168
+
169
+ logger.info('No access keys configured');
170
+ logger.log('\nAdd a key:\n gssh access add <pubkey>');
171
+ return;
172
+ }
173
+
174
+ if (options.json) {
175
+ console.log(JSON.stringify(entries, null, 2));
176
+ return;
177
+ }
178
+
179
+ // Display as table
180
+ logger.bold('Access Keys:');
181
+ logger.log('');
182
+
183
+ // Header
184
+ const labelWidth = 18;
185
+ const idWidth = 18;
186
+ const accessWidth = 16;
187
+ const dateWidth = 12;
188
+
189
+ logger.dim(
190
+ 'Label'.padEnd(labelWidth) +
191
+ 'ID'.padEnd(idWidth) +
192
+ 'Access'.padEnd(accessWidth) +
193
+ 'Added'
194
+ );
195
+ logger.dim('─'.repeat(labelWidth + idWidth + accessWidth + dateWidth));
196
+
197
+ // Entries
198
+ for (const entry of entries) {
199
+ const label = (entry.label || '<no label>').substring(0, labelWidth - 1);
200
+ const id = formatFingerprint(entry.identityId);
201
+ const access = formatAccessType(entry.accessType, entry.sessionId);
202
+ const date = new Date(entry.grantedAt).toISOString().split('T')[0];
203
+
204
+ const labelCol = label.padEnd(labelWidth);
205
+ const idCol = chalk.cyan(id).padEnd(idWidth + 9); // +9 for ANSI color codes
206
+ const accessCol = access.padEnd(accessWidth);
207
+ const dateCol = chalk.dim(date);
208
+
209
+ logger.log(labelCol + idCol + accessCol + dateCol);
210
+ }
211
+
212
+ logger.log('');
213
+ logger.dim(`Total: ${entries.length} key(s)`);
214
+ }
215
+
216
+ /**
217
+ * Remove an access key
218
+ *
219
+ * @param pubkeyOrLabel - Public key, identity ID prefix, or label
220
+ * @param options - Command options
221
+ */
222
+ export async function removeAccessKey(
223
+ pubkeyOrLabel: string,
224
+ options: {
225
+ force?: boolean;
226
+ }
227
+ ): Promise<void> {
228
+ // Try to find the entry
229
+ let entry = getAccessEntry(pubkeyOrLabel);
230
+
231
+ // If not found, try parsing as a public key
232
+ if (!entry) {
233
+ try {
234
+ const publicIdentity = parsePublicKey(pubkeyOrLabel);
235
+ entry = getAccessEntry(publicIdentity.id);
236
+ } catch {
237
+ // Not a valid public key, continue
238
+ }
239
+ }
240
+
241
+ if (!entry) {
242
+ // Provide helpful suggestions
243
+ const entries = readAccessList();
244
+ if (entries.length === 0) {
245
+ throw new SpacesError(
246
+ 'No access keys configured',
247
+ 'USER_ERROR',
248
+ 1
249
+ );
250
+ }
251
+
252
+ logger.error(`No access key found matching: ${pubkeyOrLabel}`);
253
+ logger.log('\nAvailable keys:');
254
+ for (const e of entries) {
255
+ logger.log(` ${e.label || '<no label>'} (${formatFingerprint(e.identityId)})`);
256
+ }
257
+ throw new SpacesError('Key not found', 'USER_ERROR', 1);
258
+ }
259
+
260
+ // Confirm removal unless --force
261
+ if (!options.force) {
262
+ logger.log('Found access key:');
263
+ logger.log(` ID: ${chalk.cyan(entry.identityId)}`);
264
+ logger.log(` Fingerprint: ${chalk.dim(formatFingerprint(entry.identityId))}`);
265
+ if (entry.label) {
266
+ logger.log(` Label: ${chalk.yellow(entry.label)}`);
267
+ }
268
+ logger.log(` Access: ${formatAccessType(entry.accessType, entry.sessionId)}`);
269
+ logger.log('');
270
+
271
+ const confirmed = await promptConfirm('Remove this key?', false);
272
+ if (!confirmed) {
273
+ logger.info('Cancelled');
274
+ return;
275
+ }
276
+ }
277
+
278
+ // Sync removal to relay (before local remove, so we have entry data)
279
+ await syncToRelay('remove', entry);
280
+
281
+ // Remove the entry locally
282
+ const removed = removeAccess(entry.identityId);
283
+
284
+ if (!removed) {
285
+ throw new SpacesError(
286
+ 'Failed to remove key (this should not happen)',
287
+ 'SYSTEM_ERROR',
288
+ 2
289
+ );
290
+ }
291
+
292
+ logger.success('Access key removed');
293
+ if (removed.label) {
294
+ logger.log(` Removed: ${removed.label} (${formatFingerprint(removed.identityId)})`);
295
+ } else {
296
+ logger.log(` Removed: ${formatFingerprint(removed.identityId)}`);
297
+ }
298
+ }