camofox-browser 2.1.1 → 2.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/CHANGELOG.md +126 -0
  2. package/README.md +304 -33
  3. package/dist/src/cli/commands/content.d.ts.map +1 -1
  4. package/dist/src/cli/commands/content.js +37 -0
  5. package/dist/src/cli/commands/content.js.map +1 -1
  6. package/dist/src/cli/commands/core.d.ts.map +1 -1
  7. package/dist/src/cli/commands/core.js +21 -4
  8. package/dist/src/cli/commands/core.js.map +1 -1
  9. package/dist/src/cli/commands/interaction.d.ts.map +1 -1
  10. package/dist/src/cli/commands/interaction.js +5 -14
  11. package/dist/src/cli/commands/interaction.js.map +1 -1
  12. package/dist/src/cli/commands/navigation.d.ts.map +1 -1
  13. package/dist/src/cli/commands/navigation.js +12 -6
  14. package/dist/src/cli/commands/navigation.js.map +1 -1
  15. package/dist/src/cli/commands/server.d.ts.map +1 -1
  16. package/dist/src/cli/commands/server.js +9 -3
  17. package/dist/src/cli/commands/server.js.map +1 -1
  18. package/dist/src/cli/commands/session.d.ts.map +1 -1
  19. package/dist/src/cli/commands/session.js +23 -5
  20. package/dist/src/cli/commands/session.js.map +1 -1
  21. package/dist/src/cli/server/manager.d.ts +1 -0
  22. package/dist/src/cli/server/manager.d.ts.map +1 -1
  23. package/dist/src/cli/server/manager.js +7 -12
  24. package/dist/src/cli/server/manager.js.map +1 -1
  25. package/dist/src/middleware/lifecycle-activity.d.ts +9 -0
  26. package/dist/src/middleware/lifecycle-activity.d.ts.map +1 -0
  27. package/dist/src/middleware/lifecycle-activity.js +21 -0
  28. package/dist/src/middleware/lifecycle-activity.js.map +1 -0
  29. package/dist/src/openapi/spec.d.ts +4 -0
  30. package/dist/src/openapi/spec.d.ts.map +1 -0
  31. package/dist/src/openapi/spec.js +730 -0
  32. package/dist/src/openapi/spec.js.map +1 -0
  33. package/dist/src/routes/core.d.ts.map +1 -1
  34. package/dist/src/routes/core.js +428 -53
  35. package/dist/src/routes/core.js.map +1 -1
  36. package/dist/src/routes/docs.d.ts +3 -0
  37. package/dist/src/routes/docs.d.ts.map +1 -0
  38. package/dist/src/routes/docs.js +23 -0
  39. package/dist/src/routes/docs.js.map +1 -0
  40. package/dist/src/routes/openclaw.d.ts.map +1 -1
  41. package/dist/src/routes/openclaw.js +244 -90
  42. package/dist/src/routes/openclaw.js.map +1 -1
  43. package/dist/src/server.js +55 -4
  44. package/dist/src/server.js.map +1 -1
  45. package/dist/src/services/context-pool.d.ts +19 -3
  46. package/dist/src/services/context-pool.d.ts.map +1 -1
  47. package/dist/src/services/context-pool.js +248 -65
  48. package/dist/src/services/context-pool.js.map +1 -1
  49. package/dist/src/services/download.d.ts +2 -0
  50. package/dist/src/services/download.d.ts.map +1 -1
  51. package/dist/src/services/download.js +110 -80
  52. package/dist/src/services/download.js.map +1 -1
  53. package/dist/src/services/lifecycle-controller.d.ts +40 -0
  54. package/dist/src/services/lifecycle-controller.d.ts.map +1 -0
  55. package/dist/src/services/lifecycle-controller.js +106 -0
  56. package/dist/src/services/lifecycle-controller.js.map +1 -0
  57. package/dist/src/services/resource-extractor.d.ts +1 -0
  58. package/dist/src/services/resource-extractor.d.ts.map +1 -1
  59. package/dist/src/services/resource-extractor.js +7 -0
  60. package/dist/src/services/resource-extractor.js.map +1 -1
  61. package/dist/src/services/session.d.ts +84 -2
  62. package/dist/src/services/session.d.ts.map +1 -1
  63. package/dist/src/services/session.js +349 -47
  64. package/dist/src/services/session.js.map +1 -1
  65. package/dist/src/services/structured-extractor.d.ts +39 -0
  66. package/dist/src/services/structured-extractor.d.ts.map +1 -0
  67. package/dist/src/services/structured-extractor.js +487 -0
  68. package/dist/src/services/structured-extractor.js.map +1 -0
  69. package/dist/src/services/tab.d.ts +30 -3
  70. package/dist/src/services/tab.d.ts.map +1 -1
  71. package/dist/src/services/tab.js +872 -124
  72. package/dist/src/services/tab.js.map +1 -1
  73. package/dist/src/services/tracing.d.ts +7 -0
  74. package/dist/src/services/tracing.d.ts.map +1 -1
  75. package/dist/src/services/tracing.js +162 -19
  76. package/dist/src/services/tracing.js.map +1 -1
  77. package/dist/src/services/vnc.d.ts.map +1 -1
  78. package/dist/src/services/vnc.js +5 -3
  79. package/dist/src/services/vnc.js.map +1 -1
  80. package/dist/src/services/youtube.js +1 -1
  81. package/dist/src/services/youtube.js.map +1 -1
  82. package/dist/src/types.d.ts +71 -1
  83. package/dist/src/types.d.ts.map +1 -1
  84. package/dist/src/utils/config.d.ts +79 -3
  85. package/dist/src/utils/config.d.ts.map +1 -1
  86. package/dist/src/utils/config.js +145 -3
  87. package/dist/src/utils/config.js.map +1 -1
  88. package/dist/src/utils/presets.d.ts.map +1 -1
  89. package/dist/src/utils/presets.js +3 -1
  90. package/dist/src/utils/presets.js.map +1 -1
  91. package/dist/src/utils/proxy-profiles.d.ts +18 -0
  92. package/dist/src/utils/proxy-profiles.d.ts.map +1 -0
  93. package/dist/src/utils/proxy-profiles.js +197 -0
  94. package/dist/src/utils/proxy-profiles.js.map +1 -0
  95. package/dist/src/utils/sidecar-version.d.ts +12 -0
  96. package/dist/src/utils/sidecar-version.d.ts.map +1 -0
  97. package/dist/src/utils/sidecar-version.js +63 -0
  98. package/dist/src/utils/sidecar-version.js.map +1 -0
  99. package/dist/tsconfig.tsbuildinfo +1 -1
  100. package/openclaw.plugin.json +39 -0
  101. package/package.json +16 -4
  102. package/plugin.ts +949 -0
@@ -6,22 +6,28 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  const express_1 = __importDefault(require("express"));
7
7
  const core_1 = __importDefault(require("./routes/core"));
8
8
  const openclaw_1 = __importDefault(require("./routes/openclaw"));
9
+ const docs_1 = __importDefault(require("./routes/docs"));
9
10
  const errors_1 = require("./middleware/errors");
10
11
  const logging_1 = require("./middleware/logging");
12
+ const lifecycle_activity_1 = require("./middleware/lifecycle-activity");
11
13
  const config_1 = require("./utils/config");
12
14
  const browser_1 = require("./services/browser");
13
15
  const context_pool_1 = require("./services/context-pool");
16
+ const lifecycle_controller_1 = require("./services/lifecycle-controller");
14
17
  const session_1 = require("./services/session");
15
18
  const download_1 = require("./services/download");
16
19
  const health_1 = require("./services/health");
17
20
  const youtube_1 = require("./services/youtube");
18
21
  const vnc_1 = require("./services/vnc");
19
22
  const CONFIG = (0, config_1.loadConfig)();
23
+ (0, config_1.assertServerExposureSafety)(CONFIG);
20
24
  const app = (0, express_1.default)();
21
25
  app.use(express_1.default.json({ limit: '100kb' }));
22
26
  app.use(logging_1.loggingMiddleware);
27
+ app.use((0, lifecycle_activity_1.createLifecycleActivityMiddleware)(lifecycle_controller_1.lifecycleController));
23
28
  app.use(core_1.default);
24
29
  app.use(openclaw_1.default);
30
+ app.use(docs_1.default);
25
31
  // Fallback error middleware (routes largely handle their own errors to preserve legacy behavior)
26
32
  app.use((err, _req, res, _next) => {
27
33
  const anyErr = err;
@@ -44,6 +50,17 @@ void (0, youtube_1.detectYtDlp)((message) => {
44
50
  (0, logging_1.log)('info', 'yt-dlp detection', { message: String(message) });
45
51
  });
46
52
  let shuttingDown = false;
53
+ async function cleanupDaemonPidFileFromEnv() {
54
+ const pidFile = process.env.CAMOFOX_SERVER_PID_FILE;
55
+ if (!pidFile)
56
+ return;
57
+ try {
58
+ await require('node:fs/promises').rm(pidFile, { force: true });
59
+ }
60
+ catch {
61
+ // Ignore cleanup errors
62
+ }
63
+ }
47
64
  async function gracefulShutdown(signal) {
48
65
  if (shuttingDown)
49
66
  return;
@@ -70,12 +87,13 @@ async function gracefulShutdown(signal) {
70
87
  healthProbeInterval = null;
71
88
  }
72
89
  await (0, browser_1.closeBrowser)().catch(() => { });
90
+ await cleanupDaemonPidFileFromEnv();
73
91
  process.exit(0);
74
92
  }
75
93
  process.on('SIGTERM', () => void gracefulShutdown('SIGTERM'));
76
94
  process.on('SIGINT', () => void gracefulShutdown('SIGINT'));
77
- server = app.listen(PORT, () => {
78
- (0, logging_1.log)('info', 'server started', { port: PORT, pid: process.pid, nodeVersion: process.version });
95
+ server = app.listen(PORT, CONFIG.host, () => {
96
+ (0, logging_1.log)('info', 'server started', { port: PORT, host: CONFIG.host, pid: process.pid, nodeVersion: process.version });
79
97
  (0, logging_1.log)('info', 'using persistent profiles', { profilesDir: CONFIG.profilesDir });
80
98
  (0, health_1.resetHealth)();
81
99
  healthProbeInterval = setInterval(() => {
@@ -91,9 +109,42 @@ server = app.listen(PORT, () => {
91
109
  });
92
110
  }, CONFIG.healthProbeIntervalMs);
93
111
  healthProbeInterval.unref();
112
+ // Lifecycle idle cleanup check
113
+ setInterval(async () => {
114
+ const now = Date.now();
115
+ const sessionSnapshot = (0, session_1.getLifecycleSessionSnapshot)();
116
+ const contextSnapshot = context_pool_1.contextPool.getLifecycleSnapshot();
117
+ lifecycle_controller_1.lifecycleController.syncLiveState({
118
+ liveSessions: sessionSnapshot.liveSessions,
119
+ liveTabs: sessionSnapshot.liveTabs,
120
+ launchingContexts: contextSnapshot.launchingContexts,
121
+ stagedCreates: sessionSnapshot.stagedCreates,
122
+ });
123
+ // Stage 2: check for idle exit after successful cleanup
124
+ if (lifecycle_controller_1.lifecycleController.shouldExit(now)) {
125
+ (0, logging_1.log)('info', 'idle exit started');
126
+ await gracefulShutdown('IDLE_TIMEOUT');
127
+ return;
128
+ }
129
+ if (lifecycle_controller_1.lifecycleController.shouldRunCleanup(now)) {
130
+ lifecycle_controller_1.lifecycleController.markCleanupStarted(now);
131
+ // Snapshot sessions and contexts atomically BEFORE async cleanup work
132
+ const sessionsToCheck = (0, session_1.getSessionsSnapshot)();
133
+ const contextsToCheck = context_pool_1.contextPool.getPoolEntries();
134
+ try {
135
+ const result = await (0, session_1.runLifecycleIdleCleanup)(sessionsToCheck, contextsToCheck, now);
136
+ lifecycle_controller_1.lifecycleController.markCleanupFinished('success', Date.now());
137
+ (0, logging_1.log)('info', 'idle cleanup completed', result);
138
+ }
139
+ catch (error) {
140
+ lifecycle_controller_1.lifecycleController.markCleanupFinished('failed', Date.now());
141
+ (0, logging_1.log)('error', 'idle cleanup failed', { error: (0, errors_1.safeError)(error) });
142
+ }
143
+ }
144
+ }, 250);
94
145
  if (!CONFIG.apiKey) {
95
- console.warn('[camofox] ⚠️ CAMOFOX_API_KEY not set — all endpoints are open without authentication.');
96
- console.warn('[camofox] Set CAMOFOX_API_KEY for production/network-exposed deployments.');
146
+ console.warn('[camofox] ⚠️ CAMOFOX_API_KEY not set — protected endpoints are only intended for loopback-only deployments.');
147
+ console.warn('[camofox] Set CAMOFOX_API_KEY before binding CAMOFOX_HOST beyond localhost.');
97
148
  }
98
149
  });
99
150
  server.on('error', (err) => {
@@ -1 +1 @@
1
- {"version":3,"file":"server.js","sourceRoot":"","sources":["../../src/server.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../../src/server.ts"],"names":[],"mappings":";;;;;AAEA,sDAA8B;AAE9B,yDAAuC;AACvC,iEAA+C;AAC/C,yDAAuC;AACvC,gDAAsE;AACtE,kDAAgF;AAChF,wEAAoF;AACpF,2CAAwE;AACxE,gDAAkD;AAClD,0DAAsD;AACtD,0EAAsE;AACtE,gDAS4B;AAC5B,kDAG6B;AAC7B,8CAA+E;AAC/E,gDAAiD;AACjD,wCAA4C;AAE5C,MAAM,MAAM,GAAG,IAAA,mBAAU,GAAE,CAAC;AAC5B,IAAA,mCAA0B,EAAC,MAAM,CAAC,CAAC;AAEnC,MAAM,GAAG,GAAG,IAAA,iBAAO,GAAE,CAAC;AACtB,GAAG,CAAC,GAAG,CAAC,iBAAO,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC;AAC1C,GAAG,CAAC,GAAG,CAAC,2BAAiB,CAAC,CAAC;AAC3B,GAAG,CAAC,GAAG,CAAC,IAAA,sDAAiC,EAAC,0CAAmB,CAAC,CAAC,CAAC;AAEhE,GAAG,CAAC,GAAG,CAAC,cAAU,CAAC,CAAC;AACpB,GAAG,CAAC,GAAG,CAAC,kBAAc,CAAC,CAAC;AACxB,GAAG,CAAC,GAAG,CAAC,cAAU,CAAC,CAAC;AAEpB,iGAAiG;AACjG,GAAG,CAAC,GAAG,CAAC,CAAC,GAAY,EAAE,IAAqB,EAAE,GAAqB,EAAE,KAA2B,EAAE,EAAE;IACnG,MAAM,MAAM,GAAG,GAA+C,CAAC;IAC/D,MAAM,MAAM,GAAG,MAAM,CAAC,UAAU,IAAI,MAAM,CAAC,MAAM,IAAI,GAAG,CAAC;IACzD,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,IAAA,kBAAS,EAAC,GAAG,CAAC,EAAE,CAAC,CAAC;AACpD,CAAC,CAAC,CAAC;AAEH,IAAA,6BAAoB,GAAE,CAAC;AAEvB,IAAA,8BAA2B,GAAE,CAAC;AAC9B,IAAA,+BAA4B,EAAC,MAAM,CAAC,aAAa,CAAC,CAAC;AAEnD,IAAA,0BAAgB,EAAC,GAAG,EAAE;IACrB,MAAM,QAAQ,GAAG,IAAA,wBAAc,GAAE,CAAC,IAAI,CAAC;IACvC,MAAM,IAAI,GAAG,IAAA,mCAAyB,GAAE,CAAC;IACzC,MAAM,gBAAgB,GAAG,0BAAW,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;IAChD,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC,EAAE,aAAa,EAAE,CAAC,EAAE,aAAa,EAAE,CAAC,EAAE,gBAAgB,EAAE,CAAC;AAC9F,CAAC,CAAC,CAAC;AAEH,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC;AACzB,IAAI,MAAc,CAAC;AACnB,IAAI,mBAAmB,GAA0B,IAAI,CAAC;AAEtD,KAAK,IAAA,qBAAW,EAAC,CAAC,OAAgB,EAAE,EAAE;IACrC,IAAA,aAAG,EAAC,MAAM,EAAE,kBAAkB,EAAE,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;AAC/D,CAAC,CAAC,CAAC;AAEH,IAAI,YAAY,GAAG,KAAK,CAAC;AAEzB,KAAK,UAAU,2BAA2B;IACzC,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,uBAAuB,CAAC;IACpD,IAAI,CAAC,OAAO;QAAE,OAAO;IACrB,IAAI,CAAC;QACJ,MAAM,OAAO,CAAC,kBAAkB,CAAC,CAAC,EAAE,CAAC,OAAO,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;IAChE,CAAC;IAAC,MAAM,CAAC;QACR,wBAAwB;IACzB,CAAC;AACF,CAAC;AAED,KAAK,UAAU,gBAAgB,CAAC,MAAc;IAC7C,IAAI,YAAY;QAAE,OAAO;IACzB,YAAY,GAAG,IAAI,CAAC;IACpB,IAAA,sBAAa,EAAC,IAAI,CAAC,CAAC;IACpB,IAAA,aAAG,EAAC,MAAM,EAAE,eAAe,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC;IAEzC,MAAM,YAAY,GAAG,UAAU,CAAC,GAAG,EAAE;QACpC,IAAA,aAAG,EAAC,OAAO,EAAE,kCAAkC,CAAC,CAAC;QACjD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACjB,CAAC,EAAE,KAAK,CAAC,CAAC;IACV,YAAY,CAAC,KAAK,EAAE,CAAC;IAErB,IAAI,CAAC;QACJ,MAAM,CAAC,KAAK,EAAE,CAAC;IAChB,CAAC;IAAC,MAAM,CAAC;QACR,SAAS;IACV,CAAC;IAED,MAAM,IAAA,0BAAgB,GAAE,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;IACzC,IAAA,gBAAU,GAAE,CAAC;IACb,IAAA,6BAA0B,GAAE,CAAC;IAC7B,IAAA,8BAA2B,GAAE,CAAC;IAC9B,IAAI,mBAAmB,EAAE,CAAC;QACzB,aAAa,CAAC,mBAAmB,CAAC,CAAC;QACnC,mBAAmB,GAAG,IAAI,CAAC;IAC5B,CAAC;IACD,MAAM,IAAA,sBAAY,GAAE,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;IACrC,MAAM,2BAA2B,EAAE,CAAC;IACpC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AACjB,CAAC;AAED,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,GAAG,EAAE,CAAC,KAAK,gBAAgB,CAAC,SAAS,CAAC,CAAC,CAAC;AAC9D,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,GAAG,EAAE,CAAC,KAAK,gBAAgB,CAAC,QAAQ,CAAC,CAAC,CAAC;AAE5D,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE,GAAG,EAAE;IAC3C,IAAA,aAAG,EAAC,MAAM,EAAE,gBAAgB,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE,GAAG,EAAE,OAAO,CAAC,GAAG,EAAE,WAAW,EAAE,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC;IACjH,IAAA,aAAG,EAAC,MAAM,EAAE,2BAA2B,EAAE,EAAE,WAAW,EAAE,MAAM,CAAC,WAAW,EAAE,CAAC,CAAC;IAC9E,IAAA,oBAAW,GAAE,CAAC;IACd,mBAAmB,GAAG,WAAW,CAAC,GAAG,EAAE;QACtC,MAAM,MAAM,GAAG,IAAA,uBAAc,GAAE,CAAC;QAChC,IAAI,MAAM,CAAC,YAAY,IAAI,MAAM,CAAC,SAAS,GAAG,CAAC;YAAE,OAAO;QAExD,MAAM,gBAAgB,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,MAAM,CAAC,iBAAiB,CAAC;QAC/D,IAAI,gBAAgB,GAAG,OAAO;YAAE,OAAO;QAEvC,IAAA,aAAG,EAAC,MAAM,EAAE,sDAAsD,EAAE;YACnE,kBAAkB,EAAE,gBAAgB;YACpC,mBAAmB,EAAE,MAAM,CAAC,sBAAsB;SAClD,CAAC,CAAC;IACJ,CAAC,EAAE,MAAM,CAAC,qBAAqB,CAAC,CAAC;IACjC,mBAAmB,CAAC,KAAK,EAAE,CAAC;IAE5B,+BAA+B;IAC/B,WAAW,CAAC,KAAK,IAAI,EAAE;QACtB,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACvB,MAAM,eAAe,GAAG,IAAA,qCAA2B,GAAE,CAAC;QACtD,MAAM,eAAe,GAAG,0BAAW,CAAC,oBAAoB,EAAE,CAAC;QAC3D,0CAAmB,CAAC,aAAa,CAAC;YACjC,YAAY,EAAE,eAAe,CAAC,YAAY;YAC1C,QAAQ,EAAE,eAAe,CAAC,QAAQ;YAClC,iBAAiB,EAAE,eAAe,CAAC,iBAAiB;YACpD,aAAa,EAAE,eAAe,CAAC,aAAa;SAC5C,CAAC,CAAC;QAEH,wDAAwD;QACxD,IAAI,0CAAmB,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;YACzC,IAAA,aAAG,EAAC,MAAM,EAAE,mBAAmB,CAAC,CAAC;YACjC,MAAM,gBAAgB,CAAC,cAAc,CAAC,CAAC;YACvC,OAAO;QACR,CAAC;QAED,IAAI,0CAAmB,CAAC,gBAAgB,CAAC,GAAG,CAAC,EAAE,CAAC;YAC/C,0CAAmB,CAAC,kBAAkB,CAAC,GAAG,CAAC,CAAC;YAC5C,sEAAsE;YACtE,MAAM,eAAe,GAAG,IAAA,6BAAmB,GAAE,CAAC;YAC9C,MAAM,eAAe,GAAG,0BAAW,CAAC,cAAc,EAAE,CAAC;YACrD,IAAI,CAAC;gBACJ,MAAM,MAAM,GAAG,MAAM,IAAA,iCAAuB,EAAC,eAAe,EAAE,eAAe,EAAE,GAAG,CAAC,CAAC;gBACpF,0CAAmB,CAAC,mBAAmB,CAAC,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC;gBAC/D,IAAA,aAAG,EAAC,MAAM,EAAE,wBAAwB,EAAE,MAAM,CAAC,CAAC;YAC/C,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBAChB,0CAAmB,CAAC,mBAAmB,CAAC,QAAQ,EAAE,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC;gBAC9D,IAAA,aAAG,EAAC,OAAO,EAAE,qBAAqB,EAAE,EAAE,KAAK,EAAE,IAAA,kBAAS,EAAC,KAAK,CAAC,EAAE,CAAC,CAAC;YAClE,CAAC;QACF,CAAC;IACF,CAAC,EAAE,GAAG,CAAC,CAAC;IAER,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC;QACpB,OAAO,CAAC,IAAI,CAAC,8GAA8G,CAAC,CAAC;QAC7H,OAAO,CAAC,IAAI,CAAC,6EAA6E,CAAC,CAAC;IAC7F,CAAC;AACF,CAAC,CAAC,CAAC;AAEH,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,GAA0B,EAAE,EAAE;IACjD,IAAI,GAAG,CAAC,IAAI,KAAK,YAAY,EAAE,CAAC;QAC/B,IAAA,aAAG,EAAC,OAAO,EAAE,aAAa,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;QAC5C,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACjB,CAAC;IACD,IAAA,aAAG,EAAC,OAAO,EAAE,cAAc,EAAE,EAAE,KAAK,EAAE,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC;IACrD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AACjB,CAAC,CAAC,CAAC"}
@@ -1,11 +1,17 @@
1
1
  import { type BrowserContext, type BrowserContextOptions } from 'playwright-core';
2
+ import type { ResolvedProxyConfig } from '../types';
2
3
  export interface PoolEntry {
3
4
  context: BrowserContext;
4
5
  userId: string;
6
+ profileKey: string;
5
7
  profileDir: string;
6
8
  lastAccess: number;
9
+ createdAt: number;
7
10
  launching?: Promise<BrowserContext>;
11
+ staged?: boolean;
12
+ stagedGeneration?: string;
8
13
  virtualDisplay?: any;
14
+ proxyConfig?: ResolvedProxyConfig | null;
9
15
  seedOptions?: Pick<BrowserContextOptions, 'locale' | 'timezoneId' | 'geolocation' | 'viewport'>;
10
16
  }
11
17
  export declare class ContextPool {
@@ -14,16 +20,26 @@ export declare class ContextPool {
14
20
  private onEvictCallbacks;
15
21
  onEvict(callback: (userId: string) => void): void;
16
22
  private notifyEviction;
17
- getEntry(userId: string): PoolEntry | undefined;
23
+ getEntry(profileKey: string): PoolEntry | undefined;
18
24
  getDisplayForUser(userId: string): string | null;
19
25
  size(): number;
20
26
  listActiveUserIds(): string[];
27
+ getLifecycleSnapshot(): {
28
+ liveContexts: number;
29
+ launchingContexts: number;
30
+ stagedContexts: number;
31
+ };
32
+ getPoolEntries(): Map<string, PoolEntry>;
21
33
  private cleanupVirtualDisplay;
22
34
  private launchPersistentContext;
23
35
  restartContext(userId: string, headless?: boolean | 'virtual'): Promise<PoolEntry>;
24
36
  private evictIfNeeded;
25
- ensureContext(userId: string, options?: BrowserContextOptions): Promise<PoolEntry>;
26
- closeContext(userId: string): Promise<void>;
37
+ ensureContext(profileKey: string, userId: string, options?: BrowserContextOptions, resolvedProxy?: ResolvedProxyConfig | null, staged?: boolean, stagedGeneration?: string): Promise<PoolEntry>;
38
+ closeContext(profileKey: string): Promise<void>;
39
+ closeContextIfMatches(profileKey: string, expectedCreatedAt: number, expectedLastAccess?: number): Promise<void>;
40
+ closeStagedContext(profileKey: string, generation?: string): Promise<void>;
41
+ closeContextByUserId(userId: string): Promise<void>;
42
+ closeStagedContextByUserId(userId: string, generation?: string): Promise<void>;
27
43
  closeAll(): Promise<void>;
28
44
  }
29
45
  export declare const contextPool: ContextPool;
@@ -1 +1 @@
1
- {"version":3,"file":"context-pool.d.ts","sourceRoot":"","sources":["../../../src/services/context-pool.ts"],"names":[],"mappings":"AAQA,OAAO,EAAW,KAAK,cAAc,EAAE,KAAK,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AAW3F,MAAM,WAAW,SAAS;IACzB,OAAO,EAAE,cAAc,CAAC;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,OAAO,CAAC,cAAc,CAAC,CAAC;IACpC,cAAc,CAAC,EAAE,GAAG,CAAC;IACrB,WAAW,CAAC,EAAE,IAAI,CAAC,qBAAqB,EAAE,QAAQ,GAAG,YAAY,GAAG,aAAa,GAAG,UAAU,CAAC,CAAC;CAChG;AA0GD,qBAAa,WAAW;IACvB,OAAO,CAAC,IAAI,CAAqC;IACjD,OAAO,CAAC,iBAAiB,CAA0C;IACnE,OAAO,CAAC,gBAAgB,CAAuC;IAE/D,OAAO,CAAC,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,GAAG,IAAI;IAIjD,OAAO,CAAC,cAAc;IAWtB,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,GAAG,SAAS;IAI/C,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IAiBhD,IAAI,IAAI,MAAM;IAId,iBAAiB,IAAI,MAAM,EAAE;IAI7B,OAAO,CAAC,qBAAqB;YAWf,uBAAuB;IAgH/B,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,OAAO,GAAG,SAAS,GAAG,OAAO,CAAC,SAAS,CAAC;YAc1E,aAAa;IAerB,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,qBAAqB,GAAG,OAAO,CAAC,SAAS,CAAC;IAuElF,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAkB3C,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;CAM/B;AAED,eAAO,MAAM,WAAW,aAAoB,CAAC;AAE7C,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAE/D"}
1
+ {"version":3,"file":"context-pool.d.ts","sourceRoot":"","sources":["../../../src/services/context-pool.ts"],"names":[],"mappings":"AAYA,OAAO,EAAW,KAAK,cAAc,EAAE,KAAK,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AAK3F,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,UAAU,CAAC;AAQpD,MAAM,WAAW,SAAS;IACzB,OAAO,EAAE,cAAc,CAAC;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,OAAO,CAAC,cAAc,CAAC,CAAC;IACpC,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,cAAc,CAAC,EAAE,GAAG,CAAC;IACrB,WAAW,CAAC,EAAE,mBAAmB,GAAG,IAAI,CAAC;IACzC,WAAW,CAAC,EAAE,IAAI,CAAC,qBAAqB,EAAE,QAAQ,GAAG,YAAY,GAAG,aAAa,GAAG,UAAU,CAAC,CAAC;CAChG;AA0HD,qBAAa,WAAW;IACvB,OAAO,CAAC,IAAI,CAAqC;IACjD,OAAO,CAAC,iBAAiB,CAA0C;IACnE,OAAO,CAAC,gBAAgB,CAAuC;IAE/D,OAAO,CAAC,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,GAAG,IAAI;IAIjD,OAAO,CAAC,cAAc;IAWtB,QAAQ,CAAC,UAAU,EAAE,MAAM,GAAG,SAAS,GAAG,SAAS;IAInD,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IAoBhD,IAAI,IAAI,MAAM;IAId,iBAAiB,IAAI,MAAM,EAAE;IAU7B,oBAAoB,IAAI;QAAE,YAAY,EAAE,MAAM,CAAC;QAAC,iBAAiB,EAAE,MAAM,CAAC;QAAC,cAAc,EAAE,MAAM,CAAA;KAAE;IAcnG,cAAc,IAAI,GAAG,CAAC,MAAM,EAAE,SAAS,CAAC;IAIxC,OAAO,CAAC,qBAAqB;YAWf,uBAAuB;IAuL/B,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,OAAO,GAAG,SAAS,GAAG,OAAO,CAAC,SAAS,CAAC;YAqB1E,aAAa;IAerB,aAAa,CAClB,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,MAAM,EACd,OAAO,CAAC,EAAE,qBAAqB,EAC/B,aAAa,CAAC,EAAE,mBAAmB,GAAG,IAAI,EAC1C,MAAM,UAAQ,EACd,gBAAgB,CAAC,EAAE,MAAM,GACvB,OAAO,CAAC,SAAS,CAAC;IAiFf,YAAY,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAwB/C,qBAAqB,CAAC,UAAU,EAAE,MAAM,EAAE,iBAAiB,EAAE,MAAM,EAAE,kBAAkB,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAUhH,kBAAkB,CAAC,UAAU,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAU1E,oBAAoB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAcnD,0BAA0B,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAgB9E,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;CAW/B;AAED,eAAO,MAAM,WAAW,aAAoB,CAAC;AAE7C,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAE/D"}
@@ -11,11 +11,16 @@ const node_fs_1 = __importDefault(require("node:fs"));
11
11
  const node_child_process_1 = require("node:child_process");
12
12
  const camoufox_js_1 = require("camoufox-js");
13
13
  const fingerprints_js_1 = require("camoufox-js/dist/fingerprints.js");
14
+ // camoufox-js does not export version info from its public API.
15
+ // This deep import is the only way to detect the installed engine version.
16
+ // If it breaks on upgrade, getInstalledCamoufoxVersion() catches and returns 'unknown'.
17
+ const pkgman_js_1 = require("camoufox-js/dist/pkgman.js");
14
18
  const playwright_core_1 = require("playwright-core");
15
19
  const config_1 = require("../utils/config");
20
+ const sidecar_version_1 = require("../utils/sidecar-version");
16
21
  const logging_1 = require("../middleware/logging");
17
22
  const CONFIG = (0, config_1.loadConfig)();
18
- const MAX_CONTEXTS = Math.max(1, Number.parseInt(process.env.CAMOFOX_MAX_SESSIONS || '', 10) || 50);
23
+ const MAX_CONTEXTS = CONFIG.maxSessions;
19
24
  function getHostOS() {
20
25
  const platform = node_os_1.default.platform();
21
26
  if (platform === 'darwin')
@@ -24,16 +29,32 @@ function getHostOS() {
24
29
  return 'windows';
25
30
  return 'linux';
26
31
  }
27
- function buildProxyConfig() {
28
- const { host, port, username, password } = CONFIG.proxy;
29
- if (!host || !port)
30
- return null;
32
+ function buildProxyConfig(proxy) {
33
+ if (!proxy) {
34
+ // Fallback to configured server proxy
35
+ const { host, port, username, password } = CONFIG.proxy;
36
+ if (!host || !port)
37
+ return null;
38
+ return {
39
+ server: `http://${host}:${port}`,
40
+ username: username || undefined,
41
+ password: password || undefined,
42
+ };
43
+ }
31
44
  return {
32
- server: `http://${host}:${port}`,
33
- username: username || undefined,
34
- password: password || undefined,
45
+ server: proxy.server,
46
+ username: proxy.username,
47
+ password: proxy.password,
35
48
  };
36
49
  }
50
+ function getInstalledCamoufoxVersion() {
51
+ try {
52
+ return (0, pkgman_js_1.installedVerStr)();
53
+ }
54
+ catch {
55
+ return 'unknown';
56
+ }
57
+ }
37
58
  function profileDirForUserId(userId) {
38
59
  // Avoid path traversal from untrusted route params.
39
60
  const safe = encodeURIComponent(String(userId));
@@ -131,30 +152,57 @@ class ContextPool {
131
152
  }
132
153
  }
133
154
  }
134
- getEntry(userId) {
135
- return this.pool.get(userId);
155
+ getEntry(profileKey) {
156
+ return this.pool.get(profileKey);
136
157
  }
137
158
  getDisplayForUser(userId) {
138
- const entry = this.pool.get(String(userId));
139
- if (!entry)
140
- return null;
141
- if (entry.virtualDisplay) {
142
- try {
143
- const display = entry.virtualDisplay.get();
144
- return typeof display === 'string' ? display : null;
145
- }
146
- catch {
147
- return null;
159
+ // Find any entry for this userId (there may be multiple with different profile keys)
160
+ for (const entry of this.pool.values()) {
161
+ if (entry.userId !== String(userId))
162
+ continue;
163
+ if (entry.virtualDisplay) {
164
+ try {
165
+ const display = entry.virtualDisplay.get();
166
+ return typeof display === 'string' ? display : null;
167
+ }
168
+ catch {
169
+ return null;
170
+ }
148
171
  }
172
+ const processDisplay = process.env.DISPLAY;
173
+ return typeof processDisplay === 'string' && processDisplay ? processDisplay : null;
149
174
  }
150
- const processDisplay = process.env.DISPLAY;
151
- return typeof processDisplay === 'string' && processDisplay ? processDisplay : null;
175
+ return null;
152
176
  }
153
177
  size() {
154
178
  return this.pool.size;
155
179
  }
156
180
  listActiveUserIds() {
157
- return Array.from(this.pool.keys());
181
+ const userIds = new Set();
182
+ for (const entry of this.pool.values()) {
183
+ if (!entry.staged) {
184
+ userIds.add(entry.userId);
185
+ }
186
+ }
187
+ return Array.from(userIds);
188
+ }
189
+ getLifecycleSnapshot() {
190
+ let launchingContexts = 0;
191
+ let stagedContexts = 0;
192
+ for (const entry of this.pool.values()) {
193
+ if (entry.launching)
194
+ launchingContexts++;
195
+ if (entry.staged)
196
+ stagedContexts++;
197
+ }
198
+ return {
199
+ liveContexts: this.pool.size,
200
+ launchingContexts,
201
+ stagedContexts,
202
+ };
203
+ }
204
+ getPoolEntries() {
205
+ return new Map(this.pool);
158
206
  }
159
207
  cleanupVirtualDisplay(entry) {
160
208
  if (!entry.virtualDisplay)
@@ -169,12 +217,44 @@ class ContextPool {
169
217
  entry.virtualDisplay = undefined;
170
218
  }
171
219
  }
172
- async launchPersistentContext(userId, contextOptions) {
220
+ async launchPersistentContext(userId, contextOptions, resolvedProxy) {
173
221
  const hostOS = getHostOS();
174
- const proxy = buildProxyConfig();
222
+ const proxy = buildProxyConfig(resolvedProxy);
175
223
  const headless = this.headlessOverrides.get(userId) ?? CONFIG.headless;
176
224
  const profileDir = profileDirForUserId(userId);
177
225
  node_fs_1.default.mkdirSync(profileDir, { recursive: true });
226
+ const compatPath = node_path_1.default.join(profileDir, 'compatibility.json');
227
+ try {
228
+ const currentVersion = getInstalledCamoufoxVersion();
229
+ if (currentVersion === 'unknown') {
230
+ throw new Error(`Cannot verify profile compatibility for user "${userId}": installed Camoufox version could not be determined. ` +
231
+ `Resolve the camoufox-js installation or delete the profile directory to reset: ${profileDir}`);
232
+ }
233
+ const compat = (0, sidecar_version_1.readVersionedSidecar)(compatPath, {
234
+ currentVersion: 1,
235
+ migrations: {},
236
+ label: 'profile compatibility',
237
+ });
238
+ if (compat) {
239
+ if (compat.camoufoxVersion === 'unknown') {
240
+ throw new Error(`Profile for user "${userId}" has unknown version provenance and cannot be verified. Delete the profile directory to reset: ${profileDir}`);
241
+ }
242
+ if (compat.camoufoxVersion !== currentVersion) {
243
+ throw new Error(`Profile for user "${userId}" was created with Camoufox ${compat.camoufoxVersion}, but the current version is ${currentVersion}. Delete the profile directory to reset: ${profileDir}`);
244
+ }
245
+ }
246
+ else {
247
+ (0, sidecar_version_1.writeVersionedSidecar)(compatPath, 1, {
248
+ camoufoxVersion: currentVersion,
249
+ createdAt: new Date().toISOString(),
250
+ });
251
+ }
252
+ }
253
+ catch (err) {
254
+ throw err instanceof Error
255
+ ? err
256
+ : new Error(`Profile compatibility check failed for user "${userId}": ${String(err)}. Delete the profile directory to reset: ${profileDir}`);
257
+ }
178
258
  let effectiveHeadless = headless === true;
179
259
  let virtualDisplay = undefined;
180
260
  if (headless === 'virtual' || (headless === false && hostOS === 'linux' && !hasUsableLinuxDisplay())) {
@@ -219,26 +299,46 @@ class ContextPool {
219
299
  try {
220
300
  const fpPath = node_path_1.default.join(profileDir, 'fingerprint.json');
221
301
  let fingerprint;
222
- if (node_fs_1.default.existsSync(fpPath)) {
223
- try {
224
- const parsed = JSON.parse(node_fs_1.default.readFileSync(fpPath, 'utf-8'));
225
- if (parsed && typeof parsed === 'object') {
226
- fingerprint = parsed;
227
- (0, logging_1.log)('info', 'loaded persisted fingerprint', { userId, fpPath });
228
- }
229
- else {
230
- (0, logging_1.log)('warn', 'persisted fingerprint is invalid, regenerating', { userId, fpPath });
231
- }
302
+ const configuredOs = CONFIG.fingerprintDefaults.os;
303
+ const fingerprintOperatingSystems = Array.isArray(configuredOs)
304
+ ? configuredOs
305
+ : configuredOs
306
+ ? [configuredOs]
307
+ : [hostOS];
308
+ const configuredScreen = CONFIG.fingerprintDefaults.screen;
309
+ const fingerprintScreen = configuredScreen
310
+ ? {
311
+ minWidth: configuredScreen.width,
312
+ maxWidth: configuredScreen.width,
313
+ minHeight: configuredScreen.height,
314
+ maxHeight: configuredScreen.height,
232
315
  }
233
- catch {
234
- (0, logging_1.log)('warn', 'failed to read persisted fingerprint, regenerating', { userId, fpPath });
316
+ : undefined;
317
+ try {
318
+ const persistedFingerprint = (0, sidecar_version_1.readVersionedSidecar)(fpPath, {
319
+ currentVersion: 1,
320
+ migrations: {
321
+ 0: (raw) => raw,
322
+ },
323
+ label: 'fingerprint sidecar',
324
+ });
325
+ fingerprint = persistedFingerprint ?? undefined;
326
+ if (fingerprint) {
327
+ (0, logging_1.log)('info', 'loaded persisted fingerprint', { userId, fpPath });
235
328
  }
236
329
  }
330
+ catch (err) {
331
+ throw err instanceof Error
332
+ ? err
333
+ : new Error(`Fingerprint sidecar failed for user "${userId}": ${String(err)}. Delete ${fpPath} to reset.`);
334
+ }
237
335
  if (!fingerprint) {
238
- fingerprint = (0, fingerprints_js_1.generateFingerprint)(undefined, { operatingSystems: [hostOS] });
336
+ fingerprint = (0, fingerprints_js_1.generateFingerprint)(undefined, {
337
+ operatingSystems: fingerprintOperatingSystems,
338
+ ...(fingerprintScreen ? { screen: fingerprintScreen } : {}),
339
+ });
239
340
  try {
240
- node_fs_1.default.mkdirSync(node_path_1.default.dirname(fpPath), { recursive: true });
241
- node_fs_1.default.writeFileSync(fpPath, JSON.stringify(fingerprint, null, 2), 'utf-8');
341
+ (0, sidecar_version_1.writeVersionedSidecar)(fpPath, 1, fingerprint);
242
342
  (0, logging_1.log)('info', 'generated new fingerprint and persisted it', { userId, fpPath });
243
343
  }
244
344
  catch {
@@ -248,8 +348,11 @@ class ContextPool {
248
348
  const opts = await (0, camoufox_js_1.launchOptions)({
249
349
  headless: effectiveHeadless,
250
350
  ...(virtualDisplay ? { virtual_display: virtualDisplay.get() } : {}),
251
- os: hostOS,
252
- humanize: true,
351
+ os: configuredOs ?? hostOS,
352
+ humanize: CONFIG.fingerprintDefaults.humanize ?? true,
353
+ ...(CONFIG.fingerprintDefaults.allowWebgl !== undefined
354
+ ? { allow_webgl: CONFIG.fingerprintDefaults.allowWebgl }
355
+ : {}),
253
356
  enable_cache: true,
254
357
  proxy: proxy ?? undefined,
255
358
  geoip: !!proxy,
@@ -282,18 +385,25 @@ class ContextPool {
282
385
  if (headless !== undefined) {
283
386
  this.headlessOverrides.set(normalized, headless);
284
387
  }
285
- const existing = this.pool.get(normalized);
286
- if (existing) {
287
- await this.closeContext(normalized);
388
+ // Close all contexts for this userId (there may be multiple with different profiles)
389
+ const entriesToClose = [];
390
+ for (const [profileKey, entry] of this.pool.entries()) {
391
+ if (entry.userId === normalized) {
392
+ entriesToClose.push(profileKey);
393
+ }
288
394
  }
289
- return this.ensureContext(normalized);
395
+ for (const profileKey of entriesToClose) {
396
+ await this.closeContext(profileKey);
397
+ }
398
+ // Return a new entry with default key (for backward compatibility)
399
+ return this.ensureContext(normalized, normalized);
290
400
  }
291
401
  async evictIfNeeded() {
292
402
  if (this.pool.size <= MAX_CONTEXTS)
293
403
  return;
294
404
  let lru = null;
295
405
  for (const entry of this.pool.values()) {
296
- if (entry.launching)
406
+ if (entry.launching || entry.staged)
297
407
  continue;
298
408
  if (!lru || entry.lastAccess < lru.lastAccess)
299
409
  lru = entry;
@@ -302,13 +412,17 @@ class ContextPool {
302
412
  return;
303
413
  (0, logging_1.log)('info', 'evicting persistent context (LRU)', { userId: lru.userId, profileDir: lru.profileDir });
304
414
  this.notifyEviction(lru.userId);
305
- await this.closeContext(lru.userId);
415
+ await this.closeContext(lru.profileKey);
306
416
  }
307
- async ensureContext(userId, options) {
417
+ async ensureContext(profileKey, userId, options, resolvedProxy, staged = false, stagedGeneration) {
308
418
  const normalized = String(userId);
309
- let entry = this.pool.get(normalized);
419
+ let entry = this.pool.get(profileKey);
310
420
  const seed = pickSeedOptions(options);
311
421
  if (entry) {
422
+ if (staged) {
423
+ entry.staged = true;
424
+ entry.stagedGeneration = stagedGeneration;
425
+ }
312
426
  entry.lastAccess = Date.now();
313
427
  if (entry.launching) {
314
428
  entry.context = await entry.launching;
@@ -322,7 +436,7 @@ class ContextPool {
322
436
  void entry.context.pages();
323
437
  }
324
438
  catch {
325
- this.pool.delete(normalized);
439
+ this.pool.delete(profileKey);
326
440
  entry = undefined;
327
441
  }
328
442
  }
@@ -330,6 +444,7 @@ class ContextPool {
330
444
  if (seedDiffers(entry.seedOptions, seed)) {
331
445
  (0, logging_1.log)('warn', 'persistent context already exists; ignoring new context overrides', {
332
446
  userId: normalized,
447
+ profileKey,
333
448
  profileDir: entry.profileDir,
334
449
  });
335
450
  }
@@ -340,11 +455,16 @@ class ContextPool {
340
455
  const newEntry = {
341
456
  context: null,
342
457
  userId: normalized,
458
+ profileKey,
343
459
  profileDir,
344
460
  lastAccess: Date.now(),
461
+ createdAt: Date.now(),
462
+ staged,
463
+ stagedGeneration,
464
+ proxyConfig: resolvedProxy || null,
345
465
  seedOptions: seed,
346
466
  };
347
- newEntry.launching = this.launchPersistentContext(normalized, options)
467
+ newEntry.launching = this.launchPersistentContext(normalized, options, resolvedProxy)
348
468
  .then(({ context, virtualDisplay }) => {
349
469
  newEntry.context = context;
350
470
  newEntry.virtualDisplay = virtualDisplay;
@@ -352,27 +472,27 @@ class ContextPool {
352
472
  newEntry.lastAccess = Date.now();
353
473
  context.on('close', () => {
354
474
  this.cleanupVirtualDisplay(newEntry);
355
- (0, logging_1.log)('info', 'persistent context closed', { userId: normalized, profileDir });
356
- this.pool.delete(normalized);
475
+ (0, logging_1.log)('info', 'persistent context closed', { userId: normalized, profileKey, profileDir });
476
+ this.pool.delete(profileKey);
357
477
  });
358
478
  return context;
359
479
  })
360
480
  .catch((err) => {
361
481
  this.cleanupVirtualDisplay(newEntry);
362
- this.pool.delete(normalized);
482
+ this.pool.delete(profileKey);
363
483
  const message = err instanceof Error ? err.message : String(err);
364
- (0, logging_1.log)('error', 'persistent context launch failed', { userId: normalized, profileDir, error: message });
484
+ (0, logging_1.log)('error', 'persistent context launch failed', { userId: normalized, profileKey, profileDir, error: message });
365
485
  throw err;
366
486
  });
367
- this.pool.set(normalized, newEntry);
487
+ this.pool.set(profileKey, newEntry);
368
488
  await newEntry.launching;
369
489
  await this.evictIfNeeded();
370
490
  return newEntry;
371
491
  }
372
- async closeContext(userId) {
373
- const normalized = String(userId);
492
+ async closeContext(profileKey) {
493
+ const normalized = String(profileKey);
374
494
  const entry = this.pool.get(normalized);
375
- if (!entry)
495
+ if (!entry || entry.staged)
376
496
  return;
377
497
  try {
378
498
  if (entry.launching) {
@@ -383,14 +503,77 @@ class ContextPool {
383
503
  }
384
504
  finally {
385
505
  this.cleanupVirtualDisplay(entry);
386
- this.pool.delete(normalized);
387
- (0, logging_1.log)('info', 'persistent context removed from pool', { userId: normalized, profileDir: entry.profileDir });
506
+ // Only delete if this entry is still in the pool (avoid deleting a newer entry with same key)
507
+ const currentEntry = this.pool.get(normalized);
508
+ if (currentEntry === entry) {
509
+ this.pool.delete(normalized);
510
+ (0, logging_1.log)('info', 'persistent context removed from pool', { userId: entry.userId, profileKey: normalized, profileDir: entry.profileDir });
511
+ }
512
+ else {
513
+ (0, logging_1.log)('info', 'persistent context closed but newer entry exists', { userId: entry.userId, profileKey: normalized, profileDir: entry.profileDir });
514
+ }
515
+ }
516
+ }
517
+ async closeContextIfMatches(profileKey, expectedCreatedAt, expectedLastAccess) {
518
+ const normalized = String(profileKey);
519
+ const entry = this.pool.get(normalized);
520
+ if (!entry)
521
+ return;
522
+ if (entry.createdAt !== expectedCreatedAt)
523
+ return;
524
+ // If lastAccess was provided and has changed, the context was reused - don't close
525
+ if (expectedLastAccess !== undefined && entry.lastAccess !== expectedLastAccess)
526
+ return;
527
+ await this.closeContext(normalized);
528
+ }
529
+ async closeStagedContext(profileKey, generation) {
530
+ const normalized = String(profileKey);
531
+ const entry = this.pool.get(normalized);
532
+ if (!entry?.staged)
533
+ return;
534
+ if (generation && entry.stagedGeneration !== generation)
535
+ return;
536
+ entry.staged = false;
537
+ entry.stagedGeneration = undefined;
538
+ await this.closeContext(normalized);
539
+ }
540
+ async closeContextByUserId(userId) {
541
+ const normalized = String(userId);
542
+ // Close all contexts for this userId
543
+ const entriesToClose = [];
544
+ for (const [profileKey, entry] of this.pool.entries()) {
545
+ if (entry.userId === normalized && !entry.staged) {
546
+ entriesToClose.push(profileKey);
547
+ }
548
+ }
549
+ for (const profileKey of entriesToClose) {
550
+ await this.closeContext(profileKey);
551
+ }
552
+ }
553
+ async closeStagedContextByUserId(userId, generation) {
554
+ const normalized = String(userId);
555
+ // Close all staged contexts for this userId
556
+ const entriesToClose = [];
557
+ for (const [profileKey, entry] of this.pool.entries()) {
558
+ if (entry.userId === normalized && entry.staged) {
559
+ if (!generation || entry.stagedGeneration === generation) {
560
+ entriesToClose.push(profileKey);
561
+ }
562
+ }
563
+ }
564
+ for (const profileKey of entriesToClose) {
565
+ await this.closeStagedContext(profileKey, generation);
388
566
  }
389
567
  }
390
568
  async closeAll() {
391
- const userIds = Array.from(this.pool.keys());
392
- for (const userId of userIds) {
393
- await this.closeContext(userId);
569
+ const profileKeys = Array.from(this.pool.keys());
570
+ for (const profileKey of profileKeys) {
571
+ const entry = this.pool.get(profileKey);
572
+ if (entry?.staged) {
573
+ entry.staged = false;
574
+ entry.stagedGeneration = undefined;
575
+ }
576
+ await this.closeContext(profileKey);
394
577
  }
395
578
  }
396
579
  }