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.
- package/.claude/settings.local.json +21 -0
- package/.gitspace/bundle.json +50 -0
- package/.gitspace/select/01-status.sh +40 -0
- package/.gitspace/setup/01-install-deps.sh +12 -0
- package/.gitspace/setup/02-typecheck.sh +16 -0
- package/AGENTS.md +439 -0
- package/CLAUDE.md +1 -0
- package/LICENSE +25 -0
- package/README.md +607 -0
- package/bin/gssh +62 -0
- package/bun.lock +647 -0
- package/docs/CONNECTION.md +623 -0
- package/docs/GATEWAY-WORKER.md +319 -0
- package/docs/GETTING-STARTED.md +448 -0
- package/docs/GITSPACE-PLATFORM.md +1819 -0
- package/docs/INFRASTRUCTURE.md +1347 -0
- package/docs/PROTOCOL.md +619 -0
- package/docs/QUICKSTART.md +174 -0
- package/docs/RELAY.md +327 -0
- package/docs/REMOTE-DESIGN.md +549 -0
- package/docs/ROADMAP.md +564 -0
- package/docs/SITE_DOCS_FIGMA_MAKE.md +1167 -0
- package/docs/STACK-DESIGN.md +588 -0
- package/docs/UNIFIED_ARCHITECTURE.md +292 -0
- package/experiments/pty-benchmark.ts +148 -0
- package/experiments/pty-latency.ts +100 -0
- package/experiments/router/client.ts +199 -0
- package/experiments/router/protocol.ts +74 -0
- package/experiments/router/router.ts +217 -0
- package/experiments/router/session.ts +180 -0
- package/experiments/router/test.ts +133 -0
- package/experiments/socket-bandwidth.ts +77 -0
- package/homebrew/gitspace.rb +45 -0
- package/landing-page/ATTRIBUTIONS.md +3 -0
- package/landing-page/README.md +11 -0
- package/landing-page/bun.lock +801 -0
- package/landing-page/guidelines/Guidelines.md +61 -0
- package/landing-page/index.html +37 -0
- package/landing-page/package.json +90 -0
- package/landing-page/postcss.config.mjs +15 -0
- package/landing-page/public/_redirects +1 -0
- package/landing-page/public/favicon.png +0 -0
- package/landing-page/src/app/App.tsx +53 -0
- package/landing-page/src/app/components/figma/ImageWithFallback.tsx +27 -0
- package/landing-page/src/app/components/ui/accordion.tsx +66 -0
- package/landing-page/src/app/components/ui/alert-dialog.tsx +157 -0
- package/landing-page/src/app/components/ui/alert.tsx +66 -0
- package/landing-page/src/app/components/ui/aspect-ratio.tsx +11 -0
- package/landing-page/src/app/components/ui/avatar.tsx +53 -0
- package/landing-page/src/app/components/ui/badge.tsx +46 -0
- package/landing-page/src/app/components/ui/breadcrumb.tsx +109 -0
- package/landing-page/src/app/components/ui/button.tsx +57 -0
- package/landing-page/src/app/components/ui/calendar.tsx +75 -0
- package/landing-page/src/app/components/ui/card.tsx +92 -0
- package/landing-page/src/app/components/ui/carousel.tsx +241 -0
- package/landing-page/src/app/components/ui/chart.tsx +353 -0
- package/landing-page/src/app/components/ui/checkbox.tsx +32 -0
- package/landing-page/src/app/components/ui/collapsible.tsx +33 -0
- package/landing-page/src/app/components/ui/command.tsx +177 -0
- package/landing-page/src/app/components/ui/context-menu.tsx +252 -0
- package/landing-page/src/app/components/ui/dialog.tsx +135 -0
- package/landing-page/src/app/components/ui/drawer.tsx +132 -0
- package/landing-page/src/app/components/ui/dropdown-menu.tsx +257 -0
- package/landing-page/src/app/components/ui/form.tsx +168 -0
- package/landing-page/src/app/components/ui/hover-card.tsx +44 -0
- package/landing-page/src/app/components/ui/input-otp.tsx +77 -0
- package/landing-page/src/app/components/ui/input.tsx +21 -0
- package/landing-page/src/app/components/ui/label.tsx +24 -0
- package/landing-page/src/app/components/ui/menubar.tsx +276 -0
- package/landing-page/src/app/components/ui/navigation-menu.tsx +168 -0
- package/landing-page/src/app/components/ui/pagination.tsx +127 -0
- package/landing-page/src/app/components/ui/popover.tsx +48 -0
- package/landing-page/src/app/components/ui/progress.tsx +31 -0
- package/landing-page/src/app/components/ui/radio-group.tsx +45 -0
- package/landing-page/src/app/components/ui/resizable.tsx +56 -0
- package/landing-page/src/app/components/ui/scroll-area.tsx +58 -0
- package/landing-page/src/app/components/ui/select.tsx +189 -0
- package/landing-page/src/app/components/ui/separator.tsx +28 -0
- package/landing-page/src/app/components/ui/sheet.tsx +139 -0
- package/landing-page/src/app/components/ui/sidebar.tsx +726 -0
- package/landing-page/src/app/components/ui/skeleton.tsx +13 -0
- package/landing-page/src/app/components/ui/slider.tsx +63 -0
- package/landing-page/src/app/components/ui/sonner.tsx +25 -0
- package/landing-page/src/app/components/ui/switch.tsx +31 -0
- package/landing-page/src/app/components/ui/table.tsx +116 -0
- package/landing-page/src/app/components/ui/tabs.tsx +66 -0
- package/landing-page/src/app/components/ui/textarea.tsx +18 -0
- package/landing-page/src/app/components/ui/toggle-group.tsx +73 -0
- package/landing-page/src/app/components/ui/toggle.tsx +47 -0
- package/landing-page/src/app/components/ui/tooltip.tsx +61 -0
- package/landing-page/src/app/components/ui/use-mobile.ts +21 -0
- package/landing-page/src/app/components/ui/utils.ts +6 -0
- package/landing-page/src/components/docs/DocsContent.tsx +718 -0
- package/landing-page/src/components/docs/DocsSidebar.tsx +84 -0
- package/landing-page/src/components/landing/CTA.tsx +59 -0
- package/landing-page/src/components/landing/Comparison.tsx +84 -0
- package/landing-page/src/components/landing/FaultyTerminal.tsx +424 -0
- package/landing-page/src/components/landing/Features.tsx +201 -0
- package/landing-page/src/components/landing/Hero.tsx +142 -0
- package/landing-page/src/components/landing/Pricing.tsx +140 -0
- package/landing-page/src/components/landing/Roadmap.tsx +86 -0
- package/landing-page/src/components/landing/Security.tsx +81 -0
- package/landing-page/src/components/landing/TerminalWindow.tsx +27 -0
- package/landing-page/src/components/landing/UseCases.tsx +55 -0
- package/landing-page/src/components/landing/Workflow.tsx +101 -0
- package/landing-page/src/components/layout/DashboardNavbar.tsx +37 -0
- package/landing-page/src/components/layout/Footer.tsx +55 -0
- package/landing-page/src/components/layout/LandingNavbar.tsx +82 -0
- package/landing-page/src/components/ui/badge.tsx +39 -0
- package/landing-page/src/components/ui/breadcrumb.tsx +115 -0
- package/landing-page/src/components/ui/button.tsx +57 -0
- package/landing-page/src/components/ui/card.tsx +79 -0
- package/landing-page/src/components/ui/mock-terminal.tsx +68 -0
- package/landing-page/src/components/ui/separator.tsx +28 -0
- package/landing-page/src/lib/utils.ts +6 -0
- package/landing-page/src/main.tsx +10 -0
- package/landing-page/src/pages/Dashboard.tsx +133 -0
- package/landing-page/src/pages/DocsPage.tsx +79 -0
- package/landing-page/src/pages/LandingPage.tsx +31 -0
- package/landing-page/src/pages/TerminalView.tsx +106 -0
- package/landing-page/src/styles/fonts.css +0 -0
- package/landing-page/src/styles/index.css +3 -0
- package/landing-page/src/styles/tailwind.css +4 -0
- package/landing-page/src/styles/theme.css +181 -0
- package/landing-page/vite.config.ts +19 -0
- package/npm/darwin-arm64/bin/gssh +0 -0
- package/npm/darwin-arm64/package.json +20 -0
- package/package.json +74 -0
- package/scripts/build.ts +284 -0
- package/scripts/release.ts +140 -0
- package/src/__tests__/test-utils.ts +298 -0
- package/src/commands/__tests__/serve-messages.test.ts +190 -0
- package/src/commands/access.ts +298 -0
- package/src/commands/add.ts +452 -0
- package/src/commands/auth.ts +364 -0
- package/src/commands/connect.ts +287 -0
- package/src/commands/directory.ts +16 -0
- package/src/commands/host.ts +396 -0
- package/src/commands/identity.ts +184 -0
- package/src/commands/list.ts +200 -0
- package/src/commands/relay.ts +315 -0
- package/src/commands/remove.ts +241 -0
- package/src/commands/serve.ts +1493 -0
- package/src/commands/share.ts +456 -0
- package/src/commands/status.ts +125 -0
- package/src/commands/switch.ts +353 -0
- package/src/commands/tmux.ts +317 -0
- package/src/core/__tests__/access.test.ts +240 -0
- package/src/core/access.ts +277 -0
- package/src/core/bundle.ts +342 -0
- package/src/core/config.ts +510 -0
- package/src/core/git.ts +317 -0
- package/src/core/github.ts +151 -0
- package/src/core/identity.ts +631 -0
- package/src/core/linear.ts +225 -0
- package/src/core/shell.ts +161 -0
- package/src/core/trusted-relays.ts +315 -0
- package/src/index.ts +821 -0
- package/src/lib/remote-session/index.ts +7 -0
- package/src/lib/remote-session/protocol.ts +267 -0
- package/src/lib/remote-session/session-handler.ts +581 -0
- package/src/lib/remote-session/workspace-scanner.ts +167 -0
- package/src/lib/tmux-lite/README.md +81 -0
- package/src/lib/tmux-lite/cli.ts +796 -0
- package/src/lib/tmux-lite/crypto/__tests__/helpers/handshake-runner.ts +349 -0
- package/src/lib/tmux-lite/crypto/__tests__/helpers/mock-relay.ts +291 -0
- package/src/lib/tmux-lite/crypto/__tests__/helpers/test-identities.ts +142 -0
- package/src/lib/tmux-lite/crypto/__tests__/integration/authorization.integration.test.ts +339 -0
- package/src/lib/tmux-lite/crypto/__tests__/integration/e2e-communication.integration.test.ts +477 -0
- package/src/lib/tmux-lite/crypto/__tests__/integration/error-handling.integration.test.ts +499 -0
- package/src/lib/tmux-lite/crypto/__tests__/integration/handshake.integration.test.ts +371 -0
- package/src/lib/tmux-lite/crypto/__tests__/integration/security.integration.test.ts +573 -0
- package/src/lib/tmux-lite/crypto/access-control.test.ts +512 -0
- package/src/lib/tmux-lite/crypto/access-control.ts +320 -0
- package/src/lib/tmux-lite/crypto/frames.test.ts +262 -0
- package/src/lib/tmux-lite/crypto/frames.ts +141 -0
- package/src/lib/tmux-lite/crypto/handshake.ts +894 -0
- package/src/lib/tmux-lite/crypto/identity.test.ts +220 -0
- package/src/lib/tmux-lite/crypto/identity.ts +286 -0
- package/src/lib/tmux-lite/crypto/index.ts +51 -0
- package/src/lib/tmux-lite/crypto/invites.test.ts +381 -0
- package/src/lib/tmux-lite/crypto/invites.ts +215 -0
- package/src/lib/tmux-lite/crypto/keyexchange.ts +435 -0
- package/src/lib/tmux-lite/crypto/keys.test.ts +58 -0
- package/src/lib/tmux-lite/crypto/keys.ts +47 -0
- package/src/lib/tmux-lite/crypto/secretbox.test.ts +169 -0
- package/src/lib/tmux-lite/crypto/secretbox.ts +124 -0
- package/src/lib/tmux-lite/handshake-handler.ts +451 -0
- package/src/lib/tmux-lite/protocol.test.ts +307 -0
- package/src/lib/tmux-lite/protocol.ts +266 -0
- package/src/lib/tmux-lite/relay-client.ts +506 -0
- package/src/lib/tmux-lite/server.ts +1250 -0
- package/src/lib/tmux-lite/shell-integration.sh +37 -0
- package/src/lib/tmux-lite/terminal-queries.test.ts +54 -0
- package/src/lib/tmux-lite/terminal-queries.ts +49 -0
- package/src/relay/__tests__/e2e-flow.test.ts +1284 -0
- package/src/relay/__tests__/helpers/auth.ts +354 -0
- package/src/relay/__tests__/helpers/ports.ts +51 -0
- package/src/relay/__tests__/protocol-validation.test.ts +265 -0
- package/src/relay/authorization.ts +303 -0
- package/src/relay/embedded-assets.generated.d.ts +15 -0
- package/src/relay/identity.ts +352 -0
- package/src/relay/index.ts +57 -0
- package/src/relay/pipes.test.ts +427 -0
- package/src/relay/pipes.ts +195 -0
- package/src/relay/protocol.ts +804 -0
- package/src/relay/registries.test.ts +437 -0
- package/src/relay/registries.ts +593 -0
- package/src/relay/server.test.ts +1323 -0
- package/src/relay/server.ts +1092 -0
- package/src/relay/signing.ts +238 -0
- package/src/relay/types.ts +69 -0
- package/src/serve/client-session-manager.ts +622 -0
- package/src/serve/daemon.ts +497 -0
- package/src/serve/pty-session.ts +236 -0
- package/src/serve/types.ts +169 -0
- package/src/shared/components/Flow.tsx +453 -0
- package/src/shared/components/Flow.tui.tsx +343 -0
- package/src/shared/components/Flow.web.tsx +442 -0
- package/src/shared/components/Inbox.tsx +446 -0
- package/src/shared/components/Inbox.tui.tsx +262 -0
- package/src/shared/components/Inbox.web.tsx +329 -0
- package/src/shared/components/MachineList.tsx +187 -0
- package/src/shared/components/MachineList.tui.tsx +161 -0
- package/src/shared/components/MachineList.web.tsx +210 -0
- package/src/shared/components/ProjectList.tsx +176 -0
- package/src/shared/components/ProjectList.tui.tsx +109 -0
- package/src/shared/components/ProjectList.web.tsx +143 -0
- package/src/shared/components/SpacesBrowser.tsx +332 -0
- package/src/shared/components/SpacesBrowser.tui.tsx +163 -0
- package/src/shared/components/SpacesBrowser.web.tsx +221 -0
- package/src/shared/components/index.ts +103 -0
- package/src/shared/hooks/index.ts +16 -0
- package/src/shared/hooks/useNavigation.ts +226 -0
- package/src/shared/index.ts +122 -0
- package/src/shared/providers/LocalMachineProvider.ts +425 -0
- package/src/shared/providers/MachineProvider.ts +165 -0
- package/src/shared/providers/RemoteMachineProvider.ts +444 -0
- package/src/shared/providers/index.ts +26 -0
- package/src/shared/types.ts +145 -0
- package/src/tui/adapters.ts +120 -0
- package/src/tui/app.tsx +1816 -0
- package/src/tui/components/Terminal.tsx +580 -0
- package/src/tui/hooks/index.ts +35 -0
- package/src/tui/hooks/useAppState.ts +314 -0
- package/src/tui/hooks/useDaemonStatus.ts +174 -0
- package/src/tui/hooks/useInboxTUI.ts +113 -0
- package/src/tui/hooks/useRemoteMachines.ts +209 -0
- package/src/tui/index.ts +24 -0
- package/src/tui/state.ts +299 -0
- package/src/tui/terminal-bracketed-paste.test.ts +45 -0
- package/src/tui/terminal-bracketed-paste.ts +47 -0
- package/src/types/bundle.ts +112 -0
- package/src/types/config.ts +89 -0
- package/src/types/errors.ts +206 -0
- package/src/types/identity.ts +284 -0
- package/src/types/workspace-fuzzy.ts +49 -0
- package/src/types/workspace.ts +151 -0
- package/src/utils/bun-socket-writer.ts +80 -0
- package/src/utils/deps.ts +127 -0
- package/src/utils/fuzzy-match.ts +125 -0
- package/src/utils/logger.ts +127 -0
- package/src/utils/markdown.ts +254 -0
- package/src/utils/onboarding.ts +229 -0
- package/src/utils/prompts.ts +114 -0
- package/src/utils/run-commands.ts +112 -0
- package/src/utils/run-scripts.ts +142 -0
- package/src/utils/sanitize.ts +98 -0
- package/src/utils/secrets.ts +122 -0
- package/src/utils/shell-escape.ts +40 -0
- package/src/utils/utf8.ts +79 -0
- package/src/utils/workspace-state.ts +47 -0
- package/src/web/README.md +73 -0
- package/src/web/bun.lock +575 -0
- package/src/web/eslint.config.js +23 -0
- package/src/web/index.html +16 -0
- package/src/web/package.json +37 -0
- package/src/web/public/vite.svg +1 -0
- package/src/web/src/App.tsx +604 -0
- package/src/web/src/assets/react.svg +1 -0
- package/src/web/src/components/Terminal.tsx +207 -0
- package/src/web/src/hooks/useRelayConnection.ts +224 -0
- package/src/web/src/hooks/useTerminal.ts +699 -0
- package/src/web/src/index.css +55 -0
- package/src/web/src/lib/crypto/__tests__/web-terminal.test.ts +1158 -0
- package/src/web/src/lib/crypto/frames.ts +205 -0
- package/src/web/src/lib/crypto/handshake.ts +396 -0
- package/src/web/src/lib/crypto/identity.ts +128 -0
- package/src/web/src/lib/crypto/keyexchange.ts +246 -0
- package/src/web/src/lib/crypto/relay-signing.ts +53 -0
- package/src/web/src/lib/invite.ts +58 -0
- package/src/web/src/lib/storage/identity-store.ts +94 -0
- package/src/web/src/main.tsx +10 -0
- package/src/web/src/types/identity.ts +45 -0
- package/src/web/tsconfig.app.json +28 -0
- package/src/web/tsconfig.json +7 -0
- package/src/web/tsconfig.node.json +26 -0
- package/src/web/vite.config.ts +31 -0
- package/todo-security.md +92 -0
- package/tsconfig.json +23 -0
- package/worker/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/12b7107e435bf1b9a8713a7f320472a63e543104d633d89a26f8d21f4e4ef182.sqlite +0 -0
- package/worker/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/12b7107e435bf1b9a8713a7f320472a63e543104d633d89a26f8d21f4e4ef182.sqlite-shm +0 -0
- package/worker/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/12b7107e435bf1b9a8713a7f320472a63e543104d633d89a26f8d21f4e4ef182.sqlite-wal +0 -0
- package/worker/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/1a1ac3db1ab86ecf712f90322868a9aabc2c7dc9fe2dfbe94f9b075096276b0f.sqlite +0 -0
- package/worker/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/1a1ac3db1ab86ecf712f90322868a9aabc2c7dc9fe2dfbe94f9b075096276b0f.sqlite-shm +0 -0
- package/worker/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/1a1ac3db1ab86ecf712f90322868a9aabc2c7dc9fe2dfbe94f9b075096276b0f.sqlite-wal +0 -0
- package/worker/bun.lock +237 -0
- package/worker/package.json +22 -0
- package/worker/schema.sql +96 -0
- package/worker/src/handlers/auth.ts +451 -0
- package/worker/src/handlers/subdomains.ts +376 -0
- package/worker/src/handlers/user.ts +98 -0
- package/worker/src/index.ts +70 -0
- package/worker/src/middleware/auth.ts +152 -0
- package/worker/src/services/cloudflare.ts +609 -0
- package/worker/src/types.ts +96 -0
- package/worker/tsconfig.json +15 -0
- 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
|
+
}
|