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,573 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Security Integration Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests for security fixes addressing the vulnerabilities in SECURITY_REVIEW.md:
|
|
5
|
+
* 1. Identity signature proof (Issue 1)
|
|
6
|
+
* 2. Permission enforcement (Issue 2)
|
|
7
|
+
* 3. Machine takeover prevention (Issue 3)
|
|
8
|
+
* 4. Single-use invite enforcement (Issue 4)
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { describe, it, expect, beforeEach } from "bun:test";
|
|
12
|
+
import {
|
|
13
|
+
createClientHello,
|
|
14
|
+
processServerHello,
|
|
15
|
+
createClientAuth,
|
|
16
|
+
processClientAuth,
|
|
17
|
+
createServerState,
|
|
18
|
+
processClientHello,
|
|
19
|
+
createServerHello,
|
|
20
|
+
} from "../../handshake.js";
|
|
21
|
+
import { HandshakeHandler, type HandshakeMessage } from "../../../handshake-handler.js";
|
|
22
|
+
import { AccessControlList } from "../../access-control.js";
|
|
23
|
+
import { createInviteToken } from "../../invites.js";
|
|
24
|
+
import { sign } from "../../identity.js";
|
|
25
|
+
import {
|
|
26
|
+
createTestIdentity,
|
|
27
|
+
createTestIdentityPair,
|
|
28
|
+
toPublicIdentity,
|
|
29
|
+
} from "../helpers/test-identities.js";
|
|
30
|
+
import { runCompleteHandshake } from "../helpers/handshake-runner.js";
|
|
31
|
+
import {
|
|
32
|
+
registerMachine,
|
|
33
|
+
clearAllRegistries,
|
|
34
|
+
} from "../../../../../relay/registries.js";
|
|
35
|
+
import type { Identity, X3DHAuthMessage, X3DHResponseMessage } from "../../../../../types/identity.js";
|
|
36
|
+
import {
|
|
37
|
+
createMockWebSocket,
|
|
38
|
+
asMockWs,
|
|
39
|
+
omit,
|
|
40
|
+
isReplyResult,
|
|
41
|
+
getReplyData,
|
|
42
|
+
} from "../../../../../__tests__/test-utils.js";
|
|
43
|
+
import type { ServerWebSocket } from "bun";
|
|
44
|
+
import type { WebSocketData } from "../../../../../relay/types.js";
|
|
45
|
+
|
|
46
|
+
// ============================================================================
|
|
47
|
+
// Issue 1: Identity Signature Proof Tests
|
|
48
|
+
// ============================================================================
|
|
49
|
+
|
|
50
|
+
describe("Issue 1: Identity Signature Proof", () => {
|
|
51
|
+
let client: Identity;
|
|
52
|
+
let machine: Identity;
|
|
53
|
+
let accessList: AccessControlList;
|
|
54
|
+
|
|
55
|
+
beforeEach(() => {
|
|
56
|
+
({ client, machine } = createTestIdentityPair());
|
|
57
|
+
accessList = new AccessControlList();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("should accept valid handshake with correct identity signature", async () => {
|
|
61
|
+
// Add client to access list
|
|
62
|
+
accessList.addEntry(toPublicIdentity(client), "full");
|
|
63
|
+
|
|
64
|
+
const result = await runCompleteHandshake(
|
|
65
|
+
client,
|
|
66
|
+
machine,
|
|
67
|
+
accessList,
|
|
68
|
+
{ type: "access_list" }
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
expect(result.success).toBe(true);
|
|
72
|
+
expect(result.clientKeys).toBeDefined();
|
|
73
|
+
expect(result.machineSession).toBeDefined();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("should reject ClientAuth with forged identity key", async () => {
|
|
77
|
+
// Create two identities - client will claim to be impersonator
|
|
78
|
+
const impersonator = createTestIdentity("Impersonator");
|
|
79
|
+
|
|
80
|
+
// Add the impersonator to access list (but attacker won't have their private key)
|
|
81
|
+
accessList.addEntry(toPublicIdentity(impersonator), "full");
|
|
82
|
+
|
|
83
|
+
// Manually run handshake to forge the identity key
|
|
84
|
+
const serverState = createServerState(machine);
|
|
85
|
+
const { state: clientState, message: clientHello } = createClientHello(machine.id);
|
|
86
|
+
|
|
87
|
+
const stateAfterHello = processClientHello(serverState, clientHello);
|
|
88
|
+
expect(stateAfterHello).not.toBeNull();
|
|
89
|
+
|
|
90
|
+
const { state: stateAfterServerHello, message: serverHello } = createServerHello(
|
|
91
|
+
stateAfterHello!,
|
|
92
|
+
machine
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
const clientStateAfterServerHello = processServerHello(clientState, serverHello);
|
|
96
|
+
expect(clientStateAfterServerHello).not.toBeNull();
|
|
97
|
+
|
|
98
|
+
// Create ClientAuth with client's identity but forging impersonator's public key
|
|
99
|
+
// This should fail because signature won't verify
|
|
100
|
+
const { message: clientAuth } = createClientAuth(
|
|
101
|
+
clientStateAfterServerHello!,
|
|
102
|
+
client, // Use client's keys for signing
|
|
103
|
+
{ type: "access_list" }
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
// Forge the identity key to claim impersonator's identity
|
|
107
|
+
const forgedAuth: X3DHAuthMessage = {
|
|
108
|
+
...clientAuth,
|
|
109
|
+
identityKey: Buffer.from(impersonator.signing.publicKey).toString("base64"),
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
// Process the forged ClientAuth - should fail signature verification
|
|
113
|
+
const result = processClientAuth(stateAfterServerHello, forgedAuth, machine);
|
|
114
|
+
expect(result).toBeNull(); // Should reject due to signature mismatch
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("should reject ClientAuth with missing identity signature", async () => {
|
|
118
|
+
accessList.addEntry(toPublicIdentity(client), "full");
|
|
119
|
+
|
|
120
|
+
// Manually run handshake to create ClientAuth without signature
|
|
121
|
+
const serverState = createServerState(machine);
|
|
122
|
+
const { state: clientState, message: clientHello } = createClientHello(machine.id);
|
|
123
|
+
|
|
124
|
+
const stateAfterHello = processClientHello(serverState, clientHello);
|
|
125
|
+
expect(stateAfterHello).not.toBeNull();
|
|
126
|
+
|
|
127
|
+
const { state: stateAfterServerHello, message: serverHello } = createServerHello(
|
|
128
|
+
stateAfterHello!,
|
|
129
|
+
machine
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
const clientStateAfterServerHello = processServerHello(clientState, serverHello);
|
|
133
|
+
expect(clientStateAfterServerHello).not.toBeNull();
|
|
134
|
+
|
|
135
|
+
const { message: clientAuth } = createClientAuth(
|
|
136
|
+
clientStateAfterServerHello!,
|
|
137
|
+
client,
|
|
138
|
+
{ type: "access_list" }
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
// Remove the signature using type-safe utility
|
|
142
|
+
// We cast to X3DHAuthMessage because we're deliberately testing what happens
|
|
143
|
+
// when a required field is missing - the function should reject this
|
|
144
|
+
const authWithoutSignature = omit(clientAuth, 'identitySignature') as unknown as X3DHAuthMessage;
|
|
145
|
+
|
|
146
|
+
// Process - should reject due to missing signature
|
|
147
|
+
const result = processClientAuth(stateAfterServerHello, authWithoutSignature, machine);
|
|
148
|
+
expect(result).toBeNull();
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("should reject ClientAuth with wrong signature", async () => {
|
|
152
|
+
// Another client whose signature we'll use
|
|
153
|
+
const other = createTestIdentity("Other");
|
|
154
|
+
accessList.addEntry(toPublicIdentity(client), "full");
|
|
155
|
+
|
|
156
|
+
// Manually run handshake
|
|
157
|
+
const serverState = createServerState(machine);
|
|
158
|
+
const { state: clientState, message: clientHello } = createClientHello(machine.id);
|
|
159
|
+
|
|
160
|
+
const stateAfterHello = processClientHello(serverState, clientHello);
|
|
161
|
+
expect(stateAfterHello).not.toBeNull();
|
|
162
|
+
|
|
163
|
+
const { state: stateAfterServerHello, message: serverHello } = createServerHello(
|
|
164
|
+
stateAfterHello!,
|
|
165
|
+
machine
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
const clientStateAfterServerHello = processServerHello(clientState, serverHello);
|
|
169
|
+
expect(clientStateAfterServerHello).not.toBeNull();
|
|
170
|
+
|
|
171
|
+
const { message: clientAuth } = createClientAuth(
|
|
172
|
+
clientStateAfterServerHello!,
|
|
173
|
+
client,
|
|
174
|
+
{ type: "access_list" }
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
// Create a wrong signature (signed by other identity)
|
|
178
|
+
const wrongSignature = sign(
|
|
179
|
+
new Uint8Array(128), // Some random data
|
|
180
|
+
other.signing.secretKey
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
const authWithWrongSignature: X3DHAuthMessage = {
|
|
184
|
+
...clientAuth,
|
|
185
|
+
identitySignature: Buffer.from(wrongSignature).toString("base64"),
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
// Process - should reject due to signature verification failure
|
|
189
|
+
const result = processClientAuth(stateAfterServerHello, authWithWrongSignature, machine);
|
|
190
|
+
expect(result).toBeNull();
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// ============================================================================
|
|
195
|
+
// Issue 2: Permission Enforcement Tests
|
|
196
|
+
// ============================================================================
|
|
197
|
+
|
|
198
|
+
describe("Issue 2: Permission Enforcement", () => {
|
|
199
|
+
describe("canWrite permission", () => {
|
|
200
|
+
it("should allow PTY writes for full access clients", async () => {
|
|
201
|
+
const { client, machine } = createTestIdentityPair();
|
|
202
|
+
const accessList = new AccessControlList();
|
|
203
|
+
accessList.addEntry(toPublicIdentity(client), "full");
|
|
204
|
+
|
|
205
|
+
const result = await runCompleteHandshake(
|
|
206
|
+
client,
|
|
207
|
+
machine,
|
|
208
|
+
accessList,
|
|
209
|
+
{ type: "access_list" }
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
expect(result.success).toBe(true);
|
|
213
|
+
expect(result.machineSession?.accessType).toBe("full");
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it("should grant session-invite access via invite token", async () => {
|
|
217
|
+
const { client, machine } = createTestIdentityPair();
|
|
218
|
+
const accessList = new AccessControlList();
|
|
219
|
+
// Don't add client to access list - use invite
|
|
220
|
+
|
|
221
|
+
const result = await runCompleteHandshake(
|
|
222
|
+
client,
|
|
223
|
+
machine,
|
|
224
|
+
accessList,
|
|
225
|
+
{ type: "invite", accessType: "session-invite", sessionId: "test-session-123" }
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
expect(result.success).toBe(true);
|
|
229
|
+
expect(result.machineSession?.accessType).toBe("session-invite");
|
|
230
|
+
expect(result.machineSession?.sessionId).toBe("test-session-123");
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
describe("Access type from access list", () => {
|
|
235
|
+
it("should respect session-invite access type from access list", async () => {
|
|
236
|
+
const { client, machine } = createTestIdentityPair();
|
|
237
|
+
const accessList = new AccessControlList();
|
|
238
|
+
accessList.addEntry(toPublicIdentity(client), "session-invite", "session-abc");
|
|
239
|
+
|
|
240
|
+
const result = await runCompleteHandshake(
|
|
241
|
+
client,
|
|
242
|
+
machine,
|
|
243
|
+
accessList,
|
|
244
|
+
{ type: "access_list" }
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
expect(result.success).toBe(true);
|
|
248
|
+
expect(result.machineSession?.accessType).toBe("session-invite");
|
|
249
|
+
expect(result.machineSession?.sessionId).toBe("session-abc");
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
// ============================================================================
|
|
255
|
+
// Issue 3: Machine Takeover Prevention Tests
|
|
256
|
+
// ============================================================================
|
|
257
|
+
|
|
258
|
+
describe("Issue 3: Machine Takeover Prevention", () => {
|
|
259
|
+
beforeEach(() => {
|
|
260
|
+
clearAllRegistries();
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it("should allow initial machine registration", () => {
|
|
264
|
+
const machine = createTestIdentity("Machine");
|
|
265
|
+
const mockWs = asMockWs<ServerWebSocket<WebSocketData>>(
|
|
266
|
+
createMockWebSocket({ data: { machineId: machine.id } })
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
const result = registerMachine(
|
|
270
|
+
machine.id,
|
|
271
|
+
"account-alice",
|
|
272
|
+
Buffer.from(machine.signing.publicKey).toString("base64"),
|
|
273
|
+
Buffer.from(machine.keyExchange.publicKey).toString("base64"),
|
|
274
|
+
mockWs,
|
|
275
|
+
"Alice's Machine"
|
|
276
|
+
);
|
|
277
|
+
|
|
278
|
+
expect(result.success).toBe(true);
|
|
279
|
+
if (result.success) {
|
|
280
|
+
expect(result.registration.machineId).toBe(machine.id);
|
|
281
|
+
expect(result.registration.accountId).toBe("account-alice");
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
it("should allow re-registration from same account with same keys", () => {
|
|
286
|
+
const machine = createTestIdentity("Machine");
|
|
287
|
+
const mockWs1 = asMockWs<ServerWebSocket<WebSocketData>>(
|
|
288
|
+
createMockWebSocket({ data: { machineId: machine.id, connectionId: "conn-1" } })
|
|
289
|
+
);
|
|
290
|
+
const mockWs2 = asMockWs<ServerWebSocket<WebSocketData>>(
|
|
291
|
+
createMockWebSocket({ data: { machineId: machine.id, connectionId: "conn-2" } })
|
|
292
|
+
);
|
|
293
|
+
|
|
294
|
+
const signingKey = Buffer.from(machine.signing.publicKey).toString("base64");
|
|
295
|
+
const keyExchangeKey = Buffer.from(machine.keyExchange.publicKey).toString("base64");
|
|
296
|
+
|
|
297
|
+
// First registration
|
|
298
|
+
const result1 = registerMachine(
|
|
299
|
+
machine.id,
|
|
300
|
+
"account-alice",
|
|
301
|
+
signingKey,
|
|
302
|
+
keyExchangeKey,
|
|
303
|
+
mockWs1
|
|
304
|
+
);
|
|
305
|
+
expect(result1.success).toBe(true);
|
|
306
|
+
|
|
307
|
+
// Re-registration from same account with same keys
|
|
308
|
+
const result2 = registerMachine(
|
|
309
|
+
machine.id,
|
|
310
|
+
"account-alice",
|
|
311
|
+
signingKey,
|
|
312
|
+
keyExchangeKey,
|
|
313
|
+
mockWs2
|
|
314
|
+
);
|
|
315
|
+
expect(result2.success).toBe(true);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it("should reject re-registration from different account", () => {
|
|
319
|
+
const machine = createTestIdentity("Machine");
|
|
320
|
+
const mockWs1 = asMockWs<ServerWebSocket<WebSocketData>>(
|
|
321
|
+
createMockWebSocket({ data: { machineId: machine.id, connectionId: "conn-1" } })
|
|
322
|
+
);
|
|
323
|
+
const mockWs2 = asMockWs<ServerWebSocket<WebSocketData>>(
|
|
324
|
+
createMockWebSocket({ data: { machineId: machine.id, connectionId: "conn-2" } })
|
|
325
|
+
);
|
|
326
|
+
|
|
327
|
+
const signingKey = Buffer.from(machine.signing.publicKey).toString("base64");
|
|
328
|
+
const keyExchangeKey = Buffer.from(machine.keyExchange.publicKey).toString("base64");
|
|
329
|
+
|
|
330
|
+
// First registration by Alice
|
|
331
|
+
const result1 = registerMachine(
|
|
332
|
+
machine.id,
|
|
333
|
+
"account-alice",
|
|
334
|
+
signingKey,
|
|
335
|
+
keyExchangeKey,
|
|
336
|
+
mockWs1
|
|
337
|
+
);
|
|
338
|
+
expect(result1.success).toBe(true);
|
|
339
|
+
|
|
340
|
+
// Attacker Eve tries to hijack
|
|
341
|
+
const result2 = registerMachine(
|
|
342
|
+
machine.id,
|
|
343
|
+
"account-eve", // Different account!
|
|
344
|
+
signingKey,
|
|
345
|
+
keyExchangeKey,
|
|
346
|
+
mockWs2
|
|
347
|
+
);
|
|
348
|
+
|
|
349
|
+
expect(result2.success).toBe(false);
|
|
350
|
+
if (!result2.success) {
|
|
351
|
+
expect(result2.error).toContain("different account");
|
|
352
|
+
}
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
it("should reject re-registration with different signing key", () => {
|
|
356
|
+
const machine = createTestIdentity("Machine");
|
|
357
|
+
const attacker = createTestIdentity("Attacker");
|
|
358
|
+
const mockWs1 = asMockWs<ServerWebSocket<WebSocketData>>(
|
|
359
|
+
createMockWebSocket({ data: { machineId: machine.id, connectionId: "conn-1" } })
|
|
360
|
+
);
|
|
361
|
+
const mockWs2 = asMockWs<ServerWebSocket<WebSocketData>>(
|
|
362
|
+
createMockWebSocket({ data: { machineId: machine.id, connectionId: "conn-2" } })
|
|
363
|
+
);
|
|
364
|
+
|
|
365
|
+
const originalSigningKey = Buffer.from(machine.signing.publicKey).toString("base64");
|
|
366
|
+
const originalKeyExchangeKey = Buffer.from(machine.keyExchange.publicKey).toString("base64");
|
|
367
|
+
const attackerSigningKey = Buffer.from(attacker.signing.publicKey).toString("base64");
|
|
368
|
+
|
|
369
|
+
// First registration
|
|
370
|
+
const result1 = registerMachine(
|
|
371
|
+
machine.id,
|
|
372
|
+
"account-alice",
|
|
373
|
+
originalSigningKey,
|
|
374
|
+
originalKeyExchangeKey,
|
|
375
|
+
mockWs1
|
|
376
|
+
);
|
|
377
|
+
expect(result1.success).toBe(true);
|
|
378
|
+
|
|
379
|
+
// Same account but different signing key (key substitution attack)
|
|
380
|
+
const result2 = registerMachine(
|
|
381
|
+
machine.id,
|
|
382
|
+
"account-alice", // Same account
|
|
383
|
+
attackerSigningKey, // Different key!
|
|
384
|
+
originalKeyExchangeKey,
|
|
385
|
+
mockWs2
|
|
386
|
+
);
|
|
387
|
+
|
|
388
|
+
expect(result2.success).toBe(false);
|
|
389
|
+
if (!result2.success) {
|
|
390
|
+
expect(result2.error).toContain("Signing key mismatch");
|
|
391
|
+
}
|
|
392
|
+
});
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
// ============================================================================
|
|
396
|
+
// Issue 4: Single-Use Invite Enforcement Tests
|
|
397
|
+
// ============================================================================
|
|
398
|
+
|
|
399
|
+
describe("Issue 4: Single-Use Invite Enforcement", () => {
|
|
400
|
+
it("should NOT add client to access list for singleUse=true invites", async () => {
|
|
401
|
+
const { client, machine } = createTestIdentityPair();
|
|
402
|
+
const accessList = new AccessControlList();
|
|
403
|
+
|
|
404
|
+
// Verify client is not in access list initially
|
|
405
|
+
expect(accessList.getEntry(client.id)).toBeUndefined();
|
|
406
|
+
|
|
407
|
+
// Create single-use invite
|
|
408
|
+
const inviteToken = createInviteToken(machine, "wss://test.relay", {
|
|
409
|
+
accessType: "full",
|
|
410
|
+
singleUse: true,
|
|
411
|
+
validityMs: 3600000,
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
// Create handler with the access list
|
|
415
|
+
const handler = new HandshakeHandler({
|
|
416
|
+
identity: machine,
|
|
417
|
+
accessList,
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
// Run handshake with single-use invite
|
|
421
|
+
const connectionId = "test-conn-1";
|
|
422
|
+
|
|
423
|
+
// ClientHello
|
|
424
|
+
const { state: clientState, message: clientHello } = createClientHello(machine.id);
|
|
425
|
+
const helloResult = await handler.processMessage(connectionId, {
|
|
426
|
+
type: "handshake",
|
|
427
|
+
phase: "client_hello",
|
|
428
|
+
data: clientHello,
|
|
429
|
+
});
|
|
430
|
+
expect(helloResult.type).toBe("reply");
|
|
431
|
+
if (!isReplyResult(helloResult)) throw new Error("Expected reply");
|
|
432
|
+
|
|
433
|
+
// Process ServerHello
|
|
434
|
+
const serverHello = getReplyData<X3DHResponseMessage>(helloResult);
|
|
435
|
+
const stateAfterServerHello = processServerHello(clientState, serverHello);
|
|
436
|
+
expect(stateAfterServerHello).not.toBeNull();
|
|
437
|
+
|
|
438
|
+
// ClientAuth with single-use invite
|
|
439
|
+
const { message: clientAuth } = createClientAuth(
|
|
440
|
+
stateAfterServerHello!,
|
|
441
|
+
client,
|
|
442
|
+
{ type: "invite", inviteToken }
|
|
443
|
+
);
|
|
444
|
+
|
|
445
|
+
const authResult = await handler.processMessage(connectionId, {
|
|
446
|
+
type: "handshake",
|
|
447
|
+
phase: "client_auth",
|
|
448
|
+
data: clientAuth,
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
// Should succeed
|
|
452
|
+
expect(authResult.type).toBe("established");
|
|
453
|
+
|
|
454
|
+
// Client should NOT be in access list (single-use)
|
|
455
|
+
expect(accessList.getEntry(client.id)).toBeUndefined();
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
it("should add client to access list for singleUse=false invites", async () => {
|
|
459
|
+
const { client, machine } = createTestIdentityPair();
|
|
460
|
+
const accessList = new AccessControlList();
|
|
461
|
+
|
|
462
|
+
// Verify client is not in access list initially
|
|
463
|
+
expect(accessList.getEntry(client.id)).toBeUndefined();
|
|
464
|
+
|
|
465
|
+
// Create reusable invite (singleUse=false)
|
|
466
|
+
const inviteToken = createInviteToken(machine, "wss://test.relay", {
|
|
467
|
+
accessType: "full",
|
|
468
|
+
singleUse: false,
|
|
469
|
+
validityMs: 3600000,
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
// Create handler with the access list
|
|
473
|
+
const handler = new HandshakeHandler({
|
|
474
|
+
identity: machine,
|
|
475
|
+
accessList,
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
// Run handshake with reusable invite
|
|
479
|
+
const connectionId = "test-conn-2";
|
|
480
|
+
|
|
481
|
+
// ClientHello
|
|
482
|
+
const { state: clientState, message: clientHello } = createClientHello(machine.id);
|
|
483
|
+
const helloResult = await handler.processMessage(connectionId, {
|
|
484
|
+
type: "handshake",
|
|
485
|
+
phase: "client_hello",
|
|
486
|
+
data: clientHello,
|
|
487
|
+
});
|
|
488
|
+
expect(helloResult.type).toBe("reply");
|
|
489
|
+
if (!isReplyResult(helloResult)) throw new Error("Expected reply");
|
|
490
|
+
|
|
491
|
+
// Process ServerHello
|
|
492
|
+
const serverHello = getReplyData<X3DHResponseMessage>(helloResult);
|
|
493
|
+
const stateAfterServerHello = processServerHello(clientState, serverHello);
|
|
494
|
+
expect(stateAfterServerHello).not.toBeNull();
|
|
495
|
+
|
|
496
|
+
// ClientAuth with reusable invite
|
|
497
|
+
const { message: clientAuth } = createClientAuth(
|
|
498
|
+
stateAfterServerHello!,
|
|
499
|
+
client,
|
|
500
|
+
{ type: "invite", inviteToken }
|
|
501
|
+
);
|
|
502
|
+
|
|
503
|
+
const authResult = await handler.processMessage(connectionId, {
|
|
504
|
+
type: "handshake",
|
|
505
|
+
phase: "client_auth",
|
|
506
|
+
data: clientAuth,
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
// Should succeed
|
|
510
|
+
expect(authResult.type).toBe("established");
|
|
511
|
+
|
|
512
|
+
// Client SHOULD be in access list (not single-use)
|
|
513
|
+
const entry = accessList.getEntry(client.id);
|
|
514
|
+
expect(entry).toBeDefined();
|
|
515
|
+
expect(entry?.accessType).toBe("full");
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
it("should grant session access for single-use invite during connection", async () => {
|
|
519
|
+
const { client, machine } = createTestIdentityPair();
|
|
520
|
+
const accessList = new AccessControlList();
|
|
521
|
+
|
|
522
|
+
// Create single-use session-invite
|
|
523
|
+
const inviteToken = createInviteToken(machine, "wss://test.relay", {
|
|
524
|
+
accessType: "session-invite",
|
|
525
|
+
sessionId: "session-xyz",
|
|
526
|
+
singleUse: true,
|
|
527
|
+
validityMs: 3600000,
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
const handler = new HandshakeHandler({
|
|
531
|
+
identity: machine,
|
|
532
|
+
accessList,
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
const connectionId = "test-conn-3";
|
|
536
|
+
|
|
537
|
+
// ClientHello
|
|
538
|
+
const { state: clientState, message: clientHello } = createClientHello(machine.id);
|
|
539
|
+
const helloResult = await handler.processMessage(connectionId, {
|
|
540
|
+
type: "handshake",
|
|
541
|
+
phase: "client_hello",
|
|
542
|
+
data: clientHello,
|
|
543
|
+
});
|
|
544
|
+
if (!isReplyResult(helloResult)) throw new Error("Expected reply");
|
|
545
|
+
|
|
546
|
+
// Process ServerHello
|
|
547
|
+
const serverHello = getReplyData<X3DHResponseMessage>(helloResult);
|
|
548
|
+
const stateAfterServerHello = processServerHello(clientState, serverHello);
|
|
549
|
+
|
|
550
|
+
// ClientAuth with single-use invite
|
|
551
|
+
const { message: clientAuth } = createClientAuth(
|
|
552
|
+
stateAfterServerHello!,
|
|
553
|
+
client,
|
|
554
|
+
{ type: "invite", inviteToken }
|
|
555
|
+
);
|
|
556
|
+
|
|
557
|
+
const authResult = await handler.processMessage(connectionId, {
|
|
558
|
+
type: "handshake",
|
|
559
|
+
phase: "client_auth",
|
|
560
|
+
data: clientAuth,
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
// Should succeed with session access
|
|
564
|
+
expect(authResult.type).toBe("established");
|
|
565
|
+
if (authResult.type === "established") {
|
|
566
|
+
expect(authResult.session.accessType).toBe("session-invite");
|
|
567
|
+
expect(authResult.session.sessionId).toBe("session-xyz");
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// But client should NOT be permanently in access list
|
|
571
|
+
expect(accessList.getEntry(client.id)).toBeUndefined();
|
|
572
|
+
});
|
|
573
|
+
});
|