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.
Files changed (158) hide show
  1. package/LICENSE +11 -7
  2. package/README.md +77 -118
  3. package/dist/approval.d.ts +1 -0
  4. package/dist/approval.js +9 -0
  5. package/dist/config.d.ts +3 -0
  6. package/dist/config.js +62 -16
  7. package/dist/crypto/e2eeManager.d.ts +49 -52
  8. package/dist/crypto/e2eeManager.js +256 -181
  9. package/dist/crypto/encryption.d.ts +8 -10
  10. package/dist/crypto/encryption.js +29 -94
  11. package/dist/crypto/index.d.ts +10 -0
  12. package/dist/crypto/index.js +22 -0
  13. package/dist/crypto/keyExchange.d.ts +6 -20
  14. package/dist/crypto/keyExchange.js +18 -110
  15. package/dist/crypto/keyGeneration.d.ts +2 -13
  16. package/dist/crypto/keyGeneration.js +14 -88
  17. package/dist/crypto/keyStorage.d.ts +32 -5
  18. package/dist/crypto/keyStorage.js +152 -8
  19. package/dist/crypto/sessionPersistence.d.ts +7 -13
  20. package/dist/crypto/sessionPersistence.js +108 -33
  21. package/dist/crypto/types.d.ts +24 -3
  22. package/dist/crypto/types.js +2 -1
  23. package/dist/crypto/websocketE2EE.d.ts +6 -17
  24. package/dist/crypto/websocketE2EE.js +21 -38
  25. package/dist/index.js +203 -280
  26. package/dist/integration.d.ts +0 -1
  27. package/dist/integration.js +2 -4
  28. package/dist/logger.d.ts +15 -0
  29. package/dist/logger.js +209 -1
  30. package/dist/server.d.ts +30 -0
  31. package/dist/server.js +162 -0
  32. package/dist/startup.js +15 -6
  33. package/dist/terminal.d.ts +1 -0
  34. package/dist/terminal.js +94 -1
  35. package/dist/tools/claude-process.d.ts +8 -0
  36. package/dist/tools/claude-process.js +199 -26
  37. package/dist/tools/claude-sessions.d.ts +1 -0
  38. package/dist/tools/claude-sessions.js +36 -10
  39. package/dist/tools/detector.js +11 -3
  40. package/dist/tools/permission-hook.js +94 -27
  41. package/dist/tools/permission-ipc.d.ts +1 -0
  42. package/dist/tools/permission-ipc.js +61 -14
  43. package/dist/transcript-streamer.d.ts +1 -0
  44. package/dist/transcript-streamer.js +18 -4
  45. package/dist/usage-tracker.d.ts +45 -0
  46. package/dist/usage-tracker.js +243 -0
  47. package/dist/websocket.d.ts +43 -12
  48. package/dist/websocket.js +418 -214
  49. package/package.json +5 -4
  50. package/dist/__tests__/cli-commands.test.d.ts +0 -6
  51. package/dist/__tests__/cli-commands.test.d.ts.map +0 -1
  52. package/dist/__tests__/cli-commands.test.js +0 -213
  53. package/dist/__tests__/cli-commands.test.js.map +0 -1
  54. package/dist/__tests__/crypto/e2e-integration.test.d.ts +0 -17
  55. package/dist/__tests__/crypto/e2e-integration.test.d.ts.map +0 -1
  56. package/dist/__tests__/crypto/e2e-integration.test.js +0 -338
  57. package/dist/__tests__/crypto/e2e-integration.test.js.map +0 -1
  58. package/dist/__tests__/crypto/e2eeManager.test.d.ts +0 -2
  59. package/dist/__tests__/crypto/e2eeManager.test.d.ts.map +0 -1
  60. package/dist/__tests__/crypto/e2eeManager.test.js +0 -242
  61. package/dist/__tests__/crypto/e2eeManager.test.js.map +0 -1
  62. package/dist/__tests__/crypto/encryption.test.d.ts +0 -2
  63. package/dist/__tests__/crypto/encryption.test.d.ts.map +0 -1
  64. package/dist/__tests__/crypto/encryption.test.js +0 -116
  65. package/dist/__tests__/crypto/encryption.test.js.map +0 -1
  66. package/dist/__tests__/crypto/keyExchange.test.d.ts +0 -2
  67. package/dist/__tests__/crypto/keyExchange.test.d.ts.map +0 -1
  68. package/dist/__tests__/crypto/keyExchange.test.js +0 -84
  69. package/dist/__tests__/crypto/keyExchange.test.js.map +0 -1
  70. package/dist/__tests__/crypto/keyGeneration.test.d.ts +0 -2
  71. package/dist/__tests__/crypto/keyGeneration.test.d.ts.map +0 -1
  72. package/dist/__tests__/crypto/keyGeneration.test.js +0 -61
  73. package/dist/__tests__/crypto/keyGeneration.test.js.map +0 -1
  74. package/dist/__tests__/crypto/keyStorage.test.d.ts +0 -2
  75. package/dist/__tests__/crypto/keyStorage.test.d.ts.map +0 -1
  76. package/dist/__tests__/crypto/keyStorage.test.js +0 -133
  77. package/dist/__tests__/crypto/keyStorage.test.js.map +0 -1
  78. package/dist/__tests__/crypto/websocketIntegration.test.d.ts +0 -2
  79. package/dist/__tests__/crypto/websocketIntegration.test.d.ts.map +0 -1
  80. package/dist/__tests__/crypto/websocketIntegration.test.js +0 -259
  81. package/dist/__tests__/crypto/websocketIntegration.test.js.map +0 -1
  82. package/dist/__tests__/startup.test.d.ts +0 -11
  83. package/dist/__tests__/startup.test.d.ts.map +0 -1
  84. package/dist/__tests__/startup.test.js +0 -241
  85. package/dist/__tests__/startup.test.js.map +0 -1
  86. package/dist/__tests__/tools/claude-process.test.d.ts +0 -8
  87. package/dist/__tests__/tools/claude-process.test.d.ts.map +0 -1
  88. package/dist/__tests__/tools/claude-process.test.js +0 -430
  89. package/dist/__tests__/tools/claude-process.test.js.map +0 -1
  90. package/dist/__tests__/tools/permission-hook.test.d.ts +0 -17
  91. package/dist/__tests__/tools/permission-hook.test.d.ts.map +0 -1
  92. package/dist/__tests__/tools/permission-hook.test.js +0 -616
  93. package/dist/__tests__/tools/permission-hook.test.js.map +0 -1
  94. package/dist/__tests__/tools/permission-ipc.test.d.ts +0 -11
  95. package/dist/__tests__/tools/permission-ipc.test.d.ts.map +0 -1
  96. package/dist/__tests__/tools/permission-ipc.test.js +0 -612
  97. package/dist/__tests__/tools/permission-ipc.test.js.map +0 -1
  98. package/dist/__tests__/websocket.test.d.ts +0 -13
  99. package/dist/__tests__/websocket.test.d.ts.map +0 -1
  100. package/dist/__tests__/websocket.test.js +0 -204
  101. package/dist/__tests__/websocket.test.js.map +0 -1
  102. package/dist/api.d.ts +0 -44
  103. package/dist/api.d.ts.map +0 -1
  104. package/dist/api.js +0 -76
  105. package/dist/api.js.map +0 -1
  106. package/dist/approval.d.ts.map +0 -1
  107. package/dist/approval.js.map +0 -1
  108. package/dist/config.d.ts.map +0 -1
  109. package/dist/config.js.map +0 -1
  110. package/dist/crypto/e2eeManager.d.ts.map +0 -1
  111. package/dist/crypto/e2eeManager.js.map +0 -1
  112. package/dist/crypto/encryption.d.ts.map +0 -1
  113. package/dist/crypto/encryption.js.map +0 -1
  114. package/dist/crypto/keyExchange.d.ts.map +0 -1
  115. package/dist/crypto/keyExchange.js.map +0 -1
  116. package/dist/crypto/keyGeneration.d.ts.map +0 -1
  117. package/dist/crypto/keyGeneration.js.map +0 -1
  118. package/dist/crypto/keyStorage.d.ts.map +0 -1
  119. package/dist/crypto/keyStorage.js.map +0 -1
  120. package/dist/crypto/sessionPersistence.d.ts.map +0 -1
  121. package/dist/crypto/sessionPersistence.js.map +0 -1
  122. package/dist/crypto/types.d.ts.map +0 -1
  123. package/dist/crypto/types.js.map +0 -1
  124. package/dist/crypto/websocketE2EE.d.ts.map +0 -1
  125. package/dist/crypto/websocketE2EE.js.map +0 -1
  126. package/dist/index.d.ts.map +0 -1
  127. package/dist/index.js.map +0 -1
  128. package/dist/integration.d.ts.map +0 -1
  129. package/dist/integration.js.map +0 -1
  130. package/dist/logger.d.ts.map +0 -1
  131. package/dist/logger.js.map +0 -1
  132. package/dist/startup.d.ts.map +0 -1
  133. package/dist/startup.js.map +0 -1
  134. package/dist/terminal.d.ts.map +0 -1
  135. package/dist/terminal.js.map +0 -1
  136. package/dist/tools/__tests__/claude-sessions.test.d.ts +0 -2
  137. package/dist/tools/__tests__/claude-sessions.test.d.ts.map +0 -1
  138. package/dist/tools/__tests__/claude-sessions.test.js +0 -306
  139. package/dist/tools/__tests__/claude-sessions.test.js.map +0 -1
  140. package/dist/tools/claude-hooks.d.ts.map +0 -1
  141. package/dist/tools/claude-hooks.js.map +0 -1
  142. package/dist/tools/claude-process.d.ts.map +0 -1
  143. package/dist/tools/claude-process.js.map +0 -1
  144. package/dist/tools/claude-sessions.d.ts.map +0 -1
  145. package/dist/tools/claude-sessions.js.map +0 -1
  146. package/dist/tools/detector.d.ts.map +0 -1
  147. package/dist/tools/detector.js.map +0 -1
  148. package/dist/tools/index.d.ts.map +0 -1
  149. package/dist/tools/index.js.map +0 -1
  150. package/dist/tools/permission-hook.d.ts.map +0 -1
  151. package/dist/tools/permission-hook.js.map +0 -1
  152. package/dist/tools/permission-ipc.d.ts.map +0 -1
  153. package/dist/tools/permission-ipc.js.map +0 -1
  154. package/dist/transcript-streamer.d.ts.map +0 -1
  155. package/dist/transcript-streamer.js.map +0 -1
  156. package/dist/websocket.d.ts.map +0 -1
  157. package/dist/websocket.js.map +0 -1
  158. package/jest.config.js +0 -18
package/LICENSE CHANGED
@@ -1,12 +1,16 @@
1
- Copyright (c) 2026 ForkOff. All rights reserved.
1
+ MIT License
2
2
 
3
- This software and associated documentation files (the "Software") are the
4
- proprietary property of ForkOff. The Software is provided for viewing and
5
- reference purposes only.
3
+ Copyright (c) 2026 ForkOff
6
4
 
7
- No part of the Software may be copied, modified, merged, published,
8
- distributed, sublicensed, sold, or otherwise used without the prior written
9
- permission of ForkOff.
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>Bridge your AI coding tools to your mobile device</strong>
8
+ <strong>Control your AI coding sessions from your phone</strong>
9
9
  </p>
10
10
 
11
- > ## 🚀 **OPEN BETA**
12
- >
13
- > ForkOff is now in **open beta**! Download the mobile app and get started:
14
- >
15
- > 📱 **[Join the TestFlight Beta](https://testflight.apple.com/join/dhh5FrN7)**
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="#configuration">Configuration</a>
18
+ <a href="https://forkoff.app">Website</a> &bull;
19
+ <a href="#installation">Installation</a> &bull;
20
+ <a href="#quick-start">Quick Start</a> &bull;
21
+ <a href="#commands">Commands</a> &bull;
22
+ <a href="#programmatic-usage">API</a> &bull;
23
+ <a href="#security">Security</a>
26
24
  </p>
27
25
 
28
26
  ---
29
27
 
30
- ## Overview
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
- ForkOff CLI connects your development machine to the ForkOff mobile app, enabling you to:
33
-
34
- - **Control AI coding sessions** from your phone
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** &mdash; ForkOff CLI is now MIT licensed. Contributions welcome!
31
+ >
32
+ > **Open Beta** &mdash; [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 Mobile App
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 your ForkOff mobile app to link your device.
48
+ Scan the QR code with the ForkOff mobile app to link your device.
55
49
 
56
- ### 2. Stay Connected
50
+ ### 2. Stay connected
57
51
 
58
52
  ```bash
59
53
  forkoff connect
60
54
  ```
61
55
 
62
- Keep this running to receive commands from your mobile app.
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** &mdash; See Claude Code output on your phone as it happens
63
+ - **Interactive approvals** &mdash; Approve or deny tool use (file edits, bash commands) from mobile
64
+ - **Configurable permission rules** &mdash; Auto-approve safe tools, require approval for destructive ones
65
+ - **End-to-end encryption** &mdash; All session data encrypted between CLI and mobile
66
+ - **Usage analytics** &mdash; Track token usage, session counts, and streaks across devices
67
+ - **Multi-device support** &mdash; Connect multiple CLI instances, analytics aggregate automatically
68
+ - **Auto-start** &mdash; Optionally launch on login so your phone is always connected
69
+ - **Direct P2P connection** &mdash; 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 from server |
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 startup --enable` | Enable automatic startup |
77
- | `forkoff startup --disable` | Disable automatic startup |
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 Options
84
+ ### Configuration
82
85
 
83
86
  ```bash
84
- # Show current configuration
85
- forkoff config --show
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,64 @@ 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
- ForkOff automatically starts on login so your device stays connected without manual intervention. Startup is **enabled by default** when you run `forkoff pair` or `forkoff connect`, it registers itself to launch `forkoff connect --quiet` on login.
101
+ Startup is enabled by default &mdash; `forkoff pair` and `forkoff connect` register the CLI to launch on login.
119
102
 
120
- - **Windows**: Adds a `ForkOffCLI` entry to the `HKCU\...\Run` registry key (no admin required)
121
- - **macOS**: Installs a launchd agent (`~/Library/LaunchAgents/app.forkoff.cli.plist`) with the explicit node binary path for nvm/fnm compatibility
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
- Once disabled, `pair` and `connect` will not re-register startup. To re-enable:
112
+ ---
113
+
114
+ ## Security
130
115
 
131
- ```bash
132
- forkoff startup --enable
133
- ```
116
+ All communication between the CLI and mobile app is end-to-end encrypted. The relay server never sees plaintext session data.
117
+
118
+ | Layer | Implementation |
119
+ |-------|---------------|
120
+ | **Key exchange** | X25519 ECDH with Ed25519 identity signatures |
121
+ | **Encryption** | XSalsa20-Poly1305 authenticated encryption (NaCl) |
122
+ | **Identity** | TOFU (Trust On First Use) with key pinning |
123
+ | **Replay protection** | Per-peer monotonic message counters |
124
+ | **Key storage** | OS keychain (macOS Keychain, Windows Credential Manager, Linux libsecret) |
125
+ | **Enforcement** | Sensitive events (session content, approvals, files) never sent in plaintext |
134
126
 
135
- Running `forkoff disconnect` also removes the startup registration.
127
+ No additional setup required &mdash; E2EE is enabled automatically when you pair.
136
128
 
137
129
  ---
138
130
 
139
131
  ## Programmatic Usage
140
132
 
141
- Integrate ForkOff into your AI coding tools:
133
+ Integrate ForkOff into your own AI coding tools:
142
134
 
143
135
  ```typescript
144
136
  import { createIntegration } from 'forkoff';
145
137
 
146
138
  const forkoff = createIntegration();
147
-
148
- // Connect to ForkOff server
149
139
  await forkoff.connect();
150
140
 
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
141
  // Request approval for code changes
167
142
  const approval = await forkoff.requestApproval(
168
- sessionId,
169
- messageId,
170
- 'CODE_CHANGE',
171
- 'Add authentication middleware',
143
+ sessionId, messageId, 'CODE_CHANGE',
144
+ 'Add auth middleware',
172
145
  { filePath: 'src/middleware/auth.ts', diff: '...' }
173
146
  );
174
147
 
175
- if (approval.status === 'approved') {
176
- // Apply the changes
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');
148
+ // Stream terminal output
149
+ forkoff.sendTerminalOutput(sessionId, '> npm install\nDone', 'stdout');
185
150
  ```
186
151
 
187
152
  ---
188
153
 
189
- ## Configuration
190
-
191
- Configuration files are stored at:
154
+ ## Configuration Files
192
155
 
193
156
  | Platform | Location |
194
157
  |----------|----------|
@@ -196,26 +159,22 @@ Configuration files are stored at:
196
159
  | **macOS** | `~/Library/Preferences/forkoff-cli/config.json` |
197
160
  | **Linux** | `~/.config/forkoff-cli/config.json` |
198
161
 
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
162
+ ## Development
209
163
 
210
- No additional setup required — E2EE is enabled automatically when you pair your device.
164
+ ```bash
165
+ git clone https://github.com/Forkoff-app/forkoff-cli.git
166
+ cd forkoff-cli
167
+ npm install
168
+ npm run dev # Run with ts-node
169
+ npm run build # Compile TypeScript
170
+ npm test # Run tests
171
+ ```
211
172
 
212
173
  ## Requirements
213
174
 
214
175
  - Node.js 18+
215
- - ForkOff mobile app
176
+ - [ForkOff mobile app](https://testflight.apple.com/join/dhh5FrN7) (iOS)
216
177
 
217
- ---
178
+ ## License
218
179
 
219
- <p align="center">
220
- Made with ❤️ by the ForkOff team
221
- </p>
180
+ [MIT](LICENSE)
@@ -10,6 +10,7 @@ export interface PendingApproval {
10
10
  status: 'pending' | 'approved' | 'rejected';
11
11
  }
12
12
  declare class ApprovalManager extends EventEmitter {
13
+ private static readonly MAX_PENDING_APPROVALS;
13
14
  private pendingApprovals;
14
15
  private approvalCounter;
15
16
  /**
package/dist/approval.js CHANGED
@@ -24,6 +24,14 @@ class ApprovalManager extends events_1.EventEmitter {
24
24
  createdAt: new Date(),
25
25
  status: 'pending',
26
26
  };
27
+ // Evict oldest if at cap
28
+ while (this.pendingApprovals.size >= ApprovalManager.MAX_PENDING_APPROVALS) {
29
+ const oldestKey = this.pendingApprovals.keys().next().value;
30
+ if (oldestKey)
31
+ this.pendingApprovals.delete(oldestKey);
32
+ else
33
+ break;
34
+ }
27
35
  this.pendingApprovals.set(id, approval);
28
36
  // Send to mobile app via WebSocket
29
37
  websocket_1.wsClient.sendApprovalRequest({
@@ -114,6 +122,7 @@ class ApprovalManager extends events_1.EventEmitter {
114
122
  }
115
123
  }
116
124
  }
125
+ ApprovalManager.MAX_PENDING_APPROVALS = 50;
117
126
  exports.approvalManager = new ApprovalManager();
118
127
  exports.default = exports.approvalManager;
119
128
  //# sourceMappingURL=approval.js.map
package/dist/config.d.ts CHANGED
@@ -23,7 +23,10 @@ declare class Config {
23
23
  set startupEnabled(value: boolean | null);
24
24
  get startupBinaryPath(): string | null;
25
25
  set startupBinaryPath(value: string | null);
26
+ get relayPort(): number;
27
+ set relayPort(value: number);
26
28
  get isPaired(): boolean;
29
+ ensureDeviceId(): string;
27
30
  getMachineId(): string;
28
31
  getDeviceInfo(): {
29
32
  name: string;
package/dist/config.js CHANGED
@@ -44,17 +44,17 @@ function isLocalUrl(url) {
44
44
  try {
45
45
  const parsed = new URL(url);
46
46
  const host = parsed.hostname.toLowerCase();
47
- return host === 'localhost' ||
48
- host === '127.0.0.1' ||
49
- host.startsWith('192.168.') ||
50
- host.startsWith('10.') ||
51
- host.startsWith('172.16.') ||
52
- host.startsWith('172.17.') ||
53
- host.startsWith('172.18.') ||
54
- host.startsWith('172.19.') ||
55
- host.startsWith('172.2') ||
56
- host.startsWith('172.30.') ||
57
- host.startsWith('172.31.');
47
+ if (host === 'localhost' || host === '127.0.0.1' ||
48
+ host.startsWith('192.168.') || host.startsWith('10.')) {
49
+ return true;
50
+ }
51
+ // Check 172.16.0.0 - 172.31.255.255
52
+ if (host.startsWith('172.')) {
53
+ const secondOctet = parseInt(host.split('.')[1], 10);
54
+ if (secondOctet >= 16 && secondOctet <= 31)
55
+ return true;
56
+ }
57
+ return false;
58
58
  }
59
59
  catch {
60
60
  return false;
@@ -81,6 +81,7 @@ const defaultConfig = {
81
81
  deviceName: os.hostname(),
82
82
  apiUrl: 'https://api.forkoff.app/api',
83
83
  wsUrl: 'wss://api.forkoff.app',
84
+ relayPort: 3000,
84
85
  pairingCode: null,
85
86
  pairedAt: null,
86
87
  userId: null,
@@ -97,15 +98,30 @@ class Config {
97
98
  ? path.join(process.env.APPDATA || os.homedir(), 'forkoff-cli')
98
99
  : path.join(os.homedir(), '.config', 'forkoff-cli');
99
100
  if (!fs.existsSync(configDir)) {
100
- fs.mkdirSync(configDir, { recursive: true });
101
+ fs.mkdirSync(configDir, { recursive: true, mode: 0o700 });
101
102
  }
102
103
  return path.join(configDir, 'config.json');
103
104
  }
104
105
  load() {
105
106
  try {
106
107
  if (fs.existsSync(this.configPath)) {
107
- const content = fs.readFileSync(this.configPath, 'utf-8');
108
- return { ...defaultConfig, ...JSON.parse(content) };
108
+ // SECURITY: Atomic symlink check — open fd then fstat to avoid TOCTOU
109
+ const fd = fs.openSync(this.configPath, 'r');
110
+ try {
111
+ const stat = fs.fstatSync(fd);
112
+ // If the file was replaced with a symlink between open and fstat,
113
+ // fstat returns the symlink target info. Check lstat separately.
114
+ const lstat = fs.lstatSync(this.configPath);
115
+ if (lstat.isSymbolicLink()) {
116
+ console.error('[Security] Symlink detected at config file, refusing to read');
117
+ return { ...defaultConfig };
118
+ }
119
+ const content = fs.readFileSync(fd, 'utf-8');
120
+ return { ...defaultConfig, ...JSON.parse(content) };
121
+ }
122
+ finally {
123
+ fs.closeSync(fd);
124
+ }
109
125
  }
110
126
  }
111
127
  catch (error) {
@@ -114,7 +130,22 @@ class Config {
114
130
  return { ...defaultConfig };
115
131
  }
116
132
  save() {
117
- fs.writeFileSync(this.configPath, JSON.stringify(this.data, null, 2));
133
+ // SECURITY: Atomic write via temp file + rename to prevent TOCTOU
134
+ const tmpPath = this.configPath + '.tmp.' + process.pid;
135
+ try {
136
+ // Write to temp file with restrictive permissions
137
+ fs.writeFileSync(tmpPath, JSON.stringify(this.data, null, 2), { encoding: 'utf-8', mode: 0o600 });
138
+ // Atomic rename (same filesystem)
139
+ fs.renameSync(tmpPath, this.configPath);
140
+ }
141
+ catch (err) {
142
+ // Clean up temp file on error
143
+ try {
144
+ fs.unlinkSync(tmpPath);
145
+ }
146
+ catch { /* ignore */ }
147
+ console.error('[Security] Failed to save config:', err.message);
148
+ }
118
149
  }
119
150
  get deviceId() {
120
151
  return this.data.deviceId;
@@ -183,8 +214,23 @@ class Config {
183
214
  this.data.startupBinaryPath = value;
184
215
  this.save();
185
216
  }
217
+ get relayPort() {
218
+ return this.data.relayPort;
219
+ }
220
+ set relayPort(value) {
221
+ this.data.relayPort = value;
222
+ this.save();
223
+ }
186
224
  get isPaired() {
187
- return !!this.userId && !!this.deviceId;
225
+ return !!this.deviceId && !!this.pairedAt;
226
+ }
227
+ // Ensure deviceId exists, generating one if needed
228
+ ensureDeviceId() {
229
+ if (!this.data.deviceId) {
230
+ this.data.deviceId = this.getMachineId();
231
+ this.save();
232
+ }
233
+ return this.data.deviceId;
188
234
  }
189
235
  // Get unique machine identifier
190
236
  getMachineId() {
@@ -1,82 +1,79 @@
1
1
  import { KeyExchangeInit, KeyExchangeAck, EncryptedMessage } from './types';
2
- /**
3
- * E2EE Manager for CLI
4
- * Orchestrates all end-to-end encryption operations
5
- */
6
2
  export declare class E2EEManager {
7
3
  private deviceId;
8
- private apiUrl;
9
- private authToken;
10
4
  private keyPair;
5
+ private signingKeyPair;
11
6
  private initialized;
12
- private pendingKeyExchanges;
13
- private outgoingCounters;
14
- private incomingCounters;
15
- private axiosInstance;
16
- constructor(deviceId: string, apiUrl: string, authToken: string);
7
+ private sessions;
8
+ private static readonly MAX_PENDING_EXCHANGES;
9
+ private static readonly PENDING_EXCHANGE_TTL_MS;
10
+ private pendingExchanges;
11
+ constructor(deviceId: string);
17
12
  /**
18
- * Initializes E2EE manager
19
- * - Loads or generates key pair
20
- * - Uploads public key to backend
13
+ * Initializes E2EE manager: loads or generates identity key pairs (DH + signing).
21
14
  */
22
15
  initialize(): Promise<void>;
16
+ /** Get the public key (for uploading to server) */
17
+ getPublicKey(): string | null;
18
+ /** Get the signing public key */
19
+ getSigningPublicKey(): string | null;
20
+ /** Check if manager is initialized */
21
+ isInitialized(): boolean;
23
22
  /**
24
- * Derives X25519 public key from private key
23
+ * Sign a key exchange payload with our Ed25519 identity key.
24
+ * The signed message is: "prefix:senderDeviceId:ephemeralPublicKey[:recipientDeviceId]"
25
25
  */
26
- private derivePublicKeyFromPrivateKey;
26
+ private signPayload;
27
27
  /**
28
- * Uploads public key to backend
28
+ * Verify a peer's signature on a key exchange payload.
29
+ * Returns true if signature is valid OR if peer has no identity key (unsigned exchange accepted with warning).
30
+ * Throws if the peer's identity key doesn't match TOFU record (potential MITM).
29
31
  */
30
- private uploadPublicKey;
32
+ private verifyPeerSignature;
31
33
  /**
32
- * Initiates key exchange with target device
33
- * Returns payload to send via WebSocket
34
+ * Create a key exchange initiation to send to a remote device.
35
+ * Generates an ephemeral key pair and signs it with our identity key.
34
36
  */
35
- initiateKeyExchange(targetDeviceId: string): Promise<KeyExchangeInit>;
36
37
  /**
37
- * Handles incoming key exchange init from sender
38
- * Returns ack payload to send back via WebSocket
38
+ * Evict expired or excess pending key exchanges.
39
39
  */
40
- handleKeyExchangeInit(senderDeviceId: string, senderEphemeralPublicKey: string): Promise<KeyExchangeAck>;
40
+ private cleanupPendingExchanges;
41
+ createKeyExchangeInit(targetDeviceId: string): KeyExchangeInit;
41
42
  /**
42
- * Handles incoming key exchange ack from recipient
43
- * Completes the key exchange by deriving the final session key
43
+ * Handle an incoming key exchange init from a remote device.
44
+ * Verifies the peer's identity signature (TOFU), computes shared key,
45
+ * and returns a signed ack.
44
46
  */
45
- handleKeyExchangeAck(recipientDeviceId: string, recipientEphemeralPublicKey: string): Promise<void>;
47
+ handleKeyExchangeInit(init: KeyExchangeInit): KeyExchangeAck;
46
48
  /**
47
- * Attempts to restore a persisted session after reconnection
48
- * Useful when IP changes cause WebSocket disconnection
49
+ * Handle an incoming key exchange ack from a remote device.
50
+ * Verifies the peer's identity signature (TOFU) and completes the key exchange.
49
51
  */
50
- restorePersistedSession(targetDeviceId: string): Promise<boolean>;
52
+ handleKeyExchangeAck(ack: KeyExchangeAck): void;
51
53
  /**
52
- * Lists all devices with persisted sessions
53
- * Useful for auto-reconnection after network changes
54
+ * Attempts to restore a persisted session after reconnection.
54
55
  */
56
+ restorePersistedSession(targetDeviceId: string): Promise<boolean>;
57
+ /** Lists all devices with persisted sessions */
55
58
  listPersistedDevices(): string[];
56
- /**
57
- * Encrypts a message for a target device
58
- */
59
- encryptMessage(plaintext: string, targetDeviceId: string, sessionId: string): EncryptedMessage;
60
- /**
61
- * Decrypts a message from a sender device
62
- */
63
- decryptMessage(encryptedMessage: EncryptedMessage, senderDeviceId: string): string;
64
- /**
65
- * Checks if a session key exists for a device
66
- */
59
+ /** Check if an encrypted session is established with a device */
67
60
  hasSessionKey(deviceId: string): boolean;
61
+ /** Encrypt a message for a specific device */
62
+ encryptMessage(plaintext: string, recipientDeviceId: string, sessionId: string): EncryptedMessage;
63
+ /** Decrypt an incoming encrypted message */
64
+ decryptMessage(message: EncryptedMessage, senderDeviceId: string): string;
65
+ /** Clear session for a specific device */
66
+ clearSession(deviceId: string): void;
68
67
  /**
69
- * Checks if manager is initialized
70
- */
71
- isInitialized(): boolean;
72
- /**
73
- * Cleans up all session keys and pending exchanges
74
- * @param deletePersisted - Whether to delete persisted sessions from disk (default: false)
68
+ * Cleans up all session keys and pending exchanges.
69
+ * @param deletePersisted - Whether to delete persisted sessions from disk
75
70
  */
76
71
  cleanup(deletePersisted?: boolean): void;
77
- /**
78
- * Removes a specific persisted session
79
- */
72
+ /** Removes a specific persisted session */
80
73
  removePersistedSession(targetDeviceId: string): void;
74
+ /** Reset TOFU trust for a peer (used on re-pair so new keys are accepted) */
75
+ resetPeerTrust(targetDeviceId: string): void;
76
+ /** Light trust reset — only clears TOFU key, preserves pending exchanges in-flight */
77
+ clearTrustOnly(targetDeviceId: string): void;
81
78
  }
82
79
  //# sourceMappingURL=e2eeManager.d.ts.map