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
|
@@ -92,6 +92,32 @@ const DEFAULT_SAFE_TOOLS = new Set([
|
|
|
92
92
|
// ---------------------------------------------------------------------------
|
|
93
93
|
// Helpers
|
|
94
94
|
// ---------------------------------------------------------------------------
|
|
95
|
+
function safeReadFile(filePath) {
|
|
96
|
+
try {
|
|
97
|
+
const stat = fs.lstatSync(filePath);
|
|
98
|
+
if (stat.isSymbolicLink()) {
|
|
99
|
+
console.error(`[Security] Symlink detected, refusing to read: ${filePath}`);
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
return fs.readFileSync(filePath, 'utf-8');
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
function safeWriteFile(filePath, content) {
|
|
109
|
+
try {
|
|
110
|
+
// SECURITY: Atomic write via temp file + rename to prevent TOCTOU symlink attacks
|
|
111
|
+
const tmpPath = filePath + '.tmp.' + process.pid;
|
|
112
|
+
fs.writeFileSync(tmpPath, content, { encoding: 'utf-8', mode: 0o600 });
|
|
113
|
+
fs.renameSync(tmpPath, filePath);
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
catch (err) {
|
|
117
|
+
console.error(`[Security] Failed to write file safely`, err.message);
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
95
121
|
/** Write the decision JSON to stdout synchronously and exit immediately. */
|
|
96
122
|
function respond(decision, reason) {
|
|
97
123
|
const output = {
|
|
@@ -106,29 +132,38 @@ function respond(decision, reason) {
|
|
|
106
132
|
fs.writeSync(1, JSON.stringify(output) + '\n');
|
|
107
133
|
process.exit(decision === 'allow' ? 0 : 2);
|
|
108
134
|
}
|
|
109
|
-
/** Ensure the temp directory exists. */
|
|
135
|
+
/** Ensure the temp directory exists and has safe permissions. */
|
|
110
136
|
function ensureTempDir() {
|
|
111
137
|
if (!fs.existsSync(TEMP_DIR)) {
|
|
112
|
-
fs.mkdirSync(TEMP_DIR, { recursive: true });
|
|
138
|
+
fs.mkdirSync(TEMP_DIR, { recursive: true, mode: 0o700 });
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
// SECURITY: Validate existing dir isn't world/group-writable (attacker pre-creation)
|
|
142
|
+
// Skip on Windows — Unix permission bits aren't enforced and produce false positives
|
|
143
|
+
if (process.platform !== 'win32') {
|
|
144
|
+
const stat = fs.statSync(TEMP_DIR);
|
|
145
|
+
const mode = stat.mode & 0o777;
|
|
146
|
+
if (mode & 0o022) { // group or other writable
|
|
147
|
+
console.error(`[Security] Temp dir has unsafe permissions (${mode.toString(8)}), aborting`);
|
|
148
|
+
respond('deny', 'Permission temp directory has unsafe permissions');
|
|
149
|
+
}
|
|
150
|
+
}
|
|
113
151
|
}
|
|
114
152
|
}
|
|
115
|
-
/** Cleanup request and response temp files. */
|
|
153
|
+
/** Cleanup request and response temp files (with symlink protection). */
|
|
116
154
|
function cleanup(promptId) {
|
|
117
155
|
const requestFile = path.join(TEMP_DIR, `${promptId}.request.json`);
|
|
118
156
|
const responseFile = path.join(TEMP_DIR, `${promptId}.response.json`);
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
fs.
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
}
|
|
130
|
-
catch {
|
|
131
|
-
// Best effort.
|
|
157
|
+
for (const filePath of [requestFile, responseFile]) {
|
|
158
|
+
try {
|
|
159
|
+
const stat = fs.lstatSync(filePath);
|
|
160
|
+
if (stat.isFile() && !stat.isSymbolicLink()) {
|
|
161
|
+
fs.unlinkSync(filePath);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
// Best effort — file may not exist or already deleted.
|
|
166
|
+
}
|
|
132
167
|
}
|
|
133
168
|
}
|
|
134
169
|
/**
|
|
@@ -137,7 +172,10 @@ function cleanup(promptId) {
|
|
|
137
172
|
*/
|
|
138
173
|
function loadRules() {
|
|
139
174
|
try {
|
|
140
|
-
const raw =
|
|
175
|
+
const raw = safeReadFile(RULES_FILE);
|
|
176
|
+
if (raw === null) {
|
|
177
|
+
return { safeTools: new Set(DEFAULT_SAFE_TOOLS), bashPatterns: [] };
|
|
178
|
+
}
|
|
141
179
|
const rules = JSON.parse(raw);
|
|
142
180
|
if (!Array.isArray(rules)) {
|
|
143
181
|
return { safeTools: new Set(DEFAULT_SAFE_TOOLS), bashPatterns: [] };
|
|
@@ -176,13 +214,38 @@ function matchesAnyPattern(command, patterns) {
|
|
|
176
214
|
/**
|
|
177
215
|
* Simple glob matching: `*` matches any sequence of characters.
|
|
178
216
|
* The entire command must match (not just a prefix).
|
|
217
|
+
* Uses iterative matching to avoid ReDoS from regex catastrophic backtracking.
|
|
179
218
|
*/
|
|
180
219
|
function globMatch(str, pattern) {
|
|
181
|
-
//
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
220
|
+
// SECURITY: Reject overly long or complex patterns to prevent DoS
|
|
221
|
+
if (pattern.length > 200)
|
|
222
|
+
return false;
|
|
223
|
+
// Iterative glob matcher (no regex — immune to ReDoS)
|
|
224
|
+
let si = 0, pi = 0;
|
|
225
|
+
let starSi = -1, starPi = -1;
|
|
226
|
+
while (si < str.length) {
|
|
227
|
+
if (pi < pattern.length && (pattern[pi] === str[si] || pattern[pi] === '?')) {
|
|
228
|
+
si++;
|
|
229
|
+
pi++;
|
|
230
|
+
}
|
|
231
|
+
else if (pi < pattern.length && pattern[pi] === '*') {
|
|
232
|
+
starPi = pi;
|
|
233
|
+
starSi = si;
|
|
234
|
+
pi++;
|
|
235
|
+
}
|
|
236
|
+
else if (starPi !== -1) {
|
|
237
|
+
pi = starPi + 1;
|
|
238
|
+
starSi++;
|
|
239
|
+
si = starSi;
|
|
240
|
+
}
|
|
241
|
+
else {
|
|
242
|
+
return false;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
while (pi < pattern.length && pattern[pi] === '*') {
|
|
246
|
+
pi++;
|
|
247
|
+
}
|
|
248
|
+
return pi === pattern.length;
|
|
186
249
|
}
|
|
187
250
|
/**
|
|
188
251
|
* Poll for the response file until it appears or the timeout elapses.
|
|
@@ -199,10 +262,12 @@ function pollForResponse(promptId) {
|
|
|
199
262
|
}
|
|
200
263
|
try {
|
|
201
264
|
if (fs.existsSync(responseFile)) {
|
|
202
|
-
const raw =
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
265
|
+
const raw = safeReadFile(responseFile);
|
|
266
|
+
if (raw !== null) {
|
|
267
|
+
const response = JSON.parse(raw);
|
|
268
|
+
resolve(response);
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
206
271
|
}
|
|
207
272
|
}
|
|
208
273
|
catch {
|
|
@@ -270,7 +335,9 @@ async function main() {
|
|
|
270
335
|
timestamp: Date.now(),
|
|
271
336
|
};
|
|
272
337
|
const requestFile = path.join(TEMP_DIR, `${promptId}.request.json`);
|
|
273
|
-
|
|
338
|
+
if (!safeWriteFile(requestFile, JSON.stringify(request, null, 2))) {
|
|
339
|
+
respond('deny', 'Failed to write permission request file');
|
|
340
|
+
}
|
|
274
341
|
console.error(`[forkoff-hook] Permission requested for ${toolName} (promptId=${promptId}), polling...`);
|
|
275
342
|
const response = await pollForResponse(promptId);
|
|
276
343
|
if (!response) {
|
|
@@ -46,6 +46,7 @@ declare class PermissionIpcManager extends EventEmitter {
|
|
|
46
46
|
private readonly POLL_INTERVAL_MS;
|
|
47
47
|
private readonly TEMP_DIR;
|
|
48
48
|
private processedFiles;
|
|
49
|
+
private static readonly MAX_PROCESSED_FILES;
|
|
49
50
|
/** Currently tracked terminal session ID */
|
|
50
51
|
private terminalSessionId;
|
|
51
52
|
/** Currently tracked session key */
|
|
@@ -57,6 +57,32 @@ const events_1 = require("events");
|
|
|
57
57
|
const fs = __importStar(require("fs"));
|
|
58
58
|
const path = __importStar(require("path"));
|
|
59
59
|
const os = __importStar(require("os"));
|
|
60
|
+
function safeReadFile(filePath) {
|
|
61
|
+
try {
|
|
62
|
+
const stat = fs.lstatSync(filePath);
|
|
63
|
+
if (stat.isSymbolicLink()) {
|
|
64
|
+
console.error(`[Security] Symlink detected, refusing to read: ${filePath}`);
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
return fs.readFileSync(filePath, 'utf-8');
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function safeWriteFile(filePath, content) {
|
|
74
|
+
try {
|
|
75
|
+
// SECURITY: Atomic write via temp file + rename to prevent TOCTOU symlink attacks
|
|
76
|
+
const tmpPath = filePath + '.tmp.' + process.pid;
|
|
77
|
+
fs.writeFileSync(tmpPath, content, { encoding: 'utf-8', mode: 0o600 });
|
|
78
|
+
fs.renameSync(tmpPath, filePath);
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
catch (err) {
|
|
82
|
+
console.error(`[Security] Failed to write file safely`, err.message);
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
60
86
|
class PermissionIpcManager extends events_1.EventEmitter {
|
|
61
87
|
constructor() {
|
|
62
88
|
super(...arguments);
|
|
@@ -89,7 +115,7 @@ class PermissionIpcManager extends events_1.EventEmitter {
|
|
|
89
115
|
this.sessionKey = sessionKey;
|
|
90
116
|
// Ensure temp directory exists
|
|
91
117
|
try {
|
|
92
|
-
fs.mkdirSync(this.TEMP_DIR, { recursive: true });
|
|
118
|
+
fs.mkdirSync(this.TEMP_DIR, { recursive: true, mode: 0o700 });
|
|
93
119
|
}
|
|
94
120
|
catch (err) {
|
|
95
121
|
console.log(`[Permission IPC] Failed to create temp dir ${this.TEMP_DIR}: ${err.message}`);
|
|
@@ -143,12 +169,11 @@ class PermissionIpcManager extends events_1.EventEmitter {
|
|
|
143
169
|
if (reason) {
|
|
144
170
|
responseData.reason = reason;
|
|
145
171
|
}
|
|
146
|
-
|
|
147
|
-
fs.writeFileSync(responseFile, JSON.stringify(responseData), 'utf-8');
|
|
172
|
+
if (safeWriteFile(responseFile, JSON.stringify(responseData))) {
|
|
148
173
|
console.log(`[Permission IPC] Wrote response for ${promptId}: ${decision}${reason ? ` (${reason})` : ''}`);
|
|
149
174
|
}
|
|
150
|
-
|
|
151
|
-
console.log(`[Permission IPC] Failed to write response file ${responseFile}
|
|
175
|
+
else {
|
|
176
|
+
console.log(`[Permission IPC] Failed to write response file ${responseFile}`);
|
|
152
177
|
}
|
|
153
178
|
}
|
|
154
179
|
/**
|
|
@@ -170,9 +195,19 @@ class PermissionIpcManager extends events_1.EventEmitter {
|
|
|
170
195
|
for (const file of requestFiles) {
|
|
171
196
|
const filePath = path.join(this.TEMP_DIR, file);
|
|
172
197
|
try {
|
|
173
|
-
const content =
|
|
198
|
+
const content = safeReadFile(filePath);
|
|
199
|
+
if (content === null) {
|
|
200
|
+
this.processedFiles.add(file);
|
|
201
|
+
console.log(`[Permission IPC] Skipped unreadable request file ${file}`);
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
174
204
|
const request = JSON.parse(content);
|
|
175
|
-
// Mark as processed to avoid duplicate handling
|
|
205
|
+
// Mark as processed to avoid duplicate handling (evict oldest if at cap)
|
|
206
|
+
if (this.processedFiles.size >= PermissionIpcManager.MAX_PROCESSED_FILES) {
|
|
207
|
+
const oldest = this.processedFiles.values().next().value;
|
|
208
|
+
if (oldest)
|
|
209
|
+
this.processedFiles.delete(oldest);
|
|
210
|
+
}
|
|
176
211
|
this.processedFiles.add(file);
|
|
177
212
|
const promptId = request.promptId || file.replace('.request.json', '');
|
|
178
213
|
const toolName = request.toolName || 'unknown';
|
|
@@ -205,6 +240,11 @@ class PermissionIpcManager extends events_1.EventEmitter {
|
|
|
205
240
|
catch (err) {
|
|
206
241
|
// File might be partially written or malformed — skip it for now
|
|
207
242
|
// but add to processed so we don't retry every poll cycle
|
|
243
|
+
if (this.processedFiles.size >= PermissionIpcManager.MAX_PROCESSED_FILES) {
|
|
244
|
+
const oldest = this.processedFiles.values().next().value;
|
|
245
|
+
if (oldest)
|
|
246
|
+
this.processedFiles.delete(oldest);
|
|
247
|
+
}
|
|
208
248
|
this.processedFiles.add(file);
|
|
209
249
|
console.log(`[Permission IPC] Failed to read request file ${file}: ${err.message}`);
|
|
210
250
|
}
|
|
@@ -240,18 +280,23 @@ class PermissionIpcManager extends events_1.EventEmitter {
|
|
|
240
280
|
*/
|
|
241
281
|
cleanup() {
|
|
242
282
|
this.stop();
|
|
243
|
-
// Try to clean up remaining temp files in the directory
|
|
283
|
+
// Try to clean up remaining request/response temp files in the directory
|
|
244
284
|
try {
|
|
245
285
|
const files = fs.readdirSync(this.TEMP_DIR);
|
|
246
|
-
|
|
286
|
+
const ipcFiles = files.filter((f) => f.endsWith('.request.json') || f.endsWith('.response.json'));
|
|
287
|
+
for (const file of ipcFiles) {
|
|
247
288
|
try {
|
|
248
|
-
|
|
289
|
+
const filePath = path.join(this.TEMP_DIR, file);
|
|
290
|
+
const stat = fs.lstatSync(filePath);
|
|
291
|
+
if (stat.isFile() && !stat.isSymbolicLink()) {
|
|
292
|
+
fs.unlinkSync(filePath);
|
|
293
|
+
}
|
|
249
294
|
}
|
|
250
295
|
catch (err) {
|
|
251
296
|
// File may already be deleted by the hook script — ignore
|
|
252
297
|
}
|
|
253
298
|
}
|
|
254
|
-
console.log(`[Permission IPC] Cleaned up ${
|
|
299
|
+
console.log(`[Permission IPC] Cleaned up ${ipcFiles.length} temp file(s)`);
|
|
255
300
|
}
|
|
256
301
|
catch (err) {
|
|
257
302
|
// Directory might not exist — that's fine
|
|
@@ -272,12 +317,13 @@ class PermissionIpcManager extends events_1.EventEmitter {
|
|
|
272
317
|
// Directory doesn't exist — nothing to clean
|
|
273
318
|
return;
|
|
274
319
|
}
|
|
320
|
+
const ipcFiles = files.filter((f) => f.endsWith('.request.json') || f.endsWith('.response.json'));
|
|
275
321
|
let cleaned = 0;
|
|
276
|
-
for (const file of
|
|
322
|
+
for (const file of ipcFiles) {
|
|
277
323
|
const filePath = path.join(tempDir, file);
|
|
278
324
|
try {
|
|
279
|
-
const stat = fs.
|
|
280
|
-
if (stat.isFile()) {
|
|
325
|
+
const stat = fs.lstatSync(filePath);
|
|
326
|
+
if (stat.isFile() && !stat.isSymbolicLink()) {
|
|
281
327
|
fs.unlinkSync(filePath);
|
|
282
328
|
cleaned++;
|
|
283
329
|
}
|
|
@@ -292,4 +338,5 @@ class PermissionIpcManager extends events_1.EventEmitter {
|
|
|
292
338
|
}
|
|
293
339
|
}
|
|
294
340
|
exports.PermissionIpcManager = PermissionIpcManager;
|
|
341
|
+
PermissionIpcManager.MAX_PROCESSED_FILES = 500; // Cap to prevent memory DoS
|
|
295
342
|
//# sourceMappingURL=permission-ipc.js.map
|
|
@@ -32,6 +32,7 @@ declare class TranscriptStreamer extends EventEmitter {
|
|
|
32
32
|
private fileSizes;
|
|
33
33
|
private lastLineNumbers;
|
|
34
34
|
private processingLock;
|
|
35
|
+
private sessionPaths;
|
|
35
36
|
/**
|
|
36
37
|
* Fetch transcript history from a JSONL file
|
|
37
38
|
* Supports reverse pagination: offset 0 = most recent entries
|
|
@@ -38,6 +38,7 @@ const fs = __importStar(require("fs"));
|
|
|
38
38
|
const readline = __importStar(require("readline"));
|
|
39
39
|
const events_1 = require("events");
|
|
40
40
|
const chokidar = __importStar(require("chokidar"));
|
|
41
|
+
const MAX_WATCHERS = 50;
|
|
41
42
|
class TranscriptStreamer extends events_1.EventEmitter {
|
|
42
43
|
constructor() {
|
|
43
44
|
super(...arguments);
|
|
@@ -45,6 +46,7 @@ class TranscriptStreamer extends events_1.EventEmitter {
|
|
|
45
46
|
this.fileSizes = new Map();
|
|
46
47
|
this.lastLineNumbers = new Map();
|
|
47
48
|
this.processingLock = new Map(); // Prevent concurrent reads
|
|
49
|
+
this.sessionPaths = new Map(); // sessionKey -> transcriptPath
|
|
48
50
|
}
|
|
49
51
|
/**
|
|
50
52
|
* Fetch transcript history from a JSONL file
|
|
@@ -107,9 +109,14 @@ class TranscriptStreamer extends events_1.EventEmitter {
|
|
|
107
109
|
subscribeToUpdates(sessionKey, transcriptPath) {
|
|
108
110
|
// Unsubscribe if already subscribed
|
|
109
111
|
this.unsubscribeFromUpdates(sessionKey);
|
|
110
|
-
|
|
112
|
+
// Guard against unbounded watcher growth
|
|
113
|
+
if (this.watchers.size >= MAX_WATCHERS) {
|
|
114
|
+
console.warn(`[Transcript] MAX_WATCHERS (${MAX_WATCHERS}) reached, refusing to subscribe: ${sessionKey}`);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
console.log(`[Transcript] subscribeToUpdates: sessionKey=${sessionKey}`);
|
|
111
118
|
if (!fs.existsSync(transcriptPath)) {
|
|
112
|
-
console.log(`[Transcript]
|
|
119
|
+
console.log(`[Transcript] Transcript file does not exist`);
|
|
113
120
|
return;
|
|
114
121
|
}
|
|
115
122
|
// Get initial file size
|
|
@@ -167,6 +174,7 @@ class TranscriptStreamer extends events_1.EventEmitter {
|
|
|
167
174
|
}
|
|
168
175
|
});
|
|
169
176
|
this.watchers.set(sessionKey, watcher);
|
|
177
|
+
this.sessionPaths.set(sessionKey, transcriptPath);
|
|
170
178
|
}
|
|
171
179
|
/**
|
|
172
180
|
* Unsubscribe from transcript updates
|
|
@@ -178,6 +186,11 @@ class TranscriptStreamer extends events_1.EventEmitter {
|
|
|
178
186
|
this.watchers.delete(sessionKey);
|
|
179
187
|
this.fileSizes.delete(sessionKey);
|
|
180
188
|
this.processingLock.delete(sessionKey);
|
|
189
|
+
const transcriptPath = this.sessionPaths.get(sessionKey);
|
|
190
|
+
if (transcriptPath) {
|
|
191
|
+
this.lastLineNumbers.delete(transcriptPath);
|
|
192
|
+
this.sessionPaths.delete(sessionKey);
|
|
193
|
+
}
|
|
181
194
|
}
|
|
182
195
|
}
|
|
183
196
|
/**
|
|
@@ -189,7 +202,7 @@ class TranscriptStreamer extends events_1.EventEmitter {
|
|
|
189
202
|
const entries = [];
|
|
190
203
|
let lineNumber = 0;
|
|
191
204
|
let newLinesFound = 0;
|
|
192
|
-
console.log(`[Transcript] readNewLines: lastLineNumber=${lastLineNumber}
|
|
205
|
+
console.log(`[Transcript] readNewLines: lastLineNumber=${lastLineNumber}`);
|
|
193
206
|
const fileStream = fs.createReadStream(transcriptPath, { encoding: 'utf8' });
|
|
194
207
|
const rl = readline.createInterface({
|
|
195
208
|
input: fileStream,
|
|
@@ -207,7 +220,7 @@ class TranscriptStreamer extends events_1.EventEmitter {
|
|
|
207
220
|
}
|
|
208
221
|
else {
|
|
209
222
|
// Log first 100 chars of skipped line to understand what's being skipped
|
|
210
|
-
console.log(`[Transcript] Line ${lineNumber} skipped (no entry)
|
|
223
|
+
console.log(`[Transcript] Line ${lineNumber} skipped (no entry)`);
|
|
211
224
|
}
|
|
212
225
|
}
|
|
213
226
|
});
|
|
@@ -453,6 +466,7 @@ class TranscriptStreamer extends events_1.EventEmitter {
|
|
|
453
466
|
this.fileSizes.clear();
|
|
454
467
|
this.lastLineNumbers.clear();
|
|
455
468
|
this.processingLock.clear();
|
|
469
|
+
this.sessionPaths.clear();
|
|
456
470
|
}
|
|
457
471
|
}
|
|
458
472
|
exports.transcriptStreamer = new TranscriptStreamer();
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export declare class UsageTracker {
|
|
2
|
+
private filePath;
|
|
3
|
+
private data;
|
|
4
|
+
private saveTimer;
|
|
5
|
+
private dirty;
|
|
6
|
+
constructor();
|
|
7
|
+
private load;
|
|
8
|
+
private scheduleSave;
|
|
9
|
+
private saveNow;
|
|
10
|
+
private ensureDay;
|
|
11
|
+
recordUsage(inputTokens: number, outputTokens: number): void;
|
|
12
|
+
recordSessionStart(): void;
|
|
13
|
+
/**
|
|
14
|
+
* Get usage stats matching the mobile UsageStats type.
|
|
15
|
+
*/
|
|
16
|
+
getUsageStats(period?: 'day' | 'week' | 'month' | 'all'): {
|
|
17
|
+
totalInputTokens: string;
|
|
18
|
+
totalOutputTokens: string;
|
|
19
|
+
totalTokens: string;
|
|
20
|
+
totalSessionCount: number;
|
|
21
|
+
estimatedCostUsd: number;
|
|
22
|
+
period: string;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Get daily usage matching the mobile TokenUsageDaily[] type.
|
|
26
|
+
*/
|
|
27
|
+
getDailyUsage(startDate?: string, endDate?: string): Array<{
|
|
28
|
+
date: string;
|
|
29
|
+
inputTokens: string;
|
|
30
|
+
outputTokens: string;
|
|
31
|
+
totalTokens: string;
|
|
32
|
+
sessionCount: number;
|
|
33
|
+
estimatedCostUsd: number | null;
|
|
34
|
+
}>;
|
|
35
|
+
/**
|
|
36
|
+
* Get streak info matching the mobile StreakInfo type.
|
|
37
|
+
*/
|
|
38
|
+
getStreakInfo(): {
|
|
39
|
+
currentStreak: number;
|
|
40
|
+
totalActiveDays: number;
|
|
41
|
+
};
|
|
42
|
+
/** Flush pending writes immediately (call before exit). */
|
|
43
|
+
flush(): void;
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=usage-tracker.d.ts.map
|