forkoff 1.0.18 → 1.1.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/LICENSE +11 -7
- package/README.md +79 -118
- package/dist/cloud-client.d.ts +30 -0
- package/dist/cloud-client.js +165 -0
- package/dist/config.d.ts +6 -0
- package/dist/config.js +24 -0
- package/dist/crypto/e2eeManager.d.ts +8 -0
- package/dist/crypto/e2eeManager.js +90 -14
- package/dist/crypto/index.d.ts +1 -1
- package/dist/crypto/index.js +2 -1
- package/dist/crypto/keyExchange.d.ts +18 -0
- package/dist/crypto/keyExchange.js +37 -1
- package/dist/crypto/keyStorage.d.ts +4 -3
- package/dist/crypto/keyStorage.js +6 -4
- package/dist/crypto/sessionPersistence.js +24 -2
- package/dist/crypto/types.d.ts +3 -1
- package/dist/index.js +142 -30
- package/dist/websocket.d.ts +14 -1
- package/dist/websocket.js +159 -42
- package/package.json +4 -2
package/LICENSE
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
|
-
|
|
1
|
+
MIT License
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
proprietary property of ForkOff. The Software is provided for viewing and
|
|
5
|
-
reference purposes only.
|
|
3
|
+
Copyright (c) 2026 ForkOff
|
|
6
4
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
10
14
|
|
|
11
15
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
12
16
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
package/README.md
CHANGED
|
@@ -5,37 +5,31 @@
|
|
|
5
5
|
<h1 align="center">ForkOff CLI</h1>
|
|
6
6
|
|
|
7
7
|
<p align="center">
|
|
8
|
-
<strong>
|
|
8
|
+
<strong>Control your AI coding sessions from your phone</strong>
|
|
9
9
|
</p>
|
|
10
10
|
|
|
11
|
-
>
|
|
12
|
-
>
|
|
13
|
-
|
|
14
|
-
>
|
|
15
|
-
>
|
|
16
|
-
|
|
17
|
-
---
|
|
11
|
+
<p align="center">
|
|
12
|
+
<a href="https://www.npmjs.com/package/forkoff"><img src="https://img.shields.io/npm/v/forkoff.svg" alt="npm version"></a>
|
|
13
|
+
<a href="https://github.com/Forkoff-app/forkoff-cli/blob/main/LICENSE"><img src="https://img.shields.io/npm/l/forkoff.svg" alt="MIT License"></a>
|
|
14
|
+
<a href="https://www.npmjs.com/package/forkoff"><img src="https://img.shields.io/npm/dm/forkoff.svg" alt="npm downloads"></a>
|
|
15
|
+
</p>
|
|
18
16
|
|
|
19
17
|
<p align="center">
|
|
20
|
-
<a href="https://forkoff.app">Website</a>
|
|
21
|
-
<a href="#installation">Installation</a>
|
|
22
|
-
<a href="#quick-start">Quick Start</a>
|
|
23
|
-
<a href="#commands">Commands</a>
|
|
24
|
-
<a href="#programmatic-usage">API</a>
|
|
25
|
-
<a href="#
|
|
18
|
+
<a href="https://forkoff.app">Website</a> •
|
|
19
|
+
<a href="#installation">Installation</a> •
|
|
20
|
+
<a href="#quick-start">Quick Start</a> •
|
|
21
|
+
<a href="#commands">Commands</a> •
|
|
22
|
+
<a href="#programmatic-usage">API</a> •
|
|
23
|
+
<a href="#security">Security</a>
|
|
26
24
|
</p>
|
|
27
25
|
|
|
28
26
|
---
|
|
29
27
|
|
|
30
|
-
|
|
28
|
+
ForkOff CLI connects [Claude Code](https://claude.ai/code) on your laptop to the ForkOff mobile app, giving you real-time monitoring, interactive approvals, and usage analytics from anywhere.
|
|
31
29
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
- **Approve code changes** on the go
|
|
36
|
-
- **Send prompts** to Claude, Cursor, and other AI tools
|
|
37
|
-
- **Monitor progress** in real-time
|
|
38
|
-
- **Get notifications** for permission requests
|
|
30
|
+
> **Open Source** — ForkOff CLI is now MIT licensed. Contributions welcome!
|
|
31
|
+
>
|
|
32
|
+
> **Open Beta** — [Join the iOS TestFlight](https://testflight.apple.com/join/dhh5FrN7)
|
|
39
33
|
|
|
40
34
|
## Installation
|
|
41
35
|
|
|
@@ -45,24 +39,35 @@ npm install -g forkoff
|
|
|
45
39
|
|
|
46
40
|
## Quick Start
|
|
47
41
|
|
|
48
|
-
### 1. Pair with
|
|
42
|
+
### 1. Pair with your phone
|
|
49
43
|
|
|
50
44
|
```bash
|
|
51
45
|
forkoff pair
|
|
52
46
|
```
|
|
53
47
|
|
|
54
|
-
Scan the QR code with
|
|
48
|
+
Scan the QR code with the ForkOff mobile app to link your device.
|
|
55
49
|
|
|
56
|
-
### 2. Stay
|
|
50
|
+
### 2. Stay connected
|
|
57
51
|
|
|
58
52
|
```bash
|
|
59
53
|
forkoff connect
|
|
60
54
|
```
|
|
61
55
|
|
|
62
|
-
Keep this running to
|
|
56
|
+
Keep this running to stream sessions to your phone in real-time.
|
|
63
57
|
|
|
64
58
|
---
|
|
65
59
|
|
|
60
|
+
## Features
|
|
61
|
+
|
|
62
|
+
- **Real-time session monitoring** — See Claude Code output on your phone as it happens
|
|
63
|
+
- **Interactive approvals** — Approve or deny tool use (file edits, bash commands) from mobile
|
|
64
|
+
- **Configurable permission rules** — Auto-approve safe tools, require approval for destructive ones
|
|
65
|
+
- **End-to-end encryption** — All session data encrypted between CLI and mobile
|
|
66
|
+
- **Usage analytics** — Track token usage, session counts, and streaks across devices
|
|
67
|
+
- **Multi-device support** — Connect multiple CLI instances, analytics aggregate automatically
|
|
68
|
+
- **Auto-start** — Optionally launch on login so your phone is always connected
|
|
69
|
+
- **Direct P2P connection** — Embedded relay server, no cloud dependency for session data
|
|
70
|
+
|
|
66
71
|
## Commands
|
|
67
72
|
|
|
68
73
|
| Command | Description |
|
|
@@ -70,31 +75,18 @@ Keep this running to receive commands from your mobile app.
|
|
|
70
75
|
| `forkoff pair` | Generate QR code to pair with mobile app |
|
|
71
76
|
| `forkoff connect` | Connect and listen for commands |
|
|
72
77
|
| `forkoff status` | Check connection status |
|
|
73
|
-
| `forkoff disconnect` | Disconnect
|
|
78
|
+
| `forkoff disconnect` | Disconnect and unpair device |
|
|
74
79
|
| `forkoff config` | View/modify configuration |
|
|
75
80
|
| `forkoff startup` | Manage automatic startup on login |
|
|
76
|
-
| `forkoff
|
|
77
|
-
| `forkoff
|
|
78
|
-
| `forkoff startup --status` | Show startup registration status |
|
|
79
|
-
| `forkoff help` | Show available commands and usage |
|
|
81
|
+
| `forkoff tools` | Detect AI coding tools on your machine |
|
|
82
|
+
| `forkoff logs` | List debug log files for troubleshooting |
|
|
80
83
|
|
|
81
|
-
### Configuration
|
|
84
|
+
### Configuration
|
|
82
85
|
|
|
83
86
|
```bash
|
|
84
|
-
# Show current
|
|
85
|
-
forkoff config --
|
|
86
|
-
|
|
87
|
-
# Set custom API URL
|
|
88
|
-
forkoff config --api https://your-server.com/api
|
|
89
|
-
|
|
90
|
-
# Set custom WebSocket URL
|
|
91
|
-
forkoff config --ws wss://your-server.com
|
|
92
|
-
|
|
93
|
-
# Set device name
|
|
94
|
-
forkoff config --name "My MacBook Pro"
|
|
95
|
-
|
|
96
|
-
# Reset all configuration
|
|
97
|
-
forkoff config --reset
|
|
87
|
+
forkoff config --show # Show current config
|
|
88
|
+
forkoff config --name "My MBP" # Set device name
|
|
89
|
+
forkoff config --reset # Reset to defaults
|
|
98
90
|
```
|
|
99
91
|
|
|
100
92
|
### Global Options
|
|
@@ -102,93 +94,66 @@ forkoff config --reset
|
|
|
102
94
|
| Option | Description |
|
|
103
95
|
|--------|-------------|
|
|
104
96
|
| `-q, --quiet` | Suppress all output (for background operation) |
|
|
105
|
-
|
|
106
|
-
### Background Operation
|
|
107
|
-
|
|
108
|
-
Run ForkOff silently in the background with no console output:
|
|
109
|
-
|
|
110
|
-
```bash
|
|
111
|
-
forkoff connect --quiet
|
|
112
|
-
```
|
|
113
|
-
|
|
114
|
-
This is used by the automatic startup feature and is useful for running ForkOff as a background service.
|
|
97
|
+
| `--debug` | Enable debug logging to file (`~/.forkoff-cli/logs/`) |
|
|
115
98
|
|
|
116
99
|
### Automatic Startup
|
|
117
100
|
|
|
118
|
-
|
|
101
|
+
Startup is enabled by default — `forkoff pair` and `forkoff connect` register the CLI to launch on login.
|
|
119
102
|
|
|
120
|
-
- **Windows**:
|
|
121
|
-
- **macOS**:
|
|
122
|
-
|
|
123
|
-
To disable automatic startup:
|
|
103
|
+
- **Windows**: Registry key (`HKCU\...\Run`)
|
|
104
|
+
- **macOS**: launchd agent (`~/Library/LaunchAgents/app.forkoff.cli.plist`)
|
|
124
105
|
|
|
125
106
|
```bash
|
|
126
|
-
forkoff startup --disable
|
|
107
|
+
forkoff startup --disable # Disable auto-start
|
|
108
|
+
forkoff startup --enable # Re-enable
|
|
109
|
+
forkoff startup --status # Check registration
|
|
127
110
|
```
|
|
128
111
|
|
|
129
|
-
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
## Security
|
|
130
115
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
116
|
+
ForkOff uses end-to-end encryption (X25519 ECDH + XSalsa20-Poly1305) so the relay server never sees your code, prompts, or approvals — only opaque encrypted blobs routed between device UUIDs.
|
|
117
|
+
|
|
118
|
+
| Layer | Implementation |
|
|
119
|
+
|-------|---------------|
|
|
120
|
+
| **Key exchange** | X25519 ECDH with HKDF-SHA256 directional key derivation |
|
|
121
|
+
| **Authentication** | Ed25519 identity signatures on ephemeral keys (MITM protection) |
|
|
122
|
+
| **Encryption** | XSalsa20-Poly1305 authenticated encryption (NaCl secretbox) |
|
|
123
|
+
| **Identity** | TOFU (Trust On First Use) with key pinning |
|
|
124
|
+
| **Replay protection** | Per-peer monotonic message counters |
|
|
125
|
+
| **Session expiry** | Automatic re-key every 24 hours or 10,000 messages |
|
|
126
|
+
| **Key storage** | OS keychain (macOS Keychain, Windows Credential Manager, Linux libsecret) |
|
|
127
|
+
| **Enforcement** | 24 sensitive event types encrypted; plaintext fallback only when E2EE unavailable |
|
|
134
128
|
|
|
135
|
-
|
|
129
|
+
No additional setup required — E2EE is enabled automatically when you pair. See [SECURITY.md](https://github.com/Forkoff-app/forkoff-cli/blob/main/docs/SECURITY.md) for the full whitepaper.
|
|
136
130
|
|
|
137
131
|
---
|
|
138
132
|
|
|
139
133
|
## Programmatic Usage
|
|
140
134
|
|
|
141
|
-
Integrate ForkOff into your AI coding tools:
|
|
135
|
+
Integrate ForkOff into your own AI coding tools:
|
|
142
136
|
|
|
143
137
|
```typescript
|
|
144
138
|
import { createIntegration } from 'forkoff';
|
|
145
139
|
|
|
146
140
|
const forkoff = createIntegration();
|
|
147
|
-
|
|
148
|
-
// Connect to ForkOff server
|
|
149
141
|
await forkoff.connect();
|
|
150
142
|
|
|
151
|
-
// Handle incoming messages from mobile app
|
|
152
|
-
forkoff.onMessageReceived((sessionId, content, requestedBy) => {
|
|
153
|
-
console.log(`Message from ${requestedBy}: ${content}`);
|
|
154
|
-
|
|
155
|
-
// Send a response
|
|
156
|
-
forkoff.sendMessage(sessionId, 'Processing your request...');
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
// Stream responses in real-time
|
|
160
|
-
const stream = forkoff.startStreaming(sessionId);
|
|
161
|
-
stream.write('Here is ');
|
|
162
|
-
stream.write('a streaming ');
|
|
163
|
-
stream.write('response.');
|
|
164
|
-
stream.end();
|
|
165
|
-
|
|
166
143
|
// Request approval for code changes
|
|
167
144
|
const approval = await forkoff.requestApproval(
|
|
168
|
-
sessionId,
|
|
169
|
-
|
|
170
|
-
'CODE_CHANGE',
|
|
171
|
-
'Add authentication middleware',
|
|
145
|
+
sessionId, messageId, 'CODE_CHANGE',
|
|
146
|
+
'Add auth middleware',
|
|
172
147
|
{ filePath: 'src/middleware/auth.ts', diff: '...' }
|
|
173
148
|
);
|
|
174
149
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
// Send terminal output
|
|
180
|
-
forkoff.sendTerminalOutput(sessionId, '> npm install\n Done', 'stdout');
|
|
181
|
-
forkoff.sendTerminalExit(sessionId, 0);
|
|
182
|
-
|
|
183
|
-
// Update device status
|
|
184
|
-
forkoff.setStatus('busy');
|
|
150
|
+
// Stream terminal output
|
|
151
|
+
forkoff.sendTerminalOutput(sessionId, '> npm install\nDone', 'stdout');
|
|
185
152
|
```
|
|
186
153
|
|
|
187
154
|
---
|
|
188
155
|
|
|
189
|
-
## Configuration
|
|
190
|
-
|
|
191
|
-
Configuration files are stored at:
|
|
156
|
+
## Configuration Files
|
|
192
157
|
|
|
193
158
|
| Platform | Location |
|
|
194
159
|
|----------|----------|
|
|
@@ -196,26 +161,22 @@ Configuration files are stored at:
|
|
|
196
161
|
| **macOS** | `~/Library/Preferences/forkoff-cli/config.json` |
|
|
197
162
|
| **Linux** | `~/.config/forkoff-cli/config.json` |
|
|
198
163
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
## Security
|
|
202
|
-
|
|
203
|
-
Your data stays yours. All communication between the ForkOff CLI and your mobile device is protected with **end-to-end encryption (E2EE)**:
|
|
204
|
-
|
|
205
|
-
- Messages, code, and commands are encrypted on-device before leaving your machine
|
|
206
|
-
- The ForkOff server never sees your plaintext data — it only relays encrypted payloads
|
|
207
|
-
- Each device pair establishes a unique encrypted channel using ephemeral key exchange
|
|
208
|
-
- Session keys are derived per-connection, so even if one session is compromised, others remain secure
|
|
164
|
+
## Development
|
|
209
165
|
|
|
210
|
-
|
|
166
|
+
```bash
|
|
167
|
+
git clone https://github.com/Forkoff-app/forkoff-cli.git
|
|
168
|
+
cd forkoff-cli
|
|
169
|
+
npm install
|
|
170
|
+
npm run dev # Run with ts-node
|
|
171
|
+
npm run build # Compile TypeScript
|
|
172
|
+
npm test # Run tests
|
|
173
|
+
```
|
|
211
174
|
|
|
212
175
|
## Requirements
|
|
213
176
|
|
|
214
177
|
- Node.js 18+
|
|
215
|
-
- ForkOff mobile app
|
|
178
|
+
- [ForkOff mobile app](https://testflight.apple.com/join/dhh5FrN7) (iOS)
|
|
216
179
|
|
|
217
|
-
|
|
180
|
+
## License
|
|
218
181
|
|
|
219
|
-
|
|
220
|
-
Made with ❤️ by the ForkOff team
|
|
221
|
-
</p>
|
|
182
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { EventEmitter } from 'events';
|
|
2
|
+
export interface CloudRelayOptions {
|
|
3
|
+
url: string;
|
|
4
|
+
deviceId: string;
|
|
5
|
+
deviceName: string;
|
|
6
|
+
relayToken?: string | null;
|
|
7
|
+
}
|
|
8
|
+
export declare class CloudRelayClient extends EventEmitter {
|
|
9
|
+
private socket;
|
|
10
|
+
private url;
|
|
11
|
+
private deviceId;
|
|
12
|
+
private deviceName;
|
|
13
|
+
private relayToken;
|
|
14
|
+
/** The pairing code the CLI generated — sent to relay for registration */
|
|
15
|
+
private currentPairingCode;
|
|
16
|
+
constructor(options: CloudRelayOptions);
|
|
17
|
+
/** Set the pairing code — will be registered with the relay on connect */
|
|
18
|
+
setPairingCode(code: string): void;
|
|
19
|
+
/** Connect to the cloud relay as a CLI client */
|
|
20
|
+
start(): Promise<void>;
|
|
21
|
+
/** Emit an event to the mobile client via the relay */
|
|
22
|
+
emitToMobile(event: string, data: any): void;
|
|
23
|
+
/** Check if mobile is connected (based on relay notifications) */
|
|
24
|
+
hasMobileConnection(): boolean;
|
|
25
|
+
/** Get the connected mobile device ID (set after pairing or mobile_connected) */
|
|
26
|
+
getMobileDeviceId(): string | null;
|
|
27
|
+
/** Graceful shutdown */
|
|
28
|
+
stop(): Promise<void>;
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=cloud-client.d.ts.map
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CloudRelayClient = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Cloud relay client — CLI connects as a Socket.io CLIENT to the relay server
|
|
6
|
+
* (e.g., wss://api.forkoff.app). The relay routes events to/from the paired mobile.
|
|
7
|
+
* Same interface as EmbeddedRelayServer so WebSocketClient can use either.
|
|
8
|
+
*/
|
|
9
|
+
const socket_io_client_1 = require("socket.io-client");
|
|
10
|
+
const events_1 = require("events");
|
|
11
|
+
const config_1 = require("./config");
|
|
12
|
+
/** Events the cloud relay forwards from mobile → CLI (same set as EmbeddedRelayServer) */
|
|
13
|
+
const MOBILE_EVENTS = [
|
|
14
|
+
'terminal_command', 'terminal_create', 'user_message',
|
|
15
|
+
'claude_start_session', 'claude_resume_session', 'claude_sessions_request',
|
|
16
|
+
'directory_list', 'read_file', 'transcript_fetch', 'transcript_subscribe',
|
|
17
|
+
'transcript_unsubscribe', 'approval_response', 'claude_approval_response',
|
|
18
|
+
'permission_response', 'permission_rules_sync', 'claude_abort', 'tab_complete',
|
|
19
|
+
'subscribe_device', 'unsubscribe_device',
|
|
20
|
+
'sdk_session_history', 'usage_stats_request', 'session_settings_update',
|
|
21
|
+
'transcript_subscribe_sdk',
|
|
22
|
+
// E2EE key exchange and encrypted messages from mobile
|
|
23
|
+
'encrypted_key_exchange_init', 'encrypted_key_exchange_ack', 'encrypted_message',
|
|
24
|
+
];
|
|
25
|
+
class CloudRelayClient extends events_1.EventEmitter {
|
|
26
|
+
constructor(options) {
|
|
27
|
+
super();
|
|
28
|
+
this.socket = null;
|
|
29
|
+
/** The pairing code the CLI generated — sent to relay for registration */
|
|
30
|
+
this.currentPairingCode = null;
|
|
31
|
+
this.url = options.url;
|
|
32
|
+
this.deviceId = options.deviceId;
|
|
33
|
+
this.deviceName = options.deviceName;
|
|
34
|
+
this.relayToken = options.relayToken ?? null;
|
|
35
|
+
}
|
|
36
|
+
/** Set the pairing code — will be registered with the relay on connect */
|
|
37
|
+
setPairingCode(code) {
|
|
38
|
+
this.currentPairingCode = code;
|
|
39
|
+
// If already connected, register immediately
|
|
40
|
+
if (this.socket?.connected) {
|
|
41
|
+
this.socket.emit('register_pairing_code', {
|
|
42
|
+
code,
|
|
43
|
+
deviceId: this.deviceId,
|
|
44
|
+
deviceName: this.deviceName,
|
|
45
|
+
platform: process.platform,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/** Connect to the cloud relay as a CLI client */
|
|
50
|
+
start() {
|
|
51
|
+
return new Promise((resolve, reject) => {
|
|
52
|
+
this.socket = (0, socket_io_client_1.io)(this.url, {
|
|
53
|
+
auth: {
|
|
54
|
+
clientType: 'cli',
|
|
55
|
+
deviceId: this.deviceId,
|
|
56
|
+
deviceName: this.deviceName,
|
|
57
|
+
platform: process.platform,
|
|
58
|
+
hostname: require('os').hostname(),
|
|
59
|
+
relayToken: this.relayToken,
|
|
60
|
+
userId: config_1.config.userId,
|
|
61
|
+
},
|
|
62
|
+
transports: ['websocket'],
|
|
63
|
+
reconnection: true,
|
|
64
|
+
reconnectionAttempts: Infinity,
|
|
65
|
+
reconnectionDelay: 1000,
|
|
66
|
+
reconnectionDelayMax: 15000,
|
|
67
|
+
timeout: 10000,
|
|
68
|
+
});
|
|
69
|
+
let resolved = false;
|
|
70
|
+
this.socket.on('connect', () => {
|
|
71
|
+
console.log(`[CloudRelay] Connected to ${this.url}`);
|
|
72
|
+
// Register pairing code if we have one
|
|
73
|
+
if (this.currentPairingCode) {
|
|
74
|
+
this.socket.emit('register_pairing_code', {
|
|
75
|
+
code: this.currentPairingCode,
|
|
76
|
+
deviceId: this.deviceId,
|
|
77
|
+
deviceName: this.deviceName,
|
|
78
|
+
platform: process.platform,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
if (!resolved) {
|
|
82
|
+
resolved = true;
|
|
83
|
+
resolve();
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
this.socket.on('connect_error', (err) => {
|
|
87
|
+
console.error(`[CloudRelay] Connection error: ${err.message}`);
|
|
88
|
+
if (!resolved) {
|
|
89
|
+
resolved = true;
|
|
90
|
+
reject(new Error(`Failed to connect to cloud relay at ${this.url}: ${err.message}`));
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
this.socket.on('disconnect', (reason) => {
|
|
94
|
+
console.log(`[CloudRelay] Disconnected: ${reason}`);
|
|
95
|
+
});
|
|
96
|
+
// Handle cloud pairing flow: relay sends pair_device when mobile enters our code
|
|
97
|
+
this.socket.on('pair_device', (data) => {
|
|
98
|
+
console.log(`[CloudRelay] Pairing request received from mobile`);
|
|
99
|
+
// Emit internally so WebSocketClient can handle it
|
|
100
|
+
this.emit('pair_device', { mobileDeviceId: data.mobileDeviceId });
|
|
101
|
+
// Send ack back through relay — relay will forward to mobile with mobileRelayToken
|
|
102
|
+
this.socket.emit('pair_device_ack', {
|
|
103
|
+
deviceId: this.deviceId,
|
|
104
|
+
deviceName: this.deviceName,
|
|
105
|
+
platform: process.platform,
|
|
106
|
+
mobileDeviceId: data.mobileDeviceId,
|
|
107
|
+
pairId: data.pairId,
|
|
108
|
+
cliRelayToken: data.cliRelayToken,
|
|
109
|
+
});
|
|
110
|
+
// Store relay credentials locally
|
|
111
|
+
if (data.cliRelayToken) {
|
|
112
|
+
config_1.config.relayToken = data.cliRelayToken;
|
|
113
|
+
this.relayToken = data.cliRelayToken;
|
|
114
|
+
}
|
|
115
|
+
if (data.pairId) {
|
|
116
|
+
config_1.config.pairId = data.pairId;
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
// Handle mobile connected notification from relay
|
|
120
|
+
this.socket.on('mobile_connected', (data) => {
|
|
121
|
+
console.log(`[CloudRelay] Mobile connected: ${data.deviceId || 'unknown'}`);
|
|
122
|
+
this.emit('mobile_connected', { deviceId: data.deviceId || data.mobileDeviceId });
|
|
123
|
+
});
|
|
124
|
+
// Handle mobile disconnected notification from relay
|
|
125
|
+
this.socket.on('mobile_disconnected', (data) => {
|
|
126
|
+
console.log(`[CloudRelay] Mobile disconnected`);
|
|
127
|
+
this.emit('mobile_disconnected', { deviceId: data.deviceId, reason: data.reason || 'disconnected' });
|
|
128
|
+
});
|
|
129
|
+
// Forward all mobile events → internal EventEmitter (same as EmbeddedRelayServer)
|
|
130
|
+
for (const event of MOBILE_EVENTS) {
|
|
131
|
+
this.socket.on(event, (data) => {
|
|
132
|
+
this.emit(event, data);
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
/** Emit an event to the mobile client via the relay */
|
|
138
|
+
emitToMobile(event, data) {
|
|
139
|
+
if (this.socket?.connected) {
|
|
140
|
+
// Relay will route this to the paired mobile client
|
|
141
|
+
this.socket.emit(event, data);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/** Check if mobile is connected (based on relay notifications) */
|
|
145
|
+
hasMobileConnection() {
|
|
146
|
+
return this.socket?.connected ?? false;
|
|
147
|
+
}
|
|
148
|
+
/** Get the connected mobile device ID (set after pairing or mobile_connected) */
|
|
149
|
+
getMobileDeviceId() {
|
|
150
|
+
// The relay manages this — we don't track directly in cloud mode
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
/** Graceful shutdown */
|
|
154
|
+
stop() {
|
|
155
|
+
return new Promise((resolve) => {
|
|
156
|
+
if (this.socket) {
|
|
157
|
+
this.socket.disconnect();
|
|
158
|
+
this.socket = null;
|
|
159
|
+
}
|
|
160
|
+
resolve();
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
exports.CloudRelayClient = CloudRelayClient;
|
|
165
|
+
//# sourceMappingURL=cloud-client.js.map
|
package/dist/config.d.ts
CHANGED
|
@@ -25,6 +25,12 @@ declare class Config {
|
|
|
25
25
|
set startupBinaryPath(value: string | null);
|
|
26
26
|
get relayPort(): number;
|
|
27
27
|
set relayPort(value: number);
|
|
28
|
+
get relayMode(): 'cloud' | 'local';
|
|
29
|
+
set relayMode(value: 'cloud' | 'local');
|
|
30
|
+
get relayToken(): string | null;
|
|
31
|
+
set relayToken(value: string | null);
|
|
32
|
+
get pairId(): string | null;
|
|
33
|
+
set pairId(value: string | null);
|
|
28
34
|
get isPaired(): boolean;
|
|
29
35
|
ensureDeviceId(): string;
|
|
30
36
|
getMachineId(): string;
|
package/dist/config.js
CHANGED
|
@@ -87,6 +87,9 @@ const defaultConfig = {
|
|
|
87
87
|
userId: null,
|
|
88
88
|
startupEnabled: true,
|
|
89
89
|
startupBinaryPath: null,
|
|
90
|
+
relayMode: 'cloud',
|
|
91
|
+
relayToken: null,
|
|
92
|
+
pairId: null,
|
|
90
93
|
};
|
|
91
94
|
class Config {
|
|
92
95
|
constructor() {
|
|
@@ -221,6 +224,27 @@ class Config {
|
|
|
221
224
|
this.data.relayPort = value;
|
|
222
225
|
this.save();
|
|
223
226
|
}
|
|
227
|
+
get relayMode() {
|
|
228
|
+
return this.data.relayMode;
|
|
229
|
+
}
|
|
230
|
+
set relayMode(value) {
|
|
231
|
+
this.data.relayMode = value;
|
|
232
|
+
this.save();
|
|
233
|
+
}
|
|
234
|
+
get relayToken() {
|
|
235
|
+
return this.data.relayToken;
|
|
236
|
+
}
|
|
237
|
+
set relayToken(value) {
|
|
238
|
+
this.data.relayToken = value;
|
|
239
|
+
this.save();
|
|
240
|
+
}
|
|
241
|
+
get pairId() {
|
|
242
|
+
return this.data.pairId;
|
|
243
|
+
}
|
|
244
|
+
set pairId(value) {
|
|
245
|
+
this.data.pairId = value;
|
|
246
|
+
this.save();
|
|
247
|
+
}
|
|
224
248
|
get isPaired() {
|
|
225
249
|
return !!this.deviceId && !!this.pairedAt;
|
|
226
250
|
}
|
|
@@ -5,6 +5,8 @@ export declare class E2EEManager {
|
|
|
5
5
|
private signingKeyPair;
|
|
6
6
|
private initialized;
|
|
7
7
|
private sessions;
|
|
8
|
+
private static readonly SESSION_MAX_AGE_MS;
|
|
9
|
+
private static readonly SESSION_MAX_MESSAGES;
|
|
8
10
|
private static readonly MAX_PENDING_EXCHANGES;
|
|
9
11
|
private static readonly PENDING_EXCHANGE_TTL_MS;
|
|
10
12
|
private pendingExchanges;
|
|
@@ -19,6 +21,11 @@ export declare class E2EEManager {
|
|
|
19
21
|
getSigningPublicKey(): string | null;
|
|
20
22
|
/** Check if manager is initialized */
|
|
21
23
|
isInitialized(): boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Check if a session has expired (age or message count).
|
|
26
|
+
* Returns true if the session should be torn down and re-keyed.
|
|
27
|
+
*/
|
|
28
|
+
isSessionExpired(deviceId: string): boolean;
|
|
22
29
|
/**
|
|
23
30
|
* Sign a key exchange payload with our Ed25519 identity key.
|
|
24
31
|
* The signed message is: "prefix:senderDeviceId:ephemeralPublicKey[:recipientDeviceId]"
|
|
@@ -52,6 +59,7 @@ export declare class E2EEManager {
|
|
|
52
59
|
handleKeyExchangeAck(ack: KeyExchangeAck): void;
|
|
53
60
|
/**
|
|
54
61
|
* Attempts to restore a persisted session after reconnection.
|
|
62
|
+
* If the session has expired, it is deleted from persistence and not restored.
|
|
55
63
|
*/
|
|
56
64
|
restorePersistedSession(targetDeviceId: string): Promise<boolean>;
|
|
57
65
|
/** Lists all devices with persisted sessions */
|