camofox-browser 2.4.1 → 2.4.4
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/CHANGELOG.md +35 -0
- package/README.md +6 -1
- package/dist/src/routes/core.d.ts.map +1 -1
- package/dist/src/routes/core.js +128 -16
- package/dist/src/routes/core.js.map +1 -1
- package/dist/src/routes/openclaw.d.ts.map +1 -1
- package/dist/src/routes/openclaw.js +82 -9
- package/dist/src/routes/openclaw.js.map +1 -1
- package/dist/src/services/context-pool.d.ts +2 -1
- package/dist/src/services/context-pool.d.ts.map +1 -1
- package/dist/src/services/context-pool.js +46 -10
- package/dist/src/services/context-pool.js.map +1 -1
- package/dist/src/services/session.d.ts +27 -4
- package/dist/src/services/session.d.ts.map +1 -1
- package/dist/src/services/session.js +331 -75
- package/dist/src/services/session.js.map +1 -1
- package/dist/src/services/tab.d.ts +2 -1
- package/dist/src/services/tab.d.ts.map +1 -1
- package/dist/src/services/tab.js +19 -0
- package/dist/src/services/tab.js.map +1 -1
- package/dist/src/services/tracing.d.ts.map +1 -1
- package/dist/src/services/tracing.js +43 -5
- package/dist/src/services/tracing.js.map +1 -1
- package/dist/src/utils/proxy-profiles.js +4 -4
- package/dist/src/utils/proxy-profiles.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +2 -2
|
@@ -11,6 +11,10 @@ exports.cleanupSessionsForUserId = cleanupSessionsForUserId;
|
|
|
11
11
|
exports.normalizeUserId = normalizeUserId;
|
|
12
12
|
exports.getSessionMapKey = getSessionMapKey;
|
|
13
13
|
exports.getEstablishedSessionProfile = getEstablishedSessionProfile;
|
|
14
|
+
exports.hasDefaultSessionProfileRuntime = hasDefaultSessionProfileRuntime;
|
|
15
|
+
exports.claimDefaultSessionProfileRuntime = claimDefaultSessionProfileRuntime;
|
|
16
|
+
exports.clearDefaultSessionProfileClaim = clearDefaultSessionProfileClaim;
|
|
17
|
+
exports.getSessionProfileLaunchSettings = getSessionProfileLaunchSettings;
|
|
14
18
|
exports.getCanonicalProfile = getCanonicalProfile;
|
|
15
19
|
exports.hasCanonicalProfile = hasCanonicalProfile;
|
|
16
20
|
exports.acquireFirstCreateMutex = acquireFirstCreateMutex;
|
|
@@ -20,6 +24,9 @@ exports.createCanonicalProfile = createCanonicalProfile;
|
|
|
20
24
|
exports.clearCanonicalProfile = clearCanonicalProfile;
|
|
21
25
|
exports.establishSessionProfile = establishSessionProfile;
|
|
22
26
|
exports.clearSessionProfile = clearSessionProfile;
|
|
27
|
+
exports.acquireSessionProfileCreateMutex = acquireSessionProfileCreateMutex;
|
|
28
|
+
exports.waitForSessionProfileCreate = waitForSessionProfileCreate;
|
|
29
|
+
exports.rollbackSessionProfileRuntime = rollbackSessionProfileRuntime;
|
|
23
30
|
exports.getSessionsForUser = getSessionsForUser;
|
|
24
31
|
exports.getAllSessions = getAllSessions;
|
|
25
32
|
exports.countTotalTabsForSessions = countTotalTabsForSessions;
|
|
@@ -54,9 +61,12 @@ const CONFIG = (0, config_1.loadConfig)();
|
|
|
54
61
|
// userId -> { context, tabGroups: Map<sessionKey, Map<tabId, TabState>>, lastAccess }
|
|
55
62
|
// Note: sessionKey was previously called listItemId - both are accepted for backward compatibility
|
|
56
63
|
const sessions = new Map();
|
|
64
|
+
const sessionOwners = new Map();
|
|
57
65
|
// sessionKey -> in-flight session creation promise
|
|
58
66
|
// Avoids storing partially-initialized sessions (e.g., context: null cast) and dedupes concurrent creates.
|
|
59
67
|
const launchingSessions = new Map();
|
|
68
|
+
const launchingSessionOwners = new Map();
|
|
69
|
+
const lifecycleIdleClosures = new Map();
|
|
60
70
|
// tabId -> sessions map key
|
|
61
71
|
// Persistent profiles are keyed only by userId, while tab endpoints only get tabId.
|
|
62
72
|
const tabSessionIndex = new Map();
|
|
@@ -65,9 +75,11 @@ const tabSessionIndex = new Map();
|
|
|
65
75
|
const canonicalProfiles = new Map();
|
|
66
76
|
// Session profiles keyed by userId::sessionKey to track separate proxy/geo profiles per session
|
|
67
77
|
const sessionProfiles = new Map();
|
|
78
|
+
const defaultSessionProfileClaims = new Map();
|
|
68
79
|
// Per-user mutex covering the entire first-create lifecycle (establishment -> tab commit).
|
|
69
80
|
// Prevents sibling requests from observing provisional canonical state.
|
|
70
81
|
const firstCreateMutexes = new Map();
|
|
82
|
+
const sessionProfileCreateMutexes = new Map();
|
|
71
83
|
const userConcurrency = new Map();
|
|
72
84
|
function __getUserConcurrencyStateForTests(userId) {
|
|
73
85
|
const key = String(userId).toLowerCase().trim();
|
|
@@ -79,6 +91,40 @@ function __getUserConcurrencyStateForTests(userId) {
|
|
|
79
91
|
function __getSessionsMapForTests() {
|
|
80
92
|
return sessions;
|
|
81
93
|
}
|
|
94
|
+
function beginLifecycleIdleClosure(userId) {
|
|
95
|
+
const key = normalizeUserId(userId);
|
|
96
|
+
let state = lifecycleIdleClosures.get(key);
|
|
97
|
+
if (!state) {
|
|
98
|
+
let resolve;
|
|
99
|
+
const promise = new Promise((r) => {
|
|
100
|
+
resolve = r;
|
|
101
|
+
});
|
|
102
|
+
state = { pending: 0, promise, resolve };
|
|
103
|
+
lifecycleIdleClosures.set(key, state);
|
|
104
|
+
}
|
|
105
|
+
state.pending++;
|
|
106
|
+
let released = false;
|
|
107
|
+
return () => {
|
|
108
|
+
if (released)
|
|
109
|
+
return;
|
|
110
|
+
released = true;
|
|
111
|
+
const current = lifecycleIdleClosures.get(key);
|
|
112
|
+
if (!current)
|
|
113
|
+
return;
|
|
114
|
+
current.pending--;
|
|
115
|
+
if (current.pending <= 0) {
|
|
116
|
+
lifecycleIdleClosures.delete(key);
|
|
117
|
+
current.resolve();
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
async function waitForLifecycleIdleClosure(userId) {
|
|
122
|
+
const key = normalizeUserId(userId);
|
|
123
|
+
const pending = lifecycleIdleClosures.get(key);
|
|
124
|
+
if (pending) {
|
|
125
|
+
await pending.promise;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
82
128
|
async function withUserLimit(userId, maxConcurrent, operation, operationTimeoutMs) {
|
|
83
129
|
const key = String(userId).toLowerCase().trim();
|
|
84
130
|
let state = userConcurrency.get(key);
|
|
@@ -130,26 +176,43 @@ async function withUserLimit(userId, maxConcurrent, operation, operationTimeoutM
|
|
|
130
176
|
}
|
|
131
177
|
}
|
|
132
178
|
}
|
|
133
|
-
function cleanupSessionsForUserId(userId, reason, clearCanonical = true) {
|
|
179
|
+
function cleanupSessionsForUserId(userId, reason, clearCanonical = true, options = {}) {
|
|
134
180
|
const key = normalizeUserId(userId);
|
|
181
|
+
const allowInternalSessionKey = options.allowInternalSessionKey ?? true;
|
|
182
|
+
const ownerKeys = new Set();
|
|
135
183
|
// If a session is currently being created, drop our reference so callers don't keep a stale placeholder.
|
|
136
|
-
launchingSessions.
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
// ignore cleanup errors
|
|
184
|
+
for (const launchKey of Array.from(launchingSessions.keys())) {
|
|
185
|
+
if (isLaunchingSessionKeyForCleanupKey(launchKey, key, allowInternalSessionKey)) {
|
|
186
|
+
ownerKeys.add(launchingSessionOwners.get(launchKey) ?? key);
|
|
187
|
+
launchingSessions.delete(launchKey);
|
|
188
|
+
launchingSessionOwners.delete(launchKey);
|
|
189
|
+
}
|
|
143
190
|
}
|
|
144
|
-
(
|
|
145
|
-
|
|
146
|
-
|
|
191
|
+
for (const [sessionKey, session] of Array.from(sessions.entries())) {
|
|
192
|
+
if (!isSessionMapKeyForCleanupKey(sessionKey, key, allowInternalSessionKey))
|
|
193
|
+
continue;
|
|
194
|
+
ownerKeys.add(sessionOwners.get(sessionKey) ?? sessionKey);
|
|
147
195
|
unindexSessionTabs(session);
|
|
148
|
-
sessions.delete(
|
|
149
|
-
|
|
196
|
+
sessions.delete(sessionKey);
|
|
197
|
+
sessionOwners.delete(sessionKey);
|
|
198
|
+
(0, logging_1.log)('info', 'session cleaned up', { userId: key, sessionKey, reason });
|
|
199
|
+
}
|
|
200
|
+
if (ownerKeys.size === 0)
|
|
201
|
+
ownerKeys.add(key);
|
|
202
|
+
for (const ownerKey of ownerKeys) {
|
|
203
|
+
void (0, vnc_1.stopVnc)(ownerKey).catch(() => { });
|
|
204
|
+
try {
|
|
205
|
+
(0, download_1.cleanupUserDownloads)(ownerKey);
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
// ignore cleanup errors
|
|
209
|
+
}
|
|
210
|
+
(0, tracing_1.cleanupTracing)(ownerKey);
|
|
211
|
+
clearDefaultSessionProfileClaimsForUser(ownerKey);
|
|
150
212
|
}
|
|
151
213
|
if (clearCanonical) {
|
|
152
214
|
canonicalProfiles.delete(key);
|
|
215
|
+
clearDefaultSessionProfileClaimsForUser(key);
|
|
153
216
|
const mutex = firstCreateMutexes.get(key);
|
|
154
217
|
if (mutex) {
|
|
155
218
|
mutex.resolve(false);
|
|
@@ -165,6 +228,12 @@ function cleanupSessionsForUserId(userId, reason, clearCanonical = true) {
|
|
|
165
228
|
for (const profileKey of profileKeysToDelete) {
|
|
166
229
|
sessionProfiles.delete(profileKey);
|
|
167
230
|
}
|
|
231
|
+
for (const [profileKey, mutex] of Array.from(sessionProfileCreateMutexes.entries())) {
|
|
232
|
+
if (mutex.userId === key) {
|
|
233
|
+
mutex.resolve(false);
|
|
234
|
+
sessionProfileCreateMutexes.delete(profileKey);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
168
237
|
}
|
|
169
238
|
userConcurrency.delete(key);
|
|
170
239
|
}
|
|
@@ -178,8 +247,33 @@ exports.MAX_TABS_PER_SESSION = CONFIG.maxTabsPerSession;
|
|
|
178
247
|
function normalizeUserId(userId) {
|
|
179
248
|
return String(userId);
|
|
180
249
|
}
|
|
250
|
+
function encodeKeyComponent(value) {
|
|
251
|
+
return Buffer.from(String(value), 'utf16le').toString('base64url');
|
|
252
|
+
}
|
|
253
|
+
function userSessionMapKey(userId) {
|
|
254
|
+
return `u:${encodeKeyComponent(normalizeUserId(userId))}`;
|
|
255
|
+
}
|
|
181
256
|
function sessionOverlayKey(userId, sessionKey) {
|
|
182
|
-
return
|
|
257
|
+
return `o:${encodeKeyComponent(normalizeUserId(userId))}:${encodeKeyComponent(sessionKey)}`;
|
|
258
|
+
}
|
|
259
|
+
function clearDefaultSessionProfileClaimsForUser(userId) {
|
|
260
|
+
const key = normalizeUserId(userId);
|
|
261
|
+
for (const [claimKey, claim] of Array.from(defaultSessionProfileClaims.entries())) {
|
|
262
|
+
if (claim.userId === key) {
|
|
263
|
+
defaultSessionProfileClaims.delete(claimKey);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
function isSessionMapKeyForUser(sessionMapKey, userKey) {
|
|
268
|
+
return sessionMapKey === userSessionMapKey(userKey) || sessionOwners.get(sessionMapKey) === userKey;
|
|
269
|
+
}
|
|
270
|
+
function isSessionMapKeyForCleanupKey(sessionMapKey, cleanupKey, allowInternalSessionKey = true) {
|
|
271
|
+
return (allowInternalSessionKey && sessionMapKey === cleanupKey) || isSessionMapKeyForUser(sessionMapKey, cleanupKey);
|
|
272
|
+
}
|
|
273
|
+
function isLaunchingSessionKeyForCleanupKey(sessionMapKey, cleanupKey, allowInternalSessionKey = true) {
|
|
274
|
+
return ((allowInternalSessionKey && sessionMapKey === cleanupKey) ||
|
|
275
|
+
sessionMapKey === userSessionMapKey(cleanupKey) ||
|
|
276
|
+
launchingSessionOwners.get(sessionMapKey) === cleanupKey);
|
|
183
277
|
}
|
|
184
278
|
// Backward compatible version - takes contextOverrides instead of session profile
|
|
185
279
|
function getSessionMapKey(userId, contextOverridesOrSessionKey, profileSignature) {
|
|
@@ -187,18 +281,94 @@ function getSessionMapKey(userId, contextOverridesOrSessionKey, profileSignature
|
|
|
187
281
|
if (typeof contextOverridesOrSessionKey === 'string') {
|
|
188
282
|
const sessionKey = contextOverridesOrSessionKey;
|
|
189
283
|
if (profileSignature) {
|
|
190
|
-
return
|
|
284
|
+
return `p:${encodeKeyComponent(normalizeUserId(userId))}:${encodeKeyComponent(sessionKey)}:${encodeKeyComponent(profileSignature)}`;
|
|
191
285
|
}
|
|
192
|
-
return
|
|
286
|
+
return `s:${encodeKeyComponent(normalizeUserId(userId))}:${encodeKeyComponent(sessionKey)}`;
|
|
193
287
|
}
|
|
194
288
|
// Old signature: (userId, contextOverrides) - backward compatibility
|
|
195
289
|
// This maintains the user-scoped behavior for existing routes
|
|
196
290
|
void contextOverridesOrSessionKey;
|
|
197
|
-
return
|
|
291
|
+
return userSessionMapKey(userId);
|
|
198
292
|
}
|
|
199
293
|
function getEstablishedSessionProfile(userId, sessionKey) {
|
|
200
294
|
return sessionProfiles.get(sessionOverlayKey(userId, sessionKey));
|
|
201
295
|
}
|
|
296
|
+
function hasDefaultSessionProfileRuntime(userId, sessionKey) {
|
|
297
|
+
const key = normalizeUserId(userId);
|
|
298
|
+
if (defaultSessionProfileClaims.has(sessionOverlayKey(key, sessionKey)))
|
|
299
|
+
return true;
|
|
300
|
+
const defaultSession = sessions.get(userSessionMapKey(key));
|
|
301
|
+
return defaultSession?.tabGroups.has(sessionKey) ?? false;
|
|
302
|
+
}
|
|
303
|
+
function claimDefaultSessionProfileRuntime(userId, sessionKey) {
|
|
304
|
+
if (getEstablishedSessionProfile(userId, sessionKey))
|
|
305
|
+
return false;
|
|
306
|
+
if (hasDefaultSessionProfileRuntime(userId, sessionKey))
|
|
307
|
+
return false;
|
|
308
|
+
const key = normalizeUserId(userId);
|
|
309
|
+
defaultSessionProfileClaims.set(sessionOverlayKey(key, sessionKey), { userId: key, sessionKey });
|
|
310
|
+
return true;
|
|
311
|
+
}
|
|
312
|
+
function clearDefaultSessionProfileClaim(userId, sessionKey) {
|
|
313
|
+
defaultSessionProfileClaims.delete(sessionOverlayKey(userId, sessionKey));
|
|
314
|
+
}
|
|
315
|
+
function contextOverridesFromProfile(profile) {
|
|
316
|
+
const overrides = {};
|
|
317
|
+
if (profile.locale !== undefined)
|
|
318
|
+
overrides.locale = profile.locale;
|
|
319
|
+
if (profile.timezoneId !== undefined)
|
|
320
|
+
overrides.timezoneId = profile.timezoneId;
|
|
321
|
+
if (profile.geolocation !== undefined)
|
|
322
|
+
overrides.geolocation = profile.geolocation;
|
|
323
|
+
if (profile.viewport !== undefined)
|
|
324
|
+
overrides.viewport = profile.viewport;
|
|
325
|
+
return Object.keys(overrides).length > 0 ? overrides : null;
|
|
326
|
+
}
|
|
327
|
+
function getSessionProfileLaunchSettings(userId, profileKey) {
|
|
328
|
+
const key = normalizeUserId(userId);
|
|
329
|
+
if (profileKey === userSessionMapKey(key)) {
|
|
330
|
+
const canonical = canonicalProfiles.get(key);
|
|
331
|
+
return canonical ? { contextOverrides: canonical.resolvedOverrides, proxy: null } : undefined;
|
|
332
|
+
}
|
|
333
|
+
for (const profile of sessionProfiles.values()) {
|
|
334
|
+
if (profile.userId !== key)
|
|
335
|
+
continue;
|
|
336
|
+
if (getSessionMapKey(key, profile.sessionKey, profile.signature) !== profileKey)
|
|
337
|
+
continue;
|
|
338
|
+
return {
|
|
339
|
+
contextOverrides: contextOverridesFromProfile(profile.resolvedProfile),
|
|
340
|
+
proxy: profile.resolvedProfile.proxy,
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
return undefined;
|
|
344
|
+
}
|
|
345
|
+
function resolveRuntimeSessionProfile(userId, sessionKey, contextOverrides) {
|
|
346
|
+
const userKey = normalizeUserId(userId);
|
|
347
|
+
if (!sessionKey) {
|
|
348
|
+
return {
|
|
349
|
+
sessionMapKey: userSessionMapKey(userKey),
|
|
350
|
+
profileKey: userSessionMapKey(userKey),
|
|
351
|
+
contextOverrides,
|
|
352
|
+
resolvedProxy: null,
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
const established = getEstablishedSessionProfile(userId, sessionKey);
|
|
356
|
+
if (!established) {
|
|
357
|
+
return {
|
|
358
|
+
sessionMapKey: userSessionMapKey(userKey),
|
|
359
|
+
profileKey: userSessionMapKey(userKey),
|
|
360
|
+
contextOverrides,
|
|
361
|
+
resolvedProxy: null,
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
const profileKey = getSessionMapKey(userId, sessionKey, established.signature);
|
|
365
|
+
return {
|
|
366
|
+
sessionMapKey: profileKey,
|
|
367
|
+
profileKey,
|
|
368
|
+
contextOverrides: contextOverridesFromProfile(established.resolvedProfile),
|
|
369
|
+
resolvedProxy: established.resolvedProfile.proxy,
|
|
370
|
+
};
|
|
371
|
+
}
|
|
202
372
|
function getCanonicalProfile(userId) {
|
|
203
373
|
return canonicalProfiles.get(normalizeUserId(userId));
|
|
204
374
|
}
|
|
@@ -310,12 +480,67 @@ function clearSessionProfile(userId, sessionKey) {
|
|
|
310
480
|
const key = sessionOverlayKey(userId, sessionKey);
|
|
311
481
|
sessionProfiles.delete(key);
|
|
312
482
|
}
|
|
483
|
+
function acquireSessionProfileCreateMutex(userId, sessionKey, signature) {
|
|
484
|
+
const key = sessionOverlayKey(userId, sessionKey);
|
|
485
|
+
const existing = sessionProfileCreateMutexes.get(key);
|
|
486
|
+
if (existing) {
|
|
487
|
+
return { acquired: false, wait: existing.promise };
|
|
488
|
+
}
|
|
489
|
+
let resolve;
|
|
490
|
+
const promise = new Promise((r) => {
|
|
491
|
+
resolve = r;
|
|
492
|
+
});
|
|
493
|
+
const mutex = {
|
|
494
|
+
userId: normalizeUserId(userId),
|
|
495
|
+
sessionKey,
|
|
496
|
+
signature,
|
|
497
|
+
promise,
|
|
498
|
+
resolve,
|
|
499
|
+
};
|
|
500
|
+
sessionProfileCreateMutexes.set(key, mutex);
|
|
501
|
+
let released = false;
|
|
502
|
+
return {
|
|
503
|
+
acquired: true,
|
|
504
|
+
release: (committed) => {
|
|
505
|
+
if (released)
|
|
506
|
+
return;
|
|
507
|
+
released = true;
|
|
508
|
+
const current = sessionProfileCreateMutexes.get(key);
|
|
509
|
+
if (current === mutex) {
|
|
510
|
+
current.resolve(committed);
|
|
511
|
+
sessionProfileCreateMutexes.delete(key);
|
|
512
|
+
}
|
|
513
|
+
},
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
async function waitForSessionProfileCreate(userId, sessionKey) {
|
|
517
|
+
const pending = sessionProfileCreateMutexes.get(sessionOverlayKey(userId, sessionKey));
|
|
518
|
+
if (pending) {
|
|
519
|
+
await pending.promise.catch(() => false);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
async function rollbackSessionProfileRuntime(userId, sessionKey, profileSignature) {
|
|
523
|
+
const overlayKey = sessionOverlayKey(userId, sessionKey);
|
|
524
|
+
const existingProfile = sessionProfiles.get(overlayKey);
|
|
525
|
+
if (existingProfile?.signature === profileSignature) {
|
|
526
|
+
sessionProfiles.delete(overlayKey);
|
|
527
|
+
}
|
|
528
|
+
const sessionMapKey = getSessionMapKey(userId, sessionKey, profileSignature);
|
|
529
|
+
const session = sessions.get(sessionMapKey);
|
|
530
|
+
if (session) {
|
|
531
|
+
unindexSessionTabs(session);
|
|
532
|
+
sessions.delete(sessionMapKey);
|
|
533
|
+
}
|
|
534
|
+
sessionOwners.delete(sessionMapKey);
|
|
535
|
+
launchingSessions.delete(sessionMapKey);
|
|
536
|
+
launchingSessionOwners.delete(sessionMapKey);
|
|
537
|
+
await context_pool_1.contextPool.closeContext(sessionMapKey).catch(() => { });
|
|
538
|
+
}
|
|
313
539
|
function getSessionsForUser(userId) {
|
|
314
540
|
if (userId === undefined || userId === null)
|
|
315
541
|
return [];
|
|
316
542
|
const key = normalizeUserId(userId);
|
|
317
|
-
|
|
318
|
-
return session ? [[key, session]] : [];
|
|
543
|
+
return Array.from(sessions.entries()).filter(([sessionKey]) => isSessionMapKeyForUser(sessionKey, key));
|
|
319
544
|
}
|
|
320
545
|
function getAllSessions() {
|
|
321
546
|
return sessions;
|
|
@@ -374,7 +599,7 @@ function findTabById(tabId, userId) {
|
|
|
374
599
|
const key = normalizeUserId(userId);
|
|
375
600
|
const indexedKey = tabSessionIndex.get(tabId);
|
|
376
601
|
if (indexedKey) {
|
|
377
|
-
if (indexedKey
|
|
602
|
+
if (!isSessionMapKeyForUser(indexedKey, key)) {
|
|
378
603
|
return null;
|
|
379
604
|
}
|
|
380
605
|
const session = sessions.get(indexedKey);
|
|
@@ -385,17 +610,18 @@ function findTabById(tabId, userId) {
|
|
|
385
610
|
}
|
|
386
611
|
tabSessionIndex.delete(tabId);
|
|
387
612
|
}
|
|
388
|
-
const
|
|
613
|
+
const defaultSessionKey = userSessionMapKey(key);
|
|
614
|
+
const session = sessions.get(defaultSessionKey);
|
|
389
615
|
if (!session)
|
|
390
616
|
return null;
|
|
391
617
|
const found = findTab(session, tabId);
|
|
392
618
|
if (found) {
|
|
393
|
-
tabSessionIndex.set(tabId,
|
|
394
|
-
return { sessionKey:
|
|
619
|
+
tabSessionIndex.set(tabId, defaultSessionKey);
|
|
620
|
+
return { sessionKey: defaultSessionKey, session, ...found };
|
|
395
621
|
}
|
|
396
622
|
return null;
|
|
397
623
|
}
|
|
398
|
-
function buildBrowserContextOptions(contextOverrides) {
|
|
624
|
+
function buildBrowserContextOptions(contextOverrides, hasSessionProxy = false) {
|
|
399
625
|
const resolved = contextOverrides || {};
|
|
400
626
|
const contextOptions = {
|
|
401
627
|
viewport: resolved.viewport || { width: 1280, height: 720 },
|
|
@@ -407,22 +633,21 @@ function buildBrowserContextOptions(contextOverrides) {
|
|
|
407
633
|
contextOverrides.geolocation !== undefined));
|
|
408
634
|
// With proxy+geoip, camoufox auto-configures locale/timezone/geo from proxy IP.
|
|
409
635
|
// If caller explicitly supplies overrides, apply them even when proxy is active.
|
|
410
|
-
if (!CONFIG.proxy.host || hasOverrides) {
|
|
636
|
+
if ((!CONFIG.proxy.host && !hasSessionProxy) || hasOverrides) {
|
|
411
637
|
contextOptions.locale = resolved.locale || 'en-US';
|
|
412
638
|
contextOptions.timezoneId = resolved.timezoneId || 'America/Los_Angeles';
|
|
413
639
|
contextOptions.geolocation = resolved.geolocation || { latitude: 37.7749, longitude: -122.4194 };
|
|
414
640
|
}
|
|
415
641
|
return contextOptions;
|
|
416
642
|
}
|
|
417
|
-
async function createStagedSession(userId, contextOverrides) {
|
|
418
|
-
const
|
|
643
|
+
async function createStagedSession(userId, contextOverrides, sessionKey) {
|
|
644
|
+
const runtimeProfile = resolveRuntimeSessionProfile(userId, sessionKey, contextOverrides);
|
|
419
645
|
if (context_pool_1.contextPool.size() >= exports.MAX_SESSIONS) {
|
|
420
646
|
throw new Error('Maximum concurrent sessions reached');
|
|
421
647
|
}
|
|
422
648
|
const generation = node_crypto_1.default.randomUUID();
|
|
423
|
-
const contextOptions = buildBrowserContextOptions(contextOverrides);
|
|
424
|
-
|
|
425
|
-
const entry = await context_pool_1.contextPool.ensureContext(key, key, contextOptions, null, true, generation);
|
|
649
|
+
const contextOptions = buildBrowserContextOptions(runtimeProfile.contextOverrides, !!runtimeProfile.resolvedProxy);
|
|
650
|
+
const entry = await context_pool_1.contextPool.ensureContext(runtimeProfile.profileKey, normalizeUserId(userId), contextOptions, runtimeProfile.resolvedProxy, true, generation);
|
|
426
651
|
const session = {
|
|
427
652
|
context: entry.context,
|
|
428
653
|
tabGroups: new Map(),
|
|
@@ -432,7 +657,7 @@ async function createStagedSession(userId, contextOverrides) {
|
|
|
432
657
|
}
|
|
433
658
|
function commitStagedFirstUse(userId, session, contextOverrides, tabInfo, generation) {
|
|
434
659
|
const key = normalizeUserId(userId);
|
|
435
|
-
const entry = context_pool_1.contextPool.getEntry(
|
|
660
|
+
const entry = context_pool_1.contextPool.getEntry(tabInfo.sessionMapKey);
|
|
436
661
|
if (!entry || entry.stagedGeneration !== generation)
|
|
437
662
|
return false;
|
|
438
663
|
if (!firstCreateMutexes.has(key) || canonicalProfiles.has(key)) {
|
|
@@ -441,7 +666,8 @@ function commitStagedFirstUse(userId, session, contextOverrides, tabInfo, genera
|
|
|
441
666
|
session.lastAccess = Date.now();
|
|
442
667
|
const group = getTabGroup(session, tabInfo.sessionKey);
|
|
443
668
|
group.set(tabInfo.tabId, tabInfo.tabState);
|
|
444
|
-
sessions.set(
|
|
669
|
+
sessions.set(tabInfo.sessionMapKey, session);
|
|
670
|
+
sessionOwners.set(tabInfo.sessionMapKey, key);
|
|
445
671
|
entry.staged = false;
|
|
446
672
|
entry.stagedGeneration = undefined;
|
|
447
673
|
indexTab(tabInfo.tabId, tabInfo.sessionMapKey);
|
|
@@ -450,24 +676,27 @@ function commitStagedFirstUse(userId, session, contextOverrides, tabInfo, genera
|
|
|
450
676
|
}
|
|
451
677
|
async function rollbackStagedFirstUse(userId, generation) {
|
|
452
678
|
const key = normalizeUserId(userId);
|
|
453
|
-
const entry = context_pool_1.contextPool.getEntry(key);
|
|
454
|
-
if (!(entry?.staged === true && entry.stagedGeneration === generation))
|
|
455
|
-
return;
|
|
456
679
|
try {
|
|
457
|
-
|
|
680
|
+
try {
|
|
681
|
+
(0, download_1.cleanupUserDownloads)(key);
|
|
682
|
+
}
|
|
683
|
+
catch {
|
|
684
|
+
// ignore cleanup errors
|
|
685
|
+
}
|
|
686
|
+
await context_pool_1.contextPool.closeStagedContextByUserId(key, generation);
|
|
458
687
|
}
|
|
459
|
-
|
|
460
|
-
|
|
688
|
+
finally {
|
|
689
|
+
rollbackCanonicalMutex(userId);
|
|
461
690
|
}
|
|
462
|
-
await context_pool_1.contextPool.closeStagedContext(key, generation);
|
|
463
|
-
rollbackCanonicalMutex(userId);
|
|
464
691
|
}
|
|
465
|
-
async function getSession(userId, contextOverrides) {
|
|
692
|
+
async function getSession(userId, contextOverrides, sessionKey) {
|
|
466
693
|
const key = normalizeUserId(userId);
|
|
467
|
-
|
|
468
|
-
const
|
|
694
|
+
await waitForLifecycleIdleClosure(key);
|
|
695
|
+
const runtimeProfile = resolveRuntimeSessionProfile(userId, sessionKey, contextOverrides);
|
|
696
|
+
let session = sessions.get(runtimeProfile.sessionMapKey);
|
|
697
|
+
const contextOptions = buildBrowserContextOptions(runtimeProfile.contextOverrides, !!runtimeProfile.resolvedProxy);
|
|
469
698
|
if (!session) {
|
|
470
|
-
const existingLaunch = launchingSessions.get(
|
|
699
|
+
const existingLaunch = launchingSessions.get(runtimeProfile.sessionMapKey);
|
|
471
700
|
if (existingLaunch) {
|
|
472
701
|
session = await existingLaunch;
|
|
473
702
|
session.lastAccess = Date.now();
|
|
@@ -477,25 +706,29 @@ async function getSession(userId, contextOverrides) {
|
|
|
477
706
|
throw new Error('Maximum concurrent sessions reached');
|
|
478
707
|
}
|
|
479
708
|
const launchPromise = (async () => {
|
|
480
|
-
const entry = await context_pool_1.contextPool.ensureContext(
|
|
709
|
+
const entry = await context_pool_1.contextPool.ensureContext(runtimeProfile.profileKey, key, contextOptions, runtimeProfile.resolvedProxy);
|
|
481
710
|
const created = { context: entry.context, tabGroups: new Map(), lastAccess: Date.now() };
|
|
482
|
-
sessions.set(
|
|
483
|
-
(
|
|
711
|
+
sessions.set(runtimeProfile.sessionMapKey, created);
|
|
712
|
+
sessionOwners.set(runtimeProfile.sessionMapKey, key);
|
|
713
|
+
(0, logging_1.log)('info', 'session created', { userId: key, sessionMapKey: runtimeProfile.sessionMapKey });
|
|
484
714
|
return created;
|
|
485
715
|
})();
|
|
486
|
-
|
|
716
|
+
launchingSessionOwners.set(runtimeProfile.sessionMapKey, key);
|
|
717
|
+
launchingSessions.set(runtimeProfile.sessionMapKey, launchPromise);
|
|
487
718
|
try {
|
|
488
719
|
session = await launchPromise;
|
|
489
720
|
}
|
|
490
721
|
finally {
|
|
491
|
-
launchingSessions.delete(
|
|
722
|
+
launchingSessions.delete(runtimeProfile.sessionMapKey);
|
|
723
|
+
launchingSessionOwners.delete(runtimeProfile.sessionMapKey);
|
|
492
724
|
}
|
|
493
725
|
}
|
|
494
726
|
else {
|
|
495
727
|
// Re-resolve context on each access; ContextPool de-dupes launches and detects unexpected closes.
|
|
496
|
-
const entry = await context_pool_1.contextPool.ensureContext(
|
|
728
|
+
const entry = await context_pool_1.contextPool.ensureContext(runtimeProfile.profileKey, key, contextOptions, runtimeProfile.resolvedProxy);
|
|
497
729
|
session.context = entry.context;
|
|
498
730
|
session.lastAccess = Date.now();
|
|
731
|
+
sessionOwners.set(runtimeProfile.sessionMapKey, key);
|
|
499
732
|
}
|
|
500
733
|
// For newly created sessions, lastAccess/context are already set.
|
|
501
734
|
session.lastAccess = Date.now();
|
|
@@ -510,38 +743,56 @@ function unindexTab(tabId) {
|
|
|
510
743
|
}
|
|
511
744
|
function clearAllState() {
|
|
512
745
|
sessions.clear();
|
|
746
|
+
sessionOwners.clear();
|
|
747
|
+
launchingSessions.clear();
|
|
748
|
+
launchingSessionOwners.clear();
|
|
513
749
|
tabSessionIndex.clear();
|
|
514
750
|
canonicalProfiles.clear();
|
|
515
751
|
sessionProfiles.clear();
|
|
752
|
+
defaultSessionProfileClaims.clear();
|
|
753
|
+
for (const [, mutex] of sessionProfileCreateMutexes)
|
|
754
|
+
mutex.resolve(false);
|
|
755
|
+
sessionProfileCreateMutexes.clear();
|
|
756
|
+
for (const [, state] of lifecycleIdleClosures)
|
|
757
|
+
state.resolve();
|
|
758
|
+
lifecycleIdleClosures.clear();
|
|
516
759
|
for (const [, mutex] of firstCreateMutexes)
|
|
517
760
|
mutex.resolve(false);
|
|
518
761
|
firstCreateMutexes.clear();
|
|
519
762
|
(0, tab_1.clearAllTabLocks)();
|
|
520
763
|
userConcurrency.clear();
|
|
521
764
|
}
|
|
522
|
-
async function closeSessionsForUser(userId) {
|
|
765
|
+
async function closeSessionsForUser(userId, options = {}) {
|
|
523
766
|
const key = normalizeUserId(userId);
|
|
524
767
|
await context_pool_1.contextPool.closeStagedContextByUserId(key).catch(() => { });
|
|
525
768
|
await context_pool_1.contextPool.closeContextByUserId(key).catch(() => { });
|
|
526
|
-
cleanupSessionsForUserId(key, 'explicit_close');
|
|
769
|
+
cleanupSessionsForUserId(key, 'explicit_close', options.clearProfiles ?? true, { allowInternalSessionKey: false });
|
|
527
770
|
}
|
|
528
771
|
async function closeAllSessions() {
|
|
529
772
|
await context_pool_1.contextPool.closeAll().catch(() => { });
|
|
530
|
-
for (const [
|
|
531
|
-
|
|
773
|
+
for (const [sessionKey, session] of sessions) {
|
|
774
|
+
const ownerUserId = sessionOwners.get(sessionKey) ?? sessionKey;
|
|
775
|
+
void (0, vnc_1.stopVnc)(ownerUserId).catch(() => { });
|
|
532
776
|
unindexSessionTabs(session);
|
|
533
|
-
sessions.delete(
|
|
534
|
-
|
|
777
|
+
sessions.delete(sessionKey);
|
|
778
|
+
sessionOwners.delete(sessionKey);
|
|
779
|
+
(0, tracing_1.cleanupTracing)(ownerUserId);
|
|
535
780
|
try {
|
|
536
|
-
(0, download_1.cleanupUserDownloads)(
|
|
781
|
+
(0, download_1.cleanupUserDownloads)(ownerUserId);
|
|
537
782
|
}
|
|
538
783
|
catch {
|
|
539
784
|
// ignore
|
|
540
785
|
}
|
|
541
786
|
}
|
|
542
787
|
launchingSessions.clear();
|
|
788
|
+
launchingSessionOwners.clear();
|
|
789
|
+
sessionOwners.clear();
|
|
543
790
|
canonicalProfiles.clear();
|
|
544
791
|
sessionProfiles.clear();
|
|
792
|
+
defaultSessionProfileClaims.clear();
|
|
793
|
+
for (const [, mutex] of sessionProfileCreateMutexes)
|
|
794
|
+
mutex.resolve(false);
|
|
795
|
+
sessionProfileCreateMutexes.clear();
|
|
545
796
|
for (const [, mutex] of firstCreateMutexes)
|
|
546
797
|
mutex.resolve(false);
|
|
547
798
|
firstCreateMutexes.clear();
|
|
@@ -554,12 +805,15 @@ function startCleanupInterval() {
|
|
|
554
805
|
const now = Date.now();
|
|
555
806
|
for (const [sessionKey, session] of sessions) {
|
|
556
807
|
if (now - session.lastAccess > exports.SESSION_TIMEOUT_MS) {
|
|
808
|
+
const ownerUserId = sessionOwners.get(sessionKey) ?? sessionKey;
|
|
557
809
|
// Persistent profile is preserved on disk; closing the context frees resources.
|
|
558
|
-
context_pool_1.contextPool.
|
|
810
|
+
context_pool_1.contextPool.closeContext(sessionKey).catch(() => { });
|
|
559
811
|
unindexSessionTabs(session);
|
|
812
|
+
clearDefaultSessionProfileClaimsForUser(ownerUserId);
|
|
560
813
|
sessions.delete(sessionKey);
|
|
561
|
-
|
|
562
|
-
(0,
|
|
814
|
+
sessionOwners.delete(sessionKey);
|
|
815
|
+
(0, tracing_1.cleanupTracing)(ownerUserId);
|
|
816
|
+
(0, logging_1.log)('info', 'session expired', { userId: ownerUserId, sessionKey });
|
|
563
817
|
}
|
|
564
818
|
}
|
|
565
819
|
}, 60_000);
|
|
@@ -582,46 +836,48 @@ function stopCleanupInterval() {
|
|
|
582
836
|
async function runLifecycleIdleCleanup(sessionSnapshot, contextSnapshot, cleanupStartedMs) {
|
|
583
837
|
const closedUsers = [];
|
|
584
838
|
// Use the provided snapshots to avoid race with new context creation
|
|
585
|
-
const
|
|
586
|
-
for (const [
|
|
587
|
-
const tabCount = countTotalTabsForSessions([[
|
|
839
|
+
const sessionKeysToCleanup = new Set();
|
|
840
|
+
for (const [sessionKey, session] of sessionSnapshot.entries()) {
|
|
841
|
+
const tabCount = countTotalTabsForSessions([[sessionKey, session]]);
|
|
588
842
|
// Only cleanup sessions with zero tabs
|
|
589
843
|
if (tabCount === 0) {
|
|
590
|
-
|
|
844
|
+
sessionKeysToCleanup.add(sessionKey);
|
|
591
845
|
}
|
|
592
846
|
}
|
|
593
847
|
// Close only the specific contexts from the snapshot that were created before cleanup started
|
|
594
848
|
const contextsToClose = [];
|
|
595
849
|
for (const [profileKey, entry] of contextSnapshot.entries()) {
|
|
596
850
|
// Skip staged, launching, or newly-created contexts
|
|
597
|
-
if (
|
|
851
|
+
if (sessionKeysToCleanup.has(profileKey) && !entry.staged && !entry.launching && entry.createdAt < cleanupStartedMs) {
|
|
598
852
|
contextsToClose.push({ profileKey, createdAt: entry.createdAt, lastAccess: entry.lastAccess });
|
|
599
853
|
}
|
|
600
854
|
}
|
|
601
855
|
// Close the specific contexts from the snapshot
|
|
602
|
-
const
|
|
856
|
+
const actuallyClosedSessionKeys = new Set();
|
|
603
857
|
for (const { profileKey, createdAt, lastAccess } of contextsToClose) {
|
|
858
|
+
const entry = contextSnapshot.get(profileKey);
|
|
859
|
+
const releaseIdleClosure = entry ? beginLifecycleIdleClosure(entry.userId) : null;
|
|
604
860
|
try {
|
|
605
861
|
await context_pool_1.contextPool.closeContextIfMatches(profileKey, createdAt, lastAccess);
|
|
606
862
|
// Verify the context was actually closed (not skipped due to reuse)
|
|
607
863
|
const stillExists = context_pool_1.contextPool.getEntry(profileKey);
|
|
608
864
|
if (!stillExists) {
|
|
609
|
-
// Context was actually closed, mark
|
|
610
|
-
|
|
611
|
-
if (entry) {
|
|
612
|
-
actuallyClosedUsers.add(entry.userId);
|
|
613
|
-
}
|
|
865
|
+
// Context was actually closed, mark this exact session/profile key for cleanup.
|
|
866
|
+
actuallyClosedSessionKeys.add(profileKey);
|
|
614
867
|
}
|
|
615
868
|
}
|
|
616
869
|
catch (err) {
|
|
617
870
|
const message = err instanceof Error ? err.message : String(err);
|
|
618
871
|
(0, logging_1.log)('error', 'idle cleanup failed to close context', { profileKey, error: message });
|
|
619
872
|
}
|
|
873
|
+
finally {
|
|
874
|
+
releaseIdleClosure?.();
|
|
875
|
+
}
|
|
620
876
|
}
|
|
621
|
-
// Clean up session data ONLY for
|
|
622
|
-
for (const
|
|
623
|
-
cleanupSessionsForUserId(
|
|
624
|
-
closedUsers.push(
|
|
877
|
+
// Clean up session data ONLY for session/profile keys whose contexts were actually closed.
|
|
878
|
+
for (const sessionKey of actuallyClosedSessionKeys) {
|
|
879
|
+
cleanupSessionsForUserId(sessionKey, 'idle_cleanup', false);
|
|
880
|
+
closedUsers.push(sessionKey);
|
|
625
881
|
}
|
|
626
882
|
return { closedUsers };
|
|
627
883
|
}
|