forkoff 1.0.17 → 1.0.19
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/LICENSE +11 -7
- package/README.md +77 -118
- package/dist/approval.d.ts +1 -0
- package/dist/approval.js +9 -0
- package/dist/config.d.ts +3 -0
- package/dist/config.js +62 -16
- package/dist/crypto/e2eeManager.d.ts +49 -52
- package/dist/crypto/e2eeManager.js +256 -181
- package/dist/crypto/encryption.d.ts +8 -10
- package/dist/crypto/encryption.js +29 -94
- package/dist/crypto/index.d.ts +10 -0
- package/dist/crypto/index.js +22 -0
- package/dist/crypto/keyExchange.d.ts +6 -20
- package/dist/crypto/keyExchange.js +18 -110
- package/dist/crypto/keyGeneration.d.ts +2 -13
- package/dist/crypto/keyGeneration.js +14 -88
- package/dist/crypto/keyStorage.d.ts +32 -5
- package/dist/crypto/keyStorage.js +152 -8
- package/dist/crypto/sessionPersistence.d.ts +7 -13
- package/dist/crypto/sessionPersistence.js +108 -33
- package/dist/crypto/types.d.ts +24 -3
- package/dist/crypto/types.js +2 -1
- package/dist/crypto/websocketE2EE.d.ts +6 -17
- package/dist/crypto/websocketE2EE.js +21 -38
- package/dist/index.js +203 -280
- package/dist/integration.d.ts +0 -1
- package/dist/integration.js +2 -4
- package/dist/logger.d.ts +15 -0
- package/dist/logger.js +209 -1
- package/dist/server.d.ts +30 -0
- package/dist/server.js +162 -0
- package/dist/startup.js +15 -6
- package/dist/terminal.d.ts +1 -0
- package/dist/terminal.js +94 -1
- package/dist/tools/claude-process.d.ts +8 -0
- package/dist/tools/claude-process.js +199 -26
- package/dist/tools/claude-sessions.d.ts +1 -0
- package/dist/tools/claude-sessions.js +36 -10
- package/dist/tools/detector.js +11 -3
- package/dist/tools/permission-hook.js +94 -27
- package/dist/tools/permission-ipc.d.ts +1 -0
- package/dist/tools/permission-ipc.js +61 -14
- package/dist/transcript-streamer.d.ts +1 -0
- package/dist/transcript-streamer.js +18 -4
- package/dist/usage-tracker.d.ts +45 -0
- package/dist/usage-tracker.js +243 -0
- package/dist/websocket.d.ts +43 -12
- package/dist/websocket.js +418 -214
- package/package.json +5 -4
- package/dist/__tests__/cli-commands.test.d.ts +0 -6
- package/dist/__tests__/cli-commands.test.d.ts.map +0 -1
- package/dist/__tests__/cli-commands.test.js +0 -213
- package/dist/__tests__/cli-commands.test.js.map +0 -1
- package/dist/__tests__/crypto/e2e-integration.test.d.ts +0 -17
- package/dist/__tests__/crypto/e2e-integration.test.d.ts.map +0 -1
- package/dist/__tests__/crypto/e2e-integration.test.js +0 -338
- package/dist/__tests__/crypto/e2e-integration.test.js.map +0 -1
- package/dist/__tests__/crypto/e2eeManager.test.d.ts +0 -2
- package/dist/__tests__/crypto/e2eeManager.test.d.ts.map +0 -1
- package/dist/__tests__/crypto/e2eeManager.test.js +0 -242
- package/dist/__tests__/crypto/e2eeManager.test.js.map +0 -1
- package/dist/__tests__/crypto/encryption.test.d.ts +0 -2
- package/dist/__tests__/crypto/encryption.test.d.ts.map +0 -1
- package/dist/__tests__/crypto/encryption.test.js +0 -116
- package/dist/__tests__/crypto/encryption.test.js.map +0 -1
- package/dist/__tests__/crypto/keyExchange.test.d.ts +0 -2
- package/dist/__tests__/crypto/keyExchange.test.d.ts.map +0 -1
- package/dist/__tests__/crypto/keyExchange.test.js +0 -84
- package/dist/__tests__/crypto/keyExchange.test.js.map +0 -1
- package/dist/__tests__/crypto/keyGeneration.test.d.ts +0 -2
- package/dist/__tests__/crypto/keyGeneration.test.d.ts.map +0 -1
- package/dist/__tests__/crypto/keyGeneration.test.js +0 -61
- package/dist/__tests__/crypto/keyGeneration.test.js.map +0 -1
- package/dist/__tests__/crypto/keyStorage.test.d.ts +0 -2
- package/dist/__tests__/crypto/keyStorage.test.d.ts.map +0 -1
- package/dist/__tests__/crypto/keyStorage.test.js +0 -133
- package/dist/__tests__/crypto/keyStorage.test.js.map +0 -1
- package/dist/__tests__/crypto/websocketIntegration.test.d.ts +0 -2
- package/dist/__tests__/crypto/websocketIntegration.test.d.ts.map +0 -1
- package/dist/__tests__/crypto/websocketIntegration.test.js +0 -259
- package/dist/__tests__/crypto/websocketIntegration.test.js.map +0 -1
- package/dist/__tests__/startup.test.d.ts +0 -11
- package/dist/__tests__/startup.test.d.ts.map +0 -1
- package/dist/__tests__/startup.test.js +0 -241
- package/dist/__tests__/startup.test.js.map +0 -1
- package/dist/__tests__/tools/claude-process.test.d.ts +0 -8
- package/dist/__tests__/tools/claude-process.test.d.ts.map +0 -1
- package/dist/__tests__/tools/claude-process.test.js +0 -430
- package/dist/__tests__/tools/claude-process.test.js.map +0 -1
- package/dist/__tests__/tools/permission-hook.test.d.ts +0 -17
- package/dist/__tests__/tools/permission-hook.test.d.ts.map +0 -1
- package/dist/__tests__/tools/permission-hook.test.js +0 -616
- package/dist/__tests__/tools/permission-hook.test.js.map +0 -1
- package/dist/__tests__/tools/permission-ipc.test.d.ts +0 -11
- package/dist/__tests__/tools/permission-ipc.test.d.ts.map +0 -1
- package/dist/__tests__/tools/permission-ipc.test.js +0 -612
- package/dist/__tests__/tools/permission-ipc.test.js.map +0 -1
- package/dist/__tests__/websocket.test.d.ts +0 -13
- package/dist/__tests__/websocket.test.d.ts.map +0 -1
- package/dist/__tests__/websocket.test.js +0 -204
- package/dist/__tests__/websocket.test.js.map +0 -1
- package/dist/api.d.ts +0 -44
- package/dist/api.d.ts.map +0 -1
- package/dist/api.js +0 -76
- package/dist/api.js.map +0 -1
- package/dist/approval.d.ts.map +0 -1
- package/dist/approval.js.map +0 -1
- package/dist/config.d.ts.map +0 -1
- package/dist/config.js.map +0 -1
- package/dist/crypto/e2eeManager.d.ts.map +0 -1
- package/dist/crypto/e2eeManager.js.map +0 -1
- package/dist/crypto/encryption.d.ts.map +0 -1
- package/dist/crypto/encryption.js.map +0 -1
- package/dist/crypto/keyExchange.d.ts.map +0 -1
- package/dist/crypto/keyExchange.js.map +0 -1
- package/dist/crypto/keyGeneration.d.ts.map +0 -1
- package/dist/crypto/keyGeneration.js.map +0 -1
- package/dist/crypto/keyStorage.d.ts.map +0 -1
- package/dist/crypto/keyStorage.js.map +0 -1
- package/dist/crypto/sessionPersistence.d.ts.map +0 -1
- package/dist/crypto/sessionPersistence.js.map +0 -1
- package/dist/crypto/types.d.ts.map +0 -1
- package/dist/crypto/types.js.map +0 -1
- package/dist/crypto/websocketE2EE.d.ts.map +0 -1
- package/dist/crypto/websocketE2EE.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/integration.d.ts.map +0 -1
- package/dist/integration.js.map +0 -1
- package/dist/logger.d.ts.map +0 -1
- package/dist/logger.js.map +0 -1
- package/dist/startup.d.ts.map +0 -1
- package/dist/startup.js.map +0 -1
- package/dist/terminal.d.ts.map +0 -1
- package/dist/terminal.js.map +0 -1
- package/dist/tools/__tests__/claude-sessions.test.d.ts +0 -2
- package/dist/tools/__tests__/claude-sessions.test.d.ts.map +0 -1
- package/dist/tools/__tests__/claude-sessions.test.js +0 -306
- package/dist/tools/__tests__/claude-sessions.test.js.map +0 -1
- package/dist/tools/claude-hooks.d.ts.map +0 -1
- package/dist/tools/claude-hooks.js.map +0 -1
- package/dist/tools/claude-process.d.ts.map +0 -1
- package/dist/tools/claude-process.js.map +0 -1
- package/dist/tools/claude-sessions.d.ts.map +0 -1
- package/dist/tools/claude-sessions.js.map +0 -1
- package/dist/tools/detector.d.ts.map +0 -1
- package/dist/tools/detector.js.map +0 -1
- package/dist/tools/index.d.ts.map +0 -1
- package/dist/tools/index.js.map +0 -1
- package/dist/tools/permission-hook.d.ts.map +0 -1
- package/dist/tools/permission-hook.js.map +0 -1
- package/dist/tools/permission-ipc.d.ts.map +0 -1
- package/dist/tools/permission-ipc.js.map +0 -1
- package/dist/transcript-streamer.d.ts.map +0 -1
- package/dist/transcript-streamer.js.map +0 -1
- package/dist/websocket.d.ts.map +0 -1
- package/dist/websocket.js.map +0 -1
- package/jest.config.js +0 -18
package/dist/websocket.js
CHANGED
|
@@ -1,17 +1,108 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.wsClient = exports.WebSocketClient = void 0;
|
|
4
|
-
const socket_io_client_1 = require("socket.io-client");
|
|
5
4
|
const config_1 = require("./config");
|
|
6
5
|
const events_1 = require("events");
|
|
7
6
|
const uuid_1 = require("uuid");
|
|
7
|
+
const e2eeManager_1 = require("./crypto/e2eeManager");
|
|
8
|
+
const server_1 = require("./server");
|
|
9
|
+
// Whitelist of events allowed via encrypted_message channel (inbound from mobile)
|
|
10
|
+
const ALLOWED_ENCRYPTED_EVENTS = new Set([
|
|
11
|
+
'terminal_command',
|
|
12
|
+
'terminal_create',
|
|
13
|
+
'terminal_resize',
|
|
14
|
+
'terminal_close',
|
|
15
|
+
'claude_start_session',
|
|
16
|
+
'claude_resume_session',
|
|
17
|
+
'claude_stop_session',
|
|
18
|
+
'user_message',
|
|
19
|
+
'directory_list',
|
|
20
|
+
'read_file',
|
|
21
|
+
'transcript_fetch',
|
|
22
|
+
'transcript_subscribe',
|
|
23
|
+
'transcript_unsubscribe',
|
|
24
|
+
'permission_response',
|
|
25
|
+
'permission_rules_sync',
|
|
26
|
+
'session_settings_update',
|
|
27
|
+
'transcript_subscribe_sdk',
|
|
28
|
+
'tab_complete',
|
|
29
|
+
'claude_approval_response',
|
|
30
|
+
'approval_response',
|
|
31
|
+
'sdk_session_history',
|
|
32
|
+
'claude_abort',
|
|
33
|
+
'usage_stats_request',
|
|
34
|
+
]);
|
|
35
|
+
// Events that carry user data and MUST be encrypted — plaintext fallback is refused.
|
|
36
|
+
// If E2EE is not established, these are queued (not sent in plaintext).
|
|
37
|
+
const ENFORCED_SENSITIVE_EVENTS = new Set([
|
|
38
|
+
// Core sensitive data
|
|
39
|
+
'terminal_output',
|
|
40
|
+
'read_file_response',
|
|
41
|
+
'directory_list_response',
|
|
42
|
+
'permission_prompt',
|
|
43
|
+
// Transcript data (contains full code, file contents, conversation history)
|
|
44
|
+
'transcript_history',
|
|
45
|
+
'transcript_update',
|
|
46
|
+
// Claude reasoning and session data
|
|
47
|
+
'thinking_content',
|
|
48
|
+
'task_progress',
|
|
49
|
+
'tool_activity',
|
|
50
|
+
// Approval context
|
|
51
|
+
'claude_approval_request',
|
|
52
|
+
'approval_request',
|
|
53
|
+
// Session metadata (contains directory paths, file paths, working directories)
|
|
54
|
+
'claude_session_update',
|
|
55
|
+
'claude_session_batch_update',
|
|
56
|
+
'terminal_cwd',
|
|
57
|
+
'file_changed',
|
|
58
|
+
// Token usage (contains session identifiers)
|
|
59
|
+
'token_usage',
|
|
60
|
+
// Pending permissions (contains prompt details)
|
|
61
|
+
'pending_permissions_sync',
|
|
62
|
+
// Session events (may contain error messages with paths)
|
|
63
|
+
'claude_session_event',
|
|
64
|
+
// Usage analytics sync
|
|
65
|
+
'usage_stats_sync',
|
|
66
|
+
'daily_usage_sync',
|
|
67
|
+
'streak_info_sync',
|
|
68
|
+
]);
|
|
69
|
+
// SECURITY: Inbound events from mobile that MUST arrive via E2EE decryption when active.
|
|
70
|
+
// Plaintext versions are dropped when E2EE session is established with mobile peer.
|
|
71
|
+
const ENFORCED_INBOUND_EVENTS = new Set([
|
|
72
|
+
'terminal_command', 'user_message', 'read_file', 'directory_list',
|
|
73
|
+
'tab_complete', 'permission_response', 'claude_approval_response',
|
|
74
|
+
'approval_response', 'rpc_response', 'terminal_create',
|
|
75
|
+
'sdk_session_history', 'claude_abort', 'claude_start_session',
|
|
76
|
+
'claude_resume_session', 'transcript_fetch', 'transcript_subscribe',
|
|
77
|
+
'permission_rules_sync',
|
|
78
|
+
'usage_stats_request',
|
|
79
|
+
]);
|
|
80
|
+
/** Events forwarded from server that need plaintext-drop checking */
|
|
81
|
+
const PLAINTEXT_DROP_EVENTS = [
|
|
82
|
+
'terminal_create', 'terminal_command', 'approval_response',
|
|
83
|
+
'user_message', 'claude_resume_session', 'claude_start_session',
|
|
84
|
+
'directory_list', 'transcript_fetch', 'transcript_subscribe',
|
|
85
|
+
'read_file', 'claude_approval_response', 'permission_response',
|
|
86
|
+
'permission_rules_sync', 'claude_abort', 'tab_complete',
|
|
87
|
+
'usage_stats_request',
|
|
88
|
+
];
|
|
89
|
+
/** Events forwarded from server that do NOT need plaintext-drop checking */
|
|
90
|
+
const PASSTHROUGH_EVENTS = [
|
|
91
|
+
'transcript_unsubscribe', 'claude_sessions_request', 'pair_device',
|
|
92
|
+
];
|
|
8
93
|
class WebSocketClient extends events_1.EventEmitter {
|
|
9
94
|
constructor() {
|
|
10
95
|
super(...arguments);
|
|
11
|
-
this.
|
|
12
|
-
this.reconnectAttempts = 0;
|
|
96
|
+
this.server = null;
|
|
13
97
|
this.heartbeatInterval = null;
|
|
14
98
|
this._sessionId = '';
|
|
99
|
+
this.e2eeManager = null;
|
|
100
|
+
this.e2eeInitialized = false;
|
|
101
|
+
// The mobile device ID learned from key exchange (used for encrypting CLI→mobile messages)
|
|
102
|
+
this.e2eePeerDeviceId = null;
|
|
103
|
+
// Queue for sensitive messages waiting for E2EE session establishment
|
|
104
|
+
this.pendingSensitiveMessages = [];
|
|
105
|
+
this.usageTracker = null;
|
|
15
106
|
}
|
|
16
107
|
// Unique session ID for this CLI connection
|
|
17
108
|
get sessionId() {
|
|
@@ -20,173 +111,275 @@ class WebSocketClient extends events_1.EventEmitter {
|
|
|
20
111
|
}
|
|
21
112
|
return this._sessionId;
|
|
22
113
|
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
114
|
+
/** Start the embedded relay server and wire up event forwarding */
|
|
115
|
+
async startServer(port) {
|
|
116
|
+
const deviceId = config_1.config.deviceId;
|
|
117
|
+
if (!deviceId) {
|
|
118
|
+
throw new Error('Device not registered');
|
|
119
|
+
}
|
|
120
|
+
this.server = new server_1.EmbeddedRelayServer({
|
|
121
|
+
port,
|
|
122
|
+
deviceId,
|
|
123
|
+
deviceName: config_1.config.deviceName,
|
|
124
|
+
});
|
|
125
|
+
await this.server.start();
|
|
126
|
+
// When mobile connects, emit connected + start heartbeat + initiate E2EE
|
|
127
|
+
this.server.on('mobile_connected', (data) => {
|
|
128
|
+
console.log(`[WS] Mobile connected: ${data.deviceId}`);
|
|
129
|
+
this.emit('connected');
|
|
130
|
+
this.startHeartbeat();
|
|
131
|
+
// Initiate E2EE key exchange with the connected mobile device
|
|
132
|
+
if (this.e2eeManager && this.e2eeInitialized) {
|
|
133
|
+
try {
|
|
134
|
+
// Clear any old session keys for this device — forces fresh key exchange.
|
|
135
|
+
// Without this, queued messages would be encrypted with stale keys from
|
|
136
|
+
// a previous connection that the mobile no longer has.
|
|
137
|
+
this.e2eeManager.clearSession(data.deviceId);
|
|
138
|
+
const initPayload = this.e2eeManager.createKeyExchangeInit(data.deviceId);
|
|
139
|
+
this.server?.emitToMobile('encrypted_key_exchange_init', {
|
|
140
|
+
...initPayload,
|
|
141
|
+
recipientDeviceId: data.deviceId,
|
|
142
|
+
});
|
|
143
|
+
// E2EE key exchange initiated
|
|
144
|
+
}
|
|
145
|
+
catch (err) {
|
|
146
|
+
console.warn('[E2EE] Failed to initiate key exchange');
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
this.server.on('mobile_disconnected', (data) => {
|
|
151
|
+
console.log(`[WS] Mobile disconnected: ${data.reason}`);
|
|
152
|
+
this.emit('disconnected', data.reason);
|
|
153
|
+
this.stopHeartbeat();
|
|
154
|
+
});
|
|
155
|
+
// Forward events that need plaintext-drop check
|
|
156
|
+
for (const event of PLAINTEXT_DROP_EVENTS) {
|
|
157
|
+
this.server.on(event, (data) => {
|
|
158
|
+
if (this.shouldDropPlaintextInbound())
|
|
159
|
+
return;
|
|
160
|
+
if (process.env.DEBUG) {
|
|
161
|
+
console.log(`[WS] Received ${event}`);
|
|
162
|
+
}
|
|
163
|
+
this.emit(event, data);
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
// Forward events that pass through without plaintext-drop check
|
|
167
|
+
for (const event of PASSTHROUGH_EVENTS) {
|
|
168
|
+
this.server.on(event, (data) => {
|
|
169
|
+
if (process.env.DEBUG) {
|
|
170
|
+
console.log(`[WS] Received ${event}`);
|
|
171
|
+
}
|
|
172
|
+
this.emit(event, data);
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
// On successful pairing, reset TOFU trust for that specific device (handles re-pair with new keys).
|
|
176
|
+
// Don't delete pending exchange or re-initiate — the init from mobile_connected is in-flight.
|
|
177
|
+
this.server.on('pair_device', (data) => {
|
|
178
|
+
const mobileDeviceId = data.mobileDeviceId;
|
|
179
|
+
if (mobileDeviceId && this.e2eeManager) {
|
|
180
|
+
this.e2eeManager.clearTrustOnly(mobileDeviceId);
|
|
181
|
+
// Reset TOFU trust for re-pair
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
// E2EE key exchange events — forwarded from server, handled here
|
|
185
|
+
this.server.on('encrypted_key_exchange_init', (data) => {
|
|
186
|
+
if (!this.e2eeManager)
|
|
27
187
|
return;
|
|
188
|
+
try {
|
|
189
|
+
const ack = this.e2eeManager.handleKeyExchangeInit(data);
|
|
190
|
+
this.e2eePeerDeviceId = data.senderDeviceId;
|
|
191
|
+
this.server?.emitToMobile('encrypted_key_exchange_ack', {
|
|
192
|
+
...ack,
|
|
193
|
+
senderDeviceId: config_1.config.deviceId,
|
|
194
|
+
recipientDeviceId: data.senderDeviceId,
|
|
195
|
+
});
|
|
196
|
+
this.emit('e2ee_established', { peerDeviceId: data.senderDeviceId });
|
|
197
|
+
this.flushSensitiveQueue();
|
|
198
|
+
this.sendAllUsageStats();
|
|
199
|
+
}
|
|
200
|
+
catch (err) {
|
|
201
|
+
console.error('[E2EE] Key exchange init failed');
|
|
28
202
|
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
203
|
+
});
|
|
204
|
+
this.server.on('encrypted_key_exchange_ack', (data) => {
|
|
205
|
+
if (!this.e2eeManager)
|
|
32
206
|
return;
|
|
207
|
+
try {
|
|
208
|
+
this.e2eeManager.handleKeyExchangeAck(data);
|
|
209
|
+
this.e2eePeerDeviceId = data.senderDeviceId;
|
|
210
|
+
this.emit('e2ee_established', { peerDeviceId: data.senderDeviceId });
|
|
211
|
+
this.flushSensitiveQueue();
|
|
212
|
+
this.sendAllUsageStats();
|
|
33
213
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
214
|
+
catch (err) {
|
|
215
|
+
console.error('[E2EE] Key exchange ack failed');
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
// Encrypted messages — decrypt and re-emit as original event
|
|
219
|
+
this.server.on('encrypted_message', (data) => {
|
|
220
|
+
if (!this.e2eeManager)
|
|
221
|
+
return;
|
|
222
|
+
let plaintext;
|
|
223
|
+
try {
|
|
224
|
+
plaintext = this.e2eeManager.decryptMessage(data, data.senderDeviceId);
|
|
225
|
+
}
|
|
226
|
+
catch {
|
|
227
|
+
console.error('[E2EE] Decryption failed — message dropped');
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
// Validate JSON structure separately from decryption
|
|
231
|
+
let parsed;
|
|
232
|
+
try {
|
|
233
|
+
parsed = JSON.parse(plaintext);
|
|
234
|
+
}
|
|
235
|
+
catch {
|
|
236
|
+
console.error('[E2EE] Invalid JSON in decrypted message — dropped');
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
// Validate payload structure
|
|
240
|
+
if (!parsed || typeof parsed !== 'object') {
|
|
241
|
+
console.error('[E2EE] Decrypted payload is not an object — dropped');
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
const payload = parsed;
|
|
245
|
+
const eventName = payload._event;
|
|
246
|
+
if (typeof eventName !== 'string') {
|
|
247
|
+
console.error('[E2EE] Missing or invalid _event in decrypted payload — dropped');
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
if (!ALLOWED_ENCRYPTED_EVENTS.has(eventName)) {
|
|
251
|
+
console.warn('[E2EE] Decrypted event not in whitelist — dropped');
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
this.emit(eventName, payload._data);
|
|
255
|
+
});
|
|
256
|
+
// Initialize E2EE (non-blocking — don't delay server start)
|
|
257
|
+
this.initE2EE().catch((err) => {
|
|
258
|
+
console.warn('[E2EE] \u26a0 End-to-end encryption initialization failed. Messages will be sent without E2EE protection.');
|
|
62
259
|
if (process.env.DEBUG) {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
260
|
+
console.warn(`[E2EE] Init error detail:`, err.message);
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
/** Set pairing code on the embedded server for in-process validation */
|
|
265
|
+
setPairingCode(code) {
|
|
266
|
+
this.server?.setPairingCode(code);
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Initialize E2EE manager: generate/load keys.
|
|
270
|
+
* Called automatically on startServer. Non-blocking.
|
|
271
|
+
*/
|
|
272
|
+
async initE2EE() {
|
|
273
|
+
const deviceId = config_1.config.deviceId;
|
|
274
|
+
if (!deviceId)
|
|
275
|
+
return;
|
|
276
|
+
this.e2eeManager = new e2eeManager_1.E2EEManager(deviceId);
|
|
277
|
+
await this.e2eeManager.initialize();
|
|
278
|
+
this.e2eeInitialized = true;
|
|
279
|
+
// Restore any persisted sessions
|
|
280
|
+
const persisted = this.e2eeManager.listPersistedDevices();
|
|
281
|
+
for (const targetDeviceId of persisted) {
|
|
282
|
+
const restored = await this.e2eeManager.restorePersistedSession(targetDeviceId);
|
|
283
|
+
if (restored) {
|
|
284
|
+
// Restored persisted E2EE session
|
|
68
285
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
286
|
+
}
|
|
287
|
+
// E2EE initialized
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Send a sensitive event: encrypt if E2EE session exists with the target device.
|
|
291
|
+
* For events in ENFORCED_SENSITIVE_EVENTS, plaintext fallback is REFUSED — messages
|
|
292
|
+
* are queued until E2EE session establishes, or dropped after timeout.
|
|
293
|
+
*/
|
|
294
|
+
emitSensitive(event, data, targetDeviceId) {
|
|
295
|
+
// If E2EE session exists, encrypt and send
|
|
296
|
+
if (this.e2eeManager && targetDeviceId && this.e2eeManager.hasSessionKey(targetDeviceId)) {
|
|
297
|
+
try {
|
|
298
|
+
const plaintext = JSON.stringify({ _event: event, _data: data });
|
|
299
|
+
const encrypted = this.e2eeManager.encryptMessage(plaintext, targetDeviceId, this._sessionId || 'default');
|
|
300
|
+
this.server?.emitToMobile('encrypted_message', encrypted);
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
catch (err) {
|
|
304
|
+
console.error('[E2EE] Encryption failed, message NOT sent (refusing plaintext fallback)');
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
// For enforced sensitive events: NEVER send plaintext — queue until E2EE establishes
|
|
309
|
+
if (ENFORCED_SENSITIVE_EVENTS.has(event)) {
|
|
310
|
+
if (this.e2eeInitialized) {
|
|
311
|
+
// E2EE initialized but no session yet — queue for when session establishes
|
|
312
|
+
if (this.pendingSensitiveMessages.length >= WebSocketClient.MAX_PENDING_SENSITIVE) {
|
|
313
|
+
const dropped = this.pendingSensitiveMessages.shift();
|
|
314
|
+
if (dropped) {
|
|
315
|
+
console.warn('[E2EE] Sensitive queue full, dropped oldest');
|
|
316
|
+
}
|
|
74
317
|
}
|
|
75
|
-
this.
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
318
|
+
this.pendingSensitiveMessages.push({
|
|
319
|
+
event,
|
|
320
|
+
data,
|
|
321
|
+
targetDeviceId: targetDeviceId || '__pending__',
|
|
322
|
+
queuedAt: Date.now(),
|
|
323
|
+
});
|
|
324
|
+
if (this.pendingSensitiveMessages.length === 1) {
|
|
325
|
+
console.warn('[E2EE] Queued sensitive event — waiting for E2EE session');
|
|
83
326
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
this.emit('transcript_fetch', data);
|
|
131
|
-
});
|
|
132
|
-
// Listen for transcript subscribe requests
|
|
133
|
-
this.socket.on('transcript_subscribe', (data) => {
|
|
134
|
-
console.log(`[WS] Received transcript_subscribe:`, JSON.stringify(data));
|
|
135
|
-
this.emit('transcript_subscribe', data);
|
|
136
|
-
});
|
|
137
|
-
// Listen for transcript unsubscribe requests
|
|
138
|
-
this.socket.on('transcript_unsubscribe', (data) => {
|
|
139
|
-
console.log(`[WS] Received transcript_unsubscribe:`, JSON.stringify(data));
|
|
140
|
-
this.emit('transcript_unsubscribe', data);
|
|
141
|
-
});
|
|
142
|
-
// Listen for SDK subscribe start requests from API
|
|
143
|
-
// This is sent when mobile uses transcript_subscribe_sdk
|
|
144
|
-
this.socket.on('transcript_subscribe_sdk_start', (data) => {
|
|
145
|
-
console.log(`[WS] Received transcript_subscribe_sdk_start:`, JSON.stringify(data));
|
|
146
|
-
this.emit('transcript_subscribe_sdk_start', data);
|
|
147
|
-
});
|
|
148
|
-
// Listen for claude sessions request (mobile wants current state)
|
|
149
|
-
this.socket.on('claude_sessions_request', (data) => {
|
|
150
|
-
console.log(`[WS] Received claude_sessions_request`);
|
|
151
|
-
this.emit('claude_sessions_request', data);
|
|
152
|
-
});
|
|
153
|
-
// Listen for RPC requests from the API gateway
|
|
154
|
-
this.socket.on('rpc_request', (data) => {
|
|
155
|
-
console.log(`[WS] Received rpc_request: ${data.method}, requestId: ${data.requestId}`);
|
|
156
|
-
this.emit('rpc_request', data);
|
|
157
|
-
});
|
|
158
|
-
// Listen for read file requests from mobile app
|
|
159
|
-
this.socket.on('read_file', (data) => {
|
|
160
|
-
console.log(`[WS] Received read_file: ${data.filePath}`);
|
|
161
|
-
this.emit('read_file', data);
|
|
162
|
-
});
|
|
163
|
-
// Listen for Claude approval responses from mobile
|
|
164
|
-
this.socket.on('claude_approval_response', (data) => {
|
|
165
|
-
console.log(`[WS] Received claude_approval_response: ${data.approvalId}, response: ${data.response}`);
|
|
166
|
-
this.emit('claude_approval_response', data);
|
|
167
|
-
});
|
|
168
|
-
// Listen for permission responses from mobile (hook-based approval system)
|
|
169
|
-
this.socket.on('permission_response', (data) => {
|
|
170
|
-
console.log(`[WS] Received permission_response: ${data.promptId} -> ${data.decision}`);
|
|
171
|
-
this.emit('permission_response', data);
|
|
172
|
-
});
|
|
173
|
-
// Listen for permission rules sync from mobile
|
|
174
|
-
this.socket.on('permission_rules_sync', (data) => {
|
|
175
|
-
console.log(`[WS] Received permission_rules_sync: ${data.rules?.length || 0} rules`);
|
|
176
|
-
this.emit('permission_rules_sync', data);
|
|
177
|
-
});
|
|
178
|
-
// Listen for mobile disconnect notification from backend
|
|
179
|
-
this.socket.on('mobile_disconnected', (data) => {
|
|
180
|
-
console.log(`[WS] Received mobile_disconnected for user ${data.userId}`);
|
|
181
|
-
this.emit('mobile_disconnected', data);
|
|
182
|
-
});
|
|
183
|
-
});
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
// E2EE not initialized — drop silently (no user data leaks)
|
|
330
|
+
console.error('[E2EE] Dropped sensitive event — E2EE not available, refusing plaintext');
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
// Non-sensitive events: plaintext is acceptable
|
|
334
|
+
this.server?.emitToMobile(event, data);
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* Flush queued sensitive messages now that E2EE session is established.
|
|
338
|
+
* Drops messages older than SENSITIVE_QUEUE_TTL_MS.
|
|
339
|
+
*/
|
|
340
|
+
flushSensitiveQueue() {
|
|
341
|
+
if (this.pendingSensitiveMessages.length === 0)
|
|
342
|
+
return;
|
|
343
|
+
const now = Date.now();
|
|
344
|
+
let sent = 0;
|
|
345
|
+
let dropped = 0;
|
|
346
|
+
for (const msg of this.pendingSensitiveMessages) {
|
|
347
|
+
if (now - msg.queuedAt > WebSocketClient.SENSITIVE_QUEUE_TTL_MS) {
|
|
348
|
+
dropped++;
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
// Resolve pending target device ID
|
|
352
|
+
const target = msg.targetDeviceId === '__pending__'
|
|
353
|
+
? this.e2eePeerDeviceId ?? undefined
|
|
354
|
+
: msg.targetDeviceId;
|
|
355
|
+
// Attempt to send via encryption
|
|
356
|
+
this.emitSensitive(msg.event, msg.data, target);
|
|
357
|
+
sent++;
|
|
358
|
+
}
|
|
359
|
+
this.pendingSensitiveMessages = [];
|
|
360
|
+
// Flushed sensitive queue
|
|
361
|
+
}
|
|
362
|
+
/** Get the E2EE manager (for external key exchange initiation) */
|
|
363
|
+
getE2EEManager() {
|
|
364
|
+
return this.e2eeManager;
|
|
365
|
+
}
|
|
366
|
+
/** Check if E2EE session is active with a device */
|
|
367
|
+
isE2EEActive(deviceId) {
|
|
368
|
+
return this.e2eeManager?.hasSessionKey(deviceId) ?? false;
|
|
369
|
+
}
|
|
370
|
+
/** SECURITY: Check if plaintext inbound events should be dropped (E2EE active with peer) */
|
|
371
|
+
shouldDropPlaintextInbound() {
|
|
372
|
+
return !!(this.e2eePeerDeviceId && this.e2eeManager?.hasSessionKey(this.e2eePeerDeviceId));
|
|
184
373
|
}
|
|
185
374
|
disconnect() {
|
|
186
375
|
this.stopHeartbeat();
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
376
|
+
this.e2eeManager?.cleanup(false);
|
|
377
|
+
this.e2eeManager = null;
|
|
378
|
+
this.e2eeInitialized = false;
|
|
379
|
+
this.e2eePeerDeviceId = null;
|
|
380
|
+
if (this.server) {
|
|
381
|
+
this.server.stop();
|
|
382
|
+
this.server = null;
|
|
190
383
|
}
|
|
191
384
|
}
|
|
192
385
|
startHeartbeat() {
|
|
@@ -200,124 +393,135 @@ class WebSocketClient extends events_1.EventEmitter {
|
|
|
200
393
|
this.heartbeatInterval = null;
|
|
201
394
|
}
|
|
202
395
|
}
|
|
203
|
-
// Send heartbeat to
|
|
396
|
+
// Send heartbeat to connected mobile
|
|
204
397
|
sendHeartbeat(status = 'online') {
|
|
205
|
-
this.
|
|
398
|
+
this.server?.emitToMobile('device_status', { status, deviceId: config_1.config.deviceId });
|
|
206
399
|
}
|
|
207
400
|
// Update device status
|
|
208
401
|
updateStatus(status) {
|
|
209
|
-
this.
|
|
210
|
-
}
|
|
211
|
-
// Send syncing status
|
|
212
|
-
setSyncing(syncing) {
|
|
213
|
-
this.socket?.emit('device_syncing', { syncing });
|
|
402
|
+
this.server?.emitToMobile('device_status', { status, deviceId: config_1.config.deviceId });
|
|
214
403
|
}
|
|
215
|
-
// Send Claude session update
|
|
404
|
+
// Send Claude session update (sensitive — contains directory paths)
|
|
216
405
|
sendClaudeSessionUpdate(session) {
|
|
217
|
-
this.
|
|
406
|
+
this.emitSensitive('claude_session_update', { ...session, deviceId: config_1.config.deviceId }, this.e2eePeerDeviceId ?? undefined);
|
|
218
407
|
}
|
|
219
|
-
// Send multiple Claude sessions as a single batch event
|
|
408
|
+
// Send multiple Claude sessions as a single batch event (sensitive — contains directory paths)
|
|
220
409
|
sendClaudeSessions(sessions) {
|
|
221
410
|
if (sessions.length > 0) {
|
|
222
|
-
|
|
411
|
+
const withDeviceId = sessions.map(s => ({ ...s, deviceId: config_1.config.deviceId }));
|
|
412
|
+
this.emitSensitive('claude_session_batch_update', { sessions: withDeviceId }, this.e2eePeerDeviceId ?? undefined);
|
|
223
413
|
}
|
|
224
414
|
}
|
|
225
415
|
// Send tool status update (e.g., Claude active/inactive)
|
|
226
416
|
sendToolStatusUpdate(toolType, status) {
|
|
227
|
-
this.
|
|
417
|
+
this.server?.emitToMobile('tool_status_update', {
|
|
228
418
|
toolType,
|
|
229
419
|
status,
|
|
230
420
|
timestamp: new Date().toISOString(),
|
|
231
421
|
});
|
|
232
422
|
}
|
|
233
|
-
// Send approval request
|
|
423
|
+
// Send approval request (sensitive — contains code change descriptions)
|
|
234
424
|
sendApprovalRequest(data) {
|
|
235
|
-
this.
|
|
425
|
+
this.emitSensitive('approval_request', data, this.e2eePeerDeviceId ?? undefined);
|
|
236
426
|
}
|
|
237
|
-
// Send terminal output
|
|
427
|
+
// Send terminal output (sensitive — encrypted if E2EE active)
|
|
238
428
|
sendTerminalOutput(data) {
|
|
239
|
-
this.
|
|
429
|
+
this.emitSensitive('terminal_output', data, this.e2eePeerDeviceId ?? undefined);
|
|
240
430
|
}
|
|
241
|
-
// Send working directory change
|
|
431
|
+
// Send working directory change (sensitive — contains working directory path)
|
|
242
432
|
sendTerminalCwd(data) {
|
|
243
|
-
this.
|
|
433
|
+
this.emitSensitive('terminal_cwd', data, this.e2eePeerDeviceId ?? undefined);
|
|
244
434
|
}
|
|
245
|
-
// Send directory listing response
|
|
435
|
+
// Send directory listing response (sensitive — encrypted if E2EE active)
|
|
246
436
|
sendDirectoryListResponse(data) {
|
|
247
|
-
this.
|
|
437
|
+
this.emitSensitive('directory_list_response', data, this.e2eePeerDeviceId ?? undefined);
|
|
248
438
|
}
|
|
249
|
-
// Send read file response
|
|
439
|
+
// Send read file response (sensitive — encrypted if E2EE active)
|
|
250
440
|
sendReadFileResponse(data) {
|
|
251
|
-
this.
|
|
441
|
+
this.emitSensitive('read_file_response', data, this.e2eePeerDeviceId ?? undefined);
|
|
252
442
|
}
|
|
253
|
-
// Send file change notification
|
|
443
|
+
// Send file change notification (sensitive — contains file paths)
|
|
254
444
|
sendFileChanged(data) {
|
|
255
|
-
this.
|
|
445
|
+
this.emitSensitive('file_changed', data, this.e2eePeerDeviceId ?? undefined);
|
|
256
446
|
}
|
|
257
|
-
// Send transcript history to mobile app
|
|
447
|
+
// Send transcript history to mobile app (sensitive — contains code and conversation)
|
|
258
448
|
sendTranscriptHistory(data) {
|
|
259
|
-
this.
|
|
449
|
+
this.emitSensitive('transcript_history', data, this.e2eePeerDeviceId ?? undefined);
|
|
260
450
|
}
|
|
261
|
-
// Send transcript update (new entry) to mobile app
|
|
451
|
+
// Send transcript update (new entry) to mobile app (sensitive — contains code)
|
|
262
452
|
sendTranscriptUpdate(data) {
|
|
263
|
-
this.
|
|
453
|
+
this.emitSensitive('transcript_update', data, this.e2eePeerDeviceId ?? undefined);
|
|
264
454
|
}
|
|
265
|
-
// Send
|
|
266
|
-
sendRpcResponse(data) {
|
|
267
|
-
console.log(`[WS] Sending rpc_response: ${data.requestId}, hasResult: ${!!data.result}, hasError: ${!!data.error}`);
|
|
268
|
-
this.socket?.emit('rpc_response', data);
|
|
269
|
-
}
|
|
270
|
-
// Send Claude approval request to mobile
|
|
455
|
+
// Send Claude approval request to mobile (sensitive — contains context)
|
|
271
456
|
sendClaudeApprovalRequest(data) {
|
|
272
457
|
console.log(`[WS] Sending claude_approval_request: ${data.approvalId}`);
|
|
273
|
-
this.
|
|
458
|
+
this.emitSensitive('claude_approval_request', data, this.e2eePeerDeviceId ?? undefined);
|
|
274
459
|
}
|
|
275
|
-
// Send tool activity notification to mobile (
|
|
460
|
+
// Send tool activity notification to mobile (sensitive — contains tool inputs)
|
|
276
461
|
sendToolActivity(data) {
|
|
277
462
|
console.log(`[WS] Sending tool_activity: ${data.toolName} (${data.toolId})`);
|
|
278
|
-
this.
|
|
463
|
+
this.emitSensitive('tool_activity', data, this.e2eePeerDeviceId ?? undefined);
|
|
279
464
|
}
|
|
280
|
-
// Send permission prompt to mobile (
|
|
465
|
+
// Send permission prompt to mobile (sensitive — encrypted if E2EE active)
|
|
281
466
|
sendPermissionPrompt(data) {
|
|
282
467
|
console.log(`[WS] Sending permission_prompt: ${data.toolName} (${data.promptId})`);
|
|
283
|
-
this.
|
|
468
|
+
this.emitSensitive('permission_prompt', data, this.e2eePeerDeviceId ?? undefined);
|
|
284
469
|
}
|
|
285
|
-
// Send thinking content to mobile
|
|
470
|
+
// Send thinking content to mobile (sensitive — contains Claude's reasoning about code)
|
|
286
471
|
sendThinkingContent(data) {
|
|
287
|
-
this.
|
|
472
|
+
this.emitSensitive('thinking_content', data, this.e2eePeerDeviceId ?? undefined);
|
|
288
473
|
}
|
|
289
|
-
// Send token usage to mobile
|
|
474
|
+
// Send token usage to mobile (sensitive — contains session identifiers)
|
|
290
475
|
sendTokenUsage(data) {
|
|
291
|
-
this.
|
|
476
|
+
this.emitSensitive('token_usage', data, this.e2eePeerDeviceId ?? undefined);
|
|
292
477
|
}
|
|
293
|
-
// Send pending permissions sync to mobile (
|
|
478
|
+
// Send pending permissions sync to mobile (sensitive — contains prompt details)
|
|
294
479
|
sendPendingPermissionsSync(data) {
|
|
295
480
|
console.log(`[WS] Sending pending_permissions_sync: ${data.prompts.length} prompt(s)`);
|
|
296
|
-
this.
|
|
481
|
+
this.emitSensitive('pending_permissions_sync', data, this.e2eePeerDeviceId ?? undefined);
|
|
297
482
|
}
|
|
298
|
-
// Send task progress to mobile
|
|
483
|
+
// Send task progress to mobile (sensitive — contains task subjects/descriptions)
|
|
299
484
|
sendTaskProgress(data) {
|
|
300
|
-
this.
|
|
485
|
+
this.emitSensitive('task_progress', data, this.e2eePeerDeviceId ?? undefined);
|
|
301
486
|
}
|
|
302
|
-
// E2EE event emitters
|
|
487
|
+
// E2EE event emitters — send to mobile via server
|
|
303
488
|
emitKeyExchangeInit(data) {
|
|
304
|
-
this.
|
|
489
|
+
this.server?.emitToMobile('encrypted_key_exchange_init', data);
|
|
305
490
|
}
|
|
306
491
|
emitKeyExchangeAck(data) {
|
|
307
|
-
this.
|
|
492
|
+
this.server?.emitToMobile('encrypted_key_exchange_ack', data);
|
|
308
493
|
}
|
|
309
494
|
emitEncryptedMessage(data) {
|
|
310
|
-
this.
|
|
311
|
-
}
|
|
312
|
-
|
|
495
|
+
this.server?.emitToMobile('encrypted_message', data);
|
|
496
|
+
}
|
|
497
|
+
/** Set the usage tracker instance (called from index.ts after instantiation) */
|
|
498
|
+
setUsageTracker(tracker) {
|
|
499
|
+
this.usageTracker = tracker;
|
|
500
|
+
}
|
|
501
|
+
/** Send all usage stats to mobile (called after E2EE established and on request) */
|
|
502
|
+
sendAllUsageStats() {
|
|
503
|
+
if (!this.usageTracker)
|
|
504
|
+
return;
|
|
505
|
+
const deviceId = config_1.config.deviceId;
|
|
506
|
+
const stats = this.usageTracker.getUsageStats('all');
|
|
507
|
+
const daily = this.usageTracker.getDailyUsage();
|
|
508
|
+
const streak = this.usageTracker.getStreakInfo();
|
|
509
|
+
this.emitSensitive('usage_stats_sync', { ...stats, deviceId }, this.e2eePeerDeviceId ?? undefined);
|
|
510
|
+
this.emitSensitive('daily_usage_sync', { daily, deviceId }, this.e2eePeerDeviceId ?? undefined);
|
|
511
|
+
this.emitSensitive('streak_info_sync', { ...streak, deviceId }, this.e2eePeerDeviceId ?? undefined);
|
|
512
|
+
console.log(`[WS] Sent usage stats sync to mobile`);
|
|
513
|
+
}
|
|
514
|
+
// Send Claude session event (sensitive — may contain error messages with paths)
|
|
313
515
|
sendClaudeSessionEvent(data) {
|
|
314
|
-
this.
|
|
516
|
+
this.emitSensitive('claude_session_event', data, this.e2eePeerDeviceId ?? undefined);
|
|
315
517
|
}
|
|
316
518
|
get isConnected() {
|
|
317
|
-
return this.
|
|
519
|
+
return this.server?.hasMobileConnection() ?? false;
|
|
318
520
|
}
|
|
319
521
|
}
|
|
320
522
|
exports.WebSocketClient = WebSocketClient;
|
|
523
|
+
WebSocketClient.SENSITIVE_QUEUE_TTL_MS = 30000; // 30 seconds max wait
|
|
524
|
+
WebSocketClient.MAX_PENDING_SENSITIVE = 200;
|
|
321
525
|
exports.wsClient = new WebSocketClient();
|
|
322
526
|
exports.default = exports.wsClient;
|
|
323
527
|
//# sourceMappingURL=websocket.js.map
|