agentlinkops 0.5.0 → 0.5.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/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.mcp.json +8 -0
- package/CHANGELOG.md +15 -1
- package/README.md +2 -1
- package/cli/citation.js +19 -1
- package/cli/client.js +1 -1
- package/cli/doctor.js +19 -0
- package/package.json +9 -1
- package/src/citations/browser/engine.js +196 -0
- package/src/citations/browser/gen/core/providers/_shared/google.js +4 -0
- package/src/citations/browser/gen/core/providers/_shared/rawSourcesDom.js +15 -0
- package/src/citations/browser/gen/core/providers/_shared/resetProviderPage.js +22 -0
- package/src/citations/browser/gen/core/providers/_shared/sourceUtils.js +8 -0
- package/src/citations/browser/gen/core/providers/ai-overview/index.js +70 -0
- package/src/citations/browser/gen/core/providers/ai-overview/lib/expand.js +59 -0
- package/src/citations/browser/gen/core/providers/ai-overview/lib/extractSources.js +60 -0
- package/src/citations/browser/gen/core/providers/ai-overview/lib/pageLifecycle.js +18 -0
- package/src/citations/browser/gen/core/providers/ai-overview/lib/session.js +62 -0
- package/src/citations/browser/gen/core/providers/chatgpt/index.js +28 -0
- package/src/citations/browser/gen/core/providers/chatgpt/lib/dismissAuthModal.js +39 -0
- package/src/citations/browser/gen/core/providers/chatgpt/lib/extractSources.js +66 -0
- package/src/citations/browser/gen/core/providers/chatgpt/lib/pageLifecycle.js +11 -0
- package/src/citations/browser/gen/core/providers/claude/index.js +16 -0
- package/src/citations/browser/gen/core/providers/claude/lib/extractSources.js +54 -0
- package/src/citations/browser/gen/core/providers/claude/lib/pageLifecycle.js +9 -0
- package/src/citations/browser/gen/core/providers/gemini/index.js +40 -0
- package/src/citations/browser/gen/core/providers/gemini/lib/extractSources.js +65 -0
- package/src/citations/browser/gen/core/providers/gemini/lib/pageLifecycle.js +14 -0
- package/src/citations/browser/gen/core/providers/gemini/lib/session.js +45 -0
- package/src/citations/browser/gen/core/providers/index.js +15 -0
- package/src/citations/browser/gen/core/providers/perplexity/index.js +31 -0
- package/src/citations/browser/gen/core/providers/perplexity/lib/extractSources.js +84 -0
- package/src/citations/browser/gen/core/providers/perplexity/lib/navigation.js +38 -0
- package/src/citations/browser/gen/core/providers/types.js +0 -0
- package/src/citations/browser/gen/lib/browser/camoufox.js +587 -0
- package/src/citations/browser/gen/lib/browser/display.js +147 -0
- package/src/citations/browser/gen/lib/browser/domOps.js +339 -0
- package/src/citations/browser/gen/lib/browser/humanBehavior.js +121 -0
- package/src/citations/browser/gen/lib/browser/navigate.js +46 -0
- package/src/citations/browser/gen/lib/browser/playwrightCompat.js +321 -0
- package/src/citations/browser/gen/lib/browser/proxy/forwarder.js +251 -0
- package/src/citations/browser/gen/lib/browser/runtimeTypes.js +0 -0
- package/src/citations/browser/gen/lib/extraction/sourceUtils.js +71 -0
- package/src/citations/browser/gen/lib/input/editor/assertNotBlocked.js +54 -0
- package/src/citations/browser/gen/lib/input/editor/clearInput.js +52 -0
- package/src/citations/browser/gen/lib/input/editor/findEditor.js +54 -0
- package/src/citations/browser/gen/lib/input/editor/findSendButton.js +21 -0
- package/src/citations/browser/gen/lib/input/editor/promptInput.js +117 -0
- package/src/citations/browser/gen/lib/input/markdown/converter.js +101 -0
- package/src/citations/browser/gen/lib/input/markdown/toMarkdown.js +14 -0
- package/src/citations/browser/gen/lib/input/response/detectBotPage.js +13 -0
- package/src/citations/browser/gen/lib/input/response/getText.js +10 -0
- package/src/citations/browser/gen/lib/input/response/isGenerating.js +59 -0
- package/src/citations/browser/gen/lib/input/response/waitForFinish.js +105 -0
- package/src/citations/browser/gen/lib/input/sources/findButton.js +41 -0
- package/src/citations/browser/gen/lib/input/sources/openPanel.js +11 -0
- package/src/citations/browser/gen/utils/agent-constants.js +87 -0
- package/src/citations/browser/shims/errors.js +26 -0
- package/src/citations/browser/shims/types.js +6 -0
- package/src/citations/browser/shims/utils.js +32 -0
- package/src/citations/contract.js +9 -2
- package/src/citations/runner.js +21 -5
|
@@ -0,0 +1,587 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
import { ExternalServiceError, toErrorMessage } from "../../../shims/errors.js";
|
|
4
|
+
import { resolveAppMode } from "../../../shims/types.js";
|
|
5
|
+
const execFileAsync = promisify(execFile);
|
|
6
|
+
const PYTHON_CANDIDATES = ["python3.12", "python3.11", "python3.10", "python3"];
|
|
7
|
+
const PYTHON_PROBE_TIMEOUT_MS = 5e3;
|
|
8
|
+
const CAMOUFOX_OPTIONS_TIMEOUT_MS = 3e4;
|
|
9
|
+
const SYSTEM_FONTS_TIMEOUT_MS = 15e3;
|
|
10
|
+
const DEFAULT_CAMOUFOX_HEADLESS_MODE = "virtual";
|
|
11
|
+
const PYTHON_PROBE_SCRIPT = `
|
|
12
|
+
import json
|
|
13
|
+
import sys
|
|
14
|
+
|
|
15
|
+
print(json.dumps({
|
|
16
|
+
"major": sys.version_info.major,
|
|
17
|
+
"minor": sys.version_info.minor,
|
|
18
|
+
"version": sys.version.split()[0],
|
|
19
|
+
}))
|
|
20
|
+
`;
|
|
21
|
+
const CAMOUFOX_OPTIONS_SCRIPT = `
|
|
22
|
+
import json
|
|
23
|
+
import os
|
|
24
|
+
import sys
|
|
25
|
+
|
|
26
|
+
try:
|
|
27
|
+
from browserforge.fingerprints import Fingerprint, Screen
|
|
28
|
+
from camoufox.addons import DefaultAddons
|
|
29
|
+
from camoufox.pkgman import OS_NAME
|
|
30
|
+
from camoufox.utils import launch_options
|
|
31
|
+
except Exception as exc:
|
|
32
|
+
print(f"CAMOUFOX_IMPORT_ERROR::{exc}", file=sys.stderr)
|
|
33
|
+
raise
|
|
34
|
+
|
|
35
|
+
payload = json.loads(os.environ["CAMOUFOX_OPTIONS_PAYLOAD"])
|
|
36
|
+
use_full_os_fonts = bool(payload.pop("use_full_os_fonts", False))
|
|
37
|
+
disable_default_addons = bool(payload.pop("disable_default_addons", False))
|
|
38
|
+
|
|
39
|
+
if isinstance(payload.get("screen"), dict):
|
|
40
|
+
payload["screen"] = Screen(**payload["screen"])
|
|
41
|
+
|
|
42
|
+
if isinstance(payload.get("window"), list):
|
|
43
|
+
payload["window"] = tuple(payload["window"])
|
|
44
|
+
|
|
45
|
+
if isinstance(payload.get("webgl_config"), list):
|
|
46
|
+
payload["webgl_config"] = tuple(payload["webgl_config"])
|
|
47
|
+
|
|
48
|
+
if isinstance(payload.get("fingerprint"), dict):
|
|
49
|
+
payload["fingerprint"] = Fingerprint(**payload["fingerprint"])
|
|
50
|
+
|
|
51
|
+
if isinstance(payload.get("exclude_addons"), list):
|
|
52
|
+
payload["exclude_addons"] = [DefaultAddons[item] for item in payload["exclude_addons"]]
|
|
53
|
+
|
|
54
|
+
if disable_default_addons:
|
|
55
|
+
current = payload.get("exclude_addons")
|
|
56
|
+
existing = list(current) if isinstance(current, list) else []
|
|
57
|
+
merged = []
|
|
58
|
+
seen = set()
|
|
59
|
+
for addon in [*existing, *list(DefaultAddons)]:
|
|
60
|
+
key = addon.name if hasattr(addon, "name") else str(addon)
|
|
61
|
+
if key in seen:
|
|
62
|
+
continue
|
|
63
|
+
seen.add(key)
|
|
64
|
+
merged.append(addon)
|
|
65
|
+
payload["exclude_addons"] = merged
|
|
66
|
+
|
|
67
|
+
if use_full_os_fonts:
|
|
68
|
+
fonts_path = os.path.join(os.path.dirname(__import__("camoufox").__file__), "fonts.json")
|
|
69
|
+
with open(fonts_path, "rb") as fonts_file:
|
|
70
|
+
fonts_by_os = json.load(fonts_file)
|
|
71
|
+
|
|
72
|
+
target_os = payload.get("os")
|
|
73
|
+
if isinstance(target_os, list):
|
|
74
|
+
target_os = next((item for item in target_os if isinstance(item, str)), None)
|
|
75
|
+
os_key = {"windows": "win", "macos": "mac", "linux": "lin"}.get(target_os, OS_NAME)
|
|
76
|
+
full_os_fonts = fonts_by_os.get(os_key, [])
|
|
77
|
+
if isinstance(payload.get("fonts"), list):
|
|
78
|
+
payload["fonts"] = list(dict.fromkeys([*full_os_fonts, *payload["fonts"]]))
|
|
79
|
+
else:
|
|
80
|
+
payload["fonts"] = full_os_fonts
|
|
81
|
+
|
|
82
|
+
options = launch_options(**payload)
|
|
83
|
+
print(json.dumps(options))
|
|
84
|
+
`;
|
|
85
|
+
let cachedPythonBinary = null;
|
|
86
|
+
let cachedSystemFontFamilies = null;
|
|
87
|
+
let pendingSystemFontFamilies = null;
|
|
88
|
+
const CAMOUFOX_HUMANIZE = true;
|
|
89
|
+
const CAMOUFOX_HUMANIZE_MAX_TIME_S = 1.5;
|
|
90
|
+
function isLocalAppMode() {
|
|
91
|
+
return resolveAppMode(process.env.ONEGLANSE_APP_MODE) === "local";
|
|
92
|
+
}
|
|
93
|
+
function getHumanizeValue() {
|
|
94
|
+
if (!CAMOUFOX_HUMANIZE) return false;
|
|
95
|
+
const maxTime = CAMOUFOX_HUMANIZE_MAX_TIME_S;
|
|
96
|
+
return maxTime > 0 ? maxTime : true;
|
|
97
|
+
}
|
|
98
|
+
function resolveHeadlessMode(override) {
|
|
99
|
+
return override ?? process.env.CAMOUFOX_HEADLESS_MODE ?? DEFAULT_CAMOUFOX_HEADLESS_MODE;
|
|
100
|
+
}
|
|
101
|
+
function getHeadlessValue(override) {
|
|
102
|
+
return resolveHeadlessMode(override) === "headless";
|
|
103
|
+
}
|
|
104
|
+
async function canUsePythonBinary(candidate) {
|
|
105
|
+
try {
|
|
106
|
+
const { stdout } = await execFileAsync(
|
|
107
|
+
candidate,
|
|
108
|
+
["-c", PYTHON_PROBE_SCRIPT],
|
|
109
|
+
{
|
|
110
|
+
encoding: "utf8",
|
|
111
|
+
timeout: PYTHON_PROBE_TIMEOUT_MS,
|
|
112
|
+
maxBuffer: 32 * 1024
|
|
113
|
+
}
|
|
114
|
+
);
|
|
115
|
+
const parsed = JSON.parse(stdout);
|
|
116
|
+
return typeof parsed.major === "number" && typeof parsed.minor === "number" && (parsed.major > 3 || parsed.major === 3 && parsed.minor >= 10);
|
|
117
|
+
} catch {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
async function resolvePythonBinary(provider) {
|
|
122
|
+
if (cachedPythonBinary) return cachedPythonBinary;
|
|
123
|
+
const candidates = [
|
|
124
|
+
process.env.CAMOUFOX_PYTHON_BIN?.trim(),
|
|
125
|
+
...PYTHON_CANDIDATES
|
|
126
|
+
].filter((candidate) => Boolean(candidate));
|
|
127
|
+
for (const candidate of [...new Set(candidates)]) {
|
|
128
|
+
if (await canUsePythonBinary(candidate)) {
|
|
129
|
+
cachedPythonBinary = candidate;
|
|
130
|
+
return candidate;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
throw new ExternalServiceError(
|
|
134
|
+
provider,
|
|
135
|
+
"Camoufox requires Python 3.10+ and the camoufox package. Set CAMOUFOX_PYTHON_BIN if python3 is not the correct interpreter."
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
function isJsonRecord(value) {
|
|
139
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
140
|
+
}
|
|
141
|
+
function parseJsonValue(name, raw) {
|
|
142
|
+
if (!raw) return void 0;
|
|
143
|
+
try {
|
|
144
|
+
return JSON.parse(raw);
|
|
145
|
+
} catch (error) {
|
|
146
|
+
throw new Error(
|
|
147
|
+
`${name} must be valid JSON: ${error instanceof Error ? error.message : String(error)}`
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
function parseJsonRecord(name, raw) {
|
|
152
|
+
const parsed = parseJsonValue(name, raw);
|
|
153
|
+
if (parsed === void 0) return void 0;
|
|
154
|
+
if (!isJsonRecord(parsed)) {
|
|
155
|
+
throw new Error(`${name} must be a JSON object.`);
|
|
156
|
+
}
|
|
157
|
+
return parsed;
|
|
158
|
+
}
|
|
159
|
+
function parsePrimitiveRecord(name, raw) {
|
|
160
|
+
const parsed = parseJsonRecord(name, raw);
|
|
161
|
+
if (!parsed) return void 0;
|
|
162
|
+
const result = {};
|
|
163
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
164
|
+
if (typeof value !== "string" && typeof value !== "number" && typeof value !== "boolean") {
|
|
165
|
+
throw new Error(`${name}.${key} must be a string, number, or boolean.`);
|
|
166
|
+
}
|
|
167
|
+
result[key] = value;
|
|
168
|
+
}
|
|
169
|
+
return result;
|
|
170
|
+
}
|
|
171
|
+
function toPrimitiveRecord(name, value) {
|
|
172
|
+
if (value === void 0) return void 0;
|
|
173
|
+
if (!isJsonRecord(value)) {
|
|
174
|
+
throw new Error(`${name} must be an object.`);
|
|
175
|
+
}
|
|
176
|
+
const result = {};
|
|
177
|
+
for (const [key, item] of Object.entries(value)) {
|
|
178
|
+
if (typeof item !== "string" && typeof item !== "number" && typeof item !== "boolean") {
|
|
179
|
+
throw new Error(`${name}.${key} must be a string, number, or boolean.`);
|
|
180
|
+
}
|
|
181
|
+
result[key] = item;
|
|
182
|
+
}
|
|
183
|
+
return result;
|
|
184
|
+
}
|
|
185
|
+
function parseStringList(name, raw) {
|
|
186
|
+
if (!raw) return void 0;
|
|
187
|
+
const trimmed = raw.trim();
|
|
188
|
+
if (!trimmed) return void 0;
|
|
189
|
+
if (trimmed.startsWith("[")) {
|
|
190
|
+
const parsed = parseJsonValue(name, trimmed);
|
|
191
|
+
if (!Array.isArray(parsed) || parsed.some((item) => typeof item !== "string")) {
|
|
192
|
+
throw new Error(`${name} must be a JSON array of strings.`);
|
|
193
|
+
}
|
|
194
|
+
return parsed.map((item) => item.trim()).filter(Boolean);
|
|
195
|
+
}
|
|
196
|
+
return trimmed.split(",").map((item) => item.trim()).filter(Boolean);
|
|
197
|
+
}
|
|
198
|
+
function parseStringOrList(name, raw) {
|
|
199
|
+
const parsed = parseStringList(name, raw);
|
|
200
|
+
if (!parsed || parsed.length === 0) return void 0;
|
|
201
|
+
return parsed.length === 1 ? parsed[0] : parsed;
|
|
202
|
+
}
|
|
203
|
+
function parseStringPair(name, raw) {
|
|
204
|
+
if (!raw) return void 0;
|
|
205
|
+
const parsed = parseJsonValue(name, raw);
|
|
206
|
+
if (!Array.isArray(parsed) || parsed.length !== 2 || parsed.some((item) => typeof item !== "string")) {
|
|
207
|
+
throw new Error(`${name} must be a JSON array of two strings.`);
|
|
208
|
+
}
|
|
209
|
+
return [parsed[0], parsed[1]];
|
|
210
|
+
}
|
|
211
|
+
function parsePositiveInteger(name, raw) {
|
|
212
|
+
if (!raw) return void 0;
|
|
213
|
+
const parsed = Number(raw);
|
|
214
|
+
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
215
|
+
throw new Error(`${name} must be a positive integer.`);
|
|
216
|
+
}
|
|
217
|
+
return parsed;
|
|
218
|
+
}
|
|
219
|
+
function parseFingerprintPreset(raw) {
|
|
220
|
+
if (!raw) return void 0;
|
|
221
|
+
const normalized = raw.trim().toLowerCase();
|
|
222
|
+
if (normalized === "true" || normalized === "1") return true;
|
|
223
|
+
if (normalized === "false" || normalized === "0") return false;
|
|
224
|
+
return parseJsonRecord("CAMOUFOX_FINGERPRINT_PRESET", raw);
|
|
225
|
+
}
|
|
226
|
+
function parseGeoipValue() {
|
|
227
|
+
const raw = process.env.CAMOUFOX_GEOIP?.trim();
|
|
228
|
+
if (!raw) {
|
|
229
|
+
return true;
|
|
230
|
+
}
|
|
231
|
+
const normalized = raw.toLowerCase();
|
|
232
|
+
if (normalized === "true" || normalized === "1") return true;
|
|
233
|
+
if (normalized === "false" || normalized === "0") return false;
|
|
234
|
+
return raw;
|
|
235
|
+
}
|
|
236
|
+
function normalizeFontFamilyName(value) {
|
|
237
|
+
const trimmed = value.trim();
|
|
238
|
+
if (!trimmed) return null;
|
|
239
|
+
return trimmed.replace(/^['"]|['"]$/g, "") || null;
|
|
240
|
+
}
|
|
241
|
+
function dedupeStrings(values) {
|
|
242
|
+
return [...new Set(values.map((value) => value.trim()).filter(Boolean))];
|
|
243
|
+
}
|
|
244
|
+
function parseFontFamilyLines(raw) {
|
|
245
|
+
return dedupeStrings(
|
|
246
|
+
raw.split(/\r?\n/).flatMap((line) => line.split(",")).map((part) => normalizeFontFamilyName(part)).filter((value) => Boolean(value))
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
async function listFontsWithFcList() {
|
|
250
|
+
const { stdout } = await execFileAsync("fc-list", ["--format=%{family}\\n"], {
|
|
251
|
+
encoding: "utf8",
|
|
252
|
+
timeout: SYSTEM_FONTS_TIMEOUT_MS,
|
|
253
|
+
maxBuffer: 8 * 1024 * 1024
|
|
254
|
+
});
|
|
255
|
+
return parseFontFamilyLines(stdout);
|
|
256
|
+
}
|
|
257
|
+
async function listFontsWithSystemProfiler() {
|
|
258
|
+
const { stdout } = await execFileAsync(
|
|
259
|
+
"system_profiler",
|
|
260
|
+
["SPFontsDataType", "-json"],
|
|
261
|
+
{
|
|
262
|
+
encoding: "utf8",
|
|
263
|
+
timeout: SYSTEM_FONTS_TIMEOUT_MS,
|
|
264
|
+
maxBuffer: 16 * 1024 * 1024
|
|
265
|
+
}
|
|
266
|
+
);
|
|
267
|
+
const parsed = JSON.parse(stdout);
|
|
268
|
+
return dedupeStrings(
|
|
269
|
+
(parsed.SPFontsDataType ?? []).flatMap((entry) => entry.typefaces ?? []).map((typeface) => normalizeFontFamilyName(typeface.family ?? "")).filter((value) => Boolean(value))
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
async function listFontsWithPowerShell() {
|
|
273
|
+
const script = [
|
|
274
|
+
"Add-Type -AssemblyName System.Drawing",
|
|
275
|
+
"$fonts = (New-Object System.Drawing.Text.InstalledFontCollection).Families | ForEach-Object { $_.Name }",
|
|
276
|
+
"$fonts | ConvertTo-Json -Compress"
|
|
277
|
+
].join("; ");
|
|
278
|
+
const { stdout } = await execFileAsync(
|
|
279
|
+
"powershell",
|
|
280
|
+
["-NoProfile", "-Command", script],
|
|
281
|
+
{
|
|
282
|
+
encoding: "utf8",
|
|
283
|
+
timeout: SYSTEM_FONTS_TIMEOUT_MS,
|
|
284
|
+
maxBuffer: 4 * 1024 * 1024
|
|
285
|
+
}
|
|
286
|
+
);
|
|
287
|
+
const parsed = JSON.parse(stdout);
|
|
288
|
+
if (!Array.isArray(parsed)) return [];
|
|
289
|
+
return dedupeStrings(
|
|
290
|
+
parsed.map(
|
|
291
|
+
(value) => typeof value === "string" ? normalizeFontFamilyName(value) : null
|
|
292
|
+
).filter((value) => Boolean(value))
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
async function discoverSystemFontFamilies() {
|
|
296
|
+
if (cachedSystemFontFamilies) {
|
|
297
|
+
return cachedSystemFontFamilies;
|
|
298
|
+
}
|
|
299
|
+
if (pendingSystemFontFamilies) {
|
|
300
|
+
return pendingSystemFontFamilies;
|
|
301
|
+
}
|
|
302
|
+
pendingSystemFontFamilies = (async () => {
|
|
303
|
+
const resolvers = [];
|
|
304
|
+
if (process.platform === "win32") {
|
|
305
|
+
resolvers.push(listFontsWithPowerShell);
|
|
306
|
+
} else {
|
|
307
|
+
resolvers.push(listFontsWithFcList);
|
|
308
|
+
if (process.platform === "darwin") {
|
|
309
|
+
resolvers.push(listFontsWithSystemProfiler);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
for (const resolveFonts of resolvers) {
|
|
313
|
+
try {
|
|
314
|
+
const fonts = await resolveFonts();
|
|
315
|
+
if (fonts.length > 0) {
|
|
316
|
+
cachedSystemFontFamilies = fonts;
|
|
317
|
+
return fonts;
|
|
318
|
+
}
|
|
319
|
+
} catch {
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
cachedSystemFontFamilies = [];
|
|
323
|
+
return [];
|
|
324
|
+
})().finally(() => {
|
|
325
|
+
pendingSystemFontFamilies = null;
|
|
326
|
+
});
|
|
327
|
+
return pendingSystemFontFamilies;
|
|
328
|
+
}
|
|
329
|
+
function resolveHostOs() {
|
|
330
|
+
switch (process.platform) {
|
|
331
|
+
case "win32":
|
|
332
|
+
return "windows";
|
|
333
|
+
case "darwin":
|
|
334
|
+
return "macos";
|
|
335
|
+
case "linux":
|
|
336
|
+
return "linux";
|
|
337
|
+
default:
|
|
338
|
+
return void 0;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
function pickBrowserEnv(args) {
|
|
342
|
+
const baseEnv = {};
|
|
343
|
+
for (const key of [
|
|
344
|
+
"HOME",
|
|
345
|
+
"PATH",
|
|
346
|
+
"LANG",
|
|
347
|
+
"LANGUAGE",
|
|
348
|
+
"LC_ALL",
|
|
349
|
+
"TZ",
|
|
350
|
+
"TMPDIR",
|
|
351
|
+
"TMP",
|
|
352
|
+
"TEMP",
|
|
353
|
+
"XAUTHORITY",
|
|
354
|
+
"XDG_CACHE_HOME",
|
|
355
|
+
"XDG_CONFIG_HOME",
|
|
356
|
+
"XDG_RUNTIME_DIR",
|
|
357
|
+
"LD_LIBRARY_PATH",
|
|
358
|
+
"DBUS_SESSION_BUS_ADDRESS"
|
|
359
|
+
]) {
|
|
360
|
+
const value = process.env[key];
|
|
361
|
+
if (value) {
|
|
362
|
+
baseEnv[key] = value;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
if (args?.display) {
|
|
366
|
+
baseEnv.DISPLAY = args.display;
|
|
367
|
+
}
|
|
368
|
+
if (resolveHeadlessMode(args?.headlessMode) === "headless") {
|
|
369
|
+
baseEnv.MOZ_HEADLESS = "1";
|
|
370
|
+
}
|
|
371
|
+
return {
|
|
372
|
+
...baseEnv,
|
|
373
|
+
...parsePrimitiveRecord(
|
|
374
|
+
"CAMOUFOX_ENV_JSON",
|
|
375
|
+
process.env.CAMOUFOX_ENV_JSON
|
|
376
|
+
) ?? {}
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
function stringifyEnv(input) {
|
|
380
|
+
const result = {};
|
|
381
|
+
for (const [key, value] of Object.entries(input ?? {})) {
|
|
382
|
+
if (value === void 0) continue;
|
|
383
|
+
result[key] = String(value);
|
|
384
|
+
}
|
|
385
|
+
return result;
|
|
386
|
+
}
|
|
387
|
+
async function buildLaunchPayload(args) {
|
|
388
|
+
const extraLaunch = args.plainAuthMode ? {} : args.disableFingerprinting ? {} : parseJsonRecord(
|
|
389
|
+
"CAMOUFOX_EXTRA_LAUNCH_JSON",
|
|
390
|
+
process.env.CAMOUFOX_EXTRA_LAUNCH_JSON
|
|
391
|
+
) ?? {};
|
|
392
|
+
const extraArgs = Array.isArray(extraLaunch.args) && extraLaunch.args.every((item) => typeof item === "string") ? extraLaunch.args : void 0;
|
|
393
|
+
const payload = { ...extraLaunch };
|
|
394
|
+
const config = {
|
|
395
|
+
...args.plainAuthMode ? {} : {
|
|
396
|
+
...isJsonRecord(extraLaunch.config) ? extraLaunch.config : {},
|
|
397
|
+
...parseJsonRecord(
|
|
398
|
+
"CAMOUFOX_CONFIG_JSON",
|
|
399
|
+
process.env.CAMOUFOX_CONFIG_JSON
|
|
400
|
+
) ?? {}
|
|
401
|
+
},
|
|
402
|
+
showcursor: false,
|
|
403
|
+
...args.plainAuthMode ? { disableTheming: true } : {}
|
|
404
|
+
};
|
|
405
|
+
if (Object.keys(config).length > 0) payload.config = config;
|
|
406
|
+
const firefoxUserPrefs = {
|
|
407
|
+
...args.plainAuthMode ? {
|
|
408
|
+
// Camoufox's patched Firefox binary may override prefers-color-scheme
|
|
409
|
+
// at the pref level regardless of fingerprint settings. Setting this
|
|
410
|
+
// to 2 (System) tells Firefox to follow the OS preference, which is
|
|
411
|
+
// exactly what a real browser does. Without this, Google's sign-in
|
|
412
|
+
// iframe renders with a white background when the binary default is
|
|
413
|
+
// Light (1) but the user's OS is in dark mode.
|
|
414
|
+
// Values: 0 = Dark, 1 = Light, 2 = System (follows OS)
|
|
415
|
+
"layout.css.prefers-color-scheme.content-override": 2,
|
|
416
|
+
// Auto-grant persistent storage permission without showing a dialog.
|
|
417
|
+
// Without these, Firefox prompts "Allow X to store data in persistent
|
|
418
|
+
// storage?" during auth sessions, requiring user interaction to dismiss.
|
|
419
|
+
"dom.storageManager.prompt.testing": true,
|
|
420
|
+
"dom.storageManager.prompt.testing.allow_granted": true
|
|
421
|
+
} : {
|
|
422
|
+
...isJsonRecord(extraLaunch.firefox_user_prefs) ? extraLaunch.firefox_user_prefs : {},
|
|
423
|
+
...parseJsonRecord(
|
|
424
|
+
"CAMOUFOX_FIREFOX_USER_PREFS_JSON",
|
|
425
|
+
process.env.CAMOUFOX_FIREFOX_USER_PREFS_JSON
|
|
426
|
+
) ?? {}
|
|
427
|
+
}
|
|
428
|
+
};
|
|
429
|
+
if (Object.keys(firefoxUserPrefs).length > 0) {
|
|
430
|
+
payload.firefox_user_prefs = firefoxUserPrefs;
|
|
431
|
+
}
|
|
432
|
+
payload.env = {
|
|
433
|
+
...args.plainAuthMode ? {} : toPrimitiveRecord("CAMOUFOX_EXTRA_LAUNCH_JSON.env", extraLaunch.env) ?? {},
|
|
434
|
+
...pickBrowserEnv({
|
|
435
|
+
display: args.display,
|
|
436
|
+
headlessMode: args.headlessMode
|
|
437
|
+
})
|
|
438
|
+
};
|
|
439
|
+
const headlessMode = resolveHeadlessMode(args.headlessMode);
|
|
440
|
+
payload.headless = getHeadlessValue(args.headlessMode);
|
|
441
|
+
if (args.display && headlessMode === "virtual") {
|
|
442
|
+
payload.virtual_display = args.display;
|
|
443
|
+
}
|
|
444
|
+
payload.humanize = isLocalAppMode() || headlessMode === "headful" ? false : typeof args.humanize === "boolean" ? args.humanize : getHumanizeValue();
|
|
445
|
+
if (!args.plainAuthMode) {
|
|
446
|
+
payload.block_images = process.env.CAMOUFOX_BLOCK_IMAGES === "true";
|
|
447
|
+
payload.block_webrtc = process.env.CAMOUFOX_BLOCK_WEBRTC === "true";
|
|
448
|
+
payload.block_webgl = process.env.CAMOUFOX_BLOCK_WEBGL === "true";
|
|
449
|
+
payload.disable_coop = process.env.CAMOUFOX_DISABLE_COOP === "true";
|
|
450
|
+
payload.custom_fonts_only = process.env.CAMOUFOX_CUSTOM_FONTS_ONLY === "true";
|
|
451
|
+
payload.main_world_eval = process.env.CAMOUFOX_MAIN_WORLD_EVAL === "true";
|
|
452
|
+
}
|
|
453
|
+
payload.enable_cache = process.env.CAMOUFOX_ENABLE_CACHE === "true";
|
|
454
|
+
payload.i_know_what_im_doing = process.env.CAMOUFOX_I_KNOW_WHAT_IM_DOING === "true";
|
|
455
|
+
payload.debug = process.env.CAMOUFOX_DEBUG === "true";
|
|
456
|
+
if (args.proxy) {
|
|
457
|
+
payload.proxy = args.proxy;
|
|
458
|
+
} else {
|
|
459
|
+
payload.proxy = void 0;
|
|
460
|
+
}
|
|
461
|
+
if (args.plainAuthMode || args.disableFingerprinting) {
|
|
462
|
+
payload.geoip = false;
|
|
463
|
+
} else if (!isLocalAppMode()) {
|
|
464
|
+
const geoip = parseGeoipValue();
|
|
465
|
+
if (geoip !== void 0) {
|
|
466
|
+
payload.geoip = geoip;
|
|
467
|
+
}
|
|
468
|
+
if (process.env.CAMOUFOX_GEOIP_DB) {
|
|
469
|
+
payload.geoip_db = process.env.CAMOUFOX_GEOIP_DB;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
const os = args.plainAuthMode ? resolveHostOs() : parseStringOrList("CAMOUFOX_OS", process.env.CAMOUFOX_OS) ?? (isLocalAppMode() && resolveHeadlessMode(args.headlessMode) === "headful" ? resolveHostOs() : void 0);
|
|
473
|
+
if (os !== void 0) payload.os = os;
|
|
474
|
+
const locale = args.plainAuthMode ? "en-US" : parseStringOrList("CAMOUFOX_LOCALE", process.env.CAMOUFOX_LOCALE) ?? "en-US";
|
|
475
|
+
payload.locale = locale;
|
|
476
|
+
const addons = args.plainAuthMode ? void 0 : parseStringList("CAMOUFOX_ADDONS", process.env.CAMOUFOX_ADDONS);
|
|
477
|
+
if (addons !== void 0) payload.addons = addons;
|
|
478
|
+
const fonts = args.plainAuthMode ? void 0 : parseStringList("CAMOUFOX_FONTS", process.env.CAMOUFOX_FONTS);
|
|
479
|
+
const systemFonts = args.plainAuthMode ? await discoverSystemFontFamilies() : process.env.CAMOUFOX_USE_FULL_OS_FONTS === "true" ? await discoverSystemFontFamilies() : [];
|
|
480
|
+
const mergedFonts = dedupeStrings([...fonts ?? [], ...systemFonts]);
|
|
481
|
+
if (mergedFonts.length > 0) payload.fonts = mergedFonts;
|
|
482
|
+
payload.use_full_os_fonts = args.plainAuthMode ? true : process.env.CAMOUFOX_USE_FULL_OS_FONTS === "true";
|
|
483
|
+
if (args.plainAuthMode) {
|
|
484
|
+
payload.custom_fonts_only = false;
|
|
485
|
+
}
|
|
486
|
+
if (args.disableDefaultAddons) {
|
|
487
|
+
payload.disable_default_addons = true;
|
|
488
|
+
}
|
|
489
|
+
const excludeAddons = args.plainAuthMode ? void 0 : parseStringList(
|
|
490
|
+
"CAMOUFOX_EXCLUDE_ADDONS",
|
|
491
|
+
process.env.CAMOUFOX_EXCLUDE_ADDONS
|
|
492
|
+
)?.map((value) => value.toUpperCase());
|
|
493
|
+
if (excludeAddons !== void 0) payload.exclude_addons = excludeAddons;
|
|
494
|
+
const webglConfig = args.plainAuthMode ? void 0 : parseStringPair(
|
|
495
|
+
"CAMOUFOX_WEBGL_CONFIG",
|
|
496
|
+
process.env.CAMOUFOX_WEBGL_CONFIG
|
|
497
|
+
);
|
|
498
|
+
if (webglConfig !== void 0) payload.webgl_config = webglConfig;
|
|
499
|
+
const ffVersion = args.plainAuthMode ? void 0 : parsePositiveInteger(
|
|
500
|
+
"CAMOUFOX_FF_VERSION",
|
|
501
|
+
process.env.CAMOUFOX_FF_VERSION
|
|
502
|
+
);
|
|
503
|
+
if (ffVersion !== void 0) payload.ff_version = ffVersion;
|
|
504
|
+
if (args.plainAuthMode || args.disableFingerprinting) {
|
|
505
|
+
payload.fingerprint_preset = false;
|
|
506
|
+
payload.fingerprint = void 0;
|
|
507
|
+
} else {
|
|
508
|
+
const fingerprint = parseJsonRecord(
|
|
509
|
+
"CAMOUFOX_FINGERPRINT_JSON",
|
|
510
|
+
process.env.CAMOUFOX_FINGERPRINT_JSON
|
|
511
|
+
) ?? (isJsonRecord(extraLaunch.fingerprint) ? extraLaunch.fingerprint : void 0);
|
|
512
|
+
if (fingerprint !== void 0) payload.fingerprint = fingerprint;
|
|
513
|
+
const fingerprintPreset = parseFingerprintPreset(
|
|
514
|
+
process.env.CAMOUFOX_FINGERPRINT_PRESET
|
|
515
|
+
);
|
|
516
|
+
if (fingerprintPreset !== void 0) {
|
|
517
|
+
payload.fingerprint_preset = fingerprintPreset;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
const argsList = args.plainAuthMode ? void 0 : parseStringList("CAMOUFOX_ARGS", process.env.CAMOUFOX_ARGS) ?? extraArgs;
|
|
521
|
+
if (argsList !== void 0) payload.args = argsList;
|
|
522
|
+
if (process.env.CAMOUFOX_BROWSER) {
|
|
523
|
+
payload.browser = process.env.CAMOUFOX_BROWSER;
|
|
524
|
+
}
|
|
525
|
+
if (process.env.CAMOUFOX_EXECUTABLE_PATH) {
|
|
526
|
+
payload.executable_path = process.env.CAMOUFOX_EXECUTABLE_PATH;
|
|
527
|
+
}
|
|
528
|
+
for (const key of Object.keys(payload)) {
|
|
529
|
+
const value = payload[key];
|
|
530
|
+
if (value === void 0 || isJsonRecord(value) && Object.keys(value).length === 0 || Array.isArray(value) && value.length === 0) {
|
|
531
|
+
delete payload[key];
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
return payload;
|
|
535
|
+
}
|
|
536
|
+
async function resolveCamoufoxLaunchOptions(args) {
|
|
537
|
+
const python = await resolvePythonBinary(args.provider);
|
|
538
|
+
const payload = await buildLaunchPayload(args);
|
|
539
|
+
const childEnv = {
|
|
540
|
+
...process.env,
|
|
541
|
+
...args.display ? { DISPLAY: args.display } : {},
|
|
542
|
+
CAMOUFOX_OPTIONS_PAYLOAD: JSON.stringify(payload)
|
|
543
|
+
};
|
|
544
|
+
try {
|
|
545
|
+
const { stdout } = await execFileAsync(
|
|
546
|
+
python,
|
|
547
|
+
["-c", CAMOUFOX_OPTIONS_SCRIPT],
|
|
548
|
+
{
|
|
549
|
+
encoding: "utf8",
|
|
550
|
+
env: childEnv,
|
|
551
|
+
timeout: CAMOUFOX_OPTIONS_TIMEOUT_MS,
|
|
552
|
+
maxBuffer: 512 * 1024
|
|
553
|
+
}
|
|
554
|
+
);
|
|
555
|
+
const lines = stdout.trim().split("\n");
|
|
556
|
+
const jsonLine = [...lines].reverse().find((l) => l.trimStart().startsWith("{")) ?? stdout.trim();
|
|
557
|
+
const parsed = JSON.parse(jsonLine);
|
|
558
|
+
if (!parsed.executable_path) {
|
|
559
|
+
throw new Error("Camoufox did not return an executable path.");
|
|
560
|
+
}
|
|
561
|
+
const {
|
|
562
|
+
executable_path: executablePath,
|
|
563
|
+
firefox_user_prefs: firefoxUserPrefs,
|
|
564
|
+
...rest
|
|
565
|
+
} = parsed;
|
|
566
|
+
return {
|
|
567
|
+
...rest,
|
|
568
|
+
args: parsed.args ?? [],
|
|
569
|
+
env: stringifyEnv(parsed.env),
|
|
570
|
+
executablePath,
|
|
571
|
+
firefoxUserPrefs: firefoxUserPrefs ?? {},
|
|
572
|
+
headless: parsed.headless ?? false,
|
|
573
|
+
proxy: parsed.proxy
|
|
574
|
+
};
|
|
575
|
+
} catch (error) {
|
|
576
|
+
const stderr = typeof error?.stderr === "string" ? error.stderr.trim() : "";
|
|
577
|
+
const message = stderr || toErrorMessage(error);
|
|
578
|
+
const installHint = "Install Camoufox in the runtime image, for example: python3 -m pip install 'cloverlabs-camoufox[geoip]' && python3 -m camoufox set official/stable && python3 -m camoufox fetch";
|
|
579
|
+
throw new ExternalServiceError(
|
|
580
|
+
args.provider,
|
|
581
|
+
message.includes("CAMOUFOX_IMPORT_ERROR::") ? `Camoufox is not installed for ${python}. ${installHint}` : `Failed to resolve Camoufox launch options: ${message}`
|
|
582
|
+
);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
export {
|
|
586
|
+
resolveCamoufoxLaunchOptions
|
|
587
|
+
};
|