forkoff 1.0.17 → 1.0.18

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 (156) hide show
  1. package/dist/approval.d.ts +1 -0
  2. package/dist/approval.js +9 -0
  3. package/dist/config.d.ts +3 -0
  4. package/dist/config.js +62 -16
  5. package/dist/crypto/e2eeManager.d.ts +49 -52
  6. package/dist/crypto/e2eeManager.js +256 -181
  7. package/dist/crypto/encryption.d.ts +8 -10
  8. package/dist/crypto/encryption.js +29 -94
  9. package/dist/crypto/index.d.ts +10 -0
  10. package/dist/crypto/index.js +22 -0
  11. package/dist/crypto/keyExchange.d.ts +6 -20
  12. package/dist/crypto/keyExchange.js +18 -110
  13. package/dist/crypto/keyGeneration.d.ts +2 -13
  14. package/dist/crypto/keyGeneration.js +14 -88
  15. package/dist/crypto/keyStorage.d.ts +32 -5
  16. package/dist/crypto/keyStorage.js +152 -8
  17. package/dist/crypto/sessionPersistence.d.ts +7 -13
  18. package/dist/crypto/sessionPersistence.js +108 -33
  19. package/dist/crypto/types.d.ts +24 -3
  20. package/dist/crypto/types.js +2 -1
  21. package/dist/crypto/websocketE2EE.d.ts +6 -17
  22. package/dist/crypto/websocketE2EE.js +21 -38
  23. package/dist/index.js +203 -280
  24. package/dist/integration.d.ts +0 -1
  25. package/dist/integration.js +2 -4
  26. package/dist/logger.d.ts +15 -0
  27. package/dist/logger.js +209 -1
  28. package/dist/server.d.ts +30 -0
  29. package/dist/server.js +162 -0
  30. package/dist/startup.js +15 -6
  31. package/dist/terminal.d.ts +1 -0
  32. package/dist/terminal.js +94 -1
  33. package/dist/tools/claude-process.d.ts +8 -0
  34. package/dist/tools/claude-process.js +199 -26
  35. package/dist/tools/claude-sessions.d.ts +1 -0
  36. package/dist/tools/claude-sessions.js +36 -10
  37. package/dist/tools/detector.js +11 -3
  38. package/dist/tools/permission-hook.js +94 -27
  39. package/dist/tools/permission-ipc.d.ts +1 -0
  40. package/dist/tools/permission-ipc.js +61 -14
  41. package/dist/transcript-streamer.d.ts +1 -0
  42. package/dist/transcript-streamer.js +18 -4
  43. package/dist/usage-tracker.d.ts +45 -0
  44. package/dist/usage-tracker.js +243 -0
  45. package/dist/websocket.d.ts +43 -12
  46. package/dist/websocket.js +418 -214
  47. package/package.json +4 -3
  48. package/dist/__tests__/cli-commands.test.d.ts +0 -6
  49. package/dist/__tests__/cli-commands.test.d.ts.map +0 -1
  50. package/dist/__tests__/cli-commands.test.js +0 -213
  51. package/dist/__tests__/cli-commands.test.js.map +0 -1
  52. package/dist/__tests__/crypto/e2e-integration.test.d.ts +0 -17
  53. package/dist/__tests__/crypto/e2e-integration.test.d.ts.map +0 -1
  54. package/dist/__tests__/crypto/e2e-integration.test.js +0 -338
  55. package/dist/__tests__/crypto/e2e-integration.test.js.map +0 -1
  56. package/dist/__tests__/crypto/e2eeManager.test.d.ts +0 -2
  57. package/dist/__tests__/crypto/e2eeManager.test.d.ts.map +0 -1
  58. package/dist/__tests__/crypto/e2eeManager.test.js +0 -242
  59. package/dist/__tests__/crypto/e2eeManager.test.js.map +0 -1
  60. package/dist/__tests__/crypto/encryption.test.d.ts +0 -2
  61. package/dist/__tests__/crypto/encryption.test.d.ts.map +0 -1
  62. package/dist/__tests__/crypto/encryption.test.js +0 -116
  63. package/dist/__tests__/crypto/encryption.test.js.map +0 -1
  64. package/dist/__tests__/crypto/keyExchange.test.d.ts +0 -2
  65. package/dist/__tests__/crypto/keyExchange.test.d.ts.map +0 -1
  66. package/dist/__tests__/crypto/keyExchange.test.js +0 -84
  67. package/dist/__tests__/crypto/keyExchange.test.js.map +0 -1
  68. package/dist/__tests__/crypto/keyGeneration.test.d.ts +0 -2
  69. package/dist/__tests__/crypto/keyGeneration.test.d.ts.map +0 -1
  70. package/dist/__tests__/crypto/keyGeneration.test.js +0 -61
  71. package/dist/__tests__/crypto/keyGeneration.test.js.map +0 -1
  72. package/dist/__tests__/crypto/keyStorage.test.d.ts +0 -2
  73. package/dist/__tests__/crypto/keyStorage.test.d.ts.map +0 -1
  74. package/dist/__tests__/crypto/keyStorage.test.js +0 -133
  75. package/dist/__tests__/crypto/keyStorage.test.js.map +0 -1
  76. package/dist/__tests__/crypto/websocketIntegration.test.d.ts +0 -2
  77. package/dist/__tests__/crypto/websocketIntegration.test.d.ts.map +0 -1
  78. package/dist/__tests__/crypto/websocketIntegration.test.js +0 -259
  79. package/dist/__tests__/crypto/websocketIntegration.test.js.map +0 -1
  80. package/dist/__tests__/startup.test.d.ts +0 -11
  81. package/dist/__tests__/startup.test.d.ts.map +0 -1
  82. package/dist/__tests__/startup.test.js +0 -241
  83. package/dist/__tests__/startup.test.js.map +0 -1
  84. package/dist/__tests__/tools/claude-process.test.d.ts +0 -8
  85. package/dist/__tests__/tools/claude-process.test.d.ts.map +0 -1
  86. package/dist/__tests__/tools/claude-process.test.js +0 -430
  87. package/dist/__tests__/tools/claude-process.test.js.map +0 -1
  88. package/dist/__tests__/tools/permission-hook.test.d.ts +0 -17
  89. package/dist/__tests__/tools/permission-hook.test.d.ts.map +0 -1
  90. package/dist/__tests__/tools/permission-hook.test.js +0 -616
  91. package/dist/__tests__/tools/permission-hook.test.js.map +0 -1
  92. package/dist/__tests__/tools/permission-ipc.test.d.ts +0 -11
  93. package/dist/__tests__/tools/permission-ipc.test.d.ts.map +0 -1
  94. package/dist/__tests__/tools/permission-ipc.test.js +0 -612
  95. package/dist/__tests__/tools/permission-ipc.test.js.map +0 -1
  96. package/dist/__tests__/websocket.test.d.ts +0 -13
  97. package/dist/__tests__/websocket.test.d.ts.map +0 -1
  98. package/dist/__tests__/websocket.test.js +0 -204
  99. package/dist/__tests__/websocket.test.js.map +0 -1
  100. package/dist/api.d.ts +0 -44
  101. package/dist/api.d.ts.map +0 -1
  102. package/dist/api.js +0 -76
  103. package/dist/api.js.map +0 -1
  104. package/dist/approval.d.ts.map +0 -1
  105. package/dist/approval.js.map +0 -1
  106. package/dist/config.d.ts.map +0 -1
  107. package/dist/config.js.map +0 -1
  108. package/dist/crypto/e2eeManager.d.ts.map +0 -1
  109. package/dist/crypto/e2eeManager.js.map +0 -1
  110. package/dist/crypto/encryption.d.ts.map +0 -1
  111. package/dist/crypto/encryption.js.map +0 -1
  112. package/dist/crypto/keyExchange.d.ts.map +0 -1
  113. package/dist/crypto/keyExchange.js.map +0 -1
  114. package/dist/crypto/keyGeneration.d.ts.map +0 -1
  115. package/dist/crypto/keyGeneration.js.map +0 -1
  116. package/dist/crypto/keyStorage.d.ts.map +0 -1
  117. package/dist/crypto/keyStorage.js.map +0 -1
  118. package/dist/crypto/sessionPersistence.d.ts.map +0 -1
  119. package/dist/crypto/sessionPersistence.js.map +0 -1
  120. package/dist/crypto/types.d.ts.map +0 -1
  121. package/dist/crypto/types.js.map +0 -1
  122. package/dist/crypto/websocketE2EE.d.ts.map +0 -1
  123. package/dist/crypto/websocketE2EE.js.map +0 -1
  124. package/dist/index.d.ts.map +0 -1
  125. package/dist/index.js.map +0 -1
  126. package/dist/integration.d.ts.map +0 -1
  127. package/dist/integration.js.map +0 -1
  128. package/dist/logger.d.ts.map +0 -1
  129. package/dist/logger.js.map +0 -1
  130. package/dist/startup.d.ts.map +0 -1
  131. package/dist/startup.js.map +0 -1
  132. package/dist/terminal.d.ts.map +0 -1
  133. package/dist/terminal.js.map +0 -1
  134. package/dist/tools/__tests__/claude-sessions.test.d.ts +0 -2
  135. package/dist/tools/__tests__/claude-sessions.test.d.ts.map +0 -1
  136. package/dist/tools/__tests__/claude-sessions.test.js +0 -306
  137. package/dist/tools/__tests__/claude-sessions.test.js.map +0 -1
  138. package/dist/tools/claude-hooks.d.ts.map +0 -1
  139. package/dist/tools/claude-hooks.js.map +0 -1
  140. package/dist/tools/claude-process.d.ts.map +0 -1
  141. package/dist/tools/claude-process.js.map +0 -1
  142. package/dist/tools/claude-sessions.d.ts.map +0 -1
  143. package/dist/tools/claude-sessions.js.map +0 -1
  144. package/dist/tools/detector.d.ts.map +0 -1
  145. package/dist/tools/detector.js.map +0 -1
  146. package/dist/tools/index.d.ts.map +0 -1
  147. package/dist/tools/index.js.map +0 -1
  148. package/dist/tools/permission-hook.d.ts.map +0 -1
  149. package/dist/tools/permission-hook.js.map +0 -1
  150. package/dist/tools/permission-ipc.d.ts.map +0 -1
  151. package/dist/tools/permission-ipc.js.map +0 -1
  152. package/dist/transcript-streamer.d.ts.map +0 -1
  153. package/dist/transcript-streamer.js.map +0 -1
  154. package/dist/websocket.d.ts.map +0 -1
  155. package/dist/websocket.js.map +0 -1
  156. package/jest.config.js +0 -18
@@ -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