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.
@@ -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.delete(key);
137
- void (0, vnc_1.stopVnc)(key).catch(() => { });
138
- try {
139
- (0, download_1.cleanupUserDownloads)(key);
140
- }
141
- catch {
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
- (0, tracing_1.cleanupTracing)(key);
145
- const session = sessions.get(key);
146
- if (session) {
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(key);
149
- (0, logging_1.log)('info', 'session cleaned up', { userId: key, reason });
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 `${normalizeUserId(userId)}::${sessionKey}`;
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 `${normalizeUserId(userId)}::${sessionKey}::${profileSignature}`;
284
+ return `p:${encodeKeyComponent(normalizeUserId(userId))}:${encodeKeyComponent(sessionKey)}:${encodeKeyComponent(profileSignature)}`;
191
285
  }
192
- return `${normalizeUserId(userId)}::${sessionKey}`;
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 normalizeUserId(userId);
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
- const session = sessions.get(key);
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 !== key) {
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 session = sessions.get(key);
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, key);
394
- return { sessionKey: key, session, ...found };
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 key = normalizeUserId(userId);
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
- // For backward compatibility, use userId as profileKey when no session profile is provided
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(key);
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(key, session);
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
- (0, download_1.cleanupUserDownloads)(key);
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
- catch {
460
- // ignore cleanup errors
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
- let session = sessions.get(key);
468
- const contextOptions = buildBrowserContextOptions(contextOverrides);
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(key);
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(key, key, contextOptions);
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(key, created);
483
- (0, logging_1.log)('info', 'session created', { userId: key });
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
- launchingSessions.set(key, launchPromise);
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(key);
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(key, key, contextOptions);
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 [userId, session] of sessions) {
531
- void (0, vnc_1.stopVnc)(userId).catch(() => { });
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(userId);
534
- (0, tracing_1.cleanupTracing)(userId);
777
+ sessions.delete(sessionKey);
778
+ sessionOwners.delete(sessionKey);
779
+ (0, tracing_1.cleanupTracing)(ownerUserId);
535
780
  try {
536
- (0, download_1.cleanupUserDownloads)(userId);
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.closeContextByUserId(sessionKey).catch(() => { });
810
+ context_pool_1.contextPool.closeContext(sessionKey).catch(() => { });
559
811
  unindexSessionTabs(session);
812
+ clearDefaultSessionProfileClaimsForUser(ownerUserId);
560
813
  sessions.delete(sessionKey);
561
- (0, tracing_1.cleanupTracing)(sessionKey);
562
- (0, logging_1.log)('info', 'session expired', { userId: sessionKey });
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 usersToCleanup = new Set();
586
- for (const [userId, session] of sessionSnapshot.entries()) {
587
- const tabCount = countTotalTabsForSessions([[userId, session]]);
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
- usersToCleanup.add(userId);
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 (usersToCleanup.has(entry.userId) && !entry.staged && !entry.launching && entry.createdAt < cleanupStartedMs) {
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 actuallyClosedUsers = new Set();
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 user for session cleanup
610
- const entry = contextSnapshot.get(profileKey);
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 users whose contexts were actually closed
622
- for (const userId of actuallyClosedUsers) {
623
- cleanupSessionsForUserId(userId, 'idle_cleanup', false);
624
- closedUsers.push(userId);
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
  }