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,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test identity generation helpers
|
|
3
|
+
*
|
|
4
|
+
* Provides utilities for generating test identities and public identity objects
|
|
5
|
+
* for use in integration tests.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { generateIdentity, getPublicIdentity } from "../../identity.js";
|
|
9
|
+
import type { Identity, PublicIdentity } from "../../../../../types/identity.js";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Generate a test identity with optional label
|
|
13
|
+
*
|
|
14
|
+
* @param label - Optional label for the identity
|
|
15
|
+
* @returns Complete test identity
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```typescript
|
|
19
|
+
* const alice = createTestIdentity("Alice");
|
|
20
|
+
* const bob = createTestIdentity("Bob");
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export function createTestIdentity(label?: string): Identity {
|
|
24
|
+
return generateIdentity(label);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Create a pair of test identities (client and machine)
|
|
29
|
+
*
|
|
30
|
+
* @param clientLabel - Label for client identity
|
|
31
|
+
* @param machineLabel - Label for machine identity
|
|
32
|
+
* @returns Object with client and machine identities
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* ```typescript
|
|
36
|
+
* const { client, machine } = createTestIdentityPair();
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
export function createTestIdentityPair(
|
|
40
|
+
clientLabel = "Test Client",
|
|
41
|
+
machineLabel = "Test Machine"
|
|
42
|
+
): { client: Identity; machine: Identity } {
|
|
43
|
+
return {
|
|
44
|
+
client: createTestIdentity(clientLabel),
|
|
45
|
+
machine: createTestIdentity(machineLabel),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Create multiple test identities
|
|
51
|
+
*
|
|
52
|
+
* @param count - Number of identities to create
|
|
53
|
+
* @param labelPrefix - Prefix for identity labels
|
|
54
|
+
* @returns Array of identities
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* ```typescript
|
|
58
|
+
* const clients = createTestIdentities(5, "Client");
|
|
59
|
+
* // Creates: "Client 1", "Client 2", etc.
|
|
60
|
+
* ```
|
|
61
|
+
*/
|
|
62
|
+
export function createTestIdentities(
|
|
63
|
+
count: number,
|
|
64
|
+
labelPrefix = "Identity"
|
|
65
|
+
): Identity[] {
|
|
66
|
+
return Array.from({ length: count }, (_, i) =>
|
|
67
|
+
createTestIdentity(`${labelPrefix} ${i + 1}`)
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Get public identity from a complete identity
|
|
73
|
+
*
|
|
74
|
+
* Wrapper around getPublicIdentity for convenience
|
|
75
|
+
*
|
|
76
|
+
* @param identity - Complete identity
|
|
77
|
+
* @returns Public identity (safe to share)
|
|
78
|
+
*/
|
|
79
|
+
export function toPublicIdentity(identity: Identity): PublicIdentity {
|
|
80
|
+
return getPublicIdentity(identity);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Create a fixture set of identities for testing
|
|
85
|
+
*
|
|
86
|
+
* Returns a pre-configured set of identities with meaningful names
|
|
87
|
+
* for common testing scenarios.
|
|
88
|
+
*
|
|
89
|
+
* @example
|
|
90
|
+
* ```typescript
|
|
91
|
+
* const fixtures = createIdentityFixtures();
|
|
92
|
+
* // fixtures.alice - Client identity
|
|
93
|
+
* // fixtures.bob - Another client
|
|
94
|
+
* // fixtures.machine - Machine/server identity
|
|
95
|
+
* // fixtures.untrusted - Identity not in access list
|
|
96
|
+
* ```
|
|
97
|
+
*/
|
|
98
|
+
export function createIdentityFixtures(): {
|
|
99
|
+
alice: Identity;
|
|
100
|
+
bob: Identity;
|
|
101
|
+
machine: Identity;
|
|
102
|
+
untrusted: Identity;
|
|
103
|
+
alicePublic: PublicIdentity;
|
|
104
|
+
bobPublic: PublicIdentity;
|
|
105
|
+
machinePublic: PublicIdentity;
|
|
106
|
+
untrustedPublic: PublicIdentity;
|
|
107
|
+
} {
|
|
108
|
+
const alice = createTestIdentity("Alice (Client)");
|
|
109
|
+
const bob = createTestIdentity("Bob (Client)");
|
|
110
|
+
const machine = createTestIdentity("Test Machine");
|
|
111
|
+
const untrusted = createTestIdentity("Untrusted Client");
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
alice,
|
|
115
|
+
bob,
|
|
116
|
+
machine,
|
|
117
|
+
untrusted,
|
|
118
|
+
alicePublic: toPublicIdentity(alice),
|
|
119
|
+
bobPublic: toPublicIdentity(bob),
|
|
120
|
+
machinePublic: toPublicIdentity(machine),
|
|
121
|
+
untrustedPublic: toPublicIdentity(untrusted),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Create an access control list pre-populated with test identities
|
|
127
|
+
*
|
|
128
|
+
* @param acl - AccessControlList to populate
|
|
129
|
+
* @param identities - Identities to add to the list
|
|
130
|
+
* @param accessType - Access type to grant (optional, default: 'full')
|
|
131
|
+
* @param sessionId - Session ID for session-invite (optional)
|
|
132
|
+
*/
|
|
133
|
+
export function populateAccessList(
|
|
134
|
+
acl: import("../../access-control.js").AccessControlList,
|
|
135
|
+
identities: Identity[],
|
|
136
|
+
accessType?: import("../../../../../types/identity.js").AccessType,
|
|
137
|
+
sessionId?: string
|
|
138
|
+
): void {
|
|
139
|
+
for (const identity of identities) {
|
|
140
|
+
acl.addEntry(toPublicIdentity(identity), accessType, sessionId);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration tests for authorization scenarios
|
|
3
|
+
*
|
|
4
|
+
* Tests access list and invite token authorization during handshake.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, it, expect, beforeEach } from "bun:test";
|
|
8
|
+
import {
|
|
9
|
+
HandshakeHandler,
|
|
10
|
+
type HandshakeMessage,
|
|
11
|
+
} from "../../../handshake-handler.js";
|
|
12
|
+
import { AccessControlList } from "../../access-control.js";
|
|
13
|
+
import { createInviteToken, isInviteExpired } from "../../invites.js";
|
|
14
|
+
import {
|
|
15
|
+
createTestIdentityPair,
|
|
16
|
+
createTestIdentity,
|
|
17
|
+
toPublicIdentity,
|
|
18
|
+
createIdentityFixtures,
|
|
19
|
+
} from "../helpers/test-identities.js";
|
|
20
|
+
import { runCompleteHandshake } from "../helpers/handshake-runner.js";
|
|
21
|
+
import type { X3DHResponseMessage, X3DHResultMessage } from "../../../../../types/identity.js";
|
|
22
|
+
import {
|
|
23
|
+
isReplyResult,
|
|
24
|
+
getReplyData,
|
|
25
|
+
} from "../../../../../__tests__/test-utils.js";
|
|
26
|
+
|
|
27
|
+
describe("Authorization Integration", () => {
|
|
28
|
+
describe("access list authorization", () => {
|
|
29
|
+
it("should accept client in access list", async () => {
|
|
30
|
+
const { client, machine } = createTestIdentityPair();
|
|
31
|
+
const accessList = new AccessControlList();
|
|
32
|
+
accessList.addEntry(toPublicIdentity(client));
|
|
33
|
+
|
|
34
|
+
const result = await runCompleteHandshake(
|
|
35
|
+
client,
|
|
36
|
+
machine,
|
|
37
|
+
accessList,
|
|
38
|
+
{ type: "access_list" }
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
expect(result.success).toBe(true);
|
|
42
|
+
expect(result.machineSession?.peerIdentityId).toBe(client.id);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("should reject client not in access list", async () => {
|
|
46
|
+
const { client, machine } = createTestIdentityPair();
|
|
47
|
+
const accessList = new AccessControlList();
|
|
48
|
+
// Client NOT added to access list
|
|
49
|
+
|
|
50
|
+
const result = await runCompleteHandshake(
|
|
51
|
+
client,
|
|
52
|
+
machine,
|
|
53
|
+
accessList,
|
|
54
|
+
{ type: "access_list" }
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
expect(result.success).toBe(false);
|
|
58
|
+
expect(result.error).toContain("Not in access list");
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("should respect session-invite access type", async () => {
|
|
62
|
+
const { client, machine } = createTestIdentityPair();
|
|
63
|
+
const accessList = new AccessControlList();
|
|
64
|
+
accessList.addEntry(toPublicIdentity(client), 'session-invite', 'test-session');
|
|
65
|
+
|
|
66
|
+
const result = await runCompleteHandshake(
|
|
67
|
+
client,
|
|
68
|
+
machine,
|
|
69
|
+
accessList,
|
|
70
|
+
{ type: "access_list" }
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
expect(result.success).toBe(true);
|
|
74
|
+
expect(result.machineSession?.accessType).toBe('session-invite');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("should respect full access type", async () => {
|
|
78
|
+
const { client, machine } = createTestIdentityPair();
|
|
79
|
+
const accessList = new AccessControlList();
|
|
80
|
+
accessList.addEntry(toPublicIdentity(client), 'full');
|
|
81
|
+
|
|
82
|
+
const result = await runCompleteHandshake(
|
|
83
|
+
client,
|
|
84
|
+
machine,
|
|
85
|
+
accessList,
|
|
86
|
+
{ type: "access_list" }
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
expect(result.success).toBe(true);
|
|
90
|
+
expect(result.machineSession?.accessType).toBe('full');
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("should reject with expired access entry", async () => {
|
|
94
|
+
const { client, machine } = createTestIdentityPair();
|
|
95
|
+
const accessList = new AccessControlList();
|
|
96
|
+
|
|
97
|
+
// Add with expiry in the past
|
|
98
|
+
const entry = accessList.addEntry(toPublicIdentity(client));
|
|
99
|
+
// Manually set expiry to past
|
|
100
|
+
const entries = accessList.export();
|
|
101
|
+
entries[0].expiresAt = Date.now() - 1000;
|
|
102
|
+
accessList.import(entries);
|
|
103
|
+
|
|
104
|
+
const result = await runCompleteHandshake(
|
|
105
|
+
client,
|
|
106
|
+
machine,
|
|
107
|
+
accessList,
|
|
108
|
+
{ type: "access_list" }
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
expect(result.success).toBe(false);
|
|
112
|
+
expect(result.error).toContain("Not in access list");
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
describe("invite token authorization", () => {
|
|
117
|
+
it("should accept valid invite token", async () => {
|
|
118
|
+
const { client, machine } = createTestIdentityPair();
|
|
119
|
+
const accessList = new AccessControlList();
|
|
120
|
+
|
|
121
|
+
const result = await runCompleteHandshake(
|
|
122
|
+
client,
|
|
123
|
+
machine,
|
|
124
|
+
accessList,
|
|
125
|
+
{
|
|
126
|
+
type: "invite",
|
|
127
|
+
accessType: 'full',
|
|
128
|
+
}
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
expect(result.success).toBe(true);
|
|
132
|
+
expect(result.machineSession?.peerIdentityId).toBe(client.id);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("should use access type from invite token", async () => {
|
|
136
|
+
const { client, machine } = createTestIdentityPair();
|
|
137
|
+
const accessList = new AccessControlList();
|
|
138
|
+
|
|
139
|
+
const result = await runCompleteHandshake(
|
|
140
|
+
client,
|
|
141
|
+
machine,
|
|
142
|
+
accessList,
|
|
143
|
+
{
|
|
144
|
+
type: "invite",
|
|
145
|
+
accessType: 'session-invite',
|
|
146
|
+
sessionId: 'test-session',
|
|
147
|
+
}
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
expect(result.success).toBe(true);
|
|
151
|
+
expect(result.machineSession?.accessType).toBe('session-invite');
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("should add client to access list after invite acceptance", async () => {
|
|
155
|
+
const { client, machine } = createTestIdentityPair();
|
|
156
|
+
const accessList = new AccessControlList();
|
|
157
|
+
|
|
158
|
+
expect(accessList.hasAccess(client.id)).toBe(false);
|
|
159
|
+
|
|
160
|
+
const result = await runCompleteHandshake(
|
|
161
|
+
client,
|
|
162
|
+
machine,
|
|
163
|
+
accessList,
|
|
164
|
+
{
|
|
165
|
+
type: "invite",
|
|
166
|
+
accessType: 'full',
|
|
167
|
+
}
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
expect(result.success).toBe(true);
|
|
171
|
+
|
|
172
|
+
// Client should now be in access list
|
|
173
|
+
expect(accessList.hasAccess(client.id)).toBe(true);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("should reject expired invite token", async () => {
|
|
177
|
+
const { client, machine } = createTestIdentityPair();
|
|
178
|
+
const accessList = new AccessControlList();
|
|
179
|
+
|
|
180
|
+
// Create an already-expired token
|
|
181
|
+
const result = await runCompleteHandshake(
|
|
182
|
+
client,
|
|
183
|
+
machine,
|
|
184
|
+
accessList,
|
|
185
|
+
{
|
|
186
|
+
type: "invite",
|
|
187
|
+
accessType: 'full',
|
|
188
|
+
validityMs: -1000, // Expired 1 second ago
|
|
189
|
+
}
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
expect(result.success).toBe(false);
|
|
193
|
+
expect(result.error).toContain("expired");
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it("should reject invite token from different machine", async () => {
|
|
197
|
+
const { client, machine } = createTestIdentityPair();
|
|
198
|
+
const otherMachine = createTestIdentity("Other Machine");
|
|
199
|
+
const accessList = new AccessControlList();
|
|
200
|
+
|
|
201
|
+
const handler = new HandshakeHandler({
|
|
202
|
+
identity: machine,
|
|
203
|
+
accessList,
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// Create invite from a different machine
|
|
207
|
+
const wrongToken = createInviteToken(otherMachine, "wss://test.relay", {
|
|
208
|
+
accessType: 'full',
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
// Start handshake
|
|
212
|
+
const { createClientHello, processServerHello, createClientAuth } =
|
|
213
|
+
await import("../../handshake.js");
|
|
214
|
+
|
|
215
|
+
const { state: state1, message: clientHello } = createClientHello();
|
|
216
|
+
|
|
217
|
+
const result1 = await handler.processMessage("conn-1", {
|
|
218
|
+
type: "handshake",
|
|
219
|
+
phase: "client_hello",
|
|
220
|
+
data: clientHello,
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
expect(result1.type).toBe("reply");
|
|
224
|
+
if (!isReplyResult(result1)) throw new Error("Expected reply");
|
|
225
|
+
const serverHello = getReplyData<X3DHResponseMessage>(result1);
|
|
226
|
+
|
|
227
|
+
const state2 = processServerHello(state1, serverHello);
|
|
228
|
+
expect(state2).not.toBeNull();
|
|
229
|
+
|
|
230
|
+
const { message: clientAuth } = createClientAuth(state2!, client, {
|
|
231
|
+
type: "invite",
|
|
232
|
+
inviteToken: wrongToken,
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
const result2 = await handler.processMessage("conn-1", {
|
|
236
|
+
type: "handshake",
|
|
237
|
+
phase: "client_auth",
|
|
238
|
+
data: clientAuth,
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
// Should return a reply with rejection (not an error)
|
|
242
|
+
// The ServerAuth message contains the rejection reason
|
|
243
|
+
expect(result2.type).toBe("reply");
|
|
244
|
+
if (isReplyResult(result2)) {
|
|
245
|
+
const serverAuth = getReplyData<X3DHResultMessage>(result2);
|
|
246
|
+
expect(serverAuth.result.type).toBe("rejected");
|
|
247
|
+
if (serverAuth.result.type === "rejected") {
|
|
248
|
+
expect(serverAuth.result.reason).toContain("not issued by this machine");
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
describe("multiple clients", () => {
|
|
255
|
+
it("should allow multiple clients with different access types", async () => {
|
|
256
|
+
const fixtures = createIdentityFixtures();
|
|
257
|
+
const accessList = new AccessControlList();
|
|
258
|
+
|
|
259
|
+
// Alice gets full access
|
|
260
|
+
accessList.addEntry(fixtures.alicePublic, 'full');
|
|
261
|
+
|
|
262
|
+
// Bob gets session-invite
|
|
263
|
+
accessList.addEntry(fixtures.bobPublic, 'session-invite', 'test-session');
|
|
264
|
+
|
|
265
|
+
const [aliceResult, bobResult] = await Promise.all([
|
|
266
|
+
runCompleteHandshake(
|
|
267
|
+
fixtures.alice,
|
|
268
|
+
fixtures.machine,
|
|
269
|
+
accessList,
|
|
270
|
+
{ type: "access_list" }
|
|
271
|
+
),
|
|
272
|
+
runCompleteHandshake(
|
|
273
|
+
fixtures.bob,
|
|
274
|
+
fixtures.machine,
|
|
275
|
+
accessList,
|
|
276
|
+
{ type: "access_list" }
|
|
277
|
+
),
|
|
278
|
+
]);
|
|
279
|
+
|
|
280
|
+
expect(aliceResult.success).toBe(true);
|
|
281
|
+
expect(aliceResult.machineSession?.accessType).toBe('full');
|
|
282
|
+
|
|
283
|
+
expect(bobResult.success).toBe(true);
|
|
284
|
+
expect(bobResult.machineSession?.accessType).toBe('session-invite');
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it("should accept authorized and reject unauthorized simultaneously", async () => {
|
|
288
|
+
const fixtures = createIdentityFixtures();
|
|
289
|
+
const accessList = new AccessControlList();
|
|
290
|
+
|
|
291
|
+
// Only Alice is authorized
|
|
292
|
+
accessList.addEntry(fixtures.alicePublic);
|
|
293
|
+
|
|
294
|
+
const [aliceResult, untrustedResult] = await Promise.all([
|
|
295
|
+
runCompleteHandshake(
|
|
296
|
+
fixtures.alice,
|
|
297
|
+
fixtures.machine,
|
|
298
|
+
accessList,
|
|
299
|
+
{ type: "access_list" }
|
|
300
|
+
),
|
|
301
|
+
runCompleteHandshake(
|
|
302
|
+
fixtures.untrusted,
|
|
303
|
+
fixtures.machine,
|
|
304
|
+
accessList,
|
|
305
|
+
{ type: "access_list" }
|
|
306
|
+
),
|
|
307
|
+
]);
|
|
308
|
+
|
|
309
|
+
expect(aliceResult.success).toBe(true);
|
|
310
|
+
expect(untrustedResult.success).toBe(false);
|
|
311
|
+
});
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
describe("access type combinations", () => {
|
|
315
|
+
const accessTypeCombinations: Array<{ accessType: 'full' | 'session-invite'; sessionId?: string }> = [
|
|
316
|
+
{ accessType: 'full' },
|
|
317
|
+
{ accessType: 'session-invite', sessionId: 'session-1' },
|
|
318
|
+
{ accessType: 'session-invite', sessionId: 'session-2' },
|
|
319
|
+
];
|
|
320
|
+
|
|
321
|
+
for (const config of accessTypeCombinations) {
|
|
322
|
+
it(`should correctly grant access type: ${config.accessType}${config.sessionId ? ` with sessionId=${config.sessionId}` : ''}`, async () => {
|
|
323
|
+
const { client, machine } = createTestIdentityPair();
|
|
324
|
+
const accessList = new AccessControlList();
|
|
325
|
+
accessList.addEntry(toPublicIdentity(client), config.accessType, config.sessionId);
|
|
326
|
+
|
|
327
|
+
const result = await runCompleteHandshake(
|
|
328
|
+
client,
|
|
329
|
+
machine,
|
|
330
|
+
accessList,
|
|
331
|
+
{ type: "access_list" }
|
|
332
|
+
);
|
|
333
|
+
|
|
334
|
+
expect(result.success).toBe(true);
|
|
335
|
+
expect(result.machineSession?.accessType).toBe(config.accessType);
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
});
|
|
339
|
+
});
|