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,396 @@
1
+ /**
2
+ * Host commands for gitspace.sh hosting
3
+ *
4
+ * Handles subdomain management: reserve, release, list, set-primary, status
5
+ */
6
+
7
+ import { existsSync, writeFileSync, readFileSync } from 'fs';
8
+ import { join } from 'path';
9
+ import { getSecret, setSecret, deleteSecret } from '../utils/secrets.js';
10
+ import { getSpacesDir } from '../core/config.js';
11
+ import { getPublicKeyWithoutPassword } from '../core/identity.js';
12
+ import { logger } from '../utils/logger.js';
13
+ import { SpacesError } from '../types/errors.js';
14
+
15
+ // API Configuration
16
+ const API_BASE = process.env.GITSPACE_API_URL || 'https://api.gitspace.sh';
17
+
18
+ // ============================================================================
19
+ // Types
20
+ // ============================================================================
21
+
22
+ /**
23
+ * Host configuration stored in ~/.gitspace/host.json
24
+ * Non-sensitive data only - tunnel tokens are in keychain
25
+ */
26
+ export interface HostConfig {
27
+ subdomain: string;
28
+ subdomains?: string[];
29
+ createdAt: number;
30
+ }
31
+
32
+ interface SubdomainInfo {
33
+ id: string;
34
+ subdomain: string;
35
+ status: string;
36
+ is_primary: number;
37
+ created_at: number;
38
+ updated_at: number;
39
+ }
40
+
41
+ interface SubdomainCreateResponse {
42
+ id: string;
43
+ subdomain: string;
44
+ hosts: string[];
45
+ isPrimary: boolean;
46
+ }
47
+
48
+ // ============================================================================
49
+ // Host Config Management
50
+ // ============================================================================
51
+
52
+ /**
53
+ * Get the host config file path
54
+ */
55
+ function getHostConfigPath(): string {
56
+ return join(getSpacesDir(), 'host.json');
57
+ }
58
+
59
+ /**
60
+ * Read host config from disk
61
+ */
62
+ export function readHostConfig(): HostConfig | null {
63
+ const configPath = getHostConfigPath();
64
+ if (!existsSync(configPath)) {
65
+ return null;
66
+ }
67
+
68
+ try {
69
+ const content = readFileSync(configPath, 'utf-8');
70
+ return JSON.parse(content) as HostConfig;
71
+ } catch {
72
+ return null;
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Write host config to disk
78
+ */
79
+ function writeHostConfig(config: HostConfig): void {
80
+ const configPath = getHostConfigPath();
81
+ writeFileSync(configPath, JSON.stringify(config, null, 2), {
82
+ encoding: 'utf-8',
83
+ mode: 0o600,
84
+ });
85
+ }
86
+
87
+ /**
88
+ * Update host config after subdomain changes
89
+ */
90
+ async function syncHostConfig(): Promise<void> {
91
+ const token = await getSecret('GITSPACE_TOKEN');
92
+ if (!token) return;
93
+
94
+ try {
95
+ const headers = await getAuthHeaders();
96
+ const res = await fetch(`${API_BASE}/subdomains`, { headers });
97
+
98
+ if (!res.ok) return;
99
+
100
+ const subdomains: SubdomainInfo[] = await res.json();
101
+ const activeSubdomains = subdomains.filter((s) => s.status === 'active');
102
+ const primary = activeSubdomains.find((s) => s.is_primary);
103
+
104
+ if (primary) {
105
+ writeHostConfig({
106
+ subdomain: primary.subdomain,
107
+ subdomains: activeSubdomains.map((s) => s.subdomain),
108
+ createdAt: primary.created_at,
109
+ });
110
+ }
111
+ } catch {
112
+ // Ignore sync errors
113
+ }
114
+ }
115
+
116
+ // ============================================================================
117
+ // Helper: Get Auth Token
118
+ // ============================================================================
119
+
120
+ async function getAuthToken(): Promise<string> {
121
+ const token = await getSecret('GITSPACE_TOKEN');
122
+ if (!token) {
123
+ throw new SpacesError(
124
+ 'Not logged in.\n\nRun: gssh auth login',
125
+ 'USER_ERROR'
126
+ );
127
+ }
128
+ return token;
129
+ }
130
+
131
+ async function getAuthHeaders(
132
+ extra: Record<string, string> = {}
133
+ ): Promise<Record<string, string>> {
134
+ const token = await getAuthToken();
135
+ const identity = getPublicKeyWithoutPassword();
136
+ if (!identity) {
137
+ throw new SpacesError(
138
+ 'Identity not found.\n\nRun: gssh identity init',
139
+ 'USER_ERROR',
140
+ 1
141
+ );
142
+ }
143
+ return {
144
+ Authorization: `Bearer ${token}`,
145
+ 'X-Device-Fingerprint': identity.signingPublicKey,
146
+ ...extra,
147
+ };
148
+ }
149
+
150
+ // ============================================================================
151
+ // Reserve Subdomain
152
+ // ============================================================================
153
+
154
+ /**
155
+ * Reserve a subdomain on gitspace.sh
156
+ */
157
+ export async function hostReserve(subdomain: string): Promise<void> {
158
+ const headers = await getAuthHeaders();
159
+ const jsonHeaders = { ...headers, 'Content-Type': 'application/json' };
160
+
161
+ // Normalize subdomain
162
+ subdomain = subdomain.toLowerCase().trim();
163
+
164
+ // Check availability
165
+ logger.info('Checking availability...');
166
+ const checkRes = await fetch(
167
+ `${API_BASE}/subdomains/check?name=${encodeURIComponent(subdomain)}`,
168
+ {
169
+ headers,
170
+ }
171
+ );
172
+
173
+ if (!checkRes.ok) {
174
+ throw new SpacesError(
175
+ `Failed to check availability: ${checkRes.statusText}`,
176
+ 'SYSTEM_ERROR'
177
+ );
178
+ }
179
+
180
+ const { available, reason } = await checkRes.json();
181
+ if (!available) {
182
+ throw new SpacesError(
183
+ `Subdomain "${subdomain}" is not available: ${reason}`,
184
+ 'USER_ERROR'
185
+ );
186
+ }
187
+
188
+ // Reserve
189
+ logger.info('Creating tunnel...');
190
+ const res = await fetch(`${API_BASE}/subdomains`, {
191
+ method: 'POST',
192
+ headers: jsonHeaders,
193
+ body: JSON.stringify({ subdomain }),
194
+ });
195
+
196
+ if (!res.ok) {
197
+ const error = await res.json().catch(() => ({ error: res.statusText }));
198
+ throw new SpacesError(`Failed to reserve: ${error.error}`, 'USER_ERROR');
199
+ }
200
+
201
+ const data: SubdomainCreateResponse = await res.json();
202
+
203
+ logger.info('Configuring DNS...');
204
+
205
+ // Fetch and store tunnel token in keychain
206
+ logger.info('Saving credentials...');
207
+ const tokenRes = await fetch(
208
+ `${API_BASE}/subdomains/${subdomain}/token`,
209
+ {
210
+ headers,
211
+ }
212
+ );
213
+
214
+ if (!tokenRes.ok) {
215
+ throw new SpacesError('Failed to get tunnel token', 'SYSTEM_ERROR');
216
+ }
217
+
218
+ const { tunnelToken } = await tokenRes.json();
219
+ await setSecret(`TUNNEL_TOKEN_${subdomain}`, tunnelToken);
220
+
221
+ // Update local host config
222
+ await syncHostConfig();
223
+
224
+ logger.log('');
225
+ logger.success(`Reserved: ${data.subdomain}.gitspace.sh`);
226
+ logger.log(` Wildcard: *.${data.subdomain}.gitspace.sh`);
227
+ if (data.isPrimary) {
228
+ logger.dim(' (set as primary)');
229
+ }
230
+
231
+ logger.log('');
232
+ logger.log("Run 'spaces' to start hosting.");
233
+ }
234
+
235
+ // ============================================================================
236
+ // Release Subdomain
237
+ // ============================================================================
238
+
239
+ /**
240
+ * Release a subdomain
241
+ */
242
+ export async function hostRelease(subdomain?: string): Promise<void> {
243
+ const headers = await getAuthHeaders();
244
+
245
+ // If no subdomain specified, show list and exit
246
+ if (!subdomain) {
247
+ logger.log('Please specify a subdomain to release:');
248
+ logger.command(' gssh host release <subdomain>');
249
+ logger.log('');
250
+ logger.log('To see your subdomains:');
251
+ logger.command(' gssh host list');
252
+ return;
253
+ }
254
+
255
+ subdomain = subdomain.toLowerCase().trim();
256
+
257
+ const res = await fetch(`${API_BASE}/subdomains/${subdomain}`, {
258
+ method: 'DELETE',
259
+ headers,
260
+ });
261
+
262
+ if (!res.ok) {
263
+ const error = await res.json().catch(() => ({ error: res.statusText }));
264
+ throw new SpacesError(`Failed to release: ${error.error}`, 'USER_ERROR');
265
+ }
266
+
267
+ // Clear local tunnel token
268
+ await deleteSecret(`TUNNEL_TOKEN_${subdomain}`);
269
+
270
+ // Update local host config
271
+ await syncHostConfig();
272
+
273
+ logger.success(`Released: ${subdomain}.gitspace.sh`);
274
+ }
275
+
276
+ // ============================================================================
277
+ // List Subdomains
278
+ // ============================================================================
279
+
280
+ /**
281
+ * List user's subdomains
282
+ */
283
+ export async function hostList(): Promise<void> {
284
+ const headers = await getAuthHeaders();
285
+
286
+ const res = await fetch(`${API_BASE}/subdomains`, { headers });
287
+
288
+ if (!res.ok) {
289
+ throw new SpacesError(
290
+ `Failed to list subdomains: ${res.statusText}`,
291
+ 'SYSTEM_ERROR'
292
+ );
293
+ }
294
+
295
+ const subdomains: SubdomainInfo[] = await res.json();
296
+
297
+ if (subdomains.length === 0) {
298
+ logger.log('No subdomains reserved.');
299
+ logger.log('');
300
+ logger.log('Reserve one with:');
301
+ logger.command(' gssh host reserve <name>');
302
+ return;
303
+ }
304
+
305
+ logger.log('Your subdomains:\n');
306
+ for (const sub of subdomains) {
307
+ const primary = sub.is_primary ? ' (primary)' : '';
308
+ const status = sub.status === 'active' ? '\u2713' : '\u2717';
309
+ logger.log(` ${status} ${sub.subdomain}.gitspace.sh${primary}`);
310
+ logger.dim(` Created: ${new Date(sub.created_at).toLocaleDateString()}`);
311
+ }
312
+
313
+ logger.log(`\n${subdomains.length}/3 subdomains used (free tier)`);
314
+ }
315
+
316
+ // ============================================================================
317
+ // Set Primary
318
+ // ============================================================================
319
+
320
+ /**
321
+ * Set a subdomain as primary for `gssh serve`
322
+ */
323
+ export async function hostSetPrimary(subdomain: string): Promise<void> {
324
+ const headers = await getAuthHeaders();
325
+ subdomain = subdomain.toLowerCase().trim();
326
+
327
+ const res = await fetch(`${API_BASE}/subdomains/${subdomain}/set-primary`, {
328
+ method: 'POST',
329
+ headers,
330
+ });
331
+
332
+ if (!res.ok) {
333
+ const error = await res.json().catch(() => ({ error: res.statusText }));
334
+ throw new SpacesError(`Failed: ${error.error}`, 'USER_ERROR');
335
+ }
336
+
337
+ // Update local host config
338
+ await syncHostConfig();
339
+
340
+ logger.success(`${subdomain}.gitspace.sh is now your primary subdomain`);
341
+ }
342
+
343
+ // ============================================================================
344
+ // Status
345
+ // ============================================================================
346
+
347
+ /**
348
+ * Show hosting status
349
+ */
350
+ export async function hostStatus(): Promise<void> {
351
+ let headers: Record<string, string>;
352
+ try {
353
+ headers = await getAuthHeaders();
354
+ } catch {
355
+ logger.log('Not logged in or identity not found');
356
+ logger.dim('Run: gssh auth login');
357
+ return;
358
+ }
359
+
360
+ try {
361
+ const res = await fetch(`${API_BASE}/subdomains`, { headers });
362
+
363
+ if (!res.ok) {
364
+ logger.log('Could not fetch subdomains');
365
+ return;
366
+ }
367
+
368
+ const subdomains: SubdomainInfo[] = await res.json();
369
+ const primary = subdomains.find((s) => s.is_primary && s.status === 'active');
370
+
371
+ if (!primary) {
372
+ logger.log('No primary subdomain set.');
373
+ logger.dim('Run: gssh host reserve <name>');
374
+ return;
375
+ }
376
+
377
+ logger.log(`Primary: ${primary.subdomain}.gitspace.sh`);
378
+ logger.log(`Status: ${primary.status}`);
379
+
380
+ // Check if tunnel token exists locally
381
+ const tunnelToken = await getSecret(`TUNNEL_TOKEN_${primary.subdomain}`);
382
+ logger.log(`Tunnel token: ${tunnelToken ? 'configured' : 'missing'}`);
383
+
384
+ if (!tunnelToken) {
385
+ logger.dim('Run: gssh host reserve ' + primary.subdomain + ' (to refresh token)');
386
+ }
387
+ } catch {
388
+ logger.log('Could not verify status (API unreachable)');
389
+
390
+ // Show local config
391
+ const hostConfig = readHostConfig();
392
+ if (hostConfig) {
393
+ logger.log(`Local config: ${hostConfig.subdomain}.gitspace.sh`);
394
+ }
395
+ }
396
+ }
@@ -0,0 +1,184 @@
1
+ /**
2
+ * Identity command implementation
3
+ * Handles 'gssh identity init' and 'gssh identity show'
4
+ */
5
+
6
+ import { createHash } from 'crypto';
7
+ import { logger } from '../utils/logger.js';
8
+ import { promptPassword, promptInput, promptConfirm } from '../utils/prompts.js';
9
+ import {
10
+ generateAndSaveKeypair,
11
+ loadKeypair,
12
+ keypairExists,
13
+ getPublicKeyWithoutPassword,
14
+ } from '../core/identity.js';
15
+ import {
16
+ NoIdentityError,
17
+ IdentityExistsError,
18
+ SpacesError,
19
+ } from '../types/errors.js';
20
+
21
+ /**
22
+ * Initialize a new identity keypair
23
+ */
24
+ export async function initIdentity(options: { force?: boolean } = {}): Promise<void> {
25
+ // Check if keypair already exists
26
+ if (keypairExists() && !options.force) {
27
+ throw new IdentityExistsError();
28
+ }
29
+
30
+ // If force flag is set and keypair exists, confirm
31
+ if (options.force && keypairExists()) {
32
+ const confirmed = await promptConfirm(
33
+ 'This will overwrite your existing identity. Are you sure?',
34
+ false
35
+ );
36
+
37
+ if (!confirmed) {
38
+ logger.info('Cancelled');
39
+ return;
40
+ }
41
+ }
42
+
43
+ // Prompt for password (twice for confirmation)
44
+ const password = await promptPassword('Enter password to encrypt your identity:');
45
+
46
+ if (!password) {
47
+ logger.info('Cancelled');
48
+ return;
49
+ }
50
+
51
+ if (password.length < 8) {
52
+ throw new SpacesError(
53
+ 'Password must be at least 8 characters long',
54
+ 'USER_ERROR',
55
+ 1
56
+ );
57
+ }
58
+
59
+ const confirmPassword = await promptPassword('Confirm password:');
60
+
61
+ if (!confirmPassword) {
62
+ logger.info('Cancelled');
63
+ return;
64
+ }
65
+
66
+ if (password !== confirmPassword) {
67
+ throw new SpacesError(
68
+ 'Passwords do not match',
69
+ 'USER_ERROR',
70
+ 1
71
+ );
72
+ }
73
+
74
+ // Prompt for optional label
75
+ const label = await promptInput('Enter an optional label for this identity (e.g., "My Laptop"):', {
76
+ default: '',
77
+ });
78
+
79
+ // Generate and save keypair
80
+ logger.info('Generating keypair...');
81
+ const identity = await generateAndSaveKeypair(
82
+ password,
83
+ label || undefined,
84
+ options.force || false
85
+ );
86
+
87
+ logger.success('Identity created successfully');
88
+
89
+ // Display public key info (identity is PublicIdentity, keys are base64 strings)
90
+ const signingKeyBytes = Buffer.from(identity.signingPublicKey, 'base64');
91
+ const keyExchangeKeyBytes = Buffer.from(identity.keyExchangePublicKey, 'base64');
92
+ const fingerprint = formatFingerprint(signingKeyBytes);
93
+ const publicKeyString = formatPublicKey(signingKeyBytes, keyExchangeKeyBytes);
94
+
95
+ logger.log('');
96
+ logger.bold('Identity Information:');
97
+ logger.log(` ID: ${identity.id}`);
98
+ logger.log(` Fingerprint: ${fingerprint}`);
99
+ if (identity.label) {
100
+ logger.log(` Label: ${identity.label}`);
101
+ }
102
+ logger.log('');
103
+ logger.bold('Public Key:');
104
+ logger.log(` ${publicKeyString}`);
105
+ logger.log('');
106
+ logger.dim('Keep your password safe. You will need it to use this identity.');
107
+ }
108
+
109
+ /**
110
+ * Show identity information
111
+ */
112
+ export async function showIdentity(
113
+ options: { fingerprint?: boolean; json?: boolean } = {}
114
+ ): Promise<void> {
115
+ // Check if keypair exists
116
+ if (!keypairExists()) {
117
+ throw new NoIdentityError();
118
+ }
119
+
120
+ // Read public key (no password needed)
121
+ const publicIdentity = getPublicKeyWithoutPassword();
122
+
123
+ if (!publicIdentity) {
124
+ throw new NoIdentityError();
125
+ }
126
+
127
+ // JSON output
128
+ if (options.json) {
129
+ console.log(JSON.stringify(publicIdentity, null, 2));
130
+ return;
131
+ }
132
+
133
+ // Fingerprint output
134
+ if (options.fingerprint) {
135
+ const signingPublicKeyBytes = Buffer.from(publicIdentity.signingPublicKey, 'base64');
136
+ const fingerprint = formatFingerprint(signingPublicKeyBytes);
137
+ logger.log(fingerprint);
138
+ return;
139
+ }
140
+
141
+ // Default output: full public key
142
+ const publicKeyString = formatPublicKey(
143
+ Buffer.from(publicIdentity.signingPublicKey, 'base64'),
144
+ Buffer.from(publicIdentity.keyExchangePublicKey, 'base64')
145
+ );
146
+
147
+ logger.bold('Identity Information:');
148
+ logger.log(` ID: ${publicIdentity.id}`);
149
+ if (publicIdentity.label) {
150
+ logger.log(` Label: ${publicIdentity.label}`);
151
+ }
152
+ logger.log('');
153
+ logger.bold('Public Key:');
154
+ logger.log(` ${publicKeyString}`);
155
+ logger.log('');
156
+ logger.bold('Fingerprint:');
157
+ logger.log(` ${formatFingerprint(Buffer.from(publicIdentity.signingPublicKey, 'base64'))}`);
158
+ }
159
+
160
+ /**
161
+ * Format fingerprint as first 16 hex chars of SHA-256 hash with colons
162
+ */
163
+ function formatFingerprint(signingPublicKey: Uint8Array): string {
164
+ const hash = createHash('sha256').update(signingPublicKey).digest('hex');
165
+ const first16 = hash.substring(0, 16);
166
+
167
+ // Add colons every 2 characters
168
+ const parts: string[] = [];
169
+ for (let i = 0; i < first16.length; i += 2) {
170
+ parts.push(first16.substring(i, i + 2));
171
+ }
172
+
173
+ return parts.join(':');
174
+ }
175
+
176
+ /**
177
+ * Format public key as gssh-pub:BASE64_SIGNING:BASE64_KEYEXCHANGE
178
+ */
179
+ function formatPublicKey(signingPublicKey: Uint8Array, keyExchangePublicKey: Uint8Array): string {
180
+ const signingB64 = Buffer.from(signingPublicKey).toString('base64');
181
+ const keyExchangeB64 = Buffer.from(keyExchangePublicKey).toString('base64');
182
+
183
+ return `gssh-pub:${signingB64}:${keyExchangeB64}`;
184
+ }