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
package/docs/PROTOCOL.md
ADDED
|
@@ -0,0 +1,619 @@
|
|
|
1
|
+
# Relay Protocol Reference
|
|
2
|
+
|
|
3
|
+
This document describes the WebSocket protocol between machines, clients, and the relay server.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Connection URLs
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
Machine: ws://relay:port/ws?role=machine
|
|
11
|
+
Client: ws://relay:port/ws?role=client
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Authentication
|
|
17
|
+
|
|
18
|
+
### Machine Registration
|
|
19
|
+
1. Machine connects with `?role=machine`
|
|
20
|
+
2. Relay sends `relay_identity` with a random challenge nonce
|
|
21
|
+
3. Machine signs the nonce with its Ed25519 private key
|
|
22
|
+
4. Machine sends `register_machine` including `challengeResponse`
|
|
23
|
+
5. Relay verifies the signature and checks the signing key is authorized
|
|
24
|
+
6. Relay sends `registered` confirmation and `access_list`
|
|
25
|
+
|
|
26
|
+
### Client Routing
|
|
27
|
+
1. Client connects with `?role=client`
|
|
28
|
+
2. Client signs relay messages (see "Message Signing")
|
|
29
|
+
3. Relay verifies the signature and binds `clientIdentityId` to the signing key
|
|
30
|
+
4. Relay routes via invite or direct authorization
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Message Types
|
|
35
|
+
|
|
36
|
+
All messages are JSON. The relay routes based on message type.
|
|
37
|
+
|
|
38
|
+
### Message Signing (Protocol v2)
|
|
39
|
+
|
|
40
|
+
The following client messages require an Ed25519 signature block:
|
|
41
|
+
- `list_machines`
|
|
42
|
+
- `connect_with_invite`
|
|
43
|
+
- `connect_to_machine`
|
|
44
|
+
|
|
45
|
+
Signature format:
|
|
46
|
+
```json
|
|
47
|
+
{
|
|
48
|
+
"signature": {
|
|
49
|
+
"sig": "base64-ed25519-signature",
|
|
50
|
+
"pub": "base64-ed25519-public",
|
|
51
|
+
"ts": 1704067200000
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Machine → Relay
|
|
57
|
+
|
|
58
|
+
#### Challenge Response (Deprecated)
|
|
59
|
+
Relay sends `relay_identity` with a challenge nonce; machines should include the
|
|
60
|
+
signature in `register_machine.challengeResponse`. The standalone
|
|
61
|
+
`challenge_response` message is deprecated.
|
|
62
|
+
```json
|
|
63
|
+
{
|
|
64
|
+
"type": "challenge_response",
|
|
65
|
+
"signature": "base64-ed25519-signature-of-nonce"
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
Response: `{ "type": "registered", "machineId": "abc123" }`
|
|
69
|
+
|
|
70
|
+
#### Register Machine
|
|
71
|
+
```json
|
|
72
|
+
{
|
|
73
|
+
"type": "register_machine",
|
|
74
|
+
"machineId": "abc123",
|
|
75
|
+
"signingKey": "base64-ed25519-public",
|
|
76
|
+
"keyExchangeKey": "base64-x25519-public",
|
|
77
|
+
"challengeResponse": "base64-ed25519-signature-of-nonce",
|
|
78
|
+
"label": "My MacBook"
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
Response: `{ "type": "registered", "machineId": "abc123" }`
|
|
82
|
+
|
|
83
|
+
After successful registration, relay sends the global access list:
|
|
84
|
+
```json
|
|
85
|
+
{
|
|
86
|
+
"type": "access_list",
|
|
87
|
+
"entries": [
|
|
88
|
+
{
|
|
89
|
+
"clientIdentityId": "client-xyz",
|
|
90
|
+
"signingKey": "base64-ed25519-public",
|
|
91
|
+
"keyExchangeKey": "base64-x25519-public",
|
|
92
|
+
"label": "Brad's Phone",
|
|
93
|
+
"accessType": "full",
|
|
94
|
+
"grantedAt": 1704067200000
|
|
95
|
+
}
|
|
96
|
+
]
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
#### Register Invite
|
|
101
|
+
```json
|
|
102
|
+
{
|
|
103
|
+
"type": "register_invite",
|
|
104
|
+
"inviteId": "hash-of-token",
|
|
105
|
+
"machineId": "abc123",
|
|
106
|
+
"expiresAt": 1704067200000,
|
|
107
|
+
"maxUses": 5
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
Response: `{ "type": "registered", "machineId": "abc123" }`
|
|
111
|
+
|
|
112
|
+
#### Authorize Client
|
|
113
|
+
```json
|
|
114
|
+
{
|
|
115
|
+
"type": "authorize_client",
|
|
116
|
+
"machineId": "abc123",
|
|
117
|
+
"clientIdentityId": "client-xyz",
|
|
118
|
+
"signingKey": "base64-ed25519-public",
|
|
119
|
+
"keyExchangeKey": "base64-x25519-public",
|
|
120
|
+
"permissions": { "read": true, "write": true, "manage": false }
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
Response: `{ "type": "client_authorized", "clientIdentityId": "client-xyz" }`
|
|
124
|
+
|
|
125
|
+
#### Revoke Client
|
|
126
|
+
```json
|
|
127
|
+
{
|
|
128
|
+
"type": "revoke_client",
|
|
129
|
+
"machineId": "abc123",
|
|
130
|
+
"clientIdentityId": "client-xyz"
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
Response: `{ "type": "client_revoked", "clientIdentityId": "client-xyz" }`
|
|
134
|
+
|
|
135
|
+
#### Add Global Access
|
|
136
|
+
Grant a client access to all machines on this account:
|
|
137
|
+
```json
|
|
138
|
+
{
|
|
139
|
+
"type": "add_global_access",
|
|
140
|
+
"clientIdentityId": "client-xyz",
|
|
141
|
+
"signingKey": "base64-ed25519-public",
|
|
142
|
+
"keyExchangeKey": "base64-x25519-public",
|
|
143
|
+
"label": "Brad's Phone",
|
|
144
|
+
"accessType": "full",
|
|
145
|
+
"machineIds": ["abc123", "def456"]
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
`accessType`: `"full"` or `"session-invite"`
|
|
149
|
+
`machineIds`: Optional - if set, only applies to specific machines
|
|
150
|
+
Response: `{ "type": "client_authorized", "clientIdentityId": "client-xyz" }`
|
|
151
|
+
|
|
152
|
+
#### Remove Global Access
|
|
153
|
+
```json
|
|
154
|
+
{
|
|
155
|
+
"type": "remove_global_access",
|
|
156
|
+
"clientIdentityId": "client-xyz"
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
Response: `{ "type": "client_revoked", "clientIdentityId": "client-xyz" }`
|
|
160
|
+
|
|
161
|
+
#### Send Data to Client
|
|
162
|
+
```json
|
|
163
|
+
{
|
|
164
|
+
"type": "data",
|
|
165
|
+
"connectionId": "conn-123",
|
|
166
|
+
"data": "base64-encrypted-payload"
|
|
167
|
+
}
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
### Client → Relay
|
|
173
|
+
|
|
174
|
+
#### Connect with Invite
|
|
175
|
+
```json
|
|
176
|
+
{
|
|
177
|
+
"type": "connect_with_invite",
|
|
178
|
+
"inviteId": "hash-of-token",
|
|
179
|
+
"clientIdentityId": "client-xyz",
|
|
180
|
+
"signature": {
|
|
181
|
+
"sig": "base64-ed25519-signature",
|
|
182
|
+
"pub": "base64-ed25519-public",
|
|
183
|
+
"ts": 1704067200000
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
```
|
|
187
|
+
Response: `{ "type": "connection_established", "machineId": "abc123", "connectionId": "conn-123" }`
|
|
188
|
+
|
|
189
|
+
#### Connect to Machine (Direct)
|
|
190
|
+
```json
|
|
191
|
+
{
|
|
192
|
+
"type": "connect_to_machine",
|
|
193
|
+
"machineId": "abc123",
|
|
194
|
+
"clientIdentityId": "client-xyz",
|
|
195
|
+
"signature": {
|
|
196
|
+
"sig": "base64-ed25519-signature",
|
|
197
|
+
"pub": "base64-ed25519-public",
|
|
198
|
+
"ts": 1704067200000
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
Response: `{ "type": "connection_established", "machineId": "abc123", "connectionId": "conn-123" }`
|
|
203
|
+
|
|
204
|
+
#### List Machines
|
|
205
|
+
```json
|
|
206
|
+
{
|
|
207
|
+
"type": "list_machines",
|
|
208
|
+
"clientIdentityId": "client-xyz",
|
|
209
|
+
"signature": {
|
|
210
|
+
"sig": "base64-ed25519-signature",
|
|
211
|
+
"pub": "base64-ed25519-public",
|
|
212
|
+
"ts": 1704067200000
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
```
|
|
216
|
+
Response:
|
|
217
|
+
```json
|
|
218
|
+
{
|
|
219
|
+
"type": "machine_list",
|
|
220
|
+
"machines": [
|
|
221
|
+
{
|
|
222
|
+
"machineId": "abc123",
|
|
223
|
+
"label": "My MacBook",
|
|
224
|
+
"online": true,
|
|
225
|
+
"isAuthorized": true,
|
|
226
|
+
"accessType": "full",
|
|
227
|
+
"lastConnectedAt": 1704067200000
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
"machineId": "def456",
|
|
231
|
+
"label": "Server",
|
|
232
|
+
"online": false,
|
|
233
|
+
"isAuthorized": true,
|
|
234
|
+
"accessType": "session-invite",
|
|
235
|
+
"sessionId": "session-123",
|
|
236
|
+
"lastConnectedAt": 1704060000000
|
|
237
|
+
}
|
|
238
|
+
]
|
|
239
|
+
}
|
|
240
|
+
```
|
|
241
|
+
Note: Only machines the client is authorized for are returned.
|
|
242
|
+
|
|
243
|
+
#### Send Data to Machine
|
|
244
|
+
```json
|
|
245
|
+
{
|
|
246
|
+
"type": "data",
|
|
247
|
+
"data": "base64-encrypted-payload"
|
|
248
|
+
}
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
---
|
|
252
|
+
|
|
253
|
+
### Relay → Machine
|
|
254
|
+
|
|
255
|
+
#### Relay Identity
|
|
256
|
+
Sent immediately after a machine connects. Includes relay signing key and a challenge nonce.
|
|
257
|
+
```json
|
|
258
|
+
{
|
|
259
|
+
"type": "relay_identity",
|
|
260
|
+
"publicKey": "base64-ed25519-public",
|
|
261
|
+
"fingerprint": "Kx4f:2nB9:mP3q:vR8s",
|
|
262
|
+
"label": "Relay",
|
|
263
|
+
"challenge": "base64-random-32-bytes"
|
|
264
|
+
}
|
|
265
|
+
```
|
|
266
|
+
The machine must respond by sending `register_machine` with `challengeResponse`.
|
|
267
|
+
|
|
268
|
+
#### Access List
|
|
269
|
+
Sent after successful registration with all authorized clients:
|
|
270
|
+
```json
|
|
271
|
+
{
|
|
272
|
+
"type": "access_list",
|
|
273
|
+
"entries": [
|
|
274
|
+
{
|
|
275
|
+
"clientIdentityId": "client-xyz",
|
|
276
|
+
"signingKey": "base64-ed25519-public",
|
|
277
|
+
"keyExchangeKey": "base64-x25519-public",
|
|
278
|
+
"label": "Brad's Phone",
|
|
279
|
+
"accessType": "full",
|
|
280
|
+
"sessionId": null,
|
|
281
|
+
"grantedAt": 1704067200000
|
|
282
|
+
}
|
|
283
|
+
]
|
|
284
|
+
}
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
#### Access Update
|
|
288
|
+
Incremental update when global access changes:
|
|
289
|
+
```json
|
|
290
|
+
{
|
|
291
|
+
"type": "access_update",
|
|
292
|
+
"added": [
|
|
293
|
+
{
|
|
294
|
+
"clientIdentityId": "new-client",
|
|
295
|
+
"signingKey": "base64-ed25519-public",
|
|
296
|
+
"keyExchangeKey": "base64-x25519-public",
|
|
297
|
+
"label": "New Device",
|
|
298
|
+
"accessType": "full",
|
|
299
|
+
"grantedAt": 1704067200000
|
|
300
|
+
}
|
|
301
|
+
],
|
|
302
|
+
"removed": ["old-client-id"]
|
|
303
|
+
}
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
#### Client Connected
|
|
307
|
+
```json
|
|
308
|
+
{
|
|
309
|
+
"type": "client_connected",
|
|
310
|
+
"connectionId": "conn-123",
|
|
311
|
+
"clientIdentityId": "client-xyz",
|
|
312
|
+
"viaInvite": "invite-id"
|
|
313
|
+
}
|
|
314
|
+
```
|
|
315
|
+
`viaInvite` is optional - only present when connecting via invite.
|
|
316
|
+
|
|
317
|
+
#### Client Disconnected
|
|
318
|
+
```json
|
|
319
|
+
{
|
|
320
|
+
"type": "client_disconnected",
|
|
321
|
+
"connectionId": "conn-123",
|
|
322
|
+
"reason": "Client closed connection"
|
|
323
|
+
}
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
#### Data from Client
|
|
327
|
+
```json
|
|
328
|
+
{
|
|
329
|
+
"type": "data",
|
|
330
|
+
"connectionId": "conn-123",
|
|
331
|
+
"data": "base64-encrypted-payload"
|
|
332
|
+
}
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
---
|
|
336
|
+
|
|
337
|
+
### Relay → Client
|
|
338
|
+
|
|
339
|
+
#### Connection Established
|
|
340
|
+
```json
|
|
341
|
+
{
|
|
342
|
+
"type": "connection_established",
|
|
343
|
+
"machineId": "abc123",
|
|
344
|
+
"connectionId": "conn-123"
|
|
345
|
+
}
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
#### Connection Failed
|
|
349
|
+
```json
|
|
350
|
+
{
|
|
351
|
+
"type": "error",
|
|
352
|
+
"code": "OFFLINE",
|
|
353
|
+
"message": "Machine is offline"
|
|
354
|
+
}
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
#### Data from Machine
|
|
358
|
+
```json
|
|
359
|
+
{
|
|
360
|
+
"type": "data",
|
|
361
|
+
"data": "base64-encrypted-payload"
|
|
362
|
+
}
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
---
|
|
366
|
+
|
|
367
|
+
## Error Codes
|
|
368
|
+
|
|
369
|
+
| Code | Meaning |
|
|
370
|
+
|------|---------|
|
|
371
|
+
| `FORBIDDEN` | Role doesn't allow this action |
|
|
372
|
+
| `NOT_FOUND` | Machine/invite/client not found |
|
|
373
|
+
| `INVALID` | Invite expired or exhausted |
|
|
374
|
+
| `OFFLINE` | Machine is not connected |
|
|
375
|
+
|
|
376
|
+
---
|
|
377
|
+
|
|
378
|
+
## Handshake Protocol (X3DH)
|
|
379
|
+
|
|
380
|
+
After `connection_established`, client and machine perform X3DH handshake:
|
|
381
|
+
|
|
382
|
+
### Phase 1: Client Hello
|
|
383
|
+
```json
|
|
384
|
+
{
|
|
385
|
+
"type": "handshake",
|
|
386
|
+
"phase": "client_hello",
|
|
387
|
+
"data": {
|
|
388
|
+
"clientIdentityKey": "base64-x25519-public",
|
|
389
|
+
"clientEphemeralKey": "base64-x25519-public",
|
|
390
|
+
"targetMachineId": "abc123"
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
### Phase 2: Server Hello
|
|
396
|
+
```json
|
|
397
|
+
{
|
|
398
|
+
"type": "handshake",
|
|
399
|
+
"phase": "server_hello",
|
|
400
|
+
"data": {
|
|
401
|
+
"serverIdentityKey": "base64-x25519-public",
|
|
402
|
+
"serverEphemeralKey": "base64-x25519-public",
|
|
403
|
+
"serverSigningKey": "base64-ed25519-public"
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
### Phase 3: Client Auth
|
|
409
|
+
```json
|
|
410
|
+
{
|
|
411
|
+
"type": "handshake",
|
|
412
|
+
"phase": "client_auth",
|
|
413
|
+
"data": {
|
|
414
|
+
"signature": "base64-ed25519-signature",
|
|
415
|
+
"signingKey": "base64-ed25519-public",
|
|
416
|
+
"inviteToken": "base64url-invite" // if connecting via invite
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
### Phase 4: Server Auth
|
|
422
|
+
```json
|
|
423
|
+
{
|
|
424
|
+
"type": "handshake",
|
|
425
|
+
"phase": "server_auth",
|
|
426
|
+
"data": {
|
|
427
|
+
"signature": "base64-ed25519-signature",
|
|
428
|
+
"permissions": { "read": true, "write": true, "manage": false }
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
After server_auth, both sides derive session keys and switch to encrypted communication.
|
|
434
|
+
|
|
435
|
+
---
|
|
436
|
+
|
|
437
|
+
## Encrypted Frame Format
|
|
438
|
+
|
|
439
|
+
After handshake, all data is encrypted frames:
|
|
440
|
+
|
|
441
|
+
```
|
|
442
|
+
┌──────────────────────────────────────────────────────────────┐
|
|
443
|
+
│ Encrypted Frame Structure │
|
|
444
|
+
├──────────────────────────────────────────────────────────────┤
|
|
445
|
+
│ │
|
|
446
|
+
│ Bytes 0-3: Stream ID (4 bytes, big-endian) │
|
|
447
|
+
│ Bytes 4-15: Nonce (12 bytes, random) │
|
|
448
|
+
│ Bytes 16+: Ciphertext (AES-256-GCM) │
|
|
449
|
+
│ ├── Encrypted payload │
|
|
450
|
+
│ └── 16-byte auth tag (appended) │
|
|
451
|
+
│ │
|
|
452
|
+
│ Minimum frame length: 32 bytes (4 + 12 + 16) │
|
|
453
|
+
│ │
|
|
454
|
+
└──────────────────────────────────────────────────────────────┘
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
Stream IDs:
|
|
458
|
+
- `0`: Master stream (full machine access)
|
|
459
|
+
- `1+`: Share streams (session-specific access)
|
|
460
|
+
|
|
461
|
+
---
|
|
462
|
+
|
|
463
|
+
## Session Frame Types
|
|
464
|
+
|
|
465
|
+
Within an encrypted session, two frame types are used:
|
|
466
|
+
|
|
467
|
+
### PTY Frames (Type 0x00)
|
|
468
|
+
```
|
|
469
|
+
┌────────────────────────────────────────────────────────────┐
|
|
470
|
+
│ Frame Type │ Length (4 bytes BE) │ Payload │
|
|
471
|
+
│ 0x00 │ N │ Raw terminal I/O │
|
|
472
|
+
└────────────────────────────────────────────────────────────┘
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
### Control Frames (Type 0x01)
|
|
476
|
+
```
|
|
477
|
+
┌────────────────────────────────────────────────────────────┐
|
|
478
|
+
│ Frame Type │ Length (4 bytes BE) │ JSON Payload │
|
|
479
|
+
│ 0x01 │ N │ Control message │
|
|
480
|
+
└────────────────────────────────────────────────────────────┘
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
Control message example (resize):
|
|
484
|
+
```json
|
|
485
|
+
{
|
|
486
|
+
"type": "resize",
|
|
487
|
+
"cols": 120,
|
|
488
|
+
"rows": 40
|
|
489
|
+
}
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
---
|
|
493
|
+
|
|
494
|
+
## Connection Lifecycle
|
|
495
|
+
|
|
496
|
+
```
|
|
497
|
+
┌─────────────────────────────────────────────────────────────────────────────┐
|
|
498
|
+
│ MACHINE LIFECYCLE │
|
|
499
|
+
├─────────────────────────────────────────────────────────────────────────────┤
|
|
500
|
+
│ │
|
|
501
|
+
│ 1. Connect to relay (ws://...?role=machine) │
|
|
502
|
+
│ 2. Receive relay_identity with challenge │
|
|
503
|
+
│ 3. Send register_machine (challengeResponse + keys) │
|
|
504
|
+
│ 4. Receive "registered" confirmation │
|
|
505
|
+
│ 5. Optionally register invites │
|
|
506
|
+
│ 6. Wait for client_connected notifications │
|
|
507
|
+
│ 7. Handle handshakes and route data │
|
|
508
|
+
│ 8. On disconnect: relay marks machine offline │
|
|
509
|
+
│ │
|
|
510
|
+
└─────────────────────────────────────────────────────────────────────────────┘
|
|
511
|
+
|
|
512
|
+
┌─────────────────────────────────────────────────────────────────────────────┐
|
|
513
|
+
│ CLIENT LIFECYCLE │
|
|
514
|
+
├─────────────────────────────────────────────────────────────────────────────┤
|
|
515
|
+
│ │
|
|
516
|
+
│ 1. Connect to relay (ws://...?role=client) │
|
|
517
|
+
│ 2. Send connect_with_invite OR connect_to_machine (signed) │
|
|
518
|
+
│ 3. Receive connection_established (or error) │
|
|
519
|
+
│ 4. Perform X3DH handshake │
|
|
520
|
+
│ 5. Exchange encrypted terminal data │
|
|
521
|
+
│ 6. On disconnect: relay notifies machine │
|
|
522
|
+
│ │
|
|
523
|
+
└─────────────────────────────────────────────────────────────────────────────┘
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
---
|
|
527
|
+
|
|
528
|
+
## Keepalive
|
|
529
|
+
|
|
530
|
+
The relay sends periodic WebSocket ping frames. Machines and clients should:
|
|
531
|
+
- Respond to ping frames with pong (handled automatically by most WebSocket libraries)
|
|
532
|
+
- Consider connection dead after 30 seconds without pong response
|
|
533
|
+
- Reconnect automatically on connection loss
|
|
534
|
+
|
|
535
|
+
---
|
|
536
|
+
|
|
537
|
+
## Routing Rules
|
|
538
|
+
|
|
539
|
+
The relay routes messages based on:
|
|
540
|
+
|
|
541
|
+
| Message Type | From | Routed To |
|
|
542
|
+
|--------------|------|-----------|
|
|
543
|
+
| `register_*` | Machine | Handled by relay |
|
|
544
|
+
| `authorize_client` | Machine | Handled by relay |
|
|
545
|
+
| `revoke_client` | Machine | Handled by relay |
|
|
546
|
+
| `add_global_access` | Machine | Handled by relay → broadcasts to all machines |
|
|
547
|
+
| `remove_global_access` | Machine | Handled by relay → broadcasts to all machines |
|
|
548
|
+
| `challenge_response` | Machine | Handled by relay |
|
|
549
|
+
| `connect_*` | Client | Handled by relay |
|
|
550
|
+
| `list_machines` | Client | Handled by relay |
|
|
551
|
+
| `data` | Machine | Target client (by connectionId) |
|
|
552
|
+
| `data` | Client | Connected machine |
|
|
553
|
+
| `handshake` | Client | Connected machine (wrapped in data) |
|
|
554
|
+
| `challenge` | Relay | Target machine |
|
|
555
|
+
| `access_list` | Relay | Target machine |
|
|
556
|
+
| `access_update` | Relay | All connected machines |
|
|
557
|
+
|
|
558
|
+
---
|
|
559
|
+
|
|
560
|
+
## Cryptographic Primitives
|
|
561
|
+
|
|
562
|
+
| Purpose | Algorithm | Details |
|
|
563
|
+
|---------|-----------|---------|
|
|
564
|
+
| Identity signing | Ed25519 | 32-byte public key, 64-byte secret key |
|
|
565
|
+
| Key exchange | X25519 | 32-byte keys |
|
|
566
|
+
| Symmetric encryption | AES-256-GCM | 12-byte nonce, 16-byte auth tag |
|
|
567
|
+
| Key derivation | HKDF-SHA256 | Domain-separated for send/receive keys |
|
|
568
|
+
| Relay challenge signing | Ed25519 | Machine signs relay nonce |
|
|
569
|
+
| Client message signing | Ed25519 | Signed list/connect messages |
|
|
570
|
+
|
|
571
|
+
---
|
|
572
|
+
|
|
573
|
+
---
|
|
574
|
+
|
|
575
|
+
## Inbox & Notification Protocol
|
|
576
|
+
|
|
577
|
+
The tmux-lite server tracks terminal events and maintains an inbox for notifications.
|
|
578
|
+
|
|
579
|
+
### Supported OSC Sequences
|
|
580
|
+
|
|
581
|
+
| Sequence | Source | Description |
|
|
582
|
+
|----------|--------|-------------|
|
|
583
|
+
| `OSC 777;exit:<code>` | Custom | Process exit with code |
|
|
584
|
+
| `OSC 9` | iTerm2/Growl | Notification message |
|
|
585
|
+
| `OSC 99` | Kitty | Notification |
|
|
586
|
+
| `OSC 777;notify` | rxvt | Notification |
|
|
587
|
+
| `OSC 133 D` | Semantic shell | Command done |
|
|
588
|
+
| `BEL` (0x07) | Terminal | Bell (debounced 500ms) |
|
|
589
|
+
|
|
590
|
+
### Inbox Item Types
|
|
591
|
+
|
|
592
|
+
```typescript
|
|
593
|
+
type InboxItemType = 'bell' | 'title_change' | 'idle' | 'exit';
|
|
594
|
+
|
|
595
|
+
interface InboxItem {
|
|
596
|
+
id: string;
|
|
597
|
+
sessionId: string;
|
|
598
|
+
sessionName: string;
|
|
599
|
+
type: InboxItemType;
|
|
600
|
+
timestamp: number;
|
|
601
|
+
read: boolean;
|
|
602
|
+
context?: string; // Additional context
|
|
603
|
+
processTitle?: string; // Current process title
|
|
604
|
+
exitCode?: number; // For 'exit' type
|
|
605
|
+
}
|
|
606
|
+
```
|
|
607
|
+
|
|
608
|
+
### Control Messages
|
|
609
|
+
|
|
610
|
+
```json
|
|
611
|
+
{ "type": "get_inbox" }
|
|
612
|
+
{ "type": "clear_inbox" }
|
|
613
|
+
{ "type": "mark_inbox_read", "itemId": "..." }
|
|
614
|
+
```
|
|
615
|
+
|
|
616
|
+
---
|
|
617
|
+
|
|
618
|
+
*Protocol version: 2*
|
|
619
|
+
*Last updated: 2025-01*
|