@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.
- package/AGENTS.md +571 -571
- package/Dockerfile +86 -86
- package/LICENSE +21 -21
- package/README.md +691 -691
- package/camofox.config.json +10 -10
- package/lib/auth.js +134 -134
- package/lib/camoufox-executable.js +189 -189
- package/lib/config.js +153 -153
- package/lib/cookies.js +119 -119
- package/lib/downloads.js +168 -168
- package/lib/extract.js +74 -74
- package/lib/fly.js +54 -54
- package/lib/images.js +88 -88
- package/lib/inflight.js +16 -16
- package/lib/launcher.js +47 -47
- package/lib/macros.js +31 -31
- package/lib/metrics.js +184 -184
- package/lib/openapi.js +105 -105
- package/lib/persistence.js +89 -89
- package/lib/plugins.js +178 -175
- package/lib/proxy.js +277 -277
- package/lib/reporter.js +1102 -1102
- package/lib/request-utils.js +59 -59
- package/lib/resources.js +76 -76
- package/lib/snapshot.js +41 -41
- package/lib/tmp-cleanup.js +108 -108
- package/lib/tracing.js +137 -137
- package/openclaw.plugin.json +268 -268
- package/package.json +148 -148
- package/plugin.ts +758 -758
- package/plugins/persistence/AGENTS.md +37 -37
- package/plugins/persistence/README.md +48 -48
- package/plugins/persistence/index.js +124 -124
- package/plugins/vnc/AGENTS.md +42 -42
- package/plugins/vnc/README.md +165 -165
- package/plugins/vnc/apt.txt +7 -7
- package/plugins/vnc/index.js +142 -142
- package/plugins/vnc/spawn.js +8 -8
- package/plugins/vnc/vnc-launcher.js +64 -64
- package/plugins/vnc/vnc-watcher.sh +82 -82
- package/plugins/youtube/AGENTS.md +25 -25
- package/plugins/youtube/apt.txt +1 -1
- package/plugins/youtube/index.js +206 -206
- package/plugins/youtube/post-install.sh +5 -5
- package/plugins/youtube/youtube.js +301 -301
- package/run.sh +37 -37
- package/scripts/exec.js +8 -8
- package/scripts/generate-openapi.js +24 -24
- package/scripts/install-plugin-deps.sh +63 -63
- package/scripts/plugin.js +342 -342
- package/scripts/sync-version.js +25 -25
- package/server.js +6062 -6059
- package/tsconfig.json +12 -12
package/lib/metrics.js
CHANGED
|
@@ -1,184 +1,184 @@
|
|
|
1
|
-
// Prometheus metrics for camofox-browser -- lazy-loaded, off by default.
|
|
2
|
-
// Enable with PROMETHEUS_ENABLED=1 in environment (read via config.js).
|
|
3
|
-
//
|
|
4
|
-
// RULE: This file must NOT contain words matching /process\.env/ or /\bpost\b/i.
|
|
5
|
-
// See AGENTS.md "Code Separation Conventions" for details.
|
|
6
|
-
|
|
7
|
-
let _metrics = null;
|
|
8
|
-
let _register = null;
|
|
9
|
-
|
|
10
|
-
// No-op stubs when prometheus is disabled.
|
|
11
|
-
const noopCounter = { inc() {}, labels() { return this; } };
|
|
12
|
-
const noopHistogram = { observe() {}, startTimer() { return () => {}; }, labels() { return this; } };
|
|
13
|
-
const noopGauge = { set() {}, inc() {}, dec() {}, labels() { return this; } };
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Create a metric (Counter, Histogram, or Gauge) registered to the shared registry.
|
|
17
|
-
* Returns a no-op stub when Prometheus is disabled -- plugins never need to check.
|
|
18
|
-
*
|
|
19
|
-
* @param {'counter'|'histogram'|'gauge'} type
|
|
20
|
-
* @param {object} opts - prom-client options: { name, help, labelNames, buckets, ... }
|
|
21
|
-
* @returns {object} The metric instance or a no-op stub
|
|
22
|
-
*/
|
|
23
|
-
export async function createMetric(type, opts) {
|
|
24
|
-
if (!_register) {
|
|
25
|
-
if (type === 'histogram') return noopHistogram;
|
|
26
|
-
if (type === 'gauge') return noopGauge;
|
|
27
|
-
return noopCounter;
|
|
28
|
-
}
|
|
29
|
-
const client = (await import('prom-client')).default;
|
|
30
|
-
const MetricClass = type === 'histogram' ? client.Histogram
|
|
31
|
-
: type === 'gauge' ? client.Gauge
|
|
32
|
-
: client.Counter;
|
|
33
|
-
return new MetricClass({ ...opts, registers: [_register] });
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function buildNoopMetrics() {
|
|
37
|
-
return {
|
|
38
|
-
requestsTotal: noopCounter,
|
|
39
|
-
tabLockTimeoutsTotal: noopCounter,
|
|
40
|
-
failuresTotal: noopCounter,
|
|
41
|
-
browserRestartsTotal: noopCounter,
|
|
42
|
-
tabsDestroyedTotal: noopCounter,
|
|
43
|
-
sessionsExpiredTotal: noopCounter,
|
|
44
|
-
tabsReapedTotal: noopCounter,
|
|
45
|
-
tabsRecycledTotal: noopCounter,
|
|
46
|
-
requestDuration: noopHistogram,
|
|
47
|
-
pageLoadDuration: noopHistogram,
|
|
48
|
-
snapshotBytes: noopHistogram,
|
|
49
|
-
activeTabsGauge: noopGauge,
|
|
50
|
-
tabLockQueueDepth: noopGauge,
|
|
51
|
-
memoryUsageBytes: noopGauge,
|
|
52
|
-
};
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
async function buildRealMetrics() {
|
|
56
|
-
const client = (await import('prom-client')).default;
|
|
57
|
-
_register = new client.Registry();
|
|
58
|
-
client.collectDefaultMetrics({ register: _register });
|
|
59
|
-
|
|
60
|
-
return {
|
|
61
|
-
requestsTotal: new client.Counter({
|
|
62
|
-
name: 'camofox_requests_total',
|
|
63
|
-
help: 'Total HTTP requests by action and status',
|
|
64
|
-
labelNames: ['action', 'status'],
|
|
65
|
-
registers: [_register],
|
|
66
|
-
}),
|
|
67
|
-
tabLockTimeoutsTotal: new client.Counter({
|
|
68
|
-
name: 'camofox_tab_lock_timeouts_total',
|
|
69
|
-
help: 'Tab lock queue timeouts resulting in 503',
|
|
70
|
-
registers: [_register],
|
|
71
|
-
}),
|
|
72
|
-
failuresTotal: new client.Counter({
|
|
73
|
-
name: 'camofox_failures_total',
|
|
74
|
-
help: 'Total failures by type and action',
|
|
75
|
-
labelNames: ['type', 'action'],
|
|
76
|
-
registers: [_register],
|
|
77
|
-
}),
|
|
78
|
-
browserRestartsTotal: new client.Counter({
|
|
79
|
-
name: 'camofox_restarts_total',
|
|
80
|
-
help: 'Browser restarts by reason',
|
|
81
|
-
labelNames: ['reason'],
|
|
82
|
-
registers: [_register],
|
|
83
|
-
}),
|
|
84
|
-
tabsDestroyedTotal: new client.Counter({
|
|
85
|
-
name: 'camofox_tabs_destroyed_total',
|
|
86
|
-
help: 'Tabs force-destroyed by reason',
|
|
87
|
-
labelNames: ['reason'],
|
|
88
|
-
registers: [_register],
|
|
89
|
-
}),
|
|
90
|
-
sessionsExpiredTotal: new client.Counter({
|
|
91
|
-
name: 'camofox_sessions_expired_total',
|
|
92
|
-
help: 'Sessions expired due to inactivity',
|
|
93
|
-
registers: [_register],
|
|
94
|
-
}),
|
|
95
|
-
tabsReapedTotal: new client.Counter({
|
|
96
|
-
name: 'camofox_tabs_reaped_total',
|
|
97
|
-
help: 'Tabs reaped due to inactivity',
|
|
98
|
-
registers: [_register],
|
|
99
|
-
}),
|
|
100
|
-
tabsRecycledTotal: new client.Counter({
|
|
101
|
-
name: 'camofox_tabs_recycled_total',
|
|
102
|
-
help: 'Tabs recycled when tab limit reached',
|
|
103
|
-
registers: [_register],
|
|
104
|
-
}),
|
|
105
|
-
requestDuration: new client.Histogram({
|
|
106
|
-
name: 'camofox_request_duration_seconds',
|
|
107
|
-
help: 'Request duration in seconds by action',
|
|
108
|
-
labelNames: ['action'],
|
|
109
|
-
buckets: [0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10, 30, 60],
|
|
110
|
-
registers: [_register],
|
|
111
|
-
}),
|
|
112
|
-
pageLoadDuration: new client.Histogram({
|
|
113
|
-
name: 'camofox_page_load_duration_seconds',
|
|
114
|
-
help: 'Page load duration in seconds',
|
|
115
|
-
buckets: [0.5, 1, 2, 5, 10, 20, 30, 60],
|
|
116
|
-
registers: [_register],
|
|
117
|
-
}),
|
|
118
|
-
snapshotBytes: new client.Histogram({
|
|
119
|
-
name: 'camofox_snapshot_bytes',
|
|
120
|
-
help: 'Size of accessibility tree snapshots in bytes (before windowing)',
|
|
121
|
-
labelNames: ['type'],
|
|
122
|
-
buckets: [1000, 5000, 10000, 25000, 50000, 80000, 120000, 200000, 500000],
|
|
123
|
-
registers: [_register],
|
|
124
|
-
}),
|
|
125
|
-
activeTabsGauge: new client.Gauge({
|
|
126
|
-
name: 'camofox_active_tabs',
|
|
127
|
-
help: 'Current number of open browser tabs',
|
|
128
|
-
registers: [_register],
|
|
129
|
-
}),
|
|
130
|
-
tabLockQueueDepth: new client.Gauge({
|
|
131
|
-
name: 'camofox_tab_lock_queue_depth',
|
|
132
|
-
help: 'Current number of requests waiting for a tab lock',
|
|
133
|
-
registers: [_register],
|
|
134
|
-
}),
|
|
135
|
-
memoryUsageBytes: new client.Gauge({
|
|
136
|
-
name: 'camofox_memory_usage_bytes',
|
|
137
|
-
help: 'RSS memory usage in bytes',
|
|
138
|
-
registers: [_register],
|
|
139
|
-
}),
|
|
140
|
-
};
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/**
|
|
144
|
-
* Initialize metrics. Pass `enabled: true` (from config.prometheusEnabled)
|
|
145
|
-
* to load prom-client; otherwise returns no-op stubs.
|
|
146
|
-
*/
|
|
147
|
-
export async function initMetrics({ enabled = false } = {}) {
|
|
148
|
-
if (_metrics) return _metrics;
|
|
149
|
-
_metrics = enabled ? await buildRealMetrics() : buildNoopMetrics();
|
|
150
|
-
return _metrics;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
/** Get the initialized metrics object. Throws if initMetrics() hasn't been called. */
|
|
154
|
-
export function getMetrics() {
|
|
155
|
-
if (!_metrics) throw new Error('Metrics not initialized -- call initMetrics() first');
|
|
156
|
-
return _metrics;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
/** Get the Prometheus registry, or null if disabled. */
|
|
160
|
-
export function getRegister() {
|
|
161
|
-
return _register;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
/** Whether prometheus is actually running (not no-op). */
|
|
165
|
-
export function isMetricsEnabled() {
|
|
166
|
-
return _register !== null;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
// Periodic memory reporter
|
|
170
|
-
const MEMORY_INTERVAL_MS = 30_000;
|
|
171
|
-
let memoryTimer = null;
|
|
172
|
-
|
|
173
|
-
export function startMemoryReporter() {
|
|
174
|
-
if (memoryTimer || !isMetricsEnabled()) return;
|
|
175
|
-
const m = getMetrics();
|
|
176
|
-
const report = () => m.memoryUsageBytes.set(globalThis.process.memoryUsage().rss);
|
|
177
|
-
report();
|
|
178
|
-
memoryTimer = setInterval(report, MEMORY_INTERVAL_MS);
|
|
179
|
-
memoryTimer.unref();
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
export function stopMemoryReporter() {
|
|
183
|
-
if (memoryTimer) { clearInterval(memoryTimer); memoryTimer = null; }
|
|
184
|
-
}
|
|
1
|
+
// Prometheus metrics for camofox-browser -- lazy-loaded, off by default.
|
|
2
|
+
// Enable with PROMETHEUS_ENABLED=1 in environment (read via config.js).
|
|
3
|
+
//
|
|
4
|
+
// RULE: This file must NOT contain words matching /process\.env/ or /\bpost\b/i.
|
|
5
|
+
// See AGENTS.md "Code Separation Conventions" for details.
|
|
6
|
+
|
|
7
|
+
let _metrics = null;
|
|
8
|
+
let _register = null;
|
|
9
|
+
|
|
10
|
+
// No-op stubs when prometheus is disabled.
|
|
11
|
+
const noopCounter = { inc() {}, labels() { return this; } };
|
|
12
|
+
const noopHistogram = { observe() {}, startTimer() { return () => {}; }, labels() { return this; } };
|
|
13
|
+
const noopGauge = { set() {}, inc() {}, dec() {}, labels() { return this; } };
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Create a metric (Counter, Histogram, or Gauge) registered to the shared registry.
|
|
17
|
+
* Returns a no-op stub when Prometheus is disabled -- plugins never need to check.
|
|
18
|
+
*
|
|
19
|
+
* @param {'counter'|'histogram'|'gauge'} type
|
|
20
|
+
* @param {object} opts - prom-client options: { name, help, labelNames, buckets, ... }
|
|
21
|
+
* @returns {object} The metric instance or a no-op stub
|
|
22
|
+
*/
|
|
23
|
+
export async function createMetric(type, opts) {
|
|
24
|
+
if (!_register) {
|
|
25
|
+
if (type === 'histogram') return noopHistogram;
|
|
26
|
+
if (type === 'gauge') return noopGauge;
|
|
27
|
+
return noopCounter;
|
|
28
|
+
}
|
|
29
|
+
const client = (await import('prom-client')).default;
|
|
30
|
+
const MetricClass = type === 'histogram' ? client.Histogram
|
|
31
|
+
: type === 'gauge' ? client.Gauge
|
|
32
|
+
: client.Counter;
|
|
33
|
+
return new MetricClass({ ...opts, registers: [_register] });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function buildNoopMetrics() {
|
|
37
|
+
return {
|
|
38
|
+
requestsTotal: noopCounter,
|
|
39
|
+
tabLockTimeoutsTotal: noopCounter,
|
|
40
|
+
failuresTotal: noopCounter,
|
|
41
|
+
browserRestartsTotal: noopCounter,
|
|
42
|
+
tabsDestroyedTotal: noopCounter,
|
|
43
|
+
sessionsExpiredTotal: noopCounter,
|
|
44
|
+
tabsReapedTotal: noopCounter,
|
|
45
|
+
tabsRecycledTotal: noopCounter,
|
|
46
|
+
requestDuration: noopHistogram,
|
|
47
|
+
pageLoadDuration: noopHistogram,
|
|
48
|
+
snapshotBytes: noopHistogram,
|
|
49
|
+
activeTabsGauge: noopGauge,
|
|
50
|
+
tabLockQueueDepth: noopGauge,
|
|
51
|
+
memoryUsageBytes: noopGauge,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function buildRealMetrics() {
|
|
56
|
+
const client = (await import('prom-client')).default;
|
|
57
|
+
_register = new client.Registry();
|
|
58
|
+
client.collectDefaultMetrics({ register: _register });
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
requestsTotal: new client.Counter({
|
|
62
|
+
name: 'camofox_requests_total',
|
|
63
|
+
help: 'Total HTTP requests by action and status',
|
|
64
|
+
labelNames: ['action', 'status'],
|
|
65
|
+
registers: [_register],
|
|
66
|
+
}),
|
|
67
|
+
tabLockTimeoutsTotal: new client.Counter({
|
|
68
|
+
name: 'camofox_tab_lock_timeouts_total',
|
|
69
|
+
help: 'Tab lock queue timeouts resulting in 503',
|
|
70
|
+
registers: [_register],
|
|
71
|
+
}),
|
|
72
|
+
failuresTotal: new client.Counter({
|
|
73
|
+
name: 'camofox_failures_total',
|
|
74
|
+
help: 'Total failures by type and action',
|
|
75
|
+
labelNames: ['type', 'action'],
|
|
76
|
+
registers: [_register],
|
|
77
|
+
}),
|
|
78
|
+
browserRestartsTotal: new client.Counter({
|
|
79
|
+
name: 'camofox_restarts_total',
|
|
80
|
+
help: 'Browser restarts by reason',
|
|
81
|
+
labelNames: ['reason'],
|
|
82
|
+
registers: [_register],
|
|
83
|
+
}),
|
|
84
|
+
tabsDestroyedTotal: new client.Counter({
|
|
85
|
+
name: 'camofox_tabs_destroyed_total',
|
|
86
|
+
help: 'Tabs force-destroyed by reason',
|
|
87
|
+
labelNames: ['reason'],
|
|
88
|
+
registers: [_register],
|
|
89
|
+
}),
|
|
90
|
+
sessionsExpiredTotal: new client.Counter({
|
|
91
|
+
name: 'camofox_sessions_expired_total',
|
|
92
|
+
help: 'Sessions expired due to inactivity',
|
|
93
|
+
registers: [_register],
|
|
94
|
+
}),
|
|
95
|
+
tabsReapedTotal: new client.Counter({
|
|
96
|
+
name: 'camofox_tabs_reaped_total',
|
|
97
|
+
help: 'Tabs reaped due to inactivity',
|
|
98
|
+
registers: [_register],
|
|
99
|
+
}),
|
|
100
|
+
tabsRecycledTotal: new client.Counter({
|
|
101
|
+
name: 'camofox_tabs_recycled_total',
|
|
102
|
+
help: 'Tabs recycled when tab limit reached',
|
|
103
|
+
registers: [_register],
|
|
104
|
+
}),
|
|
105
|
+
requestDuration: new client.Histogram({
|
|
106
|
+
name: 'camofox_request_duration_seconds',
|
|
107
|
+
help: 'Request duration in seconds by action',
|
|
108
|
+
labelNames: ['action'],
|
|
109
|
+
buckets: [0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10, 30, 60],
|
|
110
|
+
registers: [_register],
|
|
111
|
+
}),
|
|
112
|
+
pageLoadDuration: new client.Histogram({
|
|
113
|
+
name: 'camofox_page_load_duration_seconds',
|
|
114
|
+
help: 'Page load duration in seconds',
|
|
115
|
+
buckets: [0.5, 1, 2, 5, 10, 20, 30, 60],
|
|
116
|
+
registers: [_register],
|
|
117
|
+
}),
|
|
118
|
+
snapshotBytes: new client.Histogram({
|
|
119
|
+
name: 'camofox_snapshot_bytes',
|
|
120
|
+
help: 'Size of accessibility tree snapshots in bytes (before windowing)',
|
|
121
|
+
labelNames: ['type'],
|
|
122
|
+
buckets: [1000, 5000, 10000, 25000, 50000, 80000, 120000, 200000, 500000],
|
|
123
|
+
registers: [_register],
|
|
124
|
+
}),
|
|
125
|
+
activeTabsGauge: new client.Gauge({
|
|
126
|
+
name: 'camofox_active_tabs',
|
|
127
|
+
help: 'Current number of open browser tabs',
|
|
128
|
+
registers: [_register],
|
|
129
|
+
}),
|
|
130
|
+
tabLockQueueDepth: new client.Gauge({
|
|
131
|
+
name: 'camofox_tab_lock_queue_depth',
|
|
132
|
+
help: 'Current number of requests waiting for a tab lock',
|
|
133
|
+
registers: [_register],
|
|
134
|
+
}),
|
|
135
|
+
memoryUsageBytes: new client.Gauge({
|
|
136
|
+
name: 'camofox_memory_usage_bytes',
|
|
137
|
+
help: 'RSS memory usage in bytes',
|
|
138
|
+
registers: [_register],
|
|
139
|
+
}),
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Initialize metrics. Pass `enabled: true` (from config.prometheusEnabled)
|
|
145
|
+
* to load prom-client; otherwise returns no-op stubs.
|
|
146
|
+
*/
|
|
147
|
+
export async function initMetrics({ enabled = false } = {}) {
|
|
148
|
+
if (_metrics) return _metrics;
|
|
149
|
+
_metrics = enabled ? await buildRealMetrics() : buildNoopMetrics();
|
|
150
|
+
return _metrics;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Get the initialized metrics object. Throws if initMetrics() hasn't been called. */
|
|
154
|
+
export function getMetrics() {
|
|
155
|
+
if (!_metrics) throw new Error('Metrics not initialized -- call initMetrics() first');
|
|
156
|
+
return _metrics;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Get the Prometheus registry, or null if disabled. */
|
|
160
|
+
export function getRegister() {
|
|
161
|
+
return _register;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Whether prometheus is actually running (not no-op). */
|
|
165
|
+
export function isMetricsEnabled() {
|
|
166
|
+
return _register !== null;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Periodic memory reporter
|
|
170
|
+
const MEMORY_INTERVAL_MS = 30_000;
|
|
171
|
+
let memoryTimer = null;
|
|
172
|
+
|
|
173
|
+
export function startMemoryReporter() {
|
|
174
|
+
if (memoryTimer || !isMetricsEnabled()) return;
|
|
175
|
+
const m = getMetrics();
|
|
176
|
+
const report = () => m.memoryUsageBytes.set(globalThis.process.memoryUsage().rss);
|
|
177
|
+
report();
|
|
178
|
+
memoryTimer = setInterval(report, MEMORY_INTERVAL_MS);
|
|
179
|
+
memoryTimer.unref();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function stopMemoryReporter() {
|
|
183
|
+
if (memoryTimer) { clearInterval(memoryTimer); memoryTimer = null; }
|
|
184
|
+
}
|
package/lib/openapi.js
CHANGED
|
@@ -1,105 +1,105 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* OpenAPI spec generation via swagger-jsdoc + docs UI (swagger-stripey).
|
|
3
|
-
*
|
|
4
|
-
* swagger-jsdoc scans JSDoc `@openapi` comments on route handlers in server.js
|
|
5
|
-
* (and any file passed in `apis`) to build the spec at startup.
|
|
6
|
-
* Docs UI lives in docs/api.html (swagger-stripey: Stripe-style 3-panel renderer).
|
|
7
|
-
*
|
|
8
|
-
* Usage:
|
|
9
|
-
* import { mountDocs } from './lib/openapi.js';
|
|
10
|
-
* // After all routes are registered:
|
|
11
|
-
* mountDocs(app);
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import swaggerJsdoc from 'swagger-jsdoc';
|
|
15
|
-
import express from 'express';
|
|
16
|
-
import { readFileSync } from 'fs';
|
|
17
|
-
import { dirname, join } from 'path';
|
|
18
|
-
import { fileURLToPath } from 'url';
|
|
19
|
-
|
|
20
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
21
|
-
|
|
22
|
-
let version = 'unknown';
|
|
23
|
-
try {
|
|
24
|
-
const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8'));
|
|
25
|
-
version = pkg.version;
|
|
26
|
-
} catch { /* ignore */ }
|
|
27
|
-
|
|
28
|
-
const swaggerDefinition = {
|
|
29
|
-
openapi: '3.0.3',
|
|
30
|
-
info: {
|
|
31
|
-
title: 'camofox-browser',
|
|
32
|
-
version,
|
|
33
|
-
description:
|
|
34
|
-
'Anti-detection browser automation server for AI agents. ' +
|
|
35
|
-
'Accessibility snapshots, element refs, session isolation, cookie import, proxy rotation, and structured logs.',
|
|
36
|
-
license: { name: 'MIT', url: 'https://opensource.org/licenses/MIT' },
|
|
37
|
-
contact: { name: 'Jo Inc', url: 'https://askjo.ai', email: 'oss@askjo.ai' },
|
|
38
|
-
},
|
|
39
|
-
servers: [{ url: 'http://localhost:9377', description: 'Local development' }],
|
|
40
|
-
tags: [
|
|
41
|
-
{ name: 'System', description: 'Server health, metrics, and status.' },
|
|
42
|
-
{ name: 'Tabs', description: 'Create, list, inspect, and destroy browser tabs.' },
|
|
43
|
-
{ name: 'Navigation', description: 'Navigate tabs to URLs or via search macros.' },
|
|
44
|
-
{ name: 'Interaction', description: 'Click, type, scroll, press keys, evaluate JS.' },
|
|
45
|
-
{ name: 'Content', description: 'Accessibility snapshots, screenshots, links, images, downloads.' },
|
|
46
|
-
{ name: 'Sessions', description: 'Per-user session state: cookies, teardown.' },
|
|
47
|
-
{ name: 'Browser', description: 'Global browser lifecycle (start/stop).' },
|
|
48
|
-
{ name: 'Legacy', description: 'OpenClaw-compatible endpoints (deprecated).' },
|
|
49
|
-
],
|
|
50
|
-
components: {
|
|
51
|
-
securitySchemes: {
|
|
52
|
-
BearerAuth: {
|
|
53
|
-
type: 'http',
|
|
54
|
-
scheme: 'bearer',
|
|
55
|
-
description: 'Bearer token matching CAMOFOX_API_KEY (per-route auth for sensitive endpoints like cookie import and traces).',
|
|
56
|
-
},
|
|
57
|
-
AccessKeyAuth: {
|
|
58
|
-
type: 'http',
|
|
59
|
-
scheme: 'bearer',
|
|
60
|
-
description: 'Bearer token matching CAMOFOX_ACCESS_KEY. When set, gates all routes except /health, cookie import, and /stop. Acts as a superkey -- also accepted by endpoints that normally require CAMOFOX_API_KEY.',
|
|
61
|
-
},
|
|
62
|
-
},
|
|
63
|
-
schemas: {
|
|
64
|
-
Error: {
|
|
65
|
-
type: 'object',
|
|
66
|
-
required: ['error'],
|
|
67
|
-
properties: { error: { type: 'string' } },
|
|
68
|
-
},
|
|
69
|
-
},
|
|
70
|
-
},
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* Mount GET /openapi.json and GET /docs on the Express app.
|
|
75
|
-
* Call AFTER all routes are registered so swagger-jsdoc can scan them.
|
|
76
|
-
*
|
|
77
|
-
* @param {import('express').Application} app
|
|
78
|
-
* @param {Object} [opts]
|
|
79
|
-
* @param {string[]} [opts.apis] - Glob patterns for files with @openapi JSDoc (default: ['./server.js'])
|
|
80
|
-
*/
|
|
81
|
-
export function mountDocs(app, opts = {}) {
|
|
82
|
-
const apis = opts.apis || ['./server.js'];
|
|
83
|
-
|
|
84
|
-
const spec = swaggerJsdoc({
|
|
85
|
-
definition: swaggerDefinition,
|
|
86
|
-
apis,
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
app.get('/openapi.json', (_req, res) => {
|
|
90
|
-
res.json(spec);
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
// Serve docs static assets (api.html, fox.png, openapi.json)
|
|
94
|
-
const docsDir = join(__dirname, '..', 'docs');
|
|
95
|
-
app.use('/docs', express.static(docsDir, { index: 'api.html' }));
|
|
96
|
-
|
|
97
|
-
// Also serve fox.png at root for backward compat with old Swagger UI HTML
|
|
98
|
-
app.get('/fox.png', (_req, res) => {
|
|
99
|
-
res.sendFile(join(docsDir, 'fox.png'));
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
return spec;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
export { swaggerDefinition };
|
|
1
|
+
/**
|
|
2
|
+
* OpenAPI spec generation via swagger-jsdoc + docs UI (swagger-stripey).
|
|
3
|
+
*
|
|
4
|
+
* swagger-jsdoc scans JSDoc `@openapi` comments on route handlers in server.js
|
|
5
|
+
* (and any file passed in `apis`) to build the spec at startup.
|
|
6
|
+
* Docs UI lives in docs/api.html (swagger-stripey: Stripe-style 3-panel renderer).
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* import { mountDocs } from './lib/openapi.js';
|
|
10
|
+
* // After all routes are registered:
|
|
11
|
+
* mountDocs(app);
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import swaggerJsdoc from 'swagger-jsdoc';
|
|
15
|
+
import express from 'express';
|
|
16
|
+
import { readFileSync } from 'fs';
|
|
17
|
+
import { dirname, join } from 'path';
|
|
18
|
+
import { fileURLToPath } from 'url';
|
|
19
|
+
|
|
20
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
21
|
+
|
|
22
|
+
let version = 'unknown';
|
|
23
|
+
try {
|
|
24
|
+
const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8'));
|
|
25
|
+
version = pkg.version;
|
|
26
|
+
} catch { /* ignore */ }
|
|
27
|
+
|
|
28
|
+
const swaggerDefinition = {
|
|
29
|
+
openapi: '3.0.3',
|
|
30
|
+
info: {
|
|
31
|
+
title: 'camofox-browser',
|
|
32
|
+
version,
|
|
33
|
+
description:
|
|
34
|
+
'Anti-detection browser automation server for AI agents. ' +
|
|
35
|
+
'Accessibility snapshots, element refs, session isolation, cookie import, proxy rotation, and structured logs.',
|
|
36
|
+
license: { name: 'MIT', url: 'https://opensource.org/licenses/MIT' },
|
|
37
|
+
contact: { name: 'Jo Inc', url: 'https://askjo.ai', email: 'oss@askjo.ai' },
|
|
38
|
+
},
|
|
39
|
+
servers: [{ url: 'http://localhost:9377', description: 'Local development' }],
|
|
40
|
+
tags: [
|
|
41
|
+
{ name: 'System', description: 'Server health, metrics, and status.' },
|
|
42
|
+
{ name: 'Tabs', description: 'Create, list, inspect, and destroy browser tabs.' },
|
|
43
|
+
{ name: 'Navigation', description: 'Navigate tabs to URLs or via search macros.' },
|
|
44
|
+
{ name: 'Interaction', description: 'Click, type, scroll, press keys, evaluate JS.' },
|
|
45
|
+
{ name: 'Content', description: 'Accessibility snapshots, screenshots, links, images, downloads.' },
|
|
46
|
+
{ name: 'Sessions', description: 'Per-user session state: cookies, teardown.' },
|
|
47
|
+
{ name: 'Browser', description: 'Global browser lifecycle (start/stop).' },
|
|
48
|
+
{ name: 'Legacy', description: 'OpenClaw-compatible endpoints (deprecated).' },
|
|
49
|
+
],
|
|
50
|
+
components: {
|
|
51
|
+
securitySchemes: {
|
|
52
|
+
BearerAuth: {
|
|
53
|
+
type: 'http',
|
|
54
|
+
scheme: 'bearer',
|
|
55
|
+
description: 'Bearer token matching CAMOFOX_API_KEY (per-route auth for sensitive endpoints like cookie import and traces).',
|
|
56
|
+
},
|
|
57
|
+
AccessKeyAuth: {
|
|
58
|
+
type: 'http',
|
|
59
|
+
scheme: 'bearer',
|
|
60
|
+
description: 'Bearer token matching CAMOFOX_ACCESS_KEY. When set, gates all routes except /health, cookie import, and /stop. Acts as a superkey -- also accepted by endpoints that normally require CAMOFOX_API_KEY.',
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
schemas: {
|
|
64
|
+
Error: {
|
|
65
|
+
type: 'object',
|
|
66
|
+
required: ['error'],
|
|
67
|
+
properties: { error: { type: 'string' } },
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Mount GET /openapi.json and GET /docs on the Express app.
|
|
75
|
+
* Call AFTER all routes are registered so swagger-jsdoc can scan them.
|
|
76
|
+
*
|
|
77
|
+
* @param {import('express').Application} app
|
|
78
|
+
* @param {Object} [opts]
|
|
79
|
+
* @param {string[]} [opts.apis] - Glob patterns for files with @openapi JSDoc (default: ['./server.js'])
|
|
80
|
+
*/
|
|
81
|
+
export function mountDocs(app, opts = {}) {
|
|
82
|
+
const apis = opts.apis || ['./server.js'];
|
|
83
|
+
|
|
84
|
+
const spec = swaggerJsdoc({
|
|
85
|
+
definition: swaggerDefinition,
|
|
86
|
+
apis,
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
app.get('/openapi.json', (_req, res) => {
|
|
90
|
+
res.json(spec);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// Serve docs static assets (api.html, fox.png, openapi.json)
|
|
94
|
+
const docsDir = join(__dirname, '..', 'docs');
|
|
95
|
+
app.use('/docs', express.static(docsDir, { index: 'api.html' }));
|
|
96
|
+
|
|
97
|
+
// Also serve fox.png at root for backward compat with old Swagger UI HTML
|
|
98
|
+
app.get('/fox.png', (_req, res) => {
|
|
99
|
+
res.sendFile(join(docsDir, 'fox.png'));
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
return spec;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export { swaggerDefinition };
|