itwillsync 1.7.0 → 1.8.0
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/README.md +184 -48
- package/dist/hub/daemon.js +43 -21
- package/dist/hub/daemon.js.map +1 -1
- package/dist/hub/dashboard/assets/index-3t8d4xww.css +1 -0
- package/dist/hub/dashboard/assets/index-hmAVUiL5.js +2 -0
- package/dist/hub/dashboard/index.html +2 -2
- package/dist/index.js +97 -37
- package/dist/index.js.map +1 -1
- package/dist/web-client/assets/index-7O6Lccue.css +1 -0
- package/dist/web-client/assets/index-C2_Zd4WS.js +120 -0
- package/dist/web-client/index.html +2 -2
- package/package.json +5 -3
- package/dist/hub/dashboard/assets/index-CUdWjWFv.css +0 -1
- package/dist/hub/dashboard/assets/index-hWUVy-IH.js +0 -2
- package/dist/web-client/assets/index-Bg1a3YQa.js +0 -80
- package/dist/web-client/assets/index-CmAz03xC.css +0 -1
package/README.md
CHANGED
|
@@ -1,79 +1,215 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
1
3
|
# itwillsync
|
|
2
4
|
|
|
3
|
-
**
|
|
5
|
+
**Sync any terminal-based AI coding agent to your phone over local network.**
|
|
4
6
|
|
|
5
|
-
|
|
7
|
+
[](https://www.npmjs.com/package/itwillsync)
|
|
8
|
+
[](https://opensource.org/licenses/MIT)
|
|
9
|
+
[](https://www.npmjs.com/package/itwillsync)
|
|
10
|
+
[](https://github.com/shrijayan/itwillsync/actions/workflows/ci.yml)
|
|
6
11
|
|
|
12
|
+
Sync any coding agent to your phone — one dashboard for all your sessions. Agent-agnostic, privacy-first, zero cloud.
|
|
13
|
+
|
|
14
|
+
[Website](https://shrijayan.github.io/itwillsync/) | [Docs](https://shrijayan.github.io/itwillsync/docs/) | [Demo Video](https://youtu.be/Zc0Tb98CXh0)
|
|
15
|
+
|
|
16
|
+
<!-- TODO: Replace with demo GIF once recorded -->
|
|
17
|
+
<!-- <img src="docs/assets/demo.gif" alt="itwillsync demo: scanning QR code to connect phone to Claude Code terminal session over local WiFi" width="700"> -->
|
|
18
|
+
|
|
19
|
+
</div>
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Quick Start
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npx itwillsync claude # Claude Code
|
|
27
|
+
npx itwillsync aider # Aider
|
|
28
|
+
npx itwillsync bash # or any terminal command
|
|
7
29
|
```
|
|
8
|
-
npx itwillsync claude
|
|
9
|
-
npx itwillsync aider
|
|
10
|
-
npx itwillsync bash
|
|
11
|
-
```
|
|
12
30
|
|
|
13
|
-
|
|
31
|
+
No install needed. Node.js 20+ required.
|
|
32
|
+
|
|
33
|
+
## Why itwillsync
|
|
34
|
+
|
|
35
|
+
- **Control your agents from your phone**
|
|
36
|
+
- Scan a QR code and get full terminal access in your browser — no app install needed
|
|
37
|
+
- Type commands, approve prompts, and fix errors right from your phone
|
|
38
|
+
- Stay in the loop while you're away from your desk — kitchen, couch, coffee shop
|
|
39
|
+
|
|
40
|
+
- **One dashboard for all your sessions**
|
|
41
|
+
- See every running agent at a glance — status, working directory, uptime
|
|
42
|
+
- Tap any session to jump into the full terminal
|
|
43
|
+
- Get alerted when an agent needs your attention
|
|
44
|
+
|
|
45
|
+
- **Works with any agent**
|
|
46
|
+
- Claude Code, Aider, Codex, Goose, Cline — if it runs in a terminal, it works
|
|
47
|
+
- Switch between agents without changing your workflow
|
|
48
|
+
- Not locked to any single vendor or platform
|
|
49
|
+
|
|
50
|
+
- **Your data stays yours**
|
|
51
|
+
- Everything runs on your local network — nothing goes to the cloud
|
|
52
|
+
- No accounts, no signup, no telemetry
|
|
53
|
+
- End-to-end encrypted connections with per-session tokens
|
|
54
|
+
|
|
55
|
+
- **Zero friction to get started**
|
|
56
|
+
- One command: `npx itwillsync claude` — no install, no config
|
|
57
|
+
- Works on macOS, Windows, and Linux
|
|
58
|
+
|
|
59
|
+
## How It Works
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
┌─────────────────┐ ┌─────────────────┐
|
|
63
|
+
│ Your Laptop │ Local WiFi / Tailscale │ Your Phone │
|
|
64
|
+
│ │ │ │
|
|
65
|
+
│ Agent (Claude, │ WebSocket + Auth │ Browser-based │
|
|
66
|
+
│ Aider, etc.) │ ◄═════════════════════► │ Terminal │
|
|
67
|
+
│ ↕ │ Token in QR code │ (xterm.js) │
|
|
68
|
+
│ PTY (node-pty) │ │ Touch keyboard │
|
|
69
|
+
│ ↕ │ │ Extra keys bar │
|
|
70
|
+
│ HTTP + WS │ │ │
|
|
71
|
+
│ Server │ │ │
|
|
72
|
+
└─────────────────┘ └─────────────────┘
|
|
73
|
+
```
|
|
14
74
|
|
|
15
75
|
1. Run `itwillsync` with your agent command
|
|
16
76
|
2. A QR code appears in your terminal
|
|
17
|
-
3. Scan it on your phone —
|
|
18
|
-
4. Control your agent from your phone
|
|
77
|
+
3. Scan it on your phone — a terminal opens in your browser
|
|
78
|
+
4. Control your agent from your phone, laptop, or both simultaneously
|
|
19
79
|
|
|
20
|
-
##
|
|
80
|
+
## When To Use It
|
|
21
81
|
|
|
22
|
-
-
|
|
23
|
-
-
|
|
82
|
+
- **Walking to the kitchen** while Claude works on a long refactor — check progress from your phone
|
|
83
|
+
- **Monitoring multiple agents** from the couch via the multi-session dashboard
|
|
84
|
+
- **Getting a notification** when your agent needs attention (auto-detects BEL/OSC signals)
|
|
85
|
+
- **Working from a coffee shop** via Tailscale — no need to be on the same WiFi
|
|
86
|
+
- **Showing a colleague** what your AI agents are doing — just share the QR code
|
|
87
|
+
- **Quick approval** from your phone while you're in a meeting
|
|
24
88
|
|
|
25
|
-
##
|
|
89
|
+
## Multi-Session Dashboard
|
|
26
90
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
91
|
+
Running multiple agents? The hub daemon manages all your sessions from one place.
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
┌─────────────────────────────────────┐
|
|
95
|
+
│ itwillsync Dashboard │
|
|
96
|
+
│ │
|
|
97
|
+
│ ┌─────────────┐ ┌─────────────┐ │
|
|
98
|
+
│ │ Claude Code │ │ Aider │ │
|
|
99
|
+
│ │ ~/myproject │ │ ~/api │ │
|
|
100
|
+
│ │ ● Active │ │ ⚠ Attention │ │
|
|
101
|
+
│ │ 12m uptime │ │ 3m uptime │ │
|
|
102
|
+
│ └─────────────┘ └─────────────┘ │
|
|
103
|
+
│ │
|
|
104
|
+
│ ┌─────────────┐ │
|
|
105
|
+
│ │ Bash │ │
|
|
106
|
+
│ │ ~/scripts │ │
|
|
107
|
+
│ │ ○ Idle │ │
|
|
108
|
+
│ │ 45m uptime │ │
|
|
109
|
+
│ └─────────────┘ │
|
|
110
|
+
└─────────────────────────────────────┘
|
|
30
111
|
```
|
|
31
112
|
|
|
32
|
-
|
|
113
|
+
- Session cards show agent name, working directory, status, and uptime
|
|
114
|
+
- Real-time updates via WebSocket
|
|
115
|
+
- Tap a card to open the full terminal
|
|
116
|
+
- Attention detection alerts you when an agent needs input
|
|
117
|
+
- Sleep prevention keeps your machine awake during long tasks
|
|
33
118
|
|
|
34
|
-
##
|
|
119
|
+
## Works With
|
|
35
120
|
|
|
36
|
-
|
|
121
|
+
Claude Code, Aider, Goose, Codex, Cline, Copilot CLI — or any terminal-based tool.
|
|
37
122
|
|
|
38
123
|
```bash
|
|
39
|
-
|
|
40
|
-
itwillsync
|
|
124
|
+
npx itwillsync claude # Claude Code
|
|
125
|
+
npx itwillsync aider # Aider
|
|
126
|
+
npx itwillsync goose # Goose
|
|
127
|
+
npx itwillsync "codex --quiet" # Codex
|
|
128
|
+
npx itwillsync bash # Plain shell
|
|
129
|
+
```
|
|
41
130
|
|
|
42
|
-
|
|
43
|
-
itwillsync --tailscale claude
|
|
131
|
+
If it runs in a terminal, itwillsync can sync it.
|
|
44
132
|
|
|
45
|
-
|
|
46
|
-
itwillsync --local claude
|
|
133
|
+
## Connection Modes
|
|
47
134
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
135
|
+
| Mode | Command | When |
|
|
136
|
+
|------|---------|------|
|
|
137
|
+
| **Local WiFi** (default) | `npx itwillsync claude` | Phone on same network |
|
|
138
|
+
| **Tailscale** | `npx itwillsync --tailscale claude` | Any network, anywhere |
|
|
139
|
+
| **Cloudflare Tunnel** | `npx itwillsync --tunnel cloudflare claude` | Remote, no VPN needed |
|
|
140
|
+
| **Localhost** | `npx itwillsync --localhost claude` | Same machine only |
|
|
51
141
|
|
|
52
|
-
|
|
142
|
+
On first run, a setup wizard detects your network and saves your preference.
|
|
53
143
|
|
|
54
|
-
|
|
144
|
+
```bash
|
|
145
|
+
# First run — wizard auto-detects Tailscale
|
|
146
|
+
npx itwillsync claude
|
|
55
147
|
|
|
148
|
+
# Override for a single session
|
|
149
|
+
npx itwillsync --tailscale claude
|
|
150
|
+
npx itwillsync --local claude
|
|
151
|
+
|
|
152
|
+
# Re-run the setup wizard
|
|
153
|
+
npx itwillsync setup
|
|
56
154
|
```
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
155
|
+
|
|
156
|
+
## Commands
|
|
157
|
+
|
|
158
|
+
| Flag | Description |
|
|
159
|
+
|------|-------------|
|
|
160
|
+
| `--port <number>` | Port to listen on (default: 3456) |
|
|
161
|
+
| `--localhost` | Bind to 127.0.0.1 only |
|
|
162
|
+
| `--tailscale` | Use Tailscale for this session |
|
|
163
|
+
| `--local` | Use local WiFi for this session |
|
|
164
|
+
| `--tunnel <provider>` | Use a tunnel for remote access (cloudflare) |
|
|
165
|
+
| `--no-qr` | Don't display QR code |
|
|
166
|
+
| `setup` | Run the setup wizard |
|
|
167
|
+
| `-h, --help` | Show help |
|
|
168
|
+
| `-v, --version` | Show version |
|
|
69
169
|
|
|
70
170
|
## Security
|
|
71
171
|
|
|
72
|
-
-
|
|
73
|
-
-
|
|
74
|
-
-
|
|
75
|
-
-
|
|
172
|
+
- **E2E encrypted** — all WebSocket messages encrypted with NaCl secretbox (XSalsa20-Poly1305)
|
|
173
|
+
- **Random tokens** — each session generates a cryptographically random 64-character token
|
|
174
|
+
- **QR code auth** — token is embedded in the QR code URL, no manual entry needed
|
|
175
|
+
- **WebSocket auth** — all connections require the token (constant-time comparison)
|
|
176
|
+
- **Rate limiting** — 5 failed auth attempts locks out the IP for 60 seconds
|
|
177
|
+
- **Zero cloud** — no data leaves your local network (or Tailscale tailnet)
|
|
178
|
+
- **No accounts** — no signup, no telemetry, no tracking
|
|
179
|
+
|
|
180
|
+
## Mobile-Optimized
|
|
181
|
+
|
|
182
|
+
The phone terminal isn't just a mirror — it's built for mobile:
|
|
183
|
+
|
|
184
|
+
- **Touch-friendly extra keys bar** — Ctrl, Alt, Tab, Escape, arrows, and function keys
|
|
185
|
+
- **WebGL-accelerated rendering** on desktop, canvas fallback on mobile
|
|
186
|
+
- **Auto-reconnect** with scrollback buffer sync if connection drops
|
|
187
|
+
- **Audio notifications** when agents need attention
|
|
188
|
+
|
|
189
|
+
## Development
|
|
190
|
+
|
|
191
|
+
```bash
|
|
192
|
+
git clone https://github.com/shrijayan/itwillsync.git
|
|
193
|
+
cd itwillsync
|
|
194
|
+
nvm use # Node 22
|
|
195
|
+
pnpm install
|
|
196
|
+
pnpm build # Build all packages
|
|
197
|
+
pnpm test # Run tests
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Monorepo structure:
|
|
201
|
+
|
|
202
|
+
```
|
|
203
|
+
packages/
|
|
204
|
+
├── cli/ → Main npm package (itwillsync)
|
|
205
|
+
├── web-client/ → Browser terminal (xterm.js)
|
|
206
|
+
├── hub/ → Dashboard daemon
|
|
207
|
+
├── landing/ → Landing page
|
|
208
|
+
└── docs/ → VitePress documentation
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup and PR guidelines.
|
|
76
212
|
|
|
77
|
-
##
|
|
213
|
+
## License
|
|
78
214
|
|
|
79
|
-
|
|
215
|
+
[MIT](LICENSE)
|
package/dist/hub/daemon.js
CHANGED
|
@@ -4147,7 +4147,7 @@ function createInternalApi(options) {
|
|
|
4147
4147
|
}
|
|
4148
4148
|
res.writeHead(404);
|
|
4149
4149
|
res.end(JSON.stringify({ error: "Not found" }));
|
|
4150
|
-
} catch
|
|
4150
|
+
} catch {
|
|
4151
4151
|
res.writeHead(500);
|
|
4152
4152
|
res.end(JSON.stringify({ error: "Internal server error" }));
|
|
4153
4153
|
}
|
|
@@ -4189,6 +4189,7 @@ var import_websocket = __toESM(require_websocket(), 1);
|
|
|
4189
4189
|
var import_websocket_server = __toESM(require_websocket_server(), 1);
|
|
4190
4190
|
|
|
4191
4191
|
// src/server.ts
|
|
4192
|
+
import { deriveEncryptionKey, encrypt, decrypt } from "@itwillsync/shared/crypto";
|
|
4192
4193
|
var MIME_TYPES = {
|
|
4193
4194
|
".html": "text/html; charset=utf-8",
|
|
4194
4195
|
".js": "application/javascript; charset=utf-8",
|
|
@@ -4202,6 +4203,10 @@ var COMPRESSIBLE = /* @__PURE__ */ new Set([".html", ".js", ".css", ".json", ".s
|
|
|
4202
4203
|
var PING_INTERVAL_MS = 3e4;
|
|
4203
4204
|
function createDashboardServer(options) {
|
|
4204
4205
|
const { registry, masterToken, dashboardPath, host, port, previewCollector, toolHistory, sleepPrevention, onCreateSession } = options;
|
|
4206
|
+
const encryptionKey = deriveEncryptionKey(masterToken);
|
|
4207
|
+
function sendMsg(ws, msg) {
|
|
4208
|
+
ws.send(encrypt(JSON.stringify(msg), encryptionKey));
|
|
4209
|
+
}
|
|
4205
4210
|
const homeDir = homedir3();
|
|
4206
4211
|
const clients = /* @__PURE__ */ new Set();
|
|
4207
4212
|
const aliveMap = /* @__PURE__ */ new WeakMap();
|
|
@@ -4357,26 +4362,33 @@ function createDashboardServer(options) {
|
|
|
4357
4362
|
clients.add(ws);
|
|
4358
4363
|
aliveMap.set(ws, true);
|
|
4359
4364
|
const sessions = registry.getAll();
|
|
4360
|
-
ws
|
|
4365
|
+
sendMsg(ws, { type: "sessions", sessions });
|
|
4361
4366
|
if (sleepPrevention) {
|
|
4362
|
-
ws
|
|
4367
|
+
sendMsg(ws, { type: "sleep-state", state: sleepPrevention.getState() });
|
|
4363
4368
|
}
|
|
4364
4369
|
if (previewCollector) {
|
|
4365
4370
|
const previews = previewCollector.getAllPreviews();
|
|
4366
4371
|
for (const [sessionId, lines] of previews) {
|
|
4367
4372
|
if (lines.length > 0) {
|
|
4368
|
-
ws
|
|
4373
|
+
sendMsg(ws, { type: "preview", sessionId, lines });
|
|
4369
4374
|
}
|
|
4370
4375
|
}
|
|
4371
4376
|
}
|
|
4372
4377
|
ws.on("message", async (raw) => {
|
|
4378
|
+
let plaintext;
|
|
4379
|
+
try {
|
|
4380
|
+
const rawStr = typeof raw === "string" ? raw : raw.toString("utf-8");
|
|
4381
|
+
plaintext = decrypt(rawStr, encryptionKey);
|
|
4382
|
+
} catch {
|
|
4383
|
+
return;
|
|
4384
|
+
}
|
|
4373
4385
|
try {
|
|
4374
|
-
const msg = JSON.parse(
|
|
4386
|
+
const msg = JSON.parse(plaintext);
|
|
4375
4387
|
switch (msg.type) {
|
|
4376
4388
|
case "stop-session": {
|
|
4377
4389
|
const session = registry.getById(msg.sessionId);
|
|
4378
4390
|
if (!session) {
|
|
4379
|
-
ws
|
|
4391
|
+
sendMsg(ws, { type: "operation-error", operation: "stop", sessionId: msg.sessionId, error: "Session not found" });
|
|
4380
4392
|
return;
|
|
4381
4393
|
}
|
|
4382
4394
|
try {
|
|
@@ -4389,7 +4401,7 @@ function createDashboardServer(options) {
|
|
|
4389
4401
|
case "rename-session": {
|
|
4390
4402
|
const renamed = registry.rename(msg.sessionId, msg.name?.trim());
|
|
4391
4403
|
if (!renamed) {
|
|
4392
|
-
ws
|
|
4404
|
+
sendMsg(ws, { type: "operation-error", operation: "rename", sessionId: msg.sessionId, error: "Session not found" });
|
|
4393
4405
|
}
|
|
4394
4406
|
break;
|
|
4395
4407
|
}
|
|
@@ -4402,13 +4414,13 @@ function createDashboardServer(options) {
|
|
|
4402
4414
|
}
|
|
4403
4415
|
case "create-session": {
|
|
4404
4416
|
if (!onCreateSession) {
|
|
4405
|
-
ws
|
|
4417
|
+
sendMsg(ws, { type: "session-create-error", error: "Session creation not available" });
|
|
4406
4418
|
break;
|
|
4407
4419
|
}
|
|
4408
4420
|
const tool = (msg.tool || "").trim();
|
|
4409
4421
|
const rawCwd = (msg.cwd || "").trim();
|
|
4410
4422
|
if (!tool) {
|
|
4411
|
-
ws
|
|
4423
|
+
sendMsg(ws, { type: "session-create-error", error: "Tool name is required" });
|
|
4412
4424
|
break;
|
|
4413
4425
|
}
|
|
4414
4426
|
const cwd = rawCwd ? rawCwd.replace(/^~/, homeDir) : homeDir;
|
|
@@ -4416,31 +4428,31 @@ function createDashboardServer(options) {
|
|
|
4416
4428
|
const resolved = await realpath(cwd);
|
|
4417
4429
|
const dirStat = await stat(resolved);
|
|
4418
4430
|
if (!dirStat.isDirectory()) {
|
|
4419
|
-
ws
|
|
4431
|
+
sendMsg(ws, { type: "session-create-error", error: "Not a directory" });
|
|
4420
4432
|
break;
|
|
4421
4433
|
}
|
|
4422
|
-
ws
|
|
4434
|
+
sendMsg(ws, { type: "session-creating", tool, cwd: rawCwd || "~" });
|
|
4423
4435
|
onCreateSession(tool, resolved);
|
|
4424
4436
|
} catch (err) {
|
|
4425
|
-
ws
|
|
4437
|
+
sendMsg(ws, { type: "session-create-error", error: err.message });
|
|
4426
4438
|
}
|
|
4427
4439
|
break;
|
|
4428
4440
|
}
|
|
4429
4441
|
case "enable-sleep-prevention": {
|
|
4430
4442
|
if (!sleepPrevention) {
|
|
4431
|
-
ws
|
|
4443
|
+
sendMsg(ws, { type: "sleep-error", error: "Sleep prevention not available" });
|
|
4432
4444
|
break;
|
|
4433
4445
|
}
|
|
4434
4446
|
const password = typeof msg.password === "string" ? msg.password : "";
|
|
4435
4447
|
if (!password) {
|
|
4436
|
-
ws
|
|
4448
|
+
sendMsg(ws, { type: "sleep-error", error: "Password is required" });
|
|
4437
4449
|
break;
|
|
4438
4450
|
}
|
|
4439
4451
|
const enableResult = await sleepPrevention.enable(password);
|
|
4440
4452
|
if (enableResult.success) {
|
|
4441
4453
|
broadcast({ type: "sleep-state", state: sleepPrevention.getState() });
|
|
4442
4454
|
} else {
|
|
4443
|
-
ws
|
|
4455
|
+
sendMsg(ws, { type: "sleep-error", error: enableResult.error });
|
|
4444
4456
|
}
|
|
4445
4457
|
break;
|
|
4446
4458
|
}
|
|
@@ -4453,7 +4465,7 @@ function createDashboardServer(options) {
|
|
|
4453
4465
|
case "get-metadata": {
|
|
4454
4466
|
const session = registry.getById(msg.sessionId);
|
|
4455
4467
|
if (!session) {
|
|
4456
|
-
ws
|
|
4468
|
+
sendMsg(ws, { type: "operation-error", operation: "metadata", sessionId: msg.sessionId, error: "Session not found" });
|
|
4457
4469
|
return;
|
|
4458
4470
|
}
|
|
4459
4471
|
let memoryKB = 0;
|
|
@@ -4466,7 +4478,7 @@ function createDashboardServer(options) {
|
|
|
4466
4478
|
memoryKB = parseInt(output.trim(), 10) || 0;
|
|
4467
4479
|
} catch {
|
|
4468
4480
|
}
|
|
4469
|
-
ws
|
|
4481
|
+
sendMsg(ws, {
|
|
4470
4482
|
type: "metadata",
|
|
4471
4483
|
sessionId: msg.sessionId,
|
|
4472
4484
|
metadata: {
|
|
@@ -4478,7 +4490,7 @@ function createDashboardServer(options) {
|
|
|
4478
4490
|
uptimeMs: Date.now() - session.connectedAt,
|
|
4479
4491
|
connectedAt: session.connectedAt
|
|
4480
4492
|
}
|
|
4481
|
-
})
|
|
4493
|
+
});
|
|
4482
4494
|
break;
|
|
4483
4495
|
}
|
|
4484
4496
|
}
|
|
@@ -4496,7 +4508,7 @@ function createDashboardServer(options) {
|
|
|
4496
4508
|
});
|
|
4497
4509
|
});
|
|
4498
4510
|
function broadcast(message) {
|
|
4499
|
-
const msg = JSON.stringify(message);
|
|
4511
|
+
const msg = encrypt(JSON.stringify(message), encryptionKey);
|
|
4500
4512
|
for (const client of clients) {
|
|
4501
4513
|
if (client.readyState === client.OPEN) {
|
|
4502
4514
|
client.send(msg);
|
|
@@ -4540,6 +4552,7 @@ function createDashboardServer(options) {
|
|
|
4540
4552
|
|
|
4541
4553
|
// src/preview-collector.ts
|
|
4542
4554
|
import { EventEmitter as EventEmitter2 } from "events";
|
|
4555
|
+
import { deriveEncryptionKey as deriveEncryptionKey2, encrypt as encrypt2, decrypt as decrypt2 } from "@itwillsync/shared/crypto";
|
|
4543
4556
|
var MAX_PREVIEW_LINES = 5;
|
|
4544
4557
|
var THROTTLE_MS = 500;
|
|
4545
4558
|
var MAX_LINE_LENGTH = 80;
|
|
@@ -4571,6 +4584,7 @@ var PreviewCollector = class extends EventEmitter2 {
|
|
|
4571
4584
|
connectToSession(session) {
|
|
4572
4585
|
const conn = {
|
|
4573
4586
|
ws: null,
|
|
4587
|
+
encryptionKey: deriveEncryptionKey2(session.token),
|
|
4574
4588
|
lines: [],
|
|
4575
4589
|
rawBuffer: "",
|
|
4576
4590
|
throttleTimer: null,
|
|
@@ -4591,10 +4605,18 @@ var PreviewCollector = class extends EventEmitter2 {
|
|
|
4591
4605
|
conn.ws = ws;
|
|
4592
4606
|
ws.on("open", () => {
|
|
4593
4607
|
conn.reconnectAttempt = 0;
|
|
4608
|
+
ws.send(encrypt2(JSON.stringify({ type: "sync", lastSeq: -1 }), conn.encryptionKey));
|
|
4594
4609
|
});
|
|
4595
4610
|
ws.on("message", (raw) => {
|
|
4611
|
+
let plaintext;
|
|
4612
|
+
try {
|
|
4613
|
+
const rawStr = typeof raw === "string" ? raw : raw.toString("utf-8");
|
|
4614
|
+
plaintext = decrypt2(rawStr, conn.encryptionKey);
|
|
4615
|
+
} catch {
|
|
4616
|
+
return;
|
|
4617
|
+
}
|
|
4596
4618
|
try {
|
|
4597
|
-
const msg = JSON.parse(
|
|
4619
|
+
const msg = JSON.parse(plaintext);
|
|
4598
4620
|
if (msg.type === "data" && typeof msg.data === "string") {
|
|
4599
4621
|
this.handleData(sessionId, msg.data);
|
|
4600
4622
|
}
|
|
@@ -4612,7 +4634,7 @@ var PreviewCollector = class extends EventEmitter2 {
|
|
|
4612
4634
|
this.scheduleReconnect(sessionId, session);
|
|
4613
4635
|
}
|
|
4614
4636
|
}
|
|
4615
|
-
scheduleReconnect(sessionId,
|
|
4637
|
+
scheduleReconnect(sessionId, _session) {
|
|
4616
4638
|
const conn = this.connections.get(sessionId);
|
|
4617
4639
|
if (!conn || conn.closed) return;
|
|
4618
4640
|
conn.ws = null;
|