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