@phi-code-admin/camofox-browser 1.0.0 → 1.0.2

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 (53) hide show
  1. package/AGENTS.md +571 -571
  2. package/Dockerfile +86 -86
  3. package/LICENSE +21 -21
  4. package/README.md +691 -691
  5. package/camofox.config.json +10 -10
  6. package/lib/auth.js +134 -134
  7. package/lib/camoufox-executable.js +189 -189
  8. package/lib/config.js +153 -153
  9. package/lib/cookies.js +119 -119
  10. package/lib/downloads.js +168 -168
  11. package/lib/extract.js +74 -74
  12. package/lib/fly.js +54 -54
  13. package/lib/images.js +88 -88
  14. package/lib/inflight.js +16 -16
  15. package/lib/launcher.js +47 -47
  16. package/lib/macros.js +31 -31
  17. package/lib/metrics.js +184 -184
  18. package/lib/openapi.js +105 -105
  19. package/lib/persistence.js +89 -89
  20. package/lib/plugins.js +178 -175
  21. package/lib/proxy.js +277 -277
  22. package/lib/reporter.js +1102 -1102
  23. package/lib/request-utils.js +59 -59
  24. package/lib/resources.js +76 -76
  25. package/lib/snapshot.js +41 -41
  26. package/lib/tmp-cleanup.js +108 -108
  27. package/lib/tracing.js +137 -137
  28. package/openclaw.plugin.json +268 -268
  29. package/package.json +148 -148
  30. package/plugin.ts +758 -758
  31. package/plugins/persistence/AGENTS.md +37 -37
  32. package/plugins/persistence/README.md +48 -48
  33. package/plugins/persistence/index.js +124 -124
  34. package/plugins/vnc/AGENTS.md +42 -42
  35. package/plugins/vnc/README.md +165 -165
  36. package/plugins/vnc/apt.txt +7 -7
  37. package/plugins/vnc/index.js +142 -142
  38. package/plugins/vnc/spawn.js +8 -8
  39. package/plugins/vnc/vnc-launcher.js +64 -64
  40. package/plugins/vnc/vnc-watcher.sh +82 -82
  41. package/plugins/youtube/AGENTS.md +25 -25
  42. package/plugins/youtube/apt.txt +1 -1
  43. package/plugins/youtube/index.js +206 -206
  44. package/plugins/youtube/post-install.sh +5 -5
  45. package/plugins/youtube/youtube.js +301 -301
  46. package/run.sh +37 -37
  47. package/scripts/exec.js +8 -8
  48. package/scripts/generate-openapi.js +24 -24
  49. package/scripts/install-plugin-deps.sh +63 -63
  50. package/scripts/plugin.js +342 -342
  51. package/scripts/sync-version.js +25 -25
  52. package/server.js +6062 -6059
  53. package/tsconfig.json +12 -12
@@ -1,89 +1,89 @@
1
- import crypto from 'node:crypto';
2
- import fs from 'node:fs/promises';
3
- import path from 'node:path';
4
-
5
- function getUserPersistencePaths(profileDir, userId) {
6
- const rootDir = path.resolve(profileDir);
7
- const safeUserDir = crypto
8
- .createHash('sha256')
9
- .update(String(userId))
10
- .digest('hex')
11
- .slice(0, 32);
12
-
13
- const userDir = path.join(rootDir, safeUserDir);
14
- return {
15
- rootDir,
16
- userDir,
17
- storageStatePath: path.join(userDir, 'storage-state.json'),
18
- metaPath: path.join(userDir, 'meta.json'),
19
- };
20
- }
21
-
22
- async function loadPersistedStorageState(profileDir, userId, logger = console) {
23
- if (!profileDir) return undefined;
24
-
25
- const { storageStatePath } = getUserPersistencePaths(profileDir, userId);
26
-
27
- try {
28
- const raw = await fs.readFile(storageStatePath, 'utf8');
29
- const parsed = JSON.parse(raw);
30
- if (!parsed || typeof parsed !== 'object') return undefined;
31
- if (!Array.isArray(parsed.cookies)) return undefined;
32
- if (parsed.origins !== undefined && !Array.isArray(parsed.origins)) return undefined;
33
- return storageStatePath;
34
- } catch (err) {
35
- if (err?.code === 'ENOENT') return undefined;
36
- logger?.warn?.('failed to load persisted storage state', {
37
- userId: String(userId),
38
- storageStatePath,
39
- error: err?.message || String(err),
40
- });
41
- return undefined;
42
- }
43
- }
44
-
45
- async function persistStorageState({ profileDir, userId, context, logger = console }) {
46
- if (!profileDir || !context) {
47
- return { persisted: false, reason: 'disabled' };
48
- }
49
-
50
- const { userDir, storageStatePath, metaPath } = getUserPersistencePaths(profileDir, userId);
51
- const suffix = `.tmp-${process.pid}-${Date.now()}`;
52
- const tmpStoragePath = `${storageStatePath}${suffix}`;
53
- const tmpMetaPath = `${metaPath}${suffix}`;
54
-
55
- try {
56
- await fs.mkdir(userDir, { recursive: true });
57
- await context.storageState({ path: tmpStoragePath });
58
- await fs.rename(tmpStoragePath, storageStatePath);
59
- await fs.writeFile(
60
- tmpMetaPath,
61
- JSON.stringify(
62
- {
63
- userId: String(userId),
64
- updatedAt: new Date().toISOString(),
65
- storageStatePath,
66
- },
67
- null,
68
- 2
69
- )
70
- );
71
- await fs.rename(tmpMetaPath, metaPath);
72
- return { persisted: true, userDir, storageStatePath, metaPath };
73
- } catch (err) {
74
- await fs.unlink(tmpStoragePath).catch(() => {});
75
- await fs.unlink(tmpMetaPath).catch(() => {});
76
- logger?.warn?.('failed to persist storage state', {
77
- userId: String(userId),
78
- storageStatePath,
79
- error: err?.message || String(err),
80
- });
81
- return { persisted: false, reason: 'error', error: err };
82
- }
83
- }
84
-
85
- export {
86
- getUserPersistencePaths,
87
- loadPersistedStorageState,
88
- persistStorageState,
89
- };
1
+ import crypto from 'node:crypto';
2
+ import fs from 'node:fs/promises';
3
+ import path from 'node:path';
4
+
5
+ function getUserPersistencePaths(profileDir, userId) {
6
+ const rootDir = path.resolve(profileDir);
7
+ const safeUserDir = crypto
8
+ .createHash('sha256')
9
+ .update(String(userId))
10
+ .digest('hex')
11
+ .slice(0, 32);
12
+
13
+ const userDir = path.join(rootDir, safeUserDir);
14
+ return {
15
+ rootDir,
16
+ userDir,
17
+ storageStatePath: path.join(userDir, 'storage-state.json'),
18
+ metaPath: path.join(userDir, 'meta.json'),
19
+ };
20
+ }
21
+
22
+ async function loadPersistedStorageState(profileDir, userId, logger = console) {
23
+ if (!profileDir) return undefined;
24
+
25
+ const { storageStatePath } = getUserPersistencePaths(profileDir, userId);
26
+
27
+ try {
28
+ const raw = await fs.readFile(storageStatePath, 'utf8');
29
+ const parsed = JSON.parse(raw);
30
+ if (!parsed || typeof parsed !== 'object') return undefined;
31
+ if (!Array.isArray(parsed.cookies)) return undefined;
32
+ if (parsed.origins !== undefined && !Array.isArray(parsed.origins)) return undefined;
33
+ return storageStatePath;
34
+ } catch (err) {
35
+ if (err?.code === 'ENOENT') return undefined;
36
+ logger?.warn?.('failed to load persisted storage state', {
37
+ userId: String(userId),
38
+ storageStatePath,
39
+ error: err?.message || String(err),
40
+ });
41
+ return undefined;
42
+ }
43
+ }
44
+
45
+ async function persistStorageState({ profileDir, userId, context, logger = console }) {
46
+ if (!profileDir || !context) {
47
+ return { persisted: false, reason: 'disabled' };
48
+ }
49
+
50
+ const { userDir, storageStatePath, metaPath } = getUserPersistencePaths(profileDir, userId);
51
+ const suffix = `.tmp-${process.pid}-${Date.now()}`;
52
+ const tmpStoragePath = `${storageStatePath}${suffix}`;
53
+ const tmpMetaPath = `${metaPath}${suffix}`;
54
+
55
+ try {
56
+ await fs.mkdir(userDir, { recursive: true });
57
+ await context.storageState({ path: tmpStoragePath });
58
+ await fs.rename(tmpStoragePath, storageStatePath);
59
+ await fs.writeFile(
60
+ tmpMetaPath,
61
+ JSON.stringify(
62
+ {
63
+ userId: String(userId),
64
+ updatedAt: new Date().toISOString(),
65
+ storageStatePath,
66
+ },
67
+ null,
68
+ 2
69
+ )
70
+ );
71
+ await fs.rename(tmpMetaPath, metaPath);
72
+ return { persisted: true, userDir, storageStatePath, metaPath };
73
+ } catch (err) {
74
+ await fs.unlink(tmpStoragePath).catch(() => {});
75
+ await fs.unlink(tmpMetaPath).catch(() => {});
76
+ logger?.warn?.('failed to persist storage state', {
77
+ userId: String(userId),
78
+ storageStatePath,
79
+ error: err?.message || String(err),
80
+ });
81
+ return { persisted: false, reason: 'error', error: err };
82
+ }
83
+ }
84
+
85
+ export {
86
+ getUserPersistencePaths,
87
+ loadPersistedStorageState,
88
+ persistStorageState,
89
+ };
package/lib/plugins.js CHANGED
@@ -1,175 +1,178 @@
1
- /**
2
- * Camofox-browser plugin system.
3
- *
4
- * Plugins live in plugins/<name>/index.js and export a register(app, ctx) function.
5
- * The ctx object provides access to sessions, config, logging, auth middleware,
6
- * core functions, and an EventEmitter for lifecycle hooks.
7
- *
8
- * 29 events across 7 categories:
9
- *
10
- * BROWSER LIFECYCLE
11
- * browser:launching { options } -- mutate launch options
12
- * browser:launched { browser, display } -- after launch
13
- * browser:restart { reason } -- before restart cycle
14
- * browser:closed { reason } -- after browser closed
15
- * browser:error { error } -- uncaught browser error
16
- *
17
- * SESSION LIFECYCLE
18
- * session:creating { userId, contextOptions } -- mutate context options
19
- * session:created { userId, context } -- after context stored
20
- * session:destroying { userId, reason } -- before context close (context still alive)
21
- * session:destroyed { userId, reason } -- after cleanup
22
- * session:expired { userId, idleMs } -- reaper triggered
23
- *
24
- * TAB LIFECYCLE
25
- * tab:created { userId, tabId, page, url }
26
- * tab:navigated { userId, tabId, url, prevUrl }
27
- * tab:destroyed { userId, tabId, reason }
28
- * tab:recycled { userId, tabId }
29
- * tab:error { userId, tabId, error }
30
- *
31
- * CONTENT
32
- * tab:snapshot { userId, tabId, snapshot }
33
- * tab:screenshot { userId, tabId, buffer }
34
- * tab:evaluate { userId, tabId, expression }
35
- * tab:evaluated { userId, tabId, result }
36
- *
37
- * INPUT
38
- * tab:click { userId, tabId, ref, selector }
39
- * tab:type { userId, tabId, text, ref, mode }
40
- * tab:scroll { userId, tabId, direction, amount }
41
- * tab:press { userId, tabId, key }
42
- *
43
- * DOWNLOADS
44
- * tab:download:start { userId, tabId, filename, url }
45
- * tab:download:complete { userId, tabId, filename, path, size }
46
- *
47
- * COOKIES / AUTH
48
- * session:cookies:import { userId, count }
49
- * session:storage:export { userId }
50
- *
51
- * SERVER
52
- * server:starting { port }
53
- * server:started { port, pid }
54
- * server:shutdown { signal }
55
- *
56
- * Mutating hooks (browser:launching, session:creating) pass the options object
57
- * by reference -- plugins can modify it in place before core uses it.
58
- */
59
-
60
- import { EventEmitter } from 'events';
61
- import fs from 'fs';
62
- import path from 'path';
63
- import { fileURLToPath } from 'url';
64
-
65
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
66
- const ROOT_DIR = path.join(__dirname, '..');
67
- const PLUGINS_DIR = path.join(ROOT_DIR, 'plugins');
68
- const CONFIG_PATH = path.join(ROOT_DIR, 'camofox.config.json');
69
-
70
- /**
71
- * Read plugin configuration from camofox.config.json.
72
- * Supports two formats:
73
- * - Array of strings: ["youtube", "persistence"] (no per-plugin config)
74
- * - Object with per-plugin config: { "youtube": { "enabled": true }, "persistence": { "enabled": true, "profileDir": "/data" } }
75
- * Returns { list: string[] | null, configs: Map<string, object> }
76
- */
77
- function readPluginConfig() {
78
- const configs = new Map();
79
- try {
80
- const raw = fs.readFileSync(CONFIG_PATH, 'utf-8');
81
- const config = JSON.parse(raw);
82
- if (!config.plugins) return { list: null, configs };
83
- if (Array.isArray(config.plugins)) {
84
- return { list: config.plugins, configs };
85
- }
86
- if (typeof config.plugins === 'object') {
87
- const list = [];
88
- for (const [name, pluginConf] of Object.entries(config.plugins)) {
89
- if (pluginConf === false || (typeof pluginConf === 'object' && pluginConf.enabled === false)) continue;
90
- list.push(name);
91
- if (typeof pluginConf === 'object') configs.set(name, pluginConf);
92
- }
93
- return { list, configs };
94
- }
95
- } catch {}
96
- return { list: null, configs };
97
- }
98
-
99
- /**
100
- * Create the plugin event bus.
101
- */
102
- export function createPluginEvents() {
103
- const events = new EventEmitter();
104
- events.setMaxListeners(50); // generous for many plugins
105
-
106
- /**
107
- * Emit an event and await all listeners (including async ones).
108
- * Use for mutating hooks where plugins must finish before core continues.
109
- * Regular emit() is still used for fire-and-forget observational events.
110
- */
111
- events.emitAsync = async function emitAsync(eventName, payload) {
112
- const listeners = this.listeners(eventName);
113
- await Promise.all(listeners.map(fn => fn(payload)));
114
- };
115
-
116
- return events;
117
- }
118
-
119
- /**
120
- * Load and register all plugins from plugins/<name>/index.js.
121
- *
122
- * @param {object} app - Express app
123
- * @param {object} ctx - Plugin context: { sessions, config, log, events, auth, ensureBrowser, getSession, destroySession }
124
- * Mutable -- plugins can replace ctx.createVirtualDisplay etc.
125
- * @returns {string[]} - Names of loaded plugins
126
- */
127
- export async function loadPlugins(app, ctx) {
128
- const loaded = [];
129
-
130
- if (!fs.existsSync(PLUGINS_DIR)) {
131
- ctx.log('info', 'no plugins directory found, skipping plugin load');
132
- return loaded;
133
- }
134
-
135
- const { list: allowList, configs: pluginConfigs } = readPluginConfig();
136
- const entries = fs.readdirSync(PLUGINS_DIR, { withFileTypes: true });
137
-
138
- for (const entry of entries) {
139
- if (!entry.isDirectory()) continue;
140
- const name = entry.name;
141
-
142
- // Skip directories starting with _ or .
143
- if (name.startsWith('_') || name.startsWith('.')) continue;
144
-
145
- // If camofox.config.json specifies a plugins list, only load those
146
- if (allowList && !allowList.includes(name)) {
147
- ctx.log('debug', `plugin "${name}" not in camofox.config.json plugins list, skipping`);
148
- continue;
149
- }
150
-
151
- const indexPath = path.join(PLUGINS_DIR, name, 'index.js');
152
- if (!fs.existsSync(indexPath)) {
153
- ctx.log('warn', `plugin "${name}" has no index.js, skipping`);
154
- continue;
155
- }
156
-
157
- try {
158
- const mod = await import(indexPath);
159
- const register = mod.default || mod.register;
160
- if (typeof register !== 'function') {
161
- ctx.log('warn', `plugin "${name}" does not export a register function, skipping`);
162
- continue;
163
- }
164
-
165
- const pluginConfig = pluginConfigs.get(name) || {};
166
- await register(app, ctx, pluginConfig);
167
- loaded.push(name);
168
- ctx.log('info', 'plugin loaded', { plugin: name });
169
- } catch (err) {
170
- ctx.log('error', 'plugin load failed', { plugin: name, error: err.message, stack: err.stack });
171
- }
172
- }
173
-
174
- return loaded;
175
- }
1
+ /**
2
+ * Camofox-browser plugin system.
3
+ *
4
+ * Plugins live in plugins/<name>/index.js and export a register(app, ctx) function.
5
+ * The ctx object provides access to sessions, config, logging, auth middleware,
6
+ * core functions, and an EventEmitter for lifecycle hooks.
7
+ *
8
+ * 29 events across 7 categories:
9
+ *
10
+ * BROWSER LIFECYCLE
11
+ * browser:launching { options } -- mutate launch options
12
+ * browser:launched { browser, display } -- after launch
13
+ * browser:restart { reason } -- before restart cycle
14
+ * browser:closed { reason } -- after browser closed
15
+ * browser:error { error } -- uncaught browser error
16
+ *
17
+ * SESSION LIFECYCLE
18
+ * session:creating { userId, contextOptions } -- mutate context options
19
+ * session:created { userId, context } -- after context stored
20
+ * session:destroying { userId, reason } -- before context close (context still alive)
21
+ * session:destroyed { userId, reason } -- after cleanup
22
+ * session:expired { userId, idleMs } -- reaper triggered
23
+ *
24
+ * TAB LIFECYCLE
25
+ * tab:created { userId, tabId, page, url }
26
+ * tab:navigated { userId, tabId, url, prevUrl }
27
+ * tab:destroyed { userId, tabId, reason }
28
+ * tab:recycled { userId, tabId }
29
+ * tab:error { userId, tabId, error }
30
+ *
31
+ * CONTENT
32
+ * tab:snapshot { userId, tabId, snapshot }
33
+ * tab:screenshot { userId, tabId, buffer }
34
+ * tab:evaluate { userId, tabId, expression }
35
+ * tab:evaluated { userId, tabId, result }
36
+ *
37
+ * INPUT
38
+ * tab:click { userId, tabId, ref, selector }
39
+ * tab:type { userId, tabId, text, ref, mode }
40
+ * tab:scroll { userId, tabId, direction, amount }
41
+ * tab:press { userId, tabId, key }
42
+ *
43
+ * DOWNLOADS
44
+ * tab:download:start { userId, tabId, filename, url }
45
+ * tab:download:complete { userId, tabId, filename, path, size }
46
+ *
47
+ * COOKIES / AUTH
48
+ * session:cookies:import { userId, count }
49
+ * session:storage:export { userId }
50
+ *
51
+ * SERVER
52
+ * server:starting { port }
53
+ * server:started { port, pid }
54
+ * server:shutdown { signal }
55
+ *
56
+ * Mutating hooks (browser:launching, session:creating) pass the options object
57
+ * by reference -- plugins can modify it in place before core uses it.
58
+ */
59
+
60
+ import { EventEmitter } from 'events';
61
+ import fs from 'fs';
62
+ import path from 'path';
63
+ import { fileURLToPath, pathToFileURL } from 'url';
64
+
65
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
66
+ const ROOT_DIR = path.join(__dirname, '..');
67
+ const PLUGINS_DIR = path.join(ROOT_DIR, 'plugins');
68
+ const CONFIG_PATH = path.join(ROOT_DIR, 'camofox.config.json');
69
+
70
+ /**
71
+ * Read plugin configuration from camofox.config.json.
72
+ * Supports two formats:
73
+ * - Array of strings: ["youtube", "persistence"] (no per-plugin config)
74
+ * - Object with per-plugin config: { "youtube": { "enabled": true }, "persistence": { "enabled": true, "profileDir": "/data" } }
75
+ * Returns { list: string[] | null, configs: Map<string, object> }
76
+ */
77
+ function readPluginConfig() {
78
+ const configs = new Map();
79
+ try {
80
+ const raw = fs.readFileSync(CONFIG_PATH, 'utf-8');
81
+ const config = JSON.parse(raw);
82
+ if (!config.plugins) return { list: null, configs };
83
+ if (Array.isArray(config.plugins)) {
84
+ return { list: config.plugins, configs };
85
+ }
86
+ if (typeof config.plugins === 'object') {
87
+ const list = [];
88
+ for (const [name, pluginConf] of Object.entries(config.plugins)) {
89
+ if (pluginConf === false || (typeof pluginConf === 'object' && pluginConf.enabled === false)) continue;
90
+ list.push(name);
91
+ if (typeof pluginConf === 'object') configs.set(name, pluginConf);
92
+ }
93
+ return { list, configs };
94
+ }
95
+ } catch {}
96
+ return { list: null, configs };
97
+ }
98
+
99
+ /**
100
+ * Create the plugin event bus.
101
+ */
102
+ export function createPluginEvents() {
103
+ const events = new EventEmitter();
104
+ events.setMaxListeners(50); // generous for many plugins
105
+
106
+ /**
107
+ * Emit an event and await all listeners (including async ones).
108
+ * Use for mutating hooks where plugins must finish before core continues.
109
+ * Regular emit() is still used for fire-and-forget observational events.
110
+ */
111
+ events.emitAsync = async function emitAsync(eventName, payload) {
112
+ const listeners = this.listeners(eventName);
113
+ await Promise.all(listeners.map(fn => fn(payload)));
114
+ };
115
+
116
+ return events;
117
+ }
118
+
119
+ /**
120
+ * Load and register all plugins from plugins/<name>/index.js.
121
+ *
122
+ * @param {object} app - Express app
123
+ * @param {object} ctx - Plugin context: { sessions, config, log, events, auth, ensureBrowser, getSession, destroySession }
124
+ * Mutable -- plugins can replace ctx.createVirtualDisplay etc.
125
+ * @returns {string[]} - Names of loaded plugins
126
+ */
127
+ export async function loadPlugins(app, ctx) {
128
+ const loaded = [];
129
+
130
+ if (!fs.existsSync(PLUGINS_DIR)) {
131
+ ctx.log('info', 'no plugins directory found, skipping plugin load');
132
+ return loaded;
133
+ }
134
+
135
+ const { list: allowList, configs: pluginConfigs } = readPluginConfig();
136
+ const entries = fs.readdirSync(PLUGINS_DIR, { withFileTypes: true });
137
+
138
+ for (const entry of entries) {
139
+ if (!entry.isDirectory()) continue;
140
+ const name = entry.name;
141
+
142
+ // Skip directories starting with _ or .
143
+ if (name.startsWith('_') || name.startsWith('.')) continue;
144
+
145
+ // If camofox.config.json specifies a plugins list, only load those
146
+ if (allowList && !allowList.includes(name)) {
147
+ ctx.log('debug', `plugin "${name}" not in camofox.config.json plugins list, skipping`);
148
+ continue;
149
+ }
150
+
151
+ const indexPath = path.join(PLUGINS_DIR, name, 'index.js');
152
+ if (!fs.existsSync(indexPath)) {
153
+ ctx.log('warn', `plugin "${name}" has no index.js, skipping`);
154
+ continue;
155
+ }
156
+
157
+ try {
158
+ // PHI-VENDOR: on Windows, Node's ESM loader refuses raw absolute paths
159
+ // (e.g. "C:\foo\plugin.js") and demands a file:// URL. Convert before
160
+ // calling dynamic import so plugin loading works cross-platform.
161
+ const mod = await import(pathToFileURL(indexPath).href);
162
+ const register = mod.default || mod.register;
163
+ if (typeof register !== 'function') {
164
+ ctx.log('warn', `plugin "${name}" does not export a register function, skipping`);
165
+ continue;
166
+ }
167
+
168
+ const pluginConfig = pluginConfigs.get(name) || {};
169
+ await register(app, ctx, pluginConfig);
170
+ loaded.push(name);
171
+ ctx.log('info', 'plugin loaded', { plugin: name });
172
+ } catch (err) {
173
+ ctx.log('error', 'plugin load failed', { plugin: name, error: err.message, stack: err.stack });
174
+ }
175
+ }
176
+
177
+ return loaded;
178
+ }