agentlinkops 0.5.0 → 0.6.0

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 (174) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codex-plugin/plugin.json +1 -1
  3. package/.mcp.json +8 -0
  4. package/CHANGELOG.md +15 -1
  5. package/LICENSE +0 -0
  6. package/NOTICE +0 -0
  7. package/README.md +2 -1
  8. package/bin/agentlinkops.mjs +0 -0
  9. package/bin/linktrail.mjs +0 -0
  10. package/cli/adapters/csv.js +0 -0
  11. package/cli/adapters/sqlite-crm.js +0 -0
  12. package/cli/adapters/suppliers.js +0 -0
  13. package/cli/agent-setup.js +0 -0
  14. package/cli/agentlinkops.mjs +0 -0
  15. package/cli/args.js +0 -0
  16. package/cli/catalog.js +0 -0
  17. package/cli/check.js +0 -0
  18. package/cli/citation.js +124 -2
  19. package/cli/client.js +1 -1
  20. package/cli/commands.js +0 -0
  21. package/cli/config.js +0 -0
  22. package/cli/connection.js +0 -0
  23. package/cli/context-mcp.js +0 -0
  24. package/cli/context.js +0 -0
  25. package/cli/delivery.js +0 -0
  26. package/cli/doctor.js +19 -0
  27. package/cli/env.js +0 -0
  28. package/cli/fleet.js +0 -0
  29. package/cli/gsc-browser.js +0 -0
  30. package/cli/import.js +0 -0
  31. package/cli/ledger.js +0 -0
  32. package/cli/linktrail.mjs +0 -0
  33. package/cli/local-result.js +0 -0
  34. package/cli/main.js +0 -0
  35. package/cli/mirror.js +0 -0
  36. package/cli/mix.js +0 -0
  37. package/cli/platforms.js +0 -0
  38. package/cli/receipt-performance.js +0 -0
  39. package/cli/receipts.js +0 -0
  40. package/cli/report.js +0 -0
  41. package/cli/setup-plan.js +0 -0
  42. package/cli/skill.js +0 -0
  43. package/cli/state.js +0 -0
  44. package/cli/status.js +0 -0
  45. package/cli/sync.js +0 -0
  46. package/package.json +14 -3
  47. package/references/cli.md +0 -0
  48. package/references/disavow.md +0 -0
  49. package/references/reports.md +0 -0
  50. package/scripts/agentlinkops.py +0 -0
  51. package/scripts/disavow.py +0 -0
  52. package/scripts/linktrail.py +0 -0
  53. package/scripts/migrations/001_crm.sql +0 -0
  54. package/scripts/migrations/002_cloud_events.sql +0 -0
  55. package/scripts/migrations/003_disavow.sql +0 -0
  56. package/scripts/migrations/004_placement_costs.sql +0 -0
  57. package/scripts/report.py +0 -0
  58. package/shared/agent-clients.js +0 -0
  59. package/shared/observation-event.js +0 -0
  60. package/shared/response-contract.js +0 -0
  61. package/shared/search-connection-contract.js +0 -0
  62. package/skills/agentlinkops-assets/SKILL.md +0 -0
  63. package/skills/agentlinkops-assets/agents/openai.yaml +0 -0
  64. package/skills/agentlinkops-campaigns/SKILL.md +0 -0
  65. package/skills/agentlinkops-campaigns/agents/openai.yaml +0 -0
  66. package/skills/agentlinkops-connect/SKILL.md +0 -0
  67. package/skills/agentlinkops-connect/agents/openai.yaml +0 -0
  68. package/skills/agentlinkops-connect/references/agentlinkops.md +0 -0
  69. package/skills/agentlinkops-crm/SKILL.md +0 -0
  70. package/skills/agentlinkops-crm/agents/openai.yaml +0 -0
  71. package/skills/content-defingerprinting/SKILL.md +0 -0
  72. package/skills/content-defingerprinting/scripts/watermarks/README.md +0 -0
  73. package/skills/content-defingerprinting/scripts/watermarks/UPSTREAM.json +0 -0
  74. package/skills/content-defingerprinting/scripts/watermarks/revendor.sh +0 -0
  75. package/skills/content-defingerprinting/scripts/watermarks/vendor/LICENSE.upstream +0 -0
  76. package/skills/content-defingerprinting/scripts/watermarks/vendor/clean_text.py +0 -0
  77. package/skills/content-defingerprinting/scripts/watermarks/vendor/common.py +0 -0
  78. package/skills/content-defingerprinting/scripts/watermarks/vendor/inspect_text.py +0 -0
  79. package/skills/content-defingerprinting/scripts/watermarks/vendor/score_stylometry.py +0 -0
  80. package/skills/content-defingerprinting/scripts/watermarks/vendor/text_unicode.py +0 -0
  81. package/skills/content-defingerprinting/scripts/watermarks/verify-vendor.py +0 -0
  82. package/skills/content-defingerprinting/scripts/watermarks/wm-scan.py +0 -0
  83. package/skills/internal-linking-optimizer/SKILL.md +0 -0
  84. package/skills/internal-linking-optimizer/references/source-prompt.md +0 -0
  85. package/skills/pre-publish-review/SKILL.md +0 -0
  86. package/skills/source-cited-content-builder/SKILL.md +0 -0
  87. package/skills/source-cited-content-builder/references/source-prompt.md +0 -0
  88. package/skills/source-cited-humanizer/SKILL.md +0 -0
  89. package/src/citations/adapter.js +173 -0
  90. package/src/citations/alert.js +53 -0
  91. package/src/citations/browser/engine.js +267 -0
  92. package/src/citations/browser/gen/core/providers/_shared/google.js +4 -0
  93. package/src/citations/browser/gen/core/providers/_shared/rawSourcesDom.js +15 -0
  94. package/src/citations/browser/gen/core/providers/_shared/resetProviderPage.js +22 -0
  95. package/src/citations/browser/gen/core/providers/_shared/sourceUtils.js +8 -0
  96. package/src/citations/browser/gen/core/providers/ai-overview/index.js +70 -0
  97. package/src/citations/browser/gen/core/providers/ai-overview/lib/expand.js +59 -0
  98. package/src/citations/browser/gen/core/providers/ai-overview/lib/extractSources.js +60 -0
  99. package/src/citations/browser/gen/core/providers/ai-overview/lib/pageLifecycle.js +18 -0
  100. package/src/citations/browser/gen/core/providers/ai-overview/lib/session.js +62 -0
  101. package/src/citations/browser/gen/core/providers/chatgpt/index.js +28 -0
  102. package/src/citations/browser/gen/core/providers/chatgpt/lib/dismissAuthModal.js +39 -0
  103. package/src/citations/browser/gen/core/providers/chatgpt/lib/extractSources.js +66 -0
  104. package/src/citations/browser/gen/core/providers/chatgpt/lib/pageLifecycle.js +11 -0
  105. package/src/citations/browser/gen/core/providers/claude/index.js +16 -0
  106. package/src/citations/browser/gen/core/providers/claude/lib/extractSources.js +54 -0
  107. package/src/citations/browser/gen/core/providers/claude/lib/pageLifecycle.js +9 -0
  108. package/src/citations/browser/gen/core/providers/gemini/index.js +40 -0
  109. package/src/citations/browser/gen/core/providers/gemini/lib/extractSources.js +65 -0
  110. package/src/citations/browser/gen/core/providers/gemini/lib/pageLifecycle.js +14 -0
  111. package/src/citations/browser/gen/core/providers/gemini/lib/session.js +45 -0
  112. package/src/citations/browser/gen/core/providers/index.js +15 -0
  113. package/src/citations/browser/gen/core/providers/perplexity/index.js +31 -0
  114. package/src/citations/browser/gen/core/providers/perplexity/lib/extractSources.js +84 -0
  115. package/src/citations/browser/gen/core/providers/perplexity/lib/navigation.js +38 -0
  116. package/src/citations/browser/gen/core/providers/types.js +0 -0
  117. package/src/citations/browser/gen/lib/browser/camoufox.js +587 -0
  118. package/src/citations/browser/gen/lib/browser/display.js +147 -0
  119. package/src/citations/browser/gen/lib/browser/domOps.js +339 -0
  120. package/src/citations/browser/gen/lib/browser/humanBehavior.js +121 -0
  121. package/src/citations/browser/gen/lib/browser/navigate.js +46 -0
  122. package/src/citations/browser/gen/lib/browser/playwrightCompat.js +321 -0
  123. package/src/citations/browser/gen/lib/browser/proxy/forwarder.js +251 -0
  124. package/src/citations/browser/gen/lib/browser/runtimeTypes.js +0 -0
  125. package/src/citations/browser/gen/lib/extraction/sourceUtils.js +71 -0
  126. package/src/citations/browser/gen/lib/input/editor/assertNotBlocked.js +54 -0
  127. package/src/citations/browser/gen/lib/input/editor/clearInput.js +52 -0
  128. package/src/citations/browser/gen/lib/input/editor/findEditor.js +54 -0
  129. package/src/citations/browser/gen/lib/input/editor/findSendButton.js +21 -0
  130. package/src/citations/browser/gen/lib/input/editor/promptInput.js +117 -0
  131. package/src/citations/browser/gen/lib/input/markdown/converter.js +101 -0
  132. package/src/citations/browser/gen/lib/input/markdown/toMarkdown.js +14 -0
  133. package/src/citations/browser/gen/lib/input/response/detectBotPage.js +13 -0
  134. package/src/citations/browser/gen/lib/input/response/getText.js +10 -0
  135. package/src/citations/browser/gen/lib/input/response/isGenerating.js +59 -0
  136. package/src/citations/browser/gen/lib/input/response/waitForFinish.js +105 -0
  137. package/src/citations/browser/gen/lib/input/sources/findButton.js +41 -0
  138. package/src/citations/browser/gen/lib/input/sources/openPanel.js +11 -0
  139. package/src/citations/browser/gen/utils/agent-constants.js +87 -0
  140. package/src/citations/browser/providers.js +166 -0
  141. package/src/citations/browser/shims/errors.js +26 -0
  142. package/src/citations/browser/shims/types.js +6 -0
  143. package/src/citations/browser/shims/utils.js +32 -0
  144. package/src/citations/contract.js +19 -3
  145. package/src/citations/fixtures/dataforseo-aio.json +82 -0
  146. package/src/citations/match.js +0 -0
  147. package/src/citations/panel-builder.js +44 -0
  148. package/src/citations/report.js +165 -0
  149. package/src/citations/runner.js +43 -16
  150. package/src/citations/stats.js +63 -0
  151. package/src/citations/watch.js +85 -0
  152. package/src/competitors/contract.js +0 -0
  153. package/src/competitors/domain-mix.js +0 -0
  154. package/src/context/ga4.js +0 -0
  155. package/src/context/gsc.js +0 -0
  156. package/src/context/site-profile.js +0 -0
  157. package/src/context/util.js +0 -0
  158. package/src/context-tool-registry.js +0 -0
  159. package/src/corpus/acquire.js +0 -0
  160. package/src/corpus/seeds.js +0 -0
  161. package/src/discovery/contract.js +0 -0
  162. package/src/discovery/import.js +0 -0
  163. package/src/tool-views.js +0 -0
  164. package/src/verifier/fetch.js +0 -0
  165. package/src/verifier/index.js +0 -0
  166. package/src/verifier/observation-kind.js +0 -0
  167. package/src/verifier/parser.js +0 -0
  168. package/src/verifier/readiness.js +0 -0
  169. package/src/verifier/render-evidence.js +0 -0
  170. package/src/verifier/robots.js +0 -0
  171. package/src/verifier/transition.js +0 -0
  172. package/src/verifier/url.js +0 -0
  173. package/templates/SITE.template.md +0 -0
  174. package/templates/campaigns.v1.json +0 -0
@@ -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
+ };