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,593 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Relay registries for machine, invite, and authorization tracking
|
|
3
|
+
*
|
|
4
|
+
* In-memory storage for v1. State is lost on restart.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { ServerWebSocket } from "bun";
|
|
8
|
+
import type { WebSocketData } from "./types";
|
|
9
|
+
|
|
10
|
+
// ============================================================================
|
|
11
|
+
// Types
|
|
12
|
+
// ============================================================================
|
|
13
|
+
|
|
14
|
+
/** Registered machine information */
|
|
15
|
+
export interface MachineRegistration {
|
|
16
|
+
/** Machine identity ID */
|
|
17
|
+
machineId: string;
|
|
18
|
+
/** Account that registered this machine */
|
|
19
|
+
accountId: string;
|
|
20
|
+
/** Ed25519 signing public key (base64) */
|
|
21
|
+
signingKey: string;
|
|
22
|
+
/** X25519 key exchange public key (base64) */
|
|
23
|
+
keyExchangeKey: string;
|
|
24
|
+
/** Human-readable label */
|
|
25
|
+
label?: string;
|
|
26
|
+
/** WebSocket connection (null if offline) */
|
|
27
|
+
ws: ServerWebSocket<WebSocketData> | null;
|
|
28
|
+
/** When machine was registered */
|
|
29
|
+
registeredAt: number;
|
|
30
|
+
/** When machine last connected */
|
|
31
|
+
lastConnectedAt: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Registered invite information */
|
|
35
|
+
export interface InviteRegistration {
|
|
36
|
+
/** Invite ID (hash of token or random) */
|
|
37
|
+
inviteId: string;
|
|
38
|
+
/** Machine this invite is for */
|
|
39
|
+
machineId: string;
|
|
40
|
+
/** When invite expires */
|
|
41
|
+
expiresAt: number;
|
|
42
|
+
/** Max number of uses (null = unlimited) */
|
|
43
|
+
maxUses: number | null;
|
|
44
|
+
/** Number of times invite has been used */
|
|
45
|
+
usedCount: number;
|
|
46
|
+
/** When invite was registered */
|
|
47
|
+
registeredAt: number;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Client authorization for a machine */
|
|
51
|
+
export interface ClientAuthorization {
|
|
52
|
+
/** Client's identity ID */
|
|
53
|
+
clientIdentityId: string;
|
|
54
|
+
/** Client's signing public key (base64) */
|
|
55
|
+
signingKey: string;
|
|
56
|
+
/** Client's key exchange public key (base64) */
|
|
57
|
+
keyExchangeKey: string;
|
|
58
|
+
/** Access type: 'full' or 'session-invite' */
|
|
59
|
+
accessType: 'full' | 'session-invite';
|
|
60
|
+
/** For session-invite: specific session ID */
|
|
61
|
+
sessionId?: string;
|
|
62
|
+
/** When authorization was granted */
|
|
63
|
+
grantedAt: number;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ============================================================================
|
|
67
|
+
// Machine Registry
|
|
68
|
+
// ============================================================================
|
|
69
|
+
|
|
70
|
+
/** Registered machines by machineId */
|
|
71
|
+
const machines = new Map<string, MachineRegistration>();
|
|
72
|
+
|
|
73
|
+
/** Result of machine registration attempt */
|
|
74
|
+
export type RegisterMachineResult =
|
|
75
|
+
| { success: true; registration: MachineRegistration }
|
|
76
|
+
| { success: false; error: string };
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Register a machine
|
|
80
|
+
*
|
|
81
|
+
* Security: Re-registration requires matching accountId and signingKey
|
|
82
|
+
* to prevent machine takeover attacks.
|
|
83
|
+
*/
|
|
84
|
+
export function registerMachine(
|
|
85
|
+
machineId: string,
|
|
86
|
+
accountId: string,
|
|
87
|
+
signingKey: string,
|
|
88
|
+
keyExchangeKey: string,
|
|
89
|
+
ws: ServerWebSocket<WebSocketData>,
|
|
90
|
+
label?: string
|
|
91
|
+
): RegisterMachineResult {
|
|
92
|
+
const now = Date.now();
|
|
93
|
+
|
|
94
|
+
// Check if already registered
|
|
95
|
+
const existing = machines.get(machineId);
|
|
96
|
+
if (existing) {
|
|
97
|
+
// Security: Verify ownership - must be same account
|
|
98
|
+
if (existing.accountId !== accountId) {
|
|
99
|
+
return {
|
|
100
|
+
success: false,
|
|
101
|
+
error: "Machine already registered by different account",
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Security: Verify signing key matches - prevents key substitution attacks
|
|
106
|
+
if (existing.signingKey !== signingKey) {
|
|
107
|
+
return {
|
|
108
|
+
success: false,
|
|
109
|
+
error: "Signing key mismatch - machine identity has changed",
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Safe to update connection
|
|
114
|
+
existing.ws = ws;
|
|
115
|
+
existing.lastConnectedAt = now;
|
|
116
|
+
if (label) existing.label = label;
|
|
117
|
+
return { success: true, registration: existing };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const registration: MachineRegistration = {
|
|
121
|
+
machineId,
|
|
122
|
+
accountId,
|
|
123
|
+
signingKey,
|
|
124
|
+
keyExchangeKey,
|
|
125
|
+
label,
|
|
126
|
+
ws,
|
|
127
|
+
registeredAt: now,
|
|
128
|
+
lastConnectedAt: now,
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
machines.set(machineId, registration);
|
|
132
|
+
return { success: true, registration };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Get a registered machine
|
|
137
|
+
*/
|
|
138
|
+
export function getMachine(machineId: string): MachineRegistration | undefined {
|
|
139
|
+
return machines.get(machineId);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Check if a machine is registered
|
|
144
|
+
*/
|
|
145
|
+
export function hasMachine(machineId: string): boolean {
|
|
146
|
+
return machines.has(machineId);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Check if a machine is online (connected)
|
|
151
|
+
*/
|
|
152
|
+
export function isMachineOnline(machineId: string): boolean {
|
|
153
|
+
const machine = machines.get(machineId);
|
|
154
|
+
return machine !== undefined && machine.ws !== null;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Update machine connection status
|
|
159
|
+
*/
|
|
160
|
+
export function setMachineConnection(
|
|
161
|
+
machineId: string,
|
|
162
|
+
ws: ServerWebSocket<WebSocketData> | null
|
|
163
|
+
): void {
|
|
164
|
+
const machine = machines.get(machineId);
|
|
165
|
+
if (machine) {
|
|
166
|
+
machine.ws = ws;
|
|
167
|
+
if (ws) {
|
|
168
|
+
machine.lastConnectedAt = Date.now();
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Unregister a machine
|
|
175
|
+
*/
|
|
176
|
+
export function unregisterMachine(machineId: string): boolean {
|
|
177
|
+
return machines.delete(machineId);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Get all registered machines
|
|
182
|
+
*/
|
|
183
|
+
export function getAllMachines(): MachineRegistration[] {
|
|
184
|
+
return Array.from(machines.values());
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ============================================================================
|
|
188
|
+
// Invite Registry
|
|
189
|
+
// ============================================================================
|
|
190
|
+
|
|
191
|
+
/** Registered invites by inviteId */
|
|
192
|
+
const invites = new Map<string, InviteRegistration>();
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Register an invite
|
|
196
|
+
*/
|
|
197
|
+
export function registerInvite(
|
|
198
|
+
inviteId: string,
|
|
199
|
+
machineId: string,
|
|
200
|
+
expiresAt: number,
|
|
201
|
+
maxUses: number | null = null
|
|
202
|
+
): InviteRegistration {
|
|
203
|
+
const registration: InviteRegistration = {
|
|
204
|
+
inviteId,
|
|
205
|
+
machineId,
|
|
206
|
+
expiresAt,
|
|
207
|
+
maxUses,
|
|
208
|
+
usedCount: 0,
|
|
209
|
+
registeredAt: Date.now(),
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
invites.set(inviteId, registration);
|
|
213
|
+
return registration;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Get an invite registration
|
|
218
|
+
*/
|
|
219
|
+
export function getInvite(inviteId: string): InviteRegistration | undefined {
|
|
220
|
+
return invites.get(inviteId);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Check if an invite is valid (exists, not expired, not exhausted)
|
|
225
|
+
*/
|
|
226
|
+
export function isInviteValid(inviteId: string): boolean {
|
|
227
|
+
const invite = invites.get(inviteId);
|
|
228
|
+
if (!invite) return false;
|
|
229
|
+
if (Date.now() > invite.expiresAt) return false;
|
|
230
|
+
if (invite.maxUses !== null && invite.usedCount >= invite.maxUses) return false;
|
|
231
|
+
return true;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Use an invite (increment use count)
|
|
236
|
+
*/
|
|
237
|
+
export function useInvite(inviteId: string): boolean {
|
|
238
|
+
const invite = invites.get(inviteId);
|
|
239
|
+
if (!invite) return false;
|
|
240
|
+
|
|
241
|
+
invite.usedCount++;
|
|
242
|
+
|
|
243
|
+
// Remove if exhausted
|
|
244
|
+
if (invite.maxUses !== null && invite.usedCount >= invite.maxUses) {
|
|
245
|
+
invites.delete(inviteId);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return true;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Revoke an invite
|
|
253
|
+
*/
|
|
254
|
+
export function revokeInvite(inviteId: string): boolean {
|
|
255
|
+
return invites.delete(inviteId);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Get invites for a machine
|
|
260
|
+
*/
|
|
261
|
+
export function getInvitesForMachine(machineId: string): InviteRegistration[] {
|
|
262
|
+
return Array.from(invites.values()).filter(inv => inv.machineId === machineId);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Clean up expired invites
|
|
267
|
+
*/
|
|
268
|
+
export function cleanupExpiredInvites(): number {
|
|
269
|
+
const now = Date.now();
|
|
270
|
+
let removed = 0;
|
|
271
|
+
|
|
272
|
+
for (const [id, invite] of invites) {
|
|
273
|
+
if (now > invite.expiresAt) {
|
|
274
|
+
invites.delete(id);
|
|
275
|
+
removed++;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return removed;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// ============================================================================
|
|
283
|
+
// Authorization Registry
|
|
284
|
+
// ============================================================================
|
|
285
|
+
|
|
286
|
+
/** Client authorizations by machineId */
|
|
287
|
+
const authorizations = new Map<string, Map<string, ClientAuthorization>>();
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Authorize a client for a machine
|
|
291
|
+
*/
|
|
292
|
+
export function authorizeClient(
|
|
293
|
+
machineId: string,
|
|
294
|
+
clientIdentityId: string,
|
|
295
|
+
signingKey: string,
|
|
296
|
+
keyExchangeKey: string,
|
|
297
|
+
accessType: 'full' | 'session-invite',
|
|
298
|
+
sessionId?: string
|
|
299
|
+
): ClientAuthorization {
|
|
300
|
+
let machineAuths = authorizations.get(machineId);
|
|
301
|
+
if (!machineAuths) {
|
|
302
|
+
machineAuths = new Map();
|
|
303
|
+
authorizations.set(machineId, machineAuths);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const auth: ClientAuthorization = {
|
|
307
|
+
clientIdentityId,
|
|
308
|
+
signingKey,
|
|
309
|
+
keyExchangeKey,
|
|
310
|
+
accessType,
|
|
311
|
+
sessionId,
|
|
312
|
+
grantedAt: Date.now(),
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
machineAuths.set(clientIdentityId, auth);
|
|
316
|
+
return auth;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Check if a client is authorized for a machine
|
|
321
|
+
*/
|
|
322
|
+
export function isClientAuthorized(machineId: string, clientIdentityId: string): boolean {
|
|
323
|
+
const machineAuths = authorizations.get(machineId);
|
|
324
|
+
return machineAuths?.has(clientIdentityId) ?? false;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Get client authorization for a machine
|
|
329
|
+
*/
|
|
330
|
+
export function getClientAuthorization(
|
|
331
|
+
machineId: string,
|
|
332
|
+
clientIdentityId: string
|
|
333
|
+
): ClientAuthorization | undefined {
|
|
334
|
+
return authorizations.get(machineId)?.get(clientIdentityId);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Revoke client authorization
|
|
339
|
+
*/
|
|
340
|
+
export function revokeClientAuthorization(
|
|
341
|
+
machineId: string,
|
|
342
|
+
clientIdentityId: string
|
|
343
|
+
): boolean {
|
|
344
|
+
const machineAuths = authorizations.get(machineId);
|
|
345
|
+
if (!machineAuths) return false;
|
|
346
|
+
return machineAuths.delete(clientIdentityId);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Get all clients authorized for a machine
|
|
351
|
+
*/
|
|
352
|
+
export function getAuthorizedClients(machineId: string): ClientAuthorization[] {
|
|
353
|
+
const machineAuths = authorizations.get(machineId);
|
|
354
|
+
if (!machineAuths) return [];
|
|
355
|
+
return Array.from(machineAuths.values());
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Get all machines a client is authorized for
|
|
360
|
+
*/
|
|
361
|
+
export function getMachinesForClient(clientIdentityId: string): {
|
|
362
|
+
machineId: string;
|
|
363
|
+
machine: MachineRegistration | undefined;
|
|
364
|
+
authorization: ClientAuthorization;
|
|
365
|
+
}[] {
|
|
366
|
+
const results: {
|
|
367
|
+
machineId: string;
|
|
368
|
+
machine: MachineRegistration | undefined;
|
|
369
|
+
authorization: ClientAuthorization;
|
|
370
|
+
}[] = [];
|
|
371
|
+
|
|
372
|
+
for (const [machineId, machineAuths] of authorizations) {
|
|
373
|
+
const auth = machineAuths.get(clientIdentityId);
|
|
374
|
+
if (auth) {
|
|
375
|
+
results.push({
|
|
376
|
+
machineId,
|
|
377
|
+
machine: machines.get(machineId),
|
|
378
|
+
authorization: auth,
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
return results;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Get all registered machines with authorization status for a client
|
|
388
|
+
*
|
|
389
|
+
* Returns all machines (not just authorized ones) so clients can see
|
|
390
|
+
* what's available and understand they need authorization.
|
|
391
|
+
*/
|
|
392
|
+
export function getAllMachinesWithAuthStatus(clientIdentityId: string): {
|
|
393
|
+
machineId: string;
|
|
394
|
+
machine: MachineRegistration;
|
|
395
|
+
isAuthorized: boolean;
|
|
396
|
+
accessType?: 'full' | 'session-invite';
|
|
397
|
+
sessionId?: string;
|
|
398
|
+
}[] {
|
|
399
|
+
const results: {
|
|
400
|
+
machineId: string;
|
|
401
|
+
machine: MachineRegistration;
|
|
402
|
+
isAuthorized: boolean;
|
|
403
|
+
accessType?: 'full' | 'session-invite';
|
|
404
|
+
sessionId?: string;
|
|
405
|
+
}[] = [];
|
|
406
|
+
|
|
407
|
+
for (const [machineId, machine] of machines) {
|
|
408
|
+
const machineAuths = authorizations.get(machineId);
|
|
409
|
+
const auth = machineAuths?.get(clientIdentityId);
|
|
410
|
+
|
|
411
|
+
results.push({
|
|
412
|
+
machineId,
|
|
413
|
+
machine,
|
|
414
|
+
isAuthorized: !!auth,
|
|
415
|
+
accessType: auth?.accessType,
|
|
416
|
+
sessionId: auth?.sessionId,
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
return results;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// ============================================================================
|
|
424
|
+
// Global Access List Registry
|
|
425
|
+
// ============================================================================
|
|
426
|
+
|
|
427
|
+
/** Global access entry (applies to all machines or specific machines) */
|
|
428
|
+
export interface GlobalAccessEntry {
|
|
429
|
+
/** Client identity ID */
|
|
430
|
+
clientIdentityId: string;
|
|
431
|
+
/** Client's signing public key (base64) */
|
|
432
|
+
signingKey: string;
|
|
433
|
+
/** Client's key exchange public key (base64) */
|
|
434
|
+
keyExchangeKey: string;
|
|
435
|
+
/** Human-readable label */
|
|
436
|
+
label?: string;
|
|
437
|
+
/** Access type: 'full' or 'session-invite' */
|
|
438
|
+
accessType: 'full' | 'session-invite';
|
|
439
|
+
/** For session-invite: specific session ID */
|
|
440
|
+
sessionId?: string;
|
|
441
|
+
/** When access was granted */
|
|
442
|
+
grantedAt: number;
|
|
443
|
+
/** Account that granted this access */
|
|
444
|
+
grantedBy: string;
|
|
445
|
+
/** If set, only applies to these machines (empty = all machines) */
|
|
446
|
+
machineIds?: string[];
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/** Global access list - applies to all machines owned by an account */
|
|
450
|
+
const globalAccessList = new Map<string, GlobalAccessEntry[]>(); // accountId → entries
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Get the global access list for an account
|
|
454
|
+
*/
|
|
455
|
+
export function getGlobalAccessList(accountId: string): GlobalAccessEntry[] {
|
|
456
|
+
return globalAccessList.get(accountId) || [];
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Get effective access list for a machine
|
|
461
|
+
* Combines global entries (for the account) with machine-specific overrides
|
|
462
|
+
*/
|
|
463
|
+
export function getEffectiveAccessList(accountId: string, machineId: string): GlobalAccessEntry[] {
|
|
464
|
+
const entries = globalAccessList.get(accountId) || [];
|
|
465
|
+
return entries.filter(entry => {
|
|
466
|
+
// If no machineIds specified, applies to all
|
|
467
|
+
if (!entry.machineIds || entry.machineIds.length === 0) {
|
|
468
|
+
return true;
|
|
469
|
+
}
|
|
470
|
+
// Otherwise, check if this machine is in the list
|
|
471
|
+
return entry.machineIds.includes(machineId);
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* Add a global access entry
|
|
477
|
+
*/
|
|
478
|
+
export function addGlobalAccess(
|
|
479
|
+
accountId: string,
|
|
480
|
+
entry: Omit<GlobalAccessEntry, 'grantedAt' | 'grantedBy'>
|
|
481
|
+
): GlobalAccessEntry {
|
|
482
|
+
let entries = globalAccessList.get(accountId);
|
|
483
|
+
if (!entries) {
|
|
484
|
+
entries = [];
|
|
485
|
+
globalAccessList.set(accountId, entries);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// Check if already exists
|
|
489
|
+
const existingIndex = entries.findIndex(e => e.clientIdentityId === entry.clientIdentityId);
|
|
490
|
+
|
|
491
|
+
const fullEntry: GlobalAccessEntry = {
|
|
492
|
+
...entry,
|
|
493
|
+
grantedAt: Date.now(),
|
|
494
|
+
grantedBy: accountId,
|
|
495
|
+
};
|
|
496
|
+
|
|
497
|
+
if (existingIndex >= 0) {
|
|
498
|
+
entries[existingIndex] = fullEntry;
|
|
499
|
+
} else {
|
|
500
|
+
entries.push(fullEntry);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
return fullEntry;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* Remove a global access entry
|
|
508
|
+
*/
|
|
509
|
+
export function removeGlobalAccess(accountId: string, clientIdentityId: string): boolean {
|
|
510
|
+
const entries = globalAccessList.get(accountId);
|
|
511
|
+
if (!entries) return false;
|
|
512
|
+
|
|
513
|
+
const index = entries.findIndex(e => e.clientIdentityId === clientIdentityId);
|
|
514
|
+
if (index < 0) return false;
|
|
515
|
+
|
|
516
|
+
entries.splice(index, 1);
|
|
517
|
+
return true;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* Broadcast access list update to all connected machines for an account
|
|
522
|
+
* @param accountId - Account that owns the machines
|
|
523
|
+
* @param added - New access entries
|
|
524
|
+
* @param removed - Removed client identity IDs
|
|
525
|
+
* @param signFn - Optional signing function for message authentication
|
|
526
|
+
*/
|
|
527
|
+
export function broadcastAccessUpdate(
|
|
528
|
+
accountId: string,
|
|
529
|
+
added: GlobalAccessEntry[],
|
|
530
|
+
removed: string[],
|
|
531
|
+
signFn?: <T extends object>(msg: T) => T
|
|
532
|
+
): void {
|
|
533
|
+
// Find all machines owned by this account
|
|
534
|
+
for (const machine of machines.values()) {
|
|
535
|
+
if (machine.accountId === accountId && machine.ws) {
|
|
536
|
+
const msg = {
|
|
537
|
+
type: 'access_update' as const,
|
|
538
|
+
added,
|
|
539
|
+
removed,
|
|
540
|
+
};
|
|
541
|
+
// Sign the message if signing function is provided
|
|
542
|
+
const signedMsg = signFn ? signFn(msg) : msg;
|
|
543
|
+
machine.ws.send(JSON.stringify(signedMsg));
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
/**
|
|
549
|
+
* Get all machines for an account
|
|
550
|
+
*/
|
|
551
|
+
export function getMachinesForAccount(accountId: string): MachineRegistration[] {
|
|
552
|
+
return Array.from(machines.values()).filter(m => m.accountId === accountId);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// ============================================================================
|
|
556
|
+
// Stats
|
|
557
|
+
// ============================================================================
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* Get registry statistics
|
|
561
|
+
*/
|
|
562
|
+
export function getRegistryStats(): {
|
|
563
|
+
machineCount: number;
|
|
564
|
+
onlineMachineCount: number;
|
|
565
|
+
inviteCount: number;
|
|
566
|
+
authorizationCount: number;
|
|
567
|
+
} {
|
|
568
|
+
let onlineMachineCount = 0;
|
|
569
|
+
for (const machine of machines.values()) {
|
|
570
|
+
if (machine.ws !== null) onlineMachineCount++;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
let authorizationCount = 0;
|
|
574
|
+
for (const machineAuths of authorizations.values()) {
|
|
575
|
+
authorizationCount += machineAuths.size;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
return {
|
|
579
|
+
machineCount: machines.size,
|
|
580
|
+
onlineMachineCount,
|
|
581
|
+
inviteCount: invites.size,
|
|
582
|
+
authorizationCount,
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* Clear all registries (for testing)
|
|
588
|
+
*/
|
|
589
|
+
export function clearAllRegistries(): void {
|
|
590
|
+
machines.clear();
|
|
591
|
+
invites.clear();
|
|
592
|
+
authorizations.clear();
|
|
593
|
+
}
|