gitspace 0.2.0-rc.20 → 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 (474) hide show
  1. package/package.json +11 -6
  2. package/.claude/settings.local.json +0 -10
  3. package/.gitspace/bundle.json +0 -50
  4. package/.gitspace/events.json +0 -11
  5. package/.gitspace/processes.json +0 -23
  6. package/.gitspace/scripts/select/01-status.sh +0 -39
  7. package/.gitspace/scripts/setup/01-install-deps.sh +0 -12
  8. package/.gitspace/scripts/setup/02-typecheck.sh +0 -16
  9. package/AGENTS.md +0 -469
  10. package/CLAUDE.md +0 -1
  11. package/bun.lock +0 -794
  12. package/docs/CONNECTION.md +0 -623
  13. package/docs/GATEWAY-WORKER.md +0 -319
  14. package/docs/GETTING-STARTED.md +0 -448
  15. package/docs/GITSPACE-PLATFORM.md +0 -1819
  16. package/docs/INFRASTRUCTURE.md +0 -1347
  17. package/docs/PROTOCOL.md +0 -619
  18. package/docs/QUICKSTART.md +0 -183
  19. package/docs/RELAY.md +0 -327
  20. package/docs/REMOTE-DESIGN.md +0 -554
  21. package/docs/ROADMAP.md +0 -564
  22. package/docs/SITE_DOCS_FIGMA_MAKE.md +0 -1176
  23. package/docs/STACK-DESIGN.md +0 -588
  24. package/docs/UNIFIED_ARCHITECTURE.md +0 -138
  25. package/experiments/pty-benchmark.ts +0 -148
  26. package/experiments/pty-latency.ts +0 -100
  27. package/experiments/router/client.ts +0 -199
  28. package/experiments/router/protocol.ts +0 -74
  29. package/experiments/router/router.ts +0 -217
  30. package/experiments/router/session.ts +0 -180
  31. package/experiments/router/test.ts +0 -133
  32. package/experiments/socket-bandwidth.ts +0 -77
  33. package/homebrew/gitspace.rb +0 -45
  34. package/landing-page/ATTRIBUTIONS.md +0 -3
  35. package/landing-page/README.md +0 -11
  36. package/landing-page/bun.lock +0 -801
  37. package/landing-page/guidelines/Guidelines.md +0 -61
  38. package/landing-page/index.html +0 -37
  39. package/landing-page/package.json +0 -90
  40. package/landing-page/postcss.config.mjs +0 -15
  41. package/landing-page/public/_redirects +0 -1
  42. package/landing-page/public/favicon.png +0 -0
  43. package/landing-page/src/app/App.tsx +0 -53
  44. package/landing-page/src/app/components/figma/ImageWithFallback.tsx +0 -27
  45. package/landing-page/src/app/components/ui/accordion.tsx +0 -66
  46. package/landing-page/src/app/components/ui/alert-dialog.tsx +0 -157
  47. package/landing-page/src/app/components/ui/alert.tsx +0 -66
  48. package/landing-page/src/app/components/ui/aspect-ratio.tsx +0 -11
  49. package/landing-page/src/app/components/ui/avatar.tsx +0 -53
  50. package/landing-page/src/app/components/ui/badge.tsx +0 -46
  51. package/landing-page/src/app/components/ui/breadcrumb.tsx +0 -109
  52. package/landing-page/src/app/components/ui/button.tsx +0 -57
  53. package/landing-page/src/app/components/ui/calendar.tsx +0 -75
  54. package/landing-page/src/app/components/ui/card.tsx +0 -92
  55. package/landing-page/src/app/components/ui/carousel.tsx +0 -241
  56. package/landing-page/src/app/components/ui/chart.tsx +0 -353
  57. package/landing-page/src/app/components/ui/checkbox.tsx +0 -32
  58. package/landing-page/src/app/components/ui/collapsible.tsx +0 -33
  59. package/landing-page/src/app/components/ui/command.tsx +0 -177
  60. package/landing-page/src/app/components/ui/context-menu.tsx +0 -252
  61. package/landing-page/src/app/components/ui/dialog.tsx +0 -135
  62. package/landing-page/src/app/components/ui/drawer.tsx +0 -132
  63. package/landing-page/src/app/components/ui/dropdown-menu.tsx +0 -257
  64. package/landing-page/src/app/components/ui/form.tsx +0 -168
  65. package/landing-page/src/app/components/ui/hover-card.tsx +0 -44
  66. package/landing-page/src/app/components/ui/input-otp.tsx +0 -77
  67. package/landing-page/src/app/components/ui/input.tsx +0 -21
  68. package/landing-page/src/app/components/ui/label.tsx +0 -24
  69. package/landing-page/src/app/components/ui/menubar.tsx +0 -276
  70. package/landing-page/src/app/components/ui/navigation-menu.tsx +0 -168
  71. package/landing-page/src/app/components/ui/pagination.tsx +0 -127
  72. package/landing-page/src/app/components/ui/popover.tsx +0 -48
  73. package/landing-page/src/app/components/ui/progress.tsx +0 -31
  74. package/landing-page/src/app/components/ui/radio-group.tsx +0 -45
  75. package/landing-page/src/app/components/ui/resizable.tsx +0 -56
  76. package/landing-page/src/app/components/ui/scroll-area.tsx +0 -58
  77. package/landing-page/src/app/components/ui/select.tsx +0 -189
  78. package/landing-page/src/app/components/ui/separator.tsx +0 -28
  79. package/landing-page/src/app/components/ui/sheet.tsx +0 -139
  80. package/landing-page/src/app/components/ui/sidebar.tsx +0 -726
  81. package/landing-page/src/app/components/ui/skeleton.tsx +0 -13
  82. package/landing-page/src/app/components/ui/slider.tsx +0 -63
  83. package/landing-page/src/app/components/ui/sonner.tsx +0 -25
  84. package/landing-page/src/app/components/ui/switch.tsx +0 -31
  85. package/landing-page/src/app/components/ui/table.tsx +0 -116
  86. package/landing-page/src/app/components/ui/tabs.tsx +0 -66
  87. package/landing-page/src/app/components/ui/textarea.tsx +0 -18
  88. package/landing-page/src/app/components/ui/toggle-group.tsx +0 -73
  89. package/landing-page/src/app/components/ui/toggle.tsx +0 -47
  90. package/landing-page/src/app/components/ui/tooltip.tsx +0 -61
  91. package/landing-page/src/app/components/ui/use-mobile.ts +0 -21
  92. package/landing-page/src/app/components/ui/utils.ts +0 -6
  93. package/landing-page/src/components/docs/DocsContent.tsx +0 -801
  94. package/landing-page/src/components/docs/DocsSidebar.tsx +0 -90
  95. package/landing-page/src/components/landing/CTA.tsx +0 -59
  96. package/landing-page/src/components/landing/Comparison.tsx +0 -84
  97. package/landing-page/src/components/landing/FaultyTerminal.tsx +0 -424
  98. package/landing-page/src/components/landing/Features.tsx +0 -201
  99. package/landing-page/src/components/landing/Hero.tsx +0 -142
  100. package/landing-page/src/components/landing/Pricing.tsx +0 -140
  101. package/landing-page/src/components/landing/Roadmap.tsx +0 -86
  102. package/landing-page/src/components/landing/Security.tsx +0 -81
  103. package/landing-page/src/components/landing/TerminalWindow.tsx +0 -27
  104. package/landing-page/src/components/landing/UseCases.tsx +0 -55
  105. package/landing-page/src/components/landing/Workflow.tsx +0 -101
  106. package/landing-page/src/components/layout/DashboardNavbar.tsx +0 -37
  107. package/landing-page/src/components/layout/Footer.tsx +0 -55
  108. package/landing-page/src/components/layout/LandingNavbar.tsx +0 -82
  109. package/landing-page/src/components/ui/badge.tsx +0 -39
  110. package/landing-page/src/components/ui/breadcrumb.tsx +0 -115
  111. package/landing-page/src/components/ui/button.tsx +0 -57
  112. package/landing-page/src/components/ui/card.tsx +0 -79
  113. package/landing-page/src/components/ui/mock-terminal.tsx +0 -68
  114. package/landing-page/src/components/ui/separator.tsx +0 -28
  115. package/landing-page/src/lib/utils.ts +0 -6
  116. package/landing-page/src/main.tsx +0 -10
  117. package/landing-page/src/pages/Dashboard.tsx +0 -133
  118. package/landing-page/src/pages/DocsPage.tsx +0 -79
  119. package/landing-page/src/pages/LandingPage.tsx +0 -31
  120. package/landing-page/src/pages/TerminalView.tsx +0 -106
  121. package/landing-page/src/styles/fonts.css +0 -0
  122. package/landing-page/src/styles/index.css +0 -3
  123. package/landing-page/src/styles/tailwind.css +0 -4
  124. package/landing-page/src/styles/theme.css +0 -181
  125. package/landing-page/vite.config.ts +0 -19
  126. package/scripts/GHOSTTY_TAB_BUG.md +0 -106
  127. package/scripts/build.ts +0 -298
  128. package/scripts/migrate-secrets.ts +0 -77
  129. package/scripts/release.ts +0 -140
  130. package/scripts/sample-events.ts +0 -263
  131. package/scripts/test-tabs-minimal.ts +0 -68
  132. package/scripts/test-tabs-workaround.ts +0 -95
  133. package/scripts/test-tabs.ts +0 -171
  134. package/src/__tests__/test-utils.ts +0 -298
  135. package/src/app/input/__tests__/sessionCommands.test.ts +0 -40
  136. package/src/app/input/sessionCommands.ts +0 -94
  137. package/src/app/session/__tests__/useAttachController.test.ts +0 -229
  138. package/src/app/session/createSessionBackend.bun.ts +0 -76
  139. package/src/app/session/createSessionBackend.web.ts +0 -104
  140. package/src/app/session/types.ts +0 -16
  141. package/src/app/session/useAttachController.ts +0 -220
  142. package/src/app/session/useProcessActions.ts +0 -201
  143. package/src/app/session/useSessionClient.ts +0 -35
  144. package/src/app/session/useWorkspaceDeleteFlow.ts +0 -170
  145. package/src/app.tui.tsx +0 -2929
  146. package/src/app.web.tsx +0 -1454
  147. package/src/commands/__tests__/connect-key.test.ts +0 -10
  148. package/src/commands/__tests__/events.test.ts +0 -201
  149. package/src/commands/__tests__/notifications.test.ts +0 -349
  150. package/src/commands/__tests__/process.test.ts +0 -251
  151. package/src/commands/__tests__/serve-messages.test.ts +0 -190
  152. package/src/commands/__tests__/serve-process-hosting.test.ts +0 -63
  153. package/src/commands/access.ts +0 -298
  154. package/src/commands/add.ts +0 -455
  155. package/src/commands/auth.ts +0 -369
  156. package/src/commands/bundle.ts +0 -232
  157. package/src/commands/config.ts +0 -242
  158. package/src/commands/connect-key.ts +0 -1
  159. package/src/commands/connect.ts +0 -576
  160. package/src/commands/directory.ts +0 -16
  161. package/src/commands/events.ts +0 -157
  162. package/src/commands/host.ts +0 -566
  163. package/src/commands/identity.ts +0 -184
  164. package/src/commands/linear.ts +0 -717
  165. package/src/commands/list.ts +0 -181
  166. package/src/commands/migrate.ts +0 -52
  167. package/src/commands/notifications.ts +0 -351
  168. package/src/commands/process.ts +0 -104
  169. package/src/commands/relay.ts +0 -315
  170. package/src/commands/remove.ts +0 -279
  171. package/src/commands/review.ts +0 -787
  172. package/src/commands/serve.ts +0 -1946
  173. package/src/commands/share.ts +0 -451
  174. package/src/commands/status.ts +0 -125
  175. package/src/commands/switch.ts +0 -361
  176. package/src/commands/tmux.ts +0 -317
  177. package/src/components/DPad.web.tsx +0 -343
  178. package/src/components/DiffViewer.web.tsx +0 -1192
  179. package/src/components/Events.tsx +0 -137
  180. package/src/components/Events.tui.tsx +0 -129
  181. package/src/components/Events.web.tsx +0 -386
  182. package/src/components/FloatingControls.web.tsx +0 -112
  183. package/src/components/FloatingJogWheel.web.tsx +0 -240
  184. package/src/components/Flow.tsx +0 -458
  185. package/src/components/Flow.tui.tsx +0 -343
  186. package/src/components/Flow.web.tsx +0 -442
  187. package/src/components/Inbox.tsx +0 -448
  188. package/src/components/Inbox.tui.tsx +0 -262
  189. package/src/components/Inbox.web.tsx +0 -329
  190. package/src/components/MachineList.tsx +0 -187
  191. package/src/components/MachineList.tui.tsx +0 -161
  192. package/src/components/MachineList.web.tsx +0 -210
  193. package/src/components/NumPad.web.tsx +0 -270
  194. package/src/components/ProjectList.tsx +0 -175
  195. package/src/components/ProjectList.tui.tsx +0 -109
  196. package/src/components/ProjectList.web.tsx +0 -143
  197. package/src/components/ProjectOnboardingStep.ts +0 -23
  198. package/src/components/ProjectOnboardingStep.tui.tsx +0 -88
  199. package/src/components/ProjectOnboardingStep.web.tsx +0 -59
  200. package/src/components/RemoteMachineScreen.tui.tsx +0 -690
  201. package/src/components/ScriptTerminal.tui.tsx +0 -160
  202. package/src/components/ScriptTerminal.web.tsx +0 -89
  203. package/src/components/SessionTerminal.tui.tsx +0 -406
  204. package/src/components/SessionTerminal.web.tsx +0 -467
  205. package/src/components/SpacesBrowser.tsx +0 -540
  206. package/src/components/SpacesBrowser.tui.tsx +0 -258
  207. package/src/components/SpacesBrowser.web.tsx +0 -332
  208. package/src/components/TerminalControls.web.tsx +0 -464
  209. package/src/components/ThreadPanel.web.tsx +0 -798
  210. package/src/components/__tests__/SpacesBrowser.test.ts +0 -541
  211. package/src/components/__tests__/SpacesBrowser.tui.test.tsx +0 -249
  212. package/src/components/__tests__/script-terminal-buffer.tui.test.ts +0 -72
  213. package/src/components/index.ts +0 -105
  214. package/src/components/review-decision-colors.ts +0 -11
  215. package/src/components/script-terminal-buffer.tui.ts +0 -37
  216. package/src/components/session-terminal-page-navigation.ts +0 -48
  217. package/src/components/terminal-bracketed-paste.tui.test.ts +0 -43
  218. package/src/components/terminal-bracketed-paste.tui.ts +0 -46
  219. package/src/core/__tests__/access.test.ts +0 -240
  220. package/src/core/__tests__/bundle-refresh.test.ts +0 -567
  221. package/src/core/__tests__/bundle.test.ts +0 -209
  222. package/src/core/__tests__/github-review.test.ts +0 -781
  223. package/src/core/__tests__/project-lifecycle.test.ts +0 -137
  224. package/src/core/__tests__/workspace-lifecycle.test.ts +0 -159
  225. package/src/core/__tests__/workspace.test.ts +0 -149
  226. package/src/core/access.ts +0 -277
  227. package/src/core/bundle-refresh.ts +0 -1064
  228. package/src/core/bundle.ts +0 -326
  229. package/src/core/config.ts +0 -405
  230. package/src/core/git.ts +0 -768
  231. package/src/core/github-review.ts +0 -761
  232. package/src/core/github.ts +0 -151
  233. package/src/core/identity.ts +0 -631
  234. package/src/core/linear.ts +0 -403
  235. package/src/core/preferences-service.ts +0 -17
  236. package/src/core/project-catalog.ts +0 -52
  237. package/src/core/project-lifecycle.ts +0 -163
  238. package/src/core/review-executor.ts +0 -316
  239. package/src/core/review.ts +0 -407
  240. package/src/core/secret-runtime.ts +0 -167
  241. package/src/core/shell.ts +0 -117
  242. package/src/core/trusted-relays.ts +0 -315
  243. package/src/core/workspace-lifecycle.ts +0 -216
  244. package/src/core/workspace.ts +0 -363
  245. package/src/hooks/__tests__/useLocalSession.tui.test.ts +0 -557
  246. package/src/hooks/index.ts +0 -8
  247. package/src/hooks/index.tui.ts +0 -32
  248. package/src/hooks/useDaemonStatus.tui.ts +0 -174
  249. package/src/hooks/useLocalSession.tui.ts +0 -395
  250. package/src/hooks/useRelayConnection.web.ts +0 -54
  251. package/src/hooks/useRemoteMachines.tui.ts +0 -166
  252. package/src/hooks/useRemoteTerminal.tui.ts +0 -22
  253. package/src/hooks/useReview.web.ts +0 -248
  254. package/src/hooks/useTerminal.web.ts +0 -36
  255. package/src/hooks/useUserActivity.ts +0 -61
  256. package/src/hooks/useVisualViewport.web.ts +0 -104
  257. package/src/index.ts +0 -1376
  258. package/src/lib/events/__tests__/collector-filter.test.ts +0 -105
  259. package/src/lib/events/__tests__/store-query.test.ts +0 -103
  260. package/src/lib/events/collector.ts +0 -494
  261. package/src/lib/events/filters.ts +0 -26
  262. package/src/lib/events/index.ts +0 -11
  263. package/src/lib/events/indexer.ts +0 -14
  264. package/src/lib/events/paths.ts +0 -69
  265. package/src/lib/events/reader.ts +0 -212
  266. package/src/lib/events/store.ts +0 -141
  267. package/src/lib/invite.web.ts +0 -58
  268. package/src/lib/preferences-service.web.ts +0 -41
  269. package/src/lib/processes/__tests__/config.test.ts +0 -83
  270. package/src/lib/processes/__tests__/names.test.ts +0 -125
  271. package/src/lib/processes/__tests__/schema.test.ts +0 -208
  272. package/src/lib/processes/__tests__/watchdog.test.ts +0 -210
  273. package/src/lib/processes/autostart.ts +0 -16
  274. package/src/lib/processes/config.ts +0 -187
  275. package/src/lib/processes/control.ts +0 -53
  276. package/src/lib/processes/editor.ts +0 -32
  277. package/src/lib/processes/events-config.ts +0 -37
  278. package/src/lib/processes/index.ts +0 -14
  279. package/src/lib/processes/instances.ts +0 -20
  280. package/src/lib/processes/manager.ts +0 -131
  281. package/src/lib/processes/names.ts +0 -71
  282. package/src/lib/processes/registry.ts +0 -26
  283. package/src/lib/processes/runner.ts +0 -211
  284. package/src/lib/processes/scheduler.ts +0 -17
  285. package/src/lib/processes/schema.ts +0 -74
  286. package/src/lib/processes/session-list.ts +0 -15
  287. package/src/lib/processes/state.ts +0 -82
  288. package/src/lib/processes/watchdog.test.ts +0 -79
  289. package/src/lib/processes/watchdog.ts +0 -106
  290. package/src/lib/remote-session/__tests__/protocol.test.ts +0 -291
  291. package/src/lib/remote-session/index.ts +0 -7
  292. package/src/lib/remote-session/protocol.ts +0 -443
  293. package/src/lib/remote-session/session-handler.ts +0 -1298
  294. package/src/lib/remote-session/workspace-scanner.ts +0 -161
  295. package/src/lib/sonner.web.ts +0 -1
  296. package/src/lib/storage/identity-store.web.ts +0 -94
  297. package/src/lib/tmux-lite/README.md +0 -81
  298. package/src/lib/tmux-lite/cli.ts +0 -855
  299. package/src/lib/tmux-lite/crypto/__tests__/helpers/handshake-runner.ts +0 -349
  300. package/src/lib/tmux-lite/crypto/__tests__/helpers/mock-relay.ts +0 -291
  301. package/src/lib/tmux-lite/crypto/__tests__/helpers/test-identities.ts +0 -142
  302. package/src/lib/tmux-lite/crypto/__tests__/integration/authorization.integration.test.ts +0 -339
  303. package/src/lib/tmux-lite/crypto/__tests__/integration/e2e-communication.integration.test.ts +0 -477
  304. package/src/lib/tmux-lite/crypto/__tests__/integration/error-handling.integration.test.ts +0 -499
  305. package/src/lib/tmux-lite/crypto/__tests__/integration/handshake.integration.test.ts +0 -371
  306. package/src/lib/tmux-lite/crypto/__tests__/integration/security.integration.test.ts +0 -573
  307. package/src/lib/tmux-lite/crypto/access-control.test.ts +0 -512
  308. package/src/lib/tmux-lite/crypto/access-control.ts +0 -320
  309. package/src/lib/tmux-lite/crypto/frames.test.ts +0 -262
  310. package/src/lib/tmux-lite/crypto/frames.ts +0 -141
  311. package/src/lib/tmux-lite/crypto/handshake.ts +0 -894
  312. package/src/lib/tmux-lite/crypto/identity.test.ts +0 -220
  313. package/src/lib/tmux-lite/crypto/identity.ts +0 -286
  314. package/src/lib/tmux-lite/crypto/index.ts +0 -51
  315. package/src/lib/tmux-lite/crypto/invites.test.ts +0 -381
  316. package/src/lib/tmux-lite/crypto/invites.ts +0 -215
  317. package/src/lib/tmux-lite/crypto/keyexchange.ts +0 -435
  318. package/src/lib/tmux-lite/crypto/keys.test.ts +0 -58
  319. package/src/lib/tmux-lite/crypto/keys.ts +0 -47
  320. package/src/lib/tmux-lite/crypto/secretbox.test.ts +0 -169
  321. package/src/lib/tmux-lite/crypto/secretbox.ts +0 -124
  322. package/src/lib/tmux-lite/handshake-handler.ts +0 -451
  323. package/src/lib/tmux-lite/process-run.integration.test.ts +0 -266
  324. package/src/lib/tmux-lite/protocol.test.ts +0 -307
  325. package/src/lib/tmux-lite/protocol.ts +0 -291
  326. package/src/lib/tmux-lite/relay-client.ts +0 -506
  327. package/src/lib/tmux-lite/server-lifecycle.test.ts +0 -212
  328. package/src/lib/tmux-lite/server.ts +0 -1412
  329. package/src/lib/tmux-lite/shell-integration.sh +0 -37
  330. package/src/lib/tmux-lite/terminal-queries.test.ts +0 -54
  331. package/src/lib/tmux-lite/terminal-queries.ts +0 -49
  332. package/src/notifications/__tests__/useNotifications.test.ts +0 -739
  333. package/src/notifications/index.ts +0 -32
  334. package/src/notifications/policy.test.ts +0 -424
  335. package/src/notifications/policy.ts +0 -139
  336. package/src/notifications/types.ts +0 -82
  337. package/src/notifications/useNotifications.ts +0 -350
  338. package/src/pages/ReviewPage.web.tsx +0 -511
  339. package/src/preferences/index.ts +0 -1
  340. package/src/preferences/types.ts +0 -9
  341. package/src/relay/__tests__/e2e-flow.test.ts +0 -1284
  342. package/src/relay/__tests__/helpers/auth.ts +0 -354
  343. package/src/relay/__tests__/helpers/ports.ts +0 -51
  344. package/src/relay/__tests__/protocol-validation.test.ts +0 -265
  345. package/src/relay/authorization.ts +0 -303
  346. package/src/relay/embedded-assets.generated.d.ts +0 -15
  347. package/src/relay/identity.ts +0 -352
  348. package/src/relay/index.ts +0 -57
  349. package/src/relay/pipes.test.ts +0 -427
  350. package/src/relay/pipes.ts +0 -195
  351. package/src/relay/protocol.ts +0 -804
  352. package/src/relay/registries.test.ts +0 -437
  353. package/src/relay/registries.ts +0 -593
  354. package/src/relay/server.test.ts +0 -1323
  355. package/src/relay/server.ts +0 -1128
  356. package/src/relay/signing.ts +0 -238
  357. package/src/relay/types.ts +0 -69
  358. package/src/relay-client/__tests__/machine-directory-client.test.ts +0 -152
  359. package/src/relay-client/__tests__/useMachineDirectory.test.ts +0 -172
  360. package/src/relay-client/adapters/browser.ts +0 -27
  361. package/src/relay-client/adapters/node.ts +0 -29
  362. package/src/relay-client/index.ts +0 -33
  363. package/src/relay-client/machine-directory-client.ts +0 -244
  364. package/src/relay-client/useMachineDirectory.ts +0 -175
  365. package/src/serve/client-session-manager.ts +0 -635
  366. package/src/serve/daemon.ts +0 -497
  367. package/src/serve/pty-session.ts +0 -236
  368. package/src/serve/types.ts +0 -174
  369. package/src/session/__tests__/backend-manager.test.ts +0 -101
  370. package/src/session/__tests__/local-session-backend.test.ts +0 -1129
  371. package/src/session/__tests__/reducer.test.ts +0 -80
  372. package/src/session/__tests__/remote-session-backend.test.ts +0 -995
  373. package/src/session/__tests__/session-name.test.ts +0 -35
  374. package/src/session/__tests__/useBundleRefreshAttachFlow.test.ts +0 -431
  375. package/src/session/__tests__/useRemoteSessionClient.test.ts +0 -424
  376. package/src/session/__tests__/workspace-shell-hooks.integration.test.ts +0 -268
  377. package/src/session/__tests__/workspace-shell-hooks.test.ts +0 -24
  378. package/src/session/adapters/browser-remote.ts +0 -101
  379. package/src/session/adapters/node-remote.ts +0 -135
  380. package/src/session/backend-key.ts +0 -5
  381. package/src/session/backend-manager.ts +0 -80
  382. package/src/session/backend.ts +0 -93
  383. package/src/session/backends/local-session-backend.ts +0 -1119
  384. package/src/session/backends/remote-session-backend.ts +0 -1378
  385. package/src/session/crypto/__tests__/web-terminal.test.ts +0 -1158
  386. package/src/session/crypto/frames.web.ts +0 -205
  387. package/src/session/crypto/handshake.web.ts +0 -396
  388. package/src/session/crypto/identity.web.ts +0 -133
  389. package/src/session/crypto/keyexchange.web.ts +0 -246
  390. package/src/session/crypto/relay-signing.web.ts +0 -53
  391. package/src/session/events.ts +0 -38
  392. package/src/session/index.ts +0 -116
  393. package/src/session/reducer.ts +0 -274
  394. package/src/session/selectors.ts +0 -28
  395. package/src/session/session-name.ts +0 -50
  396. package/src/session/types.ts +0 -101
  397. package/src/session/useBundleRefreshAttachFlow.ts +0 -608
  398. package/src/session/useRemoteSessionClient.ts +0 -424
  399. package/src/session/useSessionEngine.ts +0 -432
  400. package/src/session/workspace-shell-hooks.ts +0 -35
  401. package/src/tui/__tests__/input-text.test.ts +0 -24
  402. package/src/tui/__tests__/local-terminal-sync.test.ts +0 -82
  403. package/src/tui/__tests__/session-terminal-page-navigation.test.ts +0 -94
  404. package/src/tui/app.tsx +0 -2
  405. package/src/tui/index.ts +0 -18
  406. package/src/tui/input-text.ts +0 -38
  407. package/src/tui/local-terminal-sync.ts +0 -41
  408. package/src/types/bundle-refresh.ts +0 -42
  409. package/src/types/bundle.ts +0 -130
  410. package/src/types/config.ts +0 -287
  411. package/src/types/errors.ts +0 -292
  412. package/src/types/events.ts +0 -91
  413. package/src/types/identity.ts +0 -284
  414. package/src/types/processes.ts +0 -45
  415. package/src/types/review.ts +0 -349
  416. package/src/types/script-phase.ts +0 -3
  417. package/src/types/workspace-fuzzy.ts +0 -49
  418. package/src/types/workspace.ts +0 -151
  419. package/src/utils/__tests__/onboarding.test.ts +0 -358
  420. package/src/utils/__tests__/run-scripts.test.ts +0 -535
  421. package/src/utils/__tests__/run-workspace-scripts.test.ts +0 -406
  422. package/src/utils/__tests__/workspace-setup.integration.test.ts +0 -633
  423. package/src/utils/__tests__/workspace-state.test.ts +0 -78
  424. package/src/utils/bun-socket-writer.ts +0 -80
  425. package/src/utils/clipboard.ts +0 -53
  426. package/src/utils/deps.test.ts +0 -31
  427. package/src/utils/deps.ts +0 -145
  428. package/src/utils/device.web.ts +0 -163
  429. package/src/utils/fuzzy-match.ts +0 -125
  430. package/src/utils/hostnames.ts +0 -43
  431. package/src/utils/hunk-header.ts +0 -17
  432. package/src/utils/id.ts +0 -9
  433. package/src/utils/logger.ts +0 -127
  434. package/src/utils/markdown.ts +0 -254
  435. package/src/utils/normalize-env-key.ts +0 -13
  436. package/src/utils/onboarding.ts +0 -279
  437. package/src/utils/prompts.ts +0 -176
  438. package/src/utils/run-commands.ts +0 -112
  439. package/src/utils/run-scripts.ts +0 -337
  440. package/src/utils/run-workspace-scripts.ts +0 -355
  441. package/src/utils/sanitize.test.ts +0 -149
  442. package/src/utils/sanitize.ts +0 -162
  443. package/src/utils/secrets.ts +0 -836
  444. package/src/utils/shell-escape.ts +0 -40
  445. package/src/utils/utf8.ts +0 -79
  446. package/src/utils/workspace-id.ts +0 -55
  447. package/src/utils/workspace-state.ts +0 -427
  448. package/src/version.generated.d.ts +0 -2
  449. package/todo-security.md +0 -92
  450. package/tsconfig.json +0 -29
  451. package/web/README.md +0 -73
  452. package/web/bun.lock +0 -675
  453. package/web/eslint.config.js +0 -23
  454. package/web/index.css +0 -249
  455. package/web/index.html +0 -16
  456. package/web/main.tsx +0 -10
  457. package/web/package.json +0 -39
  458. package/web/public/vite.svg +0 -1
  459. package/web/tsconfig.app.json +0 -35
  460. package/web/tsconfig.json +0 -7
  461. package/web/tsconfig.node.json +0 -26
  462. package/web/vite.config.ts +0 -39
  463. package/worker/bun.lock +0 -237
  464. package/worker/package.json +0 -22
  465. package/worker/schema.sql +0 -96
  466. package/worker/src/handlers/auth.ts +0 -451
  467. package/worker/src/handlers/subdomains.ts +0 -376
  468. package/worker/src/handlers/user.ts +0 -98
  469. package/worker/src/index.ts +0 -70
  470. package/worker/src/middleware/auth.ts +0 -152
  471. package/worker/src/services/cloudflare.ts +0 -609
  472. package/worker/src/types.ts +0 -96
  473. package/worker/tsconfig.json +0 -15
  474. package/worker/wrangler.toml +0 -26
@@ -1,1819 +0,0 @@
1
- # gitspace.sh Platform Specification
2
-
3
- > **Complete specification for the gitspace.sh hosting platform**
4
-
5
- ---
6
-
7
- ## Overview
8
-
9
- gitspace.sh is a lightweight platform that gives developers instant hosting via Cloudflare Tunnels. Users reserve a subdomain, get a tunnel token, and `gssh serve` handles the rest.
10
-
11
- **Core Principles**:
12
- - **Zero infrastructure for us** - Users run their own tunnels
13
- - **Instant setup** - Reserve subdomain, start serving
14
- - **Peer relay model** - One machine with subdomain can relay for others
15
- - **E2E encryption** - Terminal access remains encrypted
16
-
17
- ---
18
-
19
- ## Architecture
20
-
21
- ```
22
- ┌─────────────────────────────────────────────────────────────────────────────┐
23
- │ GITSPACE.SH PLATFORM │
24
- ├─────────────────────────────────────────────────────────────────────────────┤
25
- │ │
26
- │ CLOUDFLARE (managed by gitspace.sh) │
27
- │ ──────────────────────────────────── │
28
- │ │
29
- │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
30
- │ │ Workers │ │ Pages │ │ D1 │ │ KV │ │
31
- │ │ (API) │ │ (Portal) │ │ (Database) │ │ (Sessions) │ │
32
- │ │ │ │ │ │ │ │ │ │
33
- │ │ api. │ │ gitspace.sh │ │ users │ │ sessions │ │
34
- │ │ gitspace.sh │ │ │ │ subdomains │ │ (TTL: 7d) │ │
35
- │ │ │ │ │ │ tokens │ │ │ │
36
- │ └──────────────┘ └──────────────┘ └──────────────┘ └──────────────┘ │
37
- │ │ │ │
38
- │ │ Cloudflare Tunnel API │ │
39
- │ └────────────────┬───────────────────┘ │
40
- │ │ │
41
- │ ▼ │
42
- │ ┌─────────────────────────────────────────────────────────────────────┐ │
43
- │ │ DNS: gitspace.sh │ │
44
- │ │ ├── brad.gitspace.sh → tunnel-brad-xxx.cfargotunnel.com │ │
45
- │ │ ├── *.brad.gitspace.sh → tunnel-brad-xxx.cfargotunnel.com │ │
46
- │ │ ├── sarah.gitspace.sh → tunnel-sarah-xxx.cfargotunnel.com │ │
47
- │ │ └── *.sarah.gitspace.sh → tunnel-sarah-xxx.cfargotunnel.com │ │
48
- │ │ │ │
49
- │ │ SSL: Total TLS ($10/mo) - covers *.*.gitspace.sh │ │
50
- │ └─────────────────────────────────────────────────────────────────────┘ │
51
- │ │
52
- │ ═══════════════════════════════════════════════════════════════════════ │
53
- │ │
54
- │ USER'S MACHINES (user-owned, user-operated) │
55
- │ ─────────────────────────────────────────── │
56
- │ │
57
- │ Brad's MacBook (PRIMARY - has subdomain) │
58
- │ ┌─────────────────────────────────────────────────────────────────────┐ │
59
- │ │ gssh serve │ │
60
- │ │ ├── cloudflared (tunnel: brad.gitspace.sh) │ │
61
- │ │ ├── Local HTTP server (:8080) │ │
62
- │ │ │ ├── HTTP routes → services/Lima VMs │ │
63
- │ │ │ └── WebSocket /ws → terminal (E2E encrypted) │ │
64
- │ │ ├── Embedded relay (accepts connections from other machines) │ │
65
- │ │ └── tmux-lite server (PTY sessions) │ │
66
- │ └─────────────────────────────────────────────────────────────────────┘ │
67
- │ ▲ ▲ │
68
- │ │ WebSocket │ WebSocket │
69
- │ │ │ │
70
- │ Brad's Work Desktop Brad's Home Server │
71
- │ (SECONDARY - no subdomain) (SECONDARY - no subdomain) │
72
- │ ┌───────────────────┐ ┌───────────────────┐ │
73
- │ │ gssh serve │ │ gssh serve │ │
74
- │ │ --relay brad. │ │ --relay brad. │ │
75
- │ │ gitspace.sh │ │ gitspace.sh │ │
76
- │ └───────────────────┘ └───────────────────┘ │
77
- │ │
78
- └─────────────────────────────────────────────────────────────────────────────┘
79
- ```
80
-
81
- ---
82
-
83
- ## Authentication
84
-
85
- gitspace.sh uses **GitHub as the identity provider**. Users can authenticate via:
86
- - **Portal**: GitHub OAuth (redirect-based) for browser access
87
- - **CLI**: GitHub Device Flow for terminal access
88
-
89
- Both methods create/access the **same account** (keyed by GitHub user ID).
90
-
91
- ```
92
- ┌─────────────────────────────────────────────────────────────────────────────┐
93
- │ TWO ENTRY POINTS, ONE ACCOUNT │
94
- ├─────────────────────────────────────────────────────────────────────────────┤
95
- │ │
96
- │ PORTAL (Browser) CLI (Terminal) │
97
- │ ──────────────── ────────────── │
98
- │ │
99
- │ gitspace.sh $ gssh auth login │
100
- │ ┌─────────────────────┐ │
101
- │ │ Sign in with GitHub │ ! Code: ABCD-1234 │
102
- │ └──────────┬──────────┘ Open github.com/login/device │
103
- │ │ │ │
104
- │ ▼ ▼ │
105
- │ GitHub OAuth (redirect) GitHub Device Flow │
106
- │ │ │ │
107
- │ ▼ ▼ │
108
- │ Callback with token Poll for token │
109
- │ │ │ │
110
- │ └───────────────┬───────────────────────┘ │
111
- │ │ │
112
- │ ▼ │
113
- │ ┌───────────────────────────────────────┐ │
114
- │ │ gitspace.sh API │ │
115
- │ │ │ │
116
- │ │ 1. Verify GitHub token │ │
117
- │ │ 2. Get GitHub user ID │ │
118
- │ │ 3. Find or create account │ │
119
- │ │ (keyed by GitHub ID) │ │
120
- │ │ 4. Return session/token │ │
121
- │ └───────────────────────────────────────┘ │
122
- │ │ │
123
- │ ▼ │
124
- │ ┌───────────────────────────────────────┐ │
125
- │ │ Same account in D1: │ │
126
- │ │ { │ │
127
- │ │ id: "uuid", │ │
128
- │ │ github_id: "12345", ◄──────────── │ ── Unique identifier │
129
- │ │ github_username: "brad", │ │
130
- │ │ email: "...", │ │
131
- │ │ } │ │
132
- │ └───────────────────────────────────────┘ │
133
- │ │
134
- └─────────────────────────────────────────────────────────────────────────────┘
135
- ```
136
-
137
- ---
138
-
139
- ## User Flow
140
-
141
- ### 1. Sign Up / Login (Portal)
142
-
143
- ```
144
- ┌─────────────────────────────────────────────────────────────────────────────┐
145
- │ Browser: gitspace.sh │
146
- ├─────────────────────────────────────────────────────────────────────────────┤
147
- │ │
148
- │ ┌─────────────────────────────────────────────────────────────────────┐ │
149
- │ │ │ │
150
- │ │ gitspace.sh │ │
151
- │ │ │ │
152
- │ │ Instant hosting for your dev environment │ │
153
- │ │ │ │
154
- │ │ ┌──────────────────────┐ │ │
155
- │ │ │ Sign in with GitHub │ │ │
156
- │ │ └──────────────────────┘ │ │
157
- │ │ │ │
158
- │ └─────────────────────────────────────────────────────────────────────┘ │
159
- │ │
160
- │ OAuth flow (redirect-based): │
161
- │ 1. User clicks "Sign in with GitHub" │
162
- │ 2. Redirects to GitHub OAuth authorize URL │
163
- │ 3. User authorizes gitspace.sh app │
164
- │ 4. GitHub redirects to callback with code │
165
- │ 5. API exchanges code for token, verifies user │
166
- │ 6. Creates/updates user in D1 (keyed by github_id) │
167
- │ 7. Sets session cookie, redirects to dashboard │
168
- │ │
169
- └─────────────────────────────────────────────────────────────────────────────┘
170
- ```
171
-
172
- ### 2. Sign Up / Login (CLI - GitHub Device Flow)
173
-
174
- ```
175
- ┌─────────────────────────────────────────────────────────────────────────────┐
176
- │ Terminal │
177
- ├─────────────────────────────────────────────────────────────────────────────┤
178
- │ │
179
- │ $ gssh auth login │
180
- │ │
181
- │ ! First, copy your one-time code: ABCD-1234 │
182
- │ Press Enter to open github.com in your browser... │
183
- │ │
184
- │ ───────────────────────────────────────────────────────────────────────── │
185
- │ │
186
- │ Browser: github.com/login/device │
187
- │ ┌─────────────────────────────────────────────────────────────────────┐ │
188
- │ │ │ │
189
- │ │ Device Activation │ │
190
- │ │ │ │
191
- │ │ Enter the code displayed on your device: │ │
192
- │ │ │ │
193
- │ │ ┌──────────────────────────────────────┐ │ │
194
- │ │ │ ABCD-1234 │ │ │
195
- │ │ └──────────────────────────────────────┘ │ │
196
- │ │ │ │
197
- │ │ ┌──────────┐ │ │
198
- │ │ │ Continue │ │ │
199
- │ │ └──────────┘ │ │
200
- │ │ │ │
201
- │ └─────────────────────────────────────────────────────────────────────┘ │
202
- │ │
203
- │ ───────────────────────────────────────────────────────────────────────── │
204
- │ │
205
- │ Browser: GitHub authorization page │
206
- │ ┌─────────────────────────────────────────────────────────────────────┐ │
207
- │ │ │ │
208
- │ │ Authorize gitspace.sh │ │
209
- │ │ │ │
210
- │ │ gitspace.sh by @gitspacesh │ │
211
- │ │ wants to access your account │ │
212
- │ │ │ │
213
- │ │ This will allow gitspace.sh to: │ │
214
- │ │ • Read your profile information │ │
215
- │ │ • Read your email addresses │ │
216
- │ │ │ │
217
- │ │ ┌──────────────────────┐ │ │
218
- │ │ │ Authorize gitspace.sh │ │ │
219
- │ │ └──────────────────────┘ │ │
220
- │ │ │ │
221
- │ └─────────────────────────────────────────────────────────────────────┘ │
222
- │ │
223
- │ ───────────────────────────────────────────────────────────────────────── │
224
- │ │
225
- │ Terminal (after authorization): │
226
- │ │
227
- │ ✓ Authentication complete │
228
- │ ✓ Logged in as username │
229
- │ ✓ Token saved to keychain │
230
- │ │
231
- └─────────────────────────────────────────────────────────────────────────────┘
232
- ```
233
-
234
- ### Device Flow Sequence
235
-
236
- ```
237
- ┌─────────────────────────────────────────────────────────────────────────────┐
238
- │ GITHUB DEVICE FLOW - DETAILED SEQUENCE │
239
- ├─────────────────────────────────────────────────────────────────────────────┤
240
- │ │
241
- │ CLI GitHub gitspace.sh API │
242
- │ │ │ │ │
243
- │ │ POST /login/device/code │ │ │
244
- │ │ {client_id, scope} │ │ │
245
- │ │────────────────────────────►│ │ │
246
- │ │ │ │ │
247
- │ │◄────────────────────────────│ │ │
248
- │ │ {device_code, user_code, │ │ │
249
- │ │ verification_uri, interval}│ │ │
250
- │ │ │ │ │
251
- │ │ [Display code to user] │ │ │
252
- │ │ [Open browser] │ │ │
253
- │ │ │ │ │
254
- │ │ [User visits github.com/login/device] │ │
255
- │ │ [User enters code: ABCD-1234] │ │
256
- │ │ [User clicks "Authorize gitspace.sh"] │ │
257
- │ │ │ │ │
258
- │ │ POST /login/oauth/access_token (polling) │ │
259
- │ │ {device_code, client_id, │ │ │
260
- │ │ grant_type: device_code} │ │ │
261
- │ │────────────────────────────►│ │ │
262
- │ │ │ │ │
263
- │ │◄────────────────────────────│ │ │
264
- │ │ {access_token, token_type, │ │ │
265
- │ │ scope} │ │ │
266
- │ │ │ │ │
267
- │ │ │ │
268
- │ │ POST /auth/github/device │ │
269
- │ │ {github_token, machine_pubkey, device_name} │ │
270
- │ │───────────────────────────────────────────────────────────►│ │
271
- │ │ │ │
272
- │ │ [Verify token with GitHub API]│ │
273
- │ │ [GET github.com/user] │ │
274
- │ │ [Create/find account by │ │
275
- │ │ github_id] │ │
276
- │ │ [Create CLI token] │ │
277
- │ │ │ │
278
- │ │◄───────────────────────────────────────────────────────────│ │
279
- │ │ {token: "gst_xxx", user: {github_username, ...}} │ │
280
- │ │ │ │
281
- │ │ [Save token to keychain] │ │ │
282
- │ │ │ │ │
283
- │ │
284
- └─────────────────────────────────────────────────────────────────────────────┘
285
- ```
286
-
287
- ### 3. Reserve Subdomain
288
-
289
- ```
290
- ┌─────────────────────────────────────────────────────────────────────────────┐
291
- │ Terminal │
292
- ├─────────────────────────────────────────────────────────────────────────────┤
293
- │ │
294
- │ $ gssh host reserve brad │
295
- │ │
296
- │ Checking availability... ✓ │
297
- │ Creating tunnel... ✓ │
298
- │ Configuring DNS... ✓ │
299
- │ Saving credentials... ✓ │
300
- │ │
301
- │ ✓ Reserved: brad.gitspace.sh │
302
- │ │
303
- │ Your subdomain is ready: │
304
- │ • brad.gitspace.sh │
305
- │ • *.brad.gitspace.sh (dev.brad.gitspace.sh, api.brad.gitspace.sh, etc.) │
306
- │ │
307
- │ Run 'gssh serve' to start hosting. │
308
- │ │
309
- └─────────────────────────────────────────────────────────────────────────────┘
310
- ```
311
-
312
- ### 4. Start Serving
313
-
314
- ```
315
- ┌─────────────────────────────────────────────────────────────────────────────┐
316
- │ Terminal │
317
- ├─────────────────────────────────────────────────────────────────────────────┤
318
- │ │
319
- │ $ gssh │
320
- │ │
321
- │ Starting Gitspace... │
322
- │ │
323
- │ ✓ Identity loaded │
324
- │ ✓ Tunnel connected (brad.gitspace.sh) │
325
- │ ✓ Relay started (accepting connections from other machines) │
326
- │ ✓ HTTP server listening on :8080 │
327
- │ │
328
- │ Your machine is accessible at: │
329
- │ • https://brad.gitspace.sh │
330
- │ • wss://brad.gitspace.sh/ws (terminal) │
331
- │ │
332
- │ ┌─────────────────────────────────────────────────────────────────────┐ │
333
- │ │ GITSPACE TUI │ │
334
- │ │ ... │ │
335
- │ └─────────────────────────────────────────────────────────────────────┘ │
336
- │ │
337
- └─────────────────────────────────────────────────────────────────────────────┘
338
- ```
339
-
340
- ---
341
-
342
- ## Database Schema (D1)
343
-
344
- ```sql
345
- -- Users (via GitHub OAuth)
346
- CREATE TABLE users (
347
- id TEXT PRIMARY KEY, -- uuid
348
- github_id TEXT UNIQUE NOT NULL,
349
- github_username TEXT NOT NULL,
350
- email TEXT,
351
- name TEXT,
352
- avatar_url TEXT,
353
- created_at INTEGER NOT NULL,
354
- updated_at INTEGER NOT NULL
355
- );
356
-
357
- -- CLI Tokens (one user can have multiple devices)
358
- -- SECURITY: Tokens are hashed before storage. Only prefix is stored for display.
359
- CREATE TABLE tokens (
360
- id TEXT PRIMARY KEY, -- SHA256 hash of full token
361
- prefix TEXT NOT NULL, -- First 8 chars for display: "gst_abc1..."
362
- user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
363
- device_name TEXT,
364
- device_fingerprint TEXT, -- Machine identity public key
365
- created_at INTEGER NOT NULL,
366
- expires_at INTEGER, -- Optional expiration (90 days recommended)
367
- last_used_at INTEGER,
368
- revoked_at INTEGER
369
- );
370
-
371
- CREATE INDEX idx_tokens_user ON tokens(user_id);
372
- CREATE INDEX idx_tokens_prefix ON tokens(prefix); -- For token lookup by prefix
373
-
374
- -- Subdomains (users can have MULTIPLE subdomains)
375
- -- Free tier: 3 subdomains max
376
- -- Paid tier: 10 subdomains max
377
- CREATE TABLE subdomains (
378
- id TEXT PRIMARY KEY, -- uuid
379
- subdomain TEXT UNIQUE NOT NULL, -- "brad" (not full domain)
380
- user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
381
- tunnel_id TEXT NOT NULL, -- Cloudflare tunnel UUID
382
- dns_record_ids TEXT NOT NULL, -- JSON array of DNS record IDs for cleanup
383
- tunnel_token_encrypted TEXT NOT NULL, -- Encrypted tunnel token
384
- status TEXT NOT NULL DEFAULT 'active', -- active, suspended, deleted
385
- is_primary BOOLEAN DEFAULT false, -- Primary subdomain for this user
386
- created_at INTEGER NOT NULL,
387
- updated_at INTEGER NOT NULL
388
- );
389
-
390
- CREATE INDEX idx_subdomains_user ON subdomains(user_id);
391
- CREATE INDEX idx_subdomains_status ON subdomains(status);
392
-
393
- -- Reserved subdomains (cannot be claimed by users)
394
- CREATE TABLE reserved_subdomains (
395
- subdomain TEXT PRIMARY KEY,
396
- reason TEXT NOT NULL -- e.g., "system", "offensive", "trademark"
397
- );
398
-
399
- -- Pre-populate reserved subdomains
400
- INSERT INTO reserved_subdomains (subdomain, reason) VALUES
401
- ('api', 'system'), ('www', 'system'), ('admin', 'system'),
402
- ('mail', 'system'), ('ftp', 'system'), ('relay', 'system'),
403
- ('static', 'system'), ('cdn', 'system'), ('auth', 'system'),
404
- ('login', 'system'), ('status', 'system'), ('docs', 'system'),
405
- ('help', 'system'), ('support', 'system'), ('billing', 'system');
406
-
407
- -- Subdomain access (who can connect to your relay)
408
- CREATE TABLE subdomain_access (
409
- id TEXT PRIMARY KEY,
410
- subdomain_id TEXT NOT NULL REFERENCES subdomains(id) ON DELETE CASCADE,
411
- identity_id TEXT NOT NULL, -- Public key of authorized client
412
- label TEXT,
413
- permissions TEXT NOT NULL, -- JSON: {read, write, manage}
414
- created_at INTEGER NOT NULL
415
- );
416
-
417
- CREATE INDEX idx_subdomain_access_subdomain ON subdomain_access(subdomain_id);
418
- CREATE INDEX idx_subdomain_access_identity ON subdomain_access(identity_id);
419
- ```
420
-
421
- ## Sessions (D1)
422
-
423
- Sessions are stored in D1 (not KV) for better query support and to avoid KV write limits (1,000/day).
424
-
425
- ```sql
426
- -- Portal sessions
427
- CREATE TABLE sessions (
428
- id TEXT PRIMARY KEY, -- session ID (random UUID)
429
- user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
430
- created_at INTEGER NOT NULL,
431
- expires_at INTEGER NOT NULL, -- created_at + 7 days
432
- ip_address TEXT, -- For audit trail
433
- user_agent TEXT -- For audit trail
434
- );
435
-
436
- CREATE INDEX idx_sessions_user ON sessions(user_id);
437
- CREATE INDEX idx_sessions_expires ON sessions(expires_at);
438
- ```
439
-
440
- ```typescript
441
- // Create session
442
- const sessionId = crypto.randomUUID();
443
- const expiresAt = Date.now() + (7 * 24 * 60 * 60 * 1000); // 7 days
444
-
445
- await env.DB.prepare(`
446
- INSERT INTO sessions (id, user_id, created_at, expires_at, ip_address, user_agent)
447
- VALUES (?, ?, ?, ?, ?, ?)
448
- `).bind(sessionId, userId, Date.now(), expiresAt, request.headers.get('CF-Connecting-IP'), request.headers.get('User-Agent')).run();
449
-
450
- // Validate session (with cleanup of expired)
451
- const session = await env.DB.prepare(
452
- 'SELECT * FROM sessions WHERE id = ? AND expires_at > ?'
453
- ).bind(sessionId, Date.now()).first();
454
-
455
- // Cleanup job: DELETE FROM sessions WHERE expires_at < ?
456
- ```
457
-
458
- Note: GitHub Device Flow handles device codes entirely through GitHub's API - we never store them.
459
-
460
- ---
461
-
462
- ## API Specification
463
-
464
- ### Base URL
465
- ```
466
- https://api.gitspace.sh
467
- ```
468
-
469
- ### Authentication
470
-
471
- **Portal (browser)**: Cookie-based sessions stored in KV
472
-
473
- **CLI**: Bearer token in Authorization header
474
- ```
475
- Authorization: Bearer gst_xxxxxxxxxxxx
476
- ```
477
-
478
- ### Endpoints
479
-
480
- #### Auth
481
-
482
- ```
483
- # Portal: GitHub OAuth (redirect-based)
484
- GET /auth/github
485
- → Redirects to GitHub OAuth authorize URL
486
- → Params: client_id, redirect_uri, scope=read:user,user:email
487
-
488
- GET /auth/github/callback?code={code}
489
- → GitHub OAuth callback
490
- → Exchanges code for GitHub access token
491
- → Fetches user info from GitHub API
492
- → Creates/updates user in D1 (keyed by github_id)
493
- → Sets session cookie
494
- → Redirects to dashboard
495
-
496
- # CLI: GitHub Device Flow
497
- POST /auth/github/device
498
- Body: {
499
- github_token, # GitHub access token from device flow
500
- machine_pubkey, # Ed25519 public key (base64)
501
- device_name, # e.g., "Brad's MacBook"
502
- auth_timestamp, # Current timestamp (ms)
503
- auth_signature # Signature proving private key ownership
504
- }
505
- → Verifies signature: sign(`gitspace-device-auth:${timestamp}`, private_key)
506
- → Rejects if timestamp > 5 minutes old (prevent replay)
507
- → Verifies GitHub token by calling GitHub API /user
508
- → Creates/updates user in D1 (keyed by github_id)
509
- → Creates CLI token in D1 (hashed)
510
- → Returns: { token: "gst_xxx", user: { github_username, email, ... } }
511
-
512
- # Logout
513
- POST /auth/logout
514
- Cookie: session
515
- → Deletes session from KV
516
- ```
517
-
518
- #### User
519
-
520
- ```
521
- GET /me
522
- Auth: Bearer token
523
- → Returns: { id, github_username, email, name, avatar_url }
524
-
525
- GET /me/tokens
526
- Auth: Bearer token
527
- → Returns: [{ id, device_name, created_at, last_used_at }]
528
-
529
- DELETE /me/tokens/{tokenId}
530
- Auth: Bearer token
531
- → Revokes token
532
- ```
533
-
534
- #### Subdomains
535
-
536
- ```
537
- GET /subdomains
538
- Auth: Bearer token
539
- → Returns: [{ subdomain, status, created_at }]
540
-
541
- GET /subdomains/check?name={subdomain}
542
- Auth: Bearer token
543
- → Checks: not taken, not reserved, valid format (lowercase, alphanumeric, 3-20 chars)
544
- → Returns: { available: boolean, reason?: string }
545
-
546
- POST /subdomains
547
- Auth: Bearer token
548
- Body: { subdomain, isPrimary?: boolean }
549
- → Validates: subdomain format, not reserved, not taken
550
- → Checks limit: free=3, paid=10 subdomains per user
551
- → Creates tunnel via CF API
552
- → Creates DNS records (subdomain + wildcard), stores record IDs
553
- → Stores encrypted tunnel token
554
- → Sets isPrimary=true if user's first subdomain
555
- → Returns: { subdomain, hosts: ['brad.gitspace.sh', '*.brad.gitspace.sh'], isPrimary }
556
-
557
- POST /subdomains/{subdomain}/set-primary
558
- Auth: Bearer token
559
- → Sets this subdomain as primary, unsets others
560
- → Primary subdomain is used by default in `gssh serve`
561
-
562
- GET /subdomains/{subdomain}/token
563
- Auth: Bearer token
564
- → Returns: { tunnelToken } (decrypted)
565
- → Used by CLI to configure cloudflared
566
-
567
- DELETE /subdomains/{subdomain}
568
- Auth: Bearer token
569
- → Deletes tunnel via CF API
570
- → Deletes DNS records
571
- → Marks subdomain as deleted (or releases)
572
- ```
573
-
574
- #### Access Control (future)
575
-
576
- ```
577
- GET /subdomains/{subdomain}/access
578
- Auth: Bearer token
579
- → Returns: [{ identity_id, label, permissions }]
580
-
581
- POST /subdomains/{subdomain}/access
582
- Auth: Bearer token
583
- Body: { identityId, label, permissions }
584
- → Grants access
585
-
586
- DELETE /subdomains/{subdomain}/access/{identityId}
587
- Auth: Bearer token
588
- → Revokes access
589
- ```
590
-
591
- ---
592
-
593
- ## Worker Implementation
594
-
595
- ### Project Structure
596
-
597
- ```
598
- worker/
599
- ├── src/
600
- │ ├── index.ts # Main entry, routing
601
- │ ├── middleware/
602
- │ │ ├── auth.ts # Token/session validation
603
- │ │ └── cors.ts # CORS headers
604
- │ ├── handlers/
605
- │ │ ├── auth.ts # OAuth, device flow
606
- │ │ ├── user.ts # User endpoints
607
- │ │ └── subdomains.ts # Subdomain management
608
- │ ├── services/
609
- │ │ ├── cloudflare.ts # CF API client (tunnels, DNS)
610
- │ │ └── crypto.ts # Token encryption/decryption
611
- │ └── types.ts
612
- ├── schema.sql # D1 schema
613
- ├── wrangler.toml
614
- └── package.json
615
- ```
616
-
617
- ### wrangler.toml
618
-
619
- ```toml
620
- name = "gitspace-api"
621
- main = "src/index.ts"
622
- compatibility_date = "2024-01-01"
623
-
624
- [vars]
625
- GITHUB_CLIENT_ID = "xxx"
626
- PORTAL_URL = "https://gitspace.sh"
627
-
628
- [[d1_databases]]
629
- binding = "DB"
630
- database_name = "gitspace"
631
- database_id = "xxx"
632
-
633
- [[kv_namespaces]]
634
- binding = "KV"
635
- id = "xxx"
636
-
637
- [secrets]
638
- # Set via wrangler secret put
639
- # GITHUB_CLIENT_SECRET
640
- # CF_API_TOKEN
641
- # CF_ACCOUNT_ID
642
- # CF_ZONE_ID
643
- # ENCRYPTION_KEY
644
- ```
645
-
646
- ### Key Implementation Details
647
-
648
- ```typescript
649
- // src/services/crypto.ts - Token hashing
650
-
651
- export async function hashToken(token: string): Promise<string> {
652
- const encoder = new TextEncoder();
653
- const data = encoder.encode(token);
654
- const hashBuffer = await crypto.subtle.digest('SHA-256', data);
655
- const hashArray = Array.from(new Uint8Array(hashBuffer));
656
- return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
657
- }
658
-
659
- // src/middleware/auth.ts - Token validation
660
-
661
- export async function validateToken(
662
- request: Request,
663
- env: Env
664
- ): Promise<{ user: User } | null> {
665
- const authHeader = request.headers.get('Authorization');
666
- if (!authHeader?.startsWith('Bearer ')) return null;
667
-
668
- const tokenPlain = authHeader.slice(7);
669
- const tokenHash = await hashToken(tokenPlain);
670
-
671
- // Look up by hash, check expiration and revocation
672
- const token = await env.DB.prepare(`
673
- SELECT t.*, u.* FROM tokens t
674
- JOIN users u ON t.user_id = u.id
675
- WHERE t.id = ? AND t.revoked_at IS NULL
676
- AND (t.expires_at IS NULL OR t.expires_at > ?)
677
- `).bind(tokenHash, Date.now()).first();
678
-
679
- if (!token) return null;
680
-
681
- // Update last_used_at (fire-and-forget)
682
- env.DB.prepare('UPDATE tokens SET last_used_at = ? WHERE id = ?')
683
- .bind(Date.now(), tokenHash).run();
684
-
685
- return { user: token as User };
686
- }
687
- ```
688
-
689
- ```typescript
690
- // src/services/cloudflare.ts
691
-
692
- export async function createTunnel(
693
- env: Env,
694
- name: string
695
- ): Promise<{ id: string; token: string }> {
696
- // Generate tunnel secret (32 random bytes, base64)
697
- const secret = btoa(String.fromCharCode(...crypto.getRandomValues(new Uint8Array(32))));
698
-
699
- const response = await fetch(
700
- `https://api.cloudflare.com/client/v4/accounts/${env.CF_ACCOUNT_ID}/cfd_tunnel`,
701
- {
702
- method: 'POST',
703
- headers: {
704
- 'Authorization': `Bearer ${env.CF_API_TOKEN}`,
705
- 'Content-Type': 'application/json',
706
- },
707
- body: JSON.stringify({
708
- name: `gitspace-${name}`,
709
- tunnel_secret: secret,
710
- }),
711
- }
712
- );
713
-
714
- const data = await response.json();
715
- return {
716
- id: data.result.id,
717
- token: data.result.token,
718
- };
719
- }
720
-
721
- export async function createDNSRecords(
722
- env: Env,
723
- subdomain: string,
724
- tunnelId: string
725
- ): Promise<void> {
726
- const records = [
727
- { name: subdomain, type: 'CNAME' }, // brad.gitspace.sh
728
- { name: `*.${subdomain}`, type: 'CNAME' }, // *.brad.gitspace.sh
729
- ];
730
-
731
- for (const record of records) {
732
- await fetch(
733
- `https://api.cloudflare.com/client/v4/zones/${env.CF_ZONE_ID}/dns_records`,
734
- {
735
- method: 'POST',
736
- headers: {
737
- 'Authorization': `Bearer ${env.CF_API_TOKEN}`,
738
- 'Content-Type': 'application/json',
739
- },
740
- body: JSON.stringify({
741
- type: record.type,
742
- name: record.name,
743
- content: `${tunnelId}.cfargotunnel.com`,
744
- proxied: true,
745
- }),
746
- }
747
- );
748
- }
749
- }
750
-
751
- export async function deleteTunnel(env: Env, tunnelId: string): Promise<void> {
752
- // This immediately prevents new connections
753
- await fetch(
754
- `https://api.cloudflare.com/client/v4/accounts/${env.CF_ACCOUNT_ID}/cfd_tunnel/${tunnelId}`,
755
- {
756
- method: 'DELETE',
757
- headers: {
758
- 'Authorization': `Bearer ${env.CF_API_TOKEN}`,
759
- },
760
- }
761
- );
762
- }
763
-
764
- // src/handlers/auth.ts - GitHub Device Flow handler
765
-
766
- import { ed25519 } from '@noble/curves/ed25519';
767
-
768
- interface GitHubDeviceAuthRequest {
769
- github_token: string;
770
- machine_pubkey: string;
771
- device_name: string;
772
- auth_timestamp: number;
773
- auth_signature: string;
774
- }
775
-
776
- export async function handleGitHubDeviceAuth(
777
- request: Request,
778
- env: Env
779
- ): Promise<Response> {
780
- const body: GitHubDeviceAuthRequest = await request.json();
781
- const { github_token, machine_pubkey, device_name, auth_timestamp, auth_signature } = body;
782
-
783
- // Step 0: Verify signature to prevent device impersonation
784
- // SECURITY: Without this, an attacker could register with a stolen public key
785
- const now = Date.now();
786
- const MAX_TIMESTAMP_AGE = 5 * 60 * 1000; // 5 minutes
787
-
788
- // Check timestamp freshness (prevent replay attacks)
789
- if (Math.abs(now - auth_timestamp) > MAX_TIMESTAMP_AGE) {
790
- return Response.json(
791
- { error: 'Auth timestamp expired. Please try again.' },
792
- { status: 401 }
793
- );
794
- }
795
-
796
- // Verify the signature proves ownership of private key
797
- const authMessage = `gitspace-device-auth:${auth_timestamp}`;
798
- const messageBytes = new TextEncoder().encode(authMessage);
799
- const signatureBytes = Buffer.from(auth_signature, 'base64');
800
- const publicKeyBytes = Buffer.from(machine_pubkey, 'base64');
801
-
802
- try {
803
- const isValid = ed25519.verify(signatureBytes, messageBytes, publicKeyBytes);
804
- if (!isValid) {
805
- return Response.json(
806
- { error: 'Invalid device signature' },
807
- { status: 401 }
808
- );
809
- }
810
- } catch (err) {
811
- return Response.json(
812
- { error: 'Invalid signature format' },
813
- { status: 400 }
814
- );
815
- }
816
-
817
- // Step 1: Verify GitHub token by fetching user info
818
- const githubUserRes = await fetch('https://api.github.com/user', {
819
- headers: {
820
- 'Authorization': `Bearer ${github_token}`,
821
- 'User-Agent': 'gitspace.sh',
822
- 'Accept': 'application/vnd.github+json',
823
- },
824
- });
825
-
826
- if (!githubUserRes.ok) {
827
- return Response.json(
828
- { error: 'Invalid GitHub token' },
829
- { status: 401 }
830
- );
831
- }
832
-
833
- const githubUser = await githubUserRes.json();
834
-
835
- // Step 2: Fetch user emails (need scope: user:email)
836
- const emailsRes = await fetch('https://api.github.com/user/emails', {
837
- headers: {
838
- 'Authorization': `Bearer ${github_token}`,
839
- 'User-Agent': 'gitspace.sh',
840
- 'Accept': 'application/vnd.github+json',
841
- },
842
- });
843
-
844
- let email: string | null = null;
845
- if (emailsRes.ok) {
846
- const emails = await emailsRes.json();
847
- const primary = emails.find((e: any) => e.primary && e.verified);
848
- email = primary?.email || null;
849
- }
850
-
851
- // Step 3: Find or create user (keyed by github_id)
852
- let user = await env.DB.prepare(
853
- 'SELECT * FROM users WHERE github_id = ?'
854
- ).bind(String(githubUser.id)).first();
855
-
856
- const now = Date.now();
857
-
858
- if (!user) {
859
- // Create new user
860
- const userId = crypto.randomUUID();
861
- await env.DB.prepare(`
862
- INSERT INTO users (id, github_id, github_username, email, name, avatar_url, created_at, updated_at)
863
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
864
- `).bind(
865
- userId,
866
- String(githubUser.id),
867
- githubUser.login,
868
- email,
869
- githubUser.name,
870
- githubUser.avatar_url,
871
- now,
872
- now
873
- ).run();
874
-
875
- user = { id: userId, github_id: String(githubUser.id), github_username: githubUser.login, email };
876
- } else {
877
- // Update existing user
878
- await env.DB.prepare(`
879
- UPDATE users SET github_username = ?, email = ?, name = ?, avatar_url = ?, updated_at = ?
880
- WHERE id = ?
881
- `).bind(
882
- githubUser.login,
883
- email || user.email,
884
- githubUser.name,
885
- githubUser.avatar_url,
886
- now,
887
- user.id
888
- ).run();
889
- }
890
-
891
- // Step 4: Create CLI token (hashed for storage)
892
- const tokenPlain = `gst_${crypto.randomUUID().replace(/-/g, '')}`;
893
- const tokenPrefix = tokenPlain.slice(0, 12); // "gst_abc12345"
894
- const tokenHash = await hashToken(tokenPlain);
895
- const expiresAt = now + (90 * 24 * 60 * 60 * 1000); // 90 days
896
-
897
- await env.DB.prepare(`
898
- INSERT INTO tokens (id, prefix, user_id, device_name, device_fingerprint, created_at, expires_at, last_used_at)
899
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
900
- `).bind(
901
- tokenHash, // Store hash, not plain token
902
- tokenPrefix,
903
- user.id,
904
- device_name,
905
- machine_pubkey,
906
- now,
907
- expiresAt,
908
- now
909
- ).run();
910
-
911
- // Step 5: Return token and user info
912
- // IMPORTANT: This is the only time the plain token is returned!
913
- return Response.json({
914
- token: tokenPlain,
915
- user: {
916
- id: user.id,
917
- github_username: githubUser.login,
918
- email: email,
919
- name: githubUser.name,
920
- avatar_url: githubUser.avatar_url,
921
- },
922
- });
923
- }
924
- ```
925
-
926
- ---
927
-
928
- ## CLI Implementation
929
-
930
- ### Commands
931
-
932
- ```bash
933
- # Authentication
934
- gssh auth login # Device auth flow
935
- gssh auth logout # Clear local token
936
- gssh auth status # Show current user
937
-
938
- # Hosting (supports multiple subdomains: free=3, paid=10)
939
- gssh host reserve <name> # Reserve subdomain
940
- gssh host release [name] # Release subdomain
941
- gssh host list # List your subdomains
942
- gssh host set-primary <name> # Set primary subdomain for `gssh serve`
943
- gssh host status # Show current hosting status
944
-
945
- # Main entry (starts everything)
946
- spaces # TUI + tunnel + relay
947
- gssh --remote <subdomain> # Connect to remote machine
948
- ```
949
-
950
- ### Dependencies
951
-
952
- ```bash
953
- # New CLI dependencies
954
- bun add open # Open browser URLs cross-platform
955
- bun add which # Find executables (cloudflared check)
956
- bun add yaml # Parse/generate cloudflared config
957
- ```
958
-
959
- ### Implementation
960
-
961
- ```typescript
962
- // src/commands/auth.ts
963
-
964
- import open from 'open'; // Opens browser URLs cross-platform
965
- import os from 'os';
966
- import { getSecret, setSecret, deleteSecret } from '../utils/secrets.js';
967
- import { loadKeypair, getPublicKeyWithoutPassword } from '../core/identity.js';
968
- import { sign, serializePublicKey } from '../lib/tmux-lite/crypto/identity.js';
969
- import { promptPassword } from '../utils/prompts.js';
970
-
971
- const API_BASE = 'https://api.gitspace.sh';
972
- const GITHUB_CLIENT_ID = 'Iv1.xxxxxxxxxxxxxxxx'; // Your GitHub OAuth App client ID
973
-
974
- interface DeviceCodeResponse {
975
- device_code: string;
976
- user_code: string;
977
- verification_uri: string;
978
- expires_in: number;
979
- interval: number;
980
- }
981
-
982
- interface GitHubTokenResponse {
983
- access_token?: string;
984
- token_type?: string;
985
- scope?: string;
986
- error?: string;
987
- error_description?: string;
988
- }
989
-
990
- export async function authLogin(): Promise<void> {
991
- // Load identity (requires password to access private key for signing)
992
- const password = await promptPassword('Enter identity password: ');
993
- const identity = await loadKeypair(password);
994
-
995
- // Step 1: Request device code from GitHub
996
- console.log('Starting GitHub authentication...');
997
-
998
- const deviceRes = await fetch('https://github.com/login/device/code', {
999
- method: 'POST',
1000
- headers: {
1001
- 'Accept': 'application/json',
1002
- 'Content-Type': 'application/json',
1003
- },
1004
- body: JSON.stringify({
1005
- client_id: GITHUB_CLIENT_ID,
1006
- scope: 'read:user user:email',
1007
- }),
1008
- });
1009
-
1010
- const deviceData: DeviceCodeResponse = await deviceRes.json();
1011
- const { device_code, user_code, verification_uri, interval } = deviceData;
1012
-
1013
- // Step 2: Display code and open browser
1014
- console.log(`\n! First, copy your one-time code: ${user_code}\n`);
1015
-
1016
- // Try to open browser, with fallback for headless/SSH environments
1017
- const canOpenBrowser = process.stdout.isTTY && !process.env.SSH_CLIENT;
1018
-
1019
- if (canOpenBrowser) {
1020
- console.log(`Press Enter to open ${verification_uri} in your browser...`);
1021
- await new Promise<void>((resolve) => {
1022
- process.stdin.once('data', () => resolve());
1023
- });
1024
-
1025
- try {
1026
- await open(verification_uri);
1027
- console.log('\nWaiting for authorization...');
1028
- } catch (err) {
1029
- // Browser open failed (WSL, headless, etc.)
1030
- console.log(`\nCould not open browser automatically.`);
1031
- console.log(`Please open this URL manually: ${verification_uri}`);
1032
- console.log(`\nWaiting for authorization...`);
1033
- }
1034
- } else {
1035
- // Headless environment (SSH, CI, etc.)
1036
- console.log(`Open this URL in your browser: ${verification_uri}`);
1037
- console.log(`Enter the code: ${user_code}`);
1038
- console.log(`\nWaiting for authorization...`);
1039
- }
1040
-
1041
- // Step 3: Poll GitHub for access token
1042
- const githubToken = await pollForGitHubToken(device_code, interval);
1043
-
1044
- // Step 4: Exchange GitHub token for gitspace.sh token
1045
- // SECURITY: Sign auth request to prove private key ownership
1046
- console.log('Completing authentication...');
1047
-
1048
- const authTimestamp = Date.now();
1049
- const authMessage = `gitspace-device-auth:${authTimestamp}`;
1050
- const authSignature = sign(authMessage, identity.signingSecretKey);
1051
-
1052
- const response = await fetch(`${API_BASE}/auth/github/device`, {
1053
- method: 'POST',
1054
- headers: { 'Content-Type': 'application/json' },
1055
- body: JSON.stringify({
1056
- github_token: githubToken,
1057
- machine_pubkey: serializePublicKey(identity.signingPublicKey),
1058
- device_name: os.hostname(),
1059
- auth_timestamp: authTimestamp,
1060
- auth_signature: authSignature, // Proves private key ownership
1061
- }),
1062
- });
1063
-
1064
- if (!response.ok) {
1065
- const error = await response.json();
1066
- throw new Error(`Authentication failed: ${error.message}`);
1067
- }
1068
-
1069
- const { token, user } = await response.json();
1070
-
1071
- // Step 5: Save token to keychain
1072
- await setSecret('GITSPACE_TOKEN', token);
1073
-
1074
- console.log(`\n✓ Authentication complete`);
1075
- console.log(`✓ Logged in as ${user.github_username}`);
1076
- console.log(`✓ Token saved to keychain`);
1077
- }
1078
-
1079
- async function pollForGitHubToken(deviceCode: string, interval: number): Promise<string> {
1080
- const maxAttempts = 60; // ~5 minutes with default 5s interval
1081
-
1082
- for (let i = 0; i < maxAttempts; i++) {
1083
- await sleep(interval * 1000);
1084
-
1085
- const res = await fetch('https://github.com/login/oauth/access_token', {
1086
- method: 'POST',
1087
- headers: {
1088
- 'Accept': 'application/json',
1089
- 'Content-Type': 'application/json',
1090
- },
1091
- body: JSON.stringify({
1092
- client_id: GITHUB_CLIENT_ID,
1093
- device_code: deviceCode,
1094
- grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
1095
- }),
1096
- });
1097
-
1098
- const data: GitHubTokenResponse = await res.json();
1099
-
1100
- if (data.access_token) {
1101
- return data.access_token;
1102
- }
1103
-
1104
- if (data.error === 'authorization_pending') {
1105
- // User hasn't authorized yet, keep polling
1106
- continue;
1107
- }
1108
-
1109
- if (data.error === 'slow_down') {
1110
- // Rate limited, increase interval
1111
- interval += 5;
1112
- continue;
1113
- }
1114
-
1115
- if (data.error === 'expired_token') {
1116
- throw new Error('Authorization expired. Please try again.');
1117
- }
1118
-
1119
- if (data.error === 'access_denied') {
1120
- throw new Error('Authorization denied by user.');
1121
- }
1122
-
1123
- throw new Error(`GitHub auth error: ${data.error_description || data.error}`);
1124
- }
1125
-
1126
- throw new Error('Authorization timeout. Please try again.');
1127
- }
1128
-
1129
- function sleep(ms: number): Promise<void> {
1130
- return new Promise(resolve => setTimeout(resolve, ms));
1131
- }
1132
-
1133
- export async function authLogout(): Promise<void> {
1134
- await deleteSecret('GITSPACE_TOKEN');
1135
- console.log('✓ Logged out');
1136
- }
1137
-
1138
- export async function authStatus(): Promise<void> {
1139
- const token = await getSecret('GITSPACE_TOKEN');
1140
-
1141
- if (!token) {
1142
- console.log('Not logged in. Run: gssh auth login');
1143
- return;
1144
- }
1145
-
1146
- const res = await fetch(`${API_BASE}/me`, {
1147
- headers: { 'Authorization': `Bearer ${token}` },
1148
- });
1149
-
1150
- if (!res.ok) {
1151
- console.log('Session expired. Run: gssh auth login');
1152
- return;
1153
- }
1154
-
1155
- const user = await res.json();
1156
- console.log(`Logged in as: ${user.github_username}`);
1157
- console.log(`Email: ${user.email || '(not set)'}`);
1158
- }
1159
-
1160
- // src/commands/host.ts
1161
-
1162
- export async function hostReserve(subdomain: string): Promise<void> {
1163
- const token = await getSecret('GITSPACE_TOKEN');
1164
- if (!token) {
1165
- console.log('Not logged in. Run: gssh auth login');
1166
- return;
1167
- }
1168
-
1169
- // Check availability
1170
- console.log('Checking availability...');
1171
- const checkRes = await fetch(
1172
- `${API_BASE}/subdomains/check?name=${subdomain}`,
1173
- { headers: { 'Authorization': `Bearer ${token}` } }
1174
- );
1175
- const { available } = await checkRes.json();
1176
-
1177
- if (!available) {
1178
- console.error(`Subdomain "${subdomain}" is not available`);
1179
- return;
1180
- }
1181
-
1182
- // Reserve
1183
- console.log('Creating tunnel...');
1184
- const res = await fetch(`${API_BASE}/subdomains`, {
1185
- method: 'POST',
1186
- headers: {
1187
- 'Authorization': `Bearer ${token}`,
1188
- 'Content-Type': 'application/json',
1189
- },
1190
- body: JSON.stringify({ subdomain }),
1191
- });
1192
-
1193
- if (!res.ok) {
1194
- const { error } = await res.json();
1195
- console.error(`Failed: ${error}`);
1196
- return;
1197
- }
1198
-
1199
- const data = await res.json();
1200
- console.log(`✓ Reserved: ${data.subdomain}.gitspace.sh`);
1201
- console.log(` Wildcard: *.${data.subdomain}.gitspace.sh`);
1202
- if (data.isPrimary) {
1203
- console.log(` (set as primary)`);
1204
- }
1205
-
1206
- // Fetch and store tunnel token for this subdomain
1207
- const tokenRes = await fetch(
1208
- `${API_BASE}/subdomains/${subdomain}/token`,
1209
- { headers: { 'Authorization': `Bearer ${token}` } }
1210
- );
1211
- const { tunnelToken } = await tokenRes.json();
1212
-
1213
- // Store tunnel token in keychain (per-subdomain)
1214
- // SECURITY: Uses system keychain, not plaintext file
1215
- await setSecret(`TUNNEL_TOKEN_${subdomain}`, tunnelToken);
1216
-
1217
- console.log('\nRun `gssh` to start hosting.');
1218
- console.log(`Or `gssh host list` to see all your subdomains.`);
1219
- }
1220
-
1221
- export async function hostList(): Promise<void> {
1222
- const token = await getSecret('GITSPACE_TOKEN');
1223
- if (!token) {
1224
- console.log('Not logged in. Run: gssh auth login');
1225
- return;
1226
- }
1227
-
1228
- const res = await fetch(`${API_BASE}/subdomains`, {
1229
- headers: { 'Authorization': `Bearer ${token}` },
1230
- });
1231
-
1232
- const subdomains = await res.json();
1233
-
1234
- if (subdomains.length === 0) {
1235
- console.log('No subdomains reserved. Run: gssh host reserve <name>');
1236
- return;
1237
- }
1238
-
1239
- console.log('Your subdomains:\n');
1240
- for (const sub of subdomains) {
1241
- const primary = sub.is_primary ? ' (primary)' : '';
1242
- const status = sub.status === 'active' ? '✓' : '✗';
1243
- console.log(` ${status} ${sub.subdomain}.gitspace.sh${primary}`);
1244
- console.log(` Created: ${new Date(sub.created_at).toLocaleDateString()}`);
1245
- }
1246
-
1247
- console.log(`\n${subdomains.length}/3 subdomains used (free tier)`);
1248
- }
1249
-
1250
- export async function hostSetPrimary(subdomain: string): Promise<void> {
1251
- const token = await getSecret('GITSPACE_TOKEN');
1252
- if (!token) {
1253
- console.log('Not logged in. Run: gssh auth login');
1254
- return;
1255
- }
1256
-
1257
- const res = await fetch(`${API_BASE}/subdomains/${subdomain}/set-primary`, {
1258
- method: 'POST',
1259
- headers: { 'Authorization': `Bearer ${token}` },
1260
- });
1261
-
1262
- if (!res.ok) {
1263
- const { error } = await res.json();
1264
- console.error(`Failed: ${error}`);
1265
- return;
1266
- }
1267
-
1268
- console.log(`✓ ${subdomain}.gitspace.sh is now your primary subdomain`);
1269
- }
1270
- ```
1271
-
1272
- ### gssh serve Integration
1273
-
1274
- ```typescript
1275
- // src/commands/serve.ts - cloudflared integration
1276
-
1277
- import { spawn, type ChildProcess } from 'child_process';
1278
- import { join } from 'path';
1279
- import { writeFile } from 'fs/promises';
1280
- import * as yaml from 'yaml';
1281
-
1282
- let cloudflaredProcess: ChildProcess | null = null;
1283
-
1284
- async function startCloudflared(subdomain: string): Promise<void> {
1285
- // SECURITY: Read tunnel token from keychain (not from config file)
1286
- const tunnelToken = await getSecret(`TUNNEL_TOKEN_${subdomain}`);
1287
- if (!tunnelToken) {
1288
- throw new Error(`No tunnel token found for ${subdomain}. Run: gssh host reserve ${subdomain}`);
1289
- }
1290
-
1291
- // Write cloudflared config for spaces (separate from user's own config)
1292
- const configDir = join(os.homedir(), '.spaces');
1293
- const configPath = join(configDir, 'cloudflared.yml');
1294
-
1295
- await writeFile(configPath, yaml.stringify({
1296
- // Token-based auth (no credentials file needed)
1297
- ingress: [
1298
- // Main subdomain
1299
- {
1300
- hostname: `${subdomain}.gitspace.sh`,
1301
- service: 'http://localhost:8080'
1302
- },
1303
- // Wildcard for workspaces/services
1304
- {
1305
- hostname: `*.${subdomain}.gitspace.sh`,
1306
- service: 'http://localhost:8080'
1307
- },
1308
- // Catch-all (required)
1309
- { service: 'http_status:404' }
1310
- ],
1311
- }));
1312
-
1313
- // Check cloudflared is installed
1314
- const cloudflaredPath = await which('cloudflared').catch(() => null);
1315
- if (!cloudflaredPath) {
1316
- throw new Error(
1317
- 'cloudflared not found. Install it:\n' +
1318
- ' macOS: brew install cloudflared\n' +
1319
- ' Linux: See https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/'
1320
- );
1321
- }
1322
-
1323
- // Start cloudflared with token via env var (not CLI arg - visible in `ps`)
1324
- // SECURITY: TUNNEL_TOKEN env var is not visible to other users on the system
1325
- cloudflaredProcess = spawn('cloudflared', [
1326
- 'tunnel',
1327
- '--config', configPath,
1328
- 'run',
1329
- ], {
1330
- stdio: ['ignore', 'pipe', 'pipe'],
1331
- env: {
1332
- ...process.env,
1333
- TUNNEL_TOKEN: tunnelToken, // Pass token via env, not CLI arg
1334
- },
1335
- });
1336
-
1337
- cloudflaredProcess.stdout?.on('data', (data) => {
1338
- logger.dim(`[cloudflared] ${data.toString().trim()}`);
1339
- });
1340
-
1341
- cloudflaredProcess.stderr?.on('data', (data) => {
1342
- const msg = data.toString().trim();
1343
- if (msg.includes('error')) {
1344
- logger.error(`[cloudflared] ${msg}`);
1345
- } else {
1346
- logger.dim(`[cloudflared] ${msg}`);
1347
- }
1348
- });
1349
-
1350
- // Handle cloudflared crash - restart with backoff
1351
- cloudflaredProcess.on('exit', (code) => {
1352
- if (code !== 0 && !shuttingDown) {
1353
- logger.warn(`[cloudflared] Exited with code ${code}, restarting in 5s...`);
1354
- setTimeout(() => startCloudflared(subdomain), 5000);
1355
- }
1356
- });
1357
-
1358
- // Wait for tunnel to be ready
1359
- await waitForTunnel(subdomain);
1360
- }
1361
-
1362
- async function waitForTunnel(subdomain: string, timeout = 30000): Promise<void> {
1363
- const start = Date.now();
1364
-
1365
- while (Date.now() - start < timeout) {
1366
- try {
1367
- const res = await fetch(`https://${subdomain}.gitspace.sh/health`, {
1368
- signal: AbortSignal.timeout(2000),
1369
- });
1370
- if (res.ok) return;
1371
- } catch {
1372
- // Not ready yet
1373
- }
1374
- await sleep(1000);
1375
- }
1376
-
1377
- throw new Error('Tunnel failed to connect');
1378
- }
1379
-
1380
- function stopCloudflared(): void {
1381
- if (cloudflaredProcess) {
1382
- cloudflaredProcess.kill();
1383
- cloudflaredProcess = null;
1384
- }
1385
- }
1386
- ```
1387
-
1388
- ---
1389
-
1390
- ## Local Server (Embedded Relay + HTTP)
1391
-
1392
- ```typescript
1393
- // src/serve/local-server.ts
1394
-
1395
- import { serve } from 'bun';
1396
- import net from 'net';
1397
-
1398
- interface LocalServerConfig {
1399
- port: number;
1400
- subdomain: string;
1401
- identity: Identity;
1402
- accessList: AccessControlList;
1403
- sessionManager: ClientSessionManager;
1404
- serviceRouter: ServiceRouter;
1405
- }
1406
-
1407
- // Check if a port is available
1408
- async function isPortAvailable(port: number): Promise<boolean> {
1409
- return new Promise((resolve) => {
1410
- const server = net.createServer();
1411
- server.once('error', () => resolve(false));
1412
- server.once('listening', () => {
1413
- server.close();
1414
- resolve(true);
1415
- });
1416
- server.listen(port);
1417
- });
1418
- }
1419
-
1420
- // Find an available port, starting from preferred
1421
- async function findAvailablePort(preferred: number, maxAttempts = 10): Promise<number> {
1422
- for (let i = 0; i < maxAttempts; i++) {
1423
- const port = preferred + i;
1424
- if (await isPortAvailable(port)) {
1425
- return port;
1426
- }
1427
- }
1428
- throw new Error(`No available port found in range ${preferred}-${preferred + maxAttempts - 1}`);
1429
- }
1430
-
1431
- export async function createLocalServer(config: LocalServerConfig) {
1432
- const { subdomain, identity, accessList, sessionManager, serviceRouter } = config;
1433
-
1434
- // Find available port (fallback if 8080 is taken)
1435
- const port = await findAvailablePort(config.port);
1436
- if (port !== config.port) {
1437
- logger.warn(`Port ${config.port} in use, using ${port} instead`);
1438
- }
1439
-
1440
- return serve({
1441
- port,
1442
-
1443
- async fetch(req, server) {
1444
- const url = new URL(req.url);
1445
- const host = req.headers.get('host') || '';
1446
-
1447
- // Health check
1448
- if (url.pathname === '/health') {
1449
- return Response.json({ status: 'ok', subdomain });
1450
- }
1451
-
1452
- // WebSocket upgrade for terminal
1453
- if (url.pathname === '/ws') {
1454
- const upgraded = server.upgrade(req, {
1455
- data: { type: 'terminal' }
1456
- });
1457
- if (upgraded) return undefined;
1458
- return new Response('WebSocket upgrade failed', { status: 500 });
1459
- }
1460
-
1461
- // Route HTTP to services based on subdomain
1462
- // e.g., dev.brad.gitspace.sh → dev workspace
1463
- // e.g., api.brad.gitspace.sh → api service
1464
- const subHost = extractSubdomain(host, subdomain);
1465
- return serviceRouter.route(subHost, req);
1466
- },
1467
-
1468
- websocket: {
1469
- open(ws) {
1470
- sessionManager.handleConnect(ws.data.connectionId);
1471
- },
1472
-
1473
- message(ws, message) {
1474
- // Reuse existing terminal protocol handling
1475
- sessionManager.handleMessage(ws.data.connectionId, message);
1476
- },
1477
-
1478
- close(ws, code, reason) {
1479
- sessionManager.handleDisconnect(ws.data.connectionId, reason);
1480
- },
1481
- },
1482
- });
1483
- }
1484
-
1485
- function extractSubdomain(host: string, baseSubdomain: string): string | null {
1486
- // host: "dev.brad.gitspace.sh"
1487
- // baseSubdomain: "brad"
1488
- // returns: "dev"
1489
-
1490
- const pattern = new RegExp(`^(.+)\\.${baseSubdomain}\\.gitspace\\.sh$`);
1491
- const match = host.match(pattern);
1492
- return match ? match[1] : null;
1493
- }
1494
- ```
1495
-
1496
- ---
1497
-
1498
- ## Revocation Flow
1499
-
1500
- ```typescript
1501
- // worker/src/handlers/subdomains.ts
1502
-
1503
- export async function revokeSubdomain(
1504
- subdomain: string,
1505
- userId: string,
1506
- env: Env
1507
- ): Promise<void> {
1508
- // 1. Get subdomain record
1509
- const record = await env.DB.prepare(
1510
- 'SELECT * FROM subdomains WHERE subdomain = ? AND user_id = ?'
1511
- ).bind(subdomain, userId).first();
1512
-
1513
- if (!record) {
1514
- throw new Error('Subdomain not found');
1515
- }
1516
-
1517
- // 2. Delete tunnel (IMMEDIATE - blocks new connections)
1518
- await deleteTunnel(env, record.tunnel_id);
1519
-
1520
- // 3. Delete DNS records
1521
- await deleteDNSRecords(env, subdomain);
1522
-
1523
- // 4. Update database
1524
- await env.DB.prepare(
1525
- 'UPDATE subdomains SET status = ?, updated_at = ? WHERE id = ?'
1526
- ).bind('deleted', Date.now(), record.id).run();
1527
-
1528
- // 5. Optionally: Release subdomain for reuse after cooldown
1529
- // await scheduleSubdomainRelease(subdomain, 30 * 24 * 60 * 60 * 1000); // 30 days
1530
- }
1531
-
1532
- // Admin revocation (abuse cases)
1533
- export async function adminRevokeUser(userId: string, env: Env): Promise<void> {
1534
- // Get all user's subdomains
1535
- const subdomains = await env.DB.prepare(
1536
- 'SELECT * FROM subdomains WHERE user_id = ? AND status = ?'
1537
- ).bind(userId, 'active').all();
1538
-
1539
- // Revoke each subdomain
1540
- for (const sub of subdomains.results) {
1541
- await deleteTunnel(env, sub.tunnel_id);
1542
- await deleteDNSRecords(env, sub.subdomain);
1543
- }
1544
-
1545
- // Mark all as suspended
1546
- await env.DB.prepare(
1547
- 'UPDATE subdomains SET status = ?, updated_at = ? WHERE user_id = ?'
1548
- ).bind('suspended', Date.now(), userId).run();
1549
-
1550
- // Revoke all tokens
1551
- await env.DB.prepare(
1552
- 'UPDATE tokens SET revoked_at = ? WHERE user_id = ?'
1553
- ).bind(Date.now(), userId).run();
1554
- }
1555
- ```
1556
-
1557
- ---
1558
-
1559
- ## Peer Relay Model
1560
-
1561
- Secondary machines connect to primary machine's embedded relay:
1562
-
1563
- ```typescript
1564
- // src/commands/serve.ts
1565
-
1566
- import { getSecret } from '../utils/secrets.js';
1567
-
1568
- /**
1569
- * Host config stored in ~/gitspace/host.json (non-sensitive data only)
1570
- * Sensitive tunnel tokens are stored in keychain via Bun.secrets
1571
- */
1572
- interface HostConfig {
1573
- subdomain: string; // Primary subdomain
1574
- subdomains?: string[]; // Additional subdomains (if any)
1575
- createdAt: number;
1576
- }
1577
-
1578
- export async function serve(options: ServeOptions): Promise<void> {
1579
- const hostConfig = await getHostConfig(); // Reads non-sensitive config from ~/gitspace/
1580
-
1581
- if (hostConfig?.subdomain) {
1582
- // PRIMARY MODE: Has subdomain, runs cloudflared + relay
1583
- // Tunnel token is read from keychain inside startPrimaryMode
1584
- await startPrimaryMode(hostConfig.subdomain);
1585
- } else if (options.relay) {
1586
- // SECONDARY MODE: Connects to another machine's relay
1587
- await startSecondaryMode(options.relay);
1588
- } else {
1589
- // LOCAL ONLY MODE: No remote access
1590
- await startLocalMode();
1591
- }
1592
- }
1593
-
1594
- async function startPrimaryMode(subdomain: string): Promise<void> {
1595
- // 1. Start cloudflared (reads tunnel token from keychain)
1596
- await startCloudflared(subdomain);
1597
-
1598
- // 2. Start local HTTP/WS server
1599
- const server = createLocalServer({
1600
- port: 8080,
1601
- subdomain,
1602
- // ...
1603
- });
1604
-
1605
- // 3. Start embedded relay (accepts connections from secondary machines)
1606
- const relay = createEmbeddedRelay({
1607
- // Reuses existing relay protocol
1608
- });
1609
-
1610
- logger.success(`Primary mode: https://${subdomain}.gitspace.sh`);
1611
- }
1612
-
1613
- async function startSecondaryMode(relayUrl: string): Promise<void> {
1614
- // Connect to primary machine's relay (same as current relay connection)
1615
- const ws = new WebSocket(`wss://${relayUrl}/ws`);
1616
-
1617
- // Register this machine
1618
- ws.onopen = () => {
1619
- ws.send(JSON.stringify({
1620
- type: 'register_machine',
1621
- machineId: identity.id,
1622
- signingKey: identity.signingPublicKey,
1623
- keyExchangeKey: identity.keyExchangePublicKey,
1624
- }));
1625
- };
1626
-
1627
- // Handle client connections (same as current)
1628
- // ...
1629
-
1630
- logger.success(`Secondary mode: Connected to ${relayUrl}`);
1631
- }
1632
- ```
1633
-
1634
- ---
1635
-
1636
- ## Security Considerations
1637
-
1638
- ### Token Security
1639
-
1640
- **Storage Patterns:**
1641
-
1642
- | Data Type | Location | Rationale |
1643
- |-----------|----------|-----------|
1644
- | gitspace.sh API token (`gst_xxx`) | System keychain via `Bun.secrets` | Sensitive, needs secure storage |
1645
- | Tunnel tokens | System keychain via `Bun.secrets` | Sensitive, grants tunnel access |
1646
- | Machine identity private keys | `~/gitspace/.identity/keypair.json` (encrypted) | Already password-protected |
1647
- | Relay config (URL, machine ID) | `~/gitspace/.identity/relay.json` | Non-sensitive metadata |
1648
- | API tokens in D1 | SHA-256 hash only | Never store plaintext |
1649
-
1650
- **Device Registration Security:**
1651
- - Signature required: `sign("gitspace-device-auth:${timestamp}", privateKey)`
1652
- - Timestamp must be within 5 minutes (prevents replay attacks)
1653
- - Without signature, attacker could register with stolen public key
1654
-
1655
- **Bun.secrets Integration (Global):**
1656
- ```typescript
1657
- // src/utils/secrets.ts - Cross-platform secure secret storage
1658
-
1659
- const SERVICE_NAME = 'com.gitspace-cli';
1660
-
1661
- /**
1662
- * Store a global secret (not project-scoped)
1663
- * Uses system keychain: macOS Keychain, Linux libsecret, Windows Credential Manager
1664
- */
1665
- export async function setSecret(key: string, value: string): Promise<void> {
1666
- await Bun.secrets.set({
1667
- service: SERVICE_NAME,
1668
- name: key,
1669
- value,
1670
- });
1671
- }
1672
-
1673
- /**
1674
- * Retrieve a global secret
1675
- */
1676
- export async function getSecret(key: string): Promise<string | null> {
1677
- return Bun.secrets.get({
1678
- service: SERVICE_NAME,
1679
- name: key,
1680
- });
1681
- }
1682
-
1683
- /**
1684
- * Delete a global secret
1685
- */
1686
- export async function deleteSecret(key: string): Promise<boolean> {
1687
- return Bun.secrets.delete({
1688
- service: SERVICE_NAME,
1689
- name: key,
1690
- });
1691
- }
1692
-
1693
- // Project-scoped secrets (existing API)
1694
- function buildProjectSecretName(projectName: string, key: string): string {
1695
- return `${projectName}:${key}`;
1696
- }
1697
-
1698
- export async function setProjectSecret(
1699
- projectName: string,
1700
- key: string,
1701
- value: string
1702
- ): Promise<void> {
1703
- await Bun.secrets.set({
1704
- service: SERVICE_NAME,
1705
- name: buildProjectSecretName(projectName, key),
1706
- value,
1707
- });
1708
- }
1709
-
1710
- export async function getProjectSecret(
1711
- projectName: string,
1712
- key: string
1713
- ): Promise<string | null> {
1714
- return Bun.secrets.get({
1715
- service: SERVICE_NAME,
1716
- name: buildProjectSecretName(projectName, key),
1717
- });
1718
- }
1719
- ```
1720
-
1721
- **What goes where:**
1722
- ```
1723
- ~/gitspace/
1724
- ├── .identity/
1725
- │ ├── keypair.json # Password-encrypted Ed25519/X25519 keys
1726
- │ ├── access-list.json # Authorized public keys (not sensitive)
1727
- │ ├── machine.json # Machine ID, label (not sensitive)
1728
- │ └── relay.json # Relay URL, machine ID (not sensitive)
1729
- │ # NOTE: No secrets in relay.json anymore!
1730
- └── cloudflared.yml # Tunnel routing config (not sensitive)
1731
-
1732
- System Keychain (via Bun.secrets):
1733
- ├── GITSPACE_TOKEN # gitspace.sh API token (sensitive!)
1734
- └── TUNNEL_TOKEN_{subdomain} # Per-subdomain tunnel token (sensitive!)
1735
- ```
1736
-
1737
- ### Revocation Speed
1738
-
1739
- | Action | Effect | Speed |
1740
- |--------|--------|-------|
1741
- | Delete tunnel | New connections blocked | Immediate |
1742
- | Rotate token | Old token invalid | Immediate |
1743
- | Revoke API token | API access blocked | Immediate |
1744
-
1745
- ### Abuse Prevention
1746
-
1747
- - Rate limiting on subdomain creation
1748
- - Subdomain naming rules (no offensive terms)
1749
- - Reserved subdomains (api, www, admin, etc.)
1750
- - Cooldown period before subdomain reuse
1751
-
1752
- ---
1753
-
1754
- ## Cost Analysis
1755
-
1756
- ### Cloudflare Costs (You Pay)
1757
-
1758
- | Item | Cost | Notes |
1759
- |------|------|-------|
1760
- | Domain (gitspace.sh) | ~$10/year | One-time |
1761
- | Total TLS | $10/month | For *.*.gitspace.sh wildcards |
1762
- | Workers | Free tier | 100k requests/day |
1763
- | D1 | Free tier | 5GB storage |
1764
- | KV | Free tier | 100k reads/day |
1765
-
1766
- **Total: ~$130/year**
1767
-
1768
- ### User Costs
1769
-
1770
- | Item | Cost |
1771
- |------|------|
1772
- | Everything | $0 |
1773
-
1774
- Users run tunnels on their own machines, use their own bandwidth.
1775
-
1776
- ---
1777
-
1778
- ## Launch Checklist
1779
-
1780
- ```
1781
- □ Cloudflare Setup
1782
- □ Add gitspace.sh domain
1783
- □ Enable Total TLS ($10/mo)
1784
- □ Create API token with permissions:
1785
- □ Account > Cloudflare Tunnel > Edit
1786
- □ Zone > DNS > Edit
1787
- □ Zone > SSL and Certificates > Edit
1788
- □ Note Account ID, Zone ID
1789
-
1790
- □ GitHub OAuth App
1791
- □ Create OAuth App at github.com/settings/applications/new
1792
- □ Set homepage URL: https://gitspace.sh
1793
- □ Set callback URL: https://api.gitspace.sh/auth/github/callback
1794
- □ Enable Device Flow (checkbox in OAuth App settings)
1795
- □ Note Client ID, Client Secret
1796
-
1797
- □ Worker Deployment
1798
- □ Create D1 database, run schema.sql
1799
- □ Create KV namespace
1800
- □ Set secrets (wrangler secret put)
1801
- □ Deploy worker to api.gitspace.sh
1802
-
1803
- □ Portal Deployment
1804
- □ Deploy to gitspace.sh via Pages
1805
-
1806
- □ CLI Updates
1807
- □ gssh auth login/logout/status
1808
- □ gssh host reserve/release/list
1809
- □ cloudflared integration in gssh serve
1810
- □ Test full flow
1811
-
1812
- □ Documentation
1813
- □ Getting started guide
1814
- □ FAQ
1815
- ```
1816
-
1817
- ---
1818
-
1819
- *Last updated: 2025-01*