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.
- package/CHANGELOG.md +126 -0
- package/README.md +304 -33
- package/dist/src/cli/commands/content.d.ts.map +1 -1
- package/dist/src/cli/commands/content.js +37 -0
- package/dist/src/cli/commands/content.js.map +1 -1
- package/dist/src/cli/commands/core.d.ts.map +1 -1
- package/dist/src/cli/commands/core.js +21 -4
- package/dist/src/cli/commands/core.js.map +1 -1
- package/dist/src/cli/commands/interaction.d.ts.map +1 -1
- package/dist/src/cli/commands/interaction.js +5 -14
- package/dist/src/cli/commands/interaction.js.map +1 -1
- package/dist/src/cli/commands/navigation.d.ts.map +1 -1
- package/dist/src/cli/commands/navigation.js +12 -6
- package/dist/src/cli/commands/navigation.js.map +1 -1
- package/dist/src/cli/commands/server.d.ts.map +1 -1
- package/dist/src/cli/commands/server.js +9 -3
- package/dist/src/cli/commands/server.js.map +1 -1
- package/dist/src/cli/commands/session.d.ts.map +1 -1
- package/dist/src/cli/commands/session.js +23 -5
- package/dist/src/cli/commands/session.js.map +1 -1
- package/dist/src/cli/server/manager.d.ts +1 -0
- package/dist/src/cli/server/manager.d.ts.map +1 -1
- package/dist/src/cli/server/manager.js +7 -12
- package/dist/src/cli/server/manager.js.map +1 -1
- package/dist/src/middleware/lifecycle-activity.d.ts +9 -0
- package/dist/src/middleware/lifecycle-activity.d.ts.map +1 -0
- package/dist/src/middleware/lifecycle-activity.js +21 -0
- package/dist/src/middleware/lifecycle-activity.js.map +1 -0
- package/dist/src/openapi/spec.d.ts +4 -0
- package/dist/src/openapi/spec.d.ts.map +1 -0
- package/dist/src/openapi/spec.js +730 -0
- package/dist/src/openapi/spec.js.map +1 -0
- package/dist/src/routes/core.d.ts.map +1 -1
- package/dist/src/routes/core.js +428 -53
- package/dist/src/routes/core.js.map +1 -1
- package/dist/src/routes/docs.d.ts +3 -0
- package/dist/src/routes/docs.d.ts.map +1 -0
- package/dist/src/routes/docs.js +23 -0
- package/dist/src/routes/docs.js.map +1 -0
- package/dist/src/routes/openclaw.d.ts.map +1 -1
- package/dist/src/routes/openclaw.js +244 -90
- package/dist/src/routes/openclaw.js.map +1 -1
- package/dist/src/server.js +55 -4
- package/dist/src/server.js.map +1 -1
- package/dist/src/services/context-pool.d.ts +19 -3
- package/dist/src/services/context-pool.d.ts.map +1 -1
- package/dist/src/services/context-pool.js +248 -65
- package/dist/src/services/context-pool.js.map +1 -1
- package/dist/src/services/download.d.ts +2 -0
- package/dist/src/services/download.d.ts.map +1 -1
- package/dist/src/services/download.js +110 -80
- package/dist/src/services/download.js.map +1 -1
- package/dist/src/services/lifecycle-controller.d.ts +40 -0
- package/dist/src/services/lifecycle-controller.d.ts.map +1 -0
- package/dist/src/services/lifecycle-controller.js +106 -0
- package/dist/src/services/lifecycle-controller.js.map +1 -0
- package/dist/src/services/resource-extractor.d.ts +1 -0
- package/dist/src/services/resource-extractor.d.ts.map +1 -1
- package/dist/src/services/resource-extractor.js +7 -0
- package/dist/src/services/resource-extractor.js.map +1 -1
- package/dist/src/services/session.d.ts +84 -2
- package/dist/src/services/session.d.ts.map +1 -1
- package/dist/src/services/session.js +349 -47
- package/dist/src/services/session.js.map +1 -1
- package/dist/src/services/structured-extractor.d.ts +39 -0
- package/dist/src/services/structured-extractor.d.ts.map +1 -0
- package/dist/src/services/structured-extractor.js +487 -0
- package/dist/src/services/structured-extractor.js.map +1 -0
- package/dist/src/services/tab.d.ts +30 -3
- package/dist/src/services/tab.d.ts.map +1 -1
- package/dist/src/services/tab.js +872 -124
- package/dist/src/services/tab.js.map +1 -1
- package/dist/src/services/tracing.d.ts +7 -0
- package/dist/src/services/tracing.d.ts.map +1 -1
- package/dist/src/services/tracing.js +162 -19
- package/dist/src/services/tracing.js.map +1 -1
- package/dist/src/services/vnc.d.ts.map +1 -1
- package/dist/src/services/vnc.js +5 -3
- package/dist/src/services/vnc.js.map +1 -1
- package/dist/src/services/youtube.js +1 -1
- package/dist/src/services/youtube.js.map +1 -1
- package/dist/src/types.d.ts +71 -1
- package/dist/src/types.d.ts.map +1 -1
- package/dist/src/utils/config.d.ts +79 -3
- package/dist/src/utils/config.d.ts.map +1 -1
- package/dist/src/utils/config.js +145 -3
- package/dist/src/utils/config.js.map +1 -1
- package/dist/src/utils/presets.d.ts.map +1 -1
- package/dist/src/utils/presets.js +3 -1
- package/dist/src/utils/presets.js.map +1 -1
- package/dist/src/utils/proxy-profiles.d.ts +18 -0
- package/dist/src/utils/proxy-profiles.d.ts.map +1 -0
- package/dist/src/utils/proxy-profiles.js +197 -0
- package/dist/src/utils/proxy-profiles.js.map +1 -0
- package/dist/src/utils/sidecar-version.d.ts +12 -0
- package/dist/src/utils/sidecar-version.d.ts.map +1 -0
- package/dist/src/utils/sidecar-version.js +63 -0
- package/dist/src/utils/sidecar-version.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/openclaw.plugin.json +39 -0
- package/package.json +16 -4
- package/plugin.ts +949 -0
package/dist/src/server.js
CHANGED
|
@@ -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 —
|
|
96
|
-
console.warn('[camofox] Set CAMOFOX_API_KEY
|
|
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) => {
|
package/dist/src/server.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.js","sourceRoot":"","sources":["../../src/server.ts"],"names":[],"mappings":";;;;;AAEA,sDAA8B;AAE9B,yDAAuC;AACvC,iEAA+C;AAC/C,gDAAsE;AACtE,kDAAgF;AAChF,
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../../src/server.ts"],"names":[],"mappings":"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"}
|
|
@@ -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(
|
|
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(
|
|
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":"
|
|
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 =
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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:
|
|
33
|
-
username: username
|
|
34
|
-
password: password
|
|
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(
|
|
135
|
-
return this.pool.get(
|
|
155
|
+
getEntry(profileKey) {
|
|
156
|
+
return this.pool.get(profileKey);
|
|
136
157
|
}
|
|
137
158
|
getDisplayForUser(userId) {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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
|
-
|
|
234
|
-
|
|
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, {
|
|
336
|
+
fingerprint = (0, fingerprints_js_1.generateFingerprint)(undefined, {
|
|
337
|
+
operatingSystems: fingerprintOperatingSystems,
|
|
338
|
+
...(fingerprintScreen ? { screen: fingerprintScreen } : {}),
|
|
339
|
+
});
|
|
239
340
|
try {
|
|
240
|
-
|
|
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
|
-
|
|
286
|
-
|
|
287
|
-
|
|
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
|
-
|
|
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.
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
373
|
-
const normalized = String(
|
|
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
|
|
387
|
-
|
|
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
|
|
392
|
-
for (const
|
|
393
|
-
|
|
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
|
}
|