mcp-scraper 0.88.2 → 0.89.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 (87) hide show
  1. package/CHANGELOG.md +36 -15
  2. package/README.md +6 -5
  3. package/THIRD_PARTY_NOTICES.html +203 -0
  4. package/dist/analytics-repository-2BMT5JNE.js +1 -0
  5. package/dist/bin/api-server.js +2 -41
  6. package/dist/bin/mcp-scraper-cli.js +39 -756
  7. package/dist/bin/mcp-scraper-core.js +1 -60
  8. package/dist/bin/mcp-scraper-install.js +2 -25
  9. package/dist/bin/mcp-stdio-server.js +1 -19
  10. package/dist/bin/paa-harvest.js +1 -41
  11. package/dist/chunk-3GP5CYZX.js +1 -0
  12. package/dist/chunk-4AI7DOS7.js +59 -0
  13. package/dist/chunk-4FROKQJN.js +1 -0
  14. package/dist/chunk-7YGVI5J4.js +21710 -0
  15. package/dist/chunk-CCYWSJNG.js +16 -0
  16. package/dist/chunk-CFI6CXIV.js +182 -0
  17. package/dist/chunk-E2WRWV3A.js +1 -0
  18. package/dist/chunk-GMWKPIYX.js +1172 -0
  19. package/dist/chunk-HDPYG3XV.js +102 -0
  20. package/dist/chunk-HE45FFBU.js +1 -0
  21. package/dist/chunk-HUV2WTRW.js +1 -0
  22. package/dist/chunk-KJQXUZ4Y.js +4 -0
  23. package/dist/chunk-L4CGLFPU.js +4 -0
  24. package/dist/chunk-M22MM4N4.js +84 -0
  25. package/dist/chunk-MASR22K4.js +73 -0
  26. package/dist/chunk-MZN4U5BL.js +1 -0
  27. package/dist/chunk-PUHFVA7P.js +1280 -0
  28. package/dist/chunk-QPWPR5XG.js +10 -0
  29. package/dist/chunk-TMB56NCA.js +1 -0
  30. package/dist/chunk-TXENITMS.js +20 -0
  31. package/dist/chunk-W2BVJ7S2.js +13 -0
  32. package/dist/chunk-WO3N5FH2.js +5 -0
  33. package/dist/chunk-WSCGYRWA.js +2595 -0
  34. package/dist/chunk-X54CQLK2.js +1 -0
  35. package/dist/chunk-XLWNEVUZ.js +27 -0
  36. package/dist/chunk-XPZVJIZ2.js +100 -0
  37. package/dist/chunk-YQZGZBB4.js +1 -0
  38. package/dist/chunk-Z2QGQJS2.js +1 -0
  39. package/dist/db-F2MX63GI.js +1 -0
  40. package/dist/extract-bundle-SNUIHM3J.js +26 -0
  41. package/dist/gmail-service-BZ3H75XC.js +1 -0
  42. package/dist/index.cjs +21750 -6045
  43. package/dist/index.d.cts +14 -14
  44. package/dist/index.d.ts +14 -14
  45. package/dist/index.js +18 -315
  46. package/dist/lead-list-enrichment-repository-S2H3U7T7.js +1 -0
  47. package/dist/location-data-repository-OTWHWMV6.js +1 -0
  48. package/dist/server-RFR2A5UJ.js +7303 -0
  49. package/dist/site-extract-repository-SE776XDC.js +1 -0
  50. package/dist/worker-XUDSM3AL.js +1 -0
  51. package/package.json +17 -124
  52. package/dist/analytics-repository-GGJJCVVP.js +0 -194
  53. package/dist/chunk-4QMUF6XM.js +0 -1013
  54. package/dist/chunk-6HAV7LCE.js +0 -265
  55. package/dist/chunk-ABF2CGOZ.js +0 -113
  56. package/dist/chunk-C5Z4OFKW.js +0 -404
  57. package/dist/chunk-DNM65UCK.js +0 -299
  58. package/dist/chunk-EQGTEHLZ.js +0 -592
  59. package/dist/chunk-F5GQJWZU.js +0 -732
  60. package/dist/chunk-GGZEC22A.js +0 -215
  61. package/dist/chunk-GXBZXWXB.js +0 -184
  62. package/dist/chunk-IHXAXYIS.js +0 -843
  63. package/dist/chunk-K3Z5AQYE.js +0 -683
  64. package/dist/chunk-K45K75OF.js +0 -6
  65. package/dist/chunk-LFW2FRPJ.js +0 -224
  66. package/dist/chunk-MZDNZQWT.js +0 -2078
  67. package/dist/chunk-NVUKO5NN.js +0 -256
  68. package/dist/chunk-OM7HVEJ3.js +0 -26
  69. package/dist/chunk-OPQIGAFB.js +0 -286
  70. package/dist/chunk-OZJMVCDK.js +0 -16
  71. package/dist/chunk-P7FWOMU7.js +0 -505
  72. package/dist/chunk-PGJQDMC2.js +0 -383
  73. package/dist/chunk-PKZS6SHW.js +0 -33139
  74. package/dist/chunk-RJ7JVYKU.js +0 -68
  75. package/dist/chunk-S24LFPL7.js +0 -5262
  76. package/dist/chunk-T3MZISOF.js +0 -240
  77. package/dist/chunk-UZPTGUDV.js +0 -1915
  78. package/dist/chunk-X623GTBV.js +0 -8290
  79. package/dist/chunk-YXNDOQXN.js +0 -4018
  80. package/dist/db-Z34LPZNR.js +0 -284
  81. package/dist/extract-bundle-565SBZCR.js +0 -1003
  82. package/dist/gmail-service-E6ALS7JG.js +0 -25
  83. package/dist/lead-list-enrichment-repository-36RPVV6N.js +0 -67
  84. package/dist/location-data-repository-WPRG62GE.js +0 -34
  85. package/dist/server-SQZ3A7SY.js +0 -86606
  86. package/dist/site-extract-repository-VYFZASPU.js +0 -69
  87. package/dist/worker-LDCAULWL.js +0 -146
@@ -1,505 +0,0 @@
1
- import {
2
- isPrivateIpAddress,
3
- loadHtmlDocument
4
- } from "./chunk-GGZEC22A.js";
5
-
6
- // src/lib/contact-evidence.ts
7
- var MAX_EMAILS = 20;
8
- var MAX_SOCIAL_PROFILES = 20;
9
- var MAX_CONTACT_FORMS = 10;
10
- var MAX_CANDIDATE_PAGE_URLS = 20;
11
- var MAX_CONTEXT_CHARS = 280;
12
- var EMAIL_PATTERN = /[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/gi;
13
- var EMAIL_NOISE = [
14
- ".png",
15
- ".jpg",
16
- ".jpeg",
17
- ".gif",
18
- ".svg",
19
- ".webp",
20
- ".ico",
21
- "@2x",
22
- "sentry",
23
- "wix.com",
24
- "wixpress",
25
- "example.",
26
- "godaddy",
27
- "squarespace",
28
- "schema.org",
29
- "u003e",
30
- "core-js"
31
- ];
32
- var DEVELOPER_CONTEXT = /\b(?:website|site|web)\s+(?:by|built|design(?:ed)?|develop(?:ed)?|powered)\b|\b(?:developer|webmaster)\b/i;
33
- var CONTACT_PATH = /(?:^|\/)(?:about|bio|contact|contact-us|meet|our-team|people|profile|staff|team)(?:\/|$)/i;
34
- var SOCIAL_HOSTS = {
35
- "facebook.com": "facebook",
36
- "fb.com": "facebook",
37
- "instagram.com": "instagram",
38
- "linkedin.com": "linkedin",
39
- "twitter.com": "twitter",
40
- "x.com": "x",
41
- "youtube.com": "youtube",
42
- "youtu.be": "youtube",
43
- "tiktok.com": "tiktok",
44
- "pinterest.com": "pinterest"
45
- };
46
- var SOCIAL_NOISE = /(?:\/shar(?:e|er)|\/intent\/|\/dialog\/|\/plugins\/|\/share\?|\/developers?\/|platform\.)/i;
47
- function compact(value) {
48
- return (value ?? "").replace(/\s+/g, " ").trim();
49
- }
50
- function safeHttpUrl(value, base) {
51
- try {
52
- const parsed = new URL(value, base);
53
- if (!["http:", "https:"].includes(parsed.protocol) || parsed.username || parsed.password) return null;
54
- return parsed;
55
- } catch {
56
- return null;
57
- }
58
- }
59
- function normalizeEmail(value) {
60
- let decoded = value;
61
- try {
62
- decoded = decodeURIComponent(value);
63
- } catch {
64
- }
65
- const email = decoded.split("?")[0].trim().toLowerCase().replace(/[),.;:]+$/, "");
66
- if (email.length > 254 || email.split("@").length !== 2) return null;
67
- if (!/^[a-z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-z0-9.-]+\.[a-z]{2,}$/i.test(email)) return null;
68
- if (EMAIL_NOISE.some((part) => email.includes(part))) return null;
69
- return email;
70
- }
71
- function nearbyContext(node) {
72
- const parent = node.closest("p,li,address,section,article,div").first();
73
- return compact(parent.length ? parent.text() : node.text()).slice(0, MAX_CONTEXT_CHARS);
74
- }
75
- function addEmail(target, raw, sourceUrl, context) {
76
- const value = normalizeEmail(raw);
77
- if (!value || DEVELOPER_CONTEXT.test(context)) return;
78
- if (!target.has(value)) {
79
- target.set(value, {
80
- value,
81
- sourceUrl,
82
- context: compact(context).slice(0, MAX_CONTEXT_CHARS),
83
- sourceType: "first_party_page"
84
- });
85
- }
86
- }
87
- function normalizedSocialUrl(parsed) {
88
- parsed.hash = "";
89
- for (const key of [...parsed.searchParams.keys()]) {
90
- if (/^(?:utm_|fbclid|gclid)/i.test(key)) parsed.searchParams.delete(key);
91
- }
92
- return parsed.href;
93
- }
94
- function socialNetwork(hostname) {
95
- const host = hostname.toLowerCase().replace(/^www\./, "");
96
- const match = Object.entries(SOCIAL_HOSTS).find(([domain]) => host === domain || host.endsWith(`.${domain}`));
97
- return match?.[1] ?? null;
98
- }
99
- function extractContactEvidence(html, pageUrl) {
100
- const source = safeHttpUrl(pageUrl);
101
- if (!source) throw new Error("pageUrl must be a public HTTP(S) URL");
102
- const { $ } = loadHtmlDocument(html);
103
- const emails = /* @__PURE__ */ new Map();
104
- const socialProfiles = /* @__PURE__ */ new Map();
105
- const contactForms = /* @__PURE__ */ new Map();
106
- const candidatePageUrls = /* @__PURE__ */ new Map();
107
- $('a[href^="mailto:" i]').each((_index, element) => {
108
- const node = $(element);
109
- addEmail(emails, (node.attr("href") ?? "").slice(7), source.href, nearbyContext(node));
110
- });
111
- $("body").find("*").addBack().contents().filter((_index, node) => node.type === "text").each((_index, node) => {
112
- const parent = $(node.parent ?? node);
113
- const text = String(node.data ?? "");
114
- for (const match of text.match(EMAIL_PATTERN) ?? []) addEmail(emails, match, source.href, nearbyContext(parent));
115
- });
116
- $("a[href]").each((_index, element) => {
117
- const node = $(element);
118
- const parsed = safeHttpUrl(node.attr("href") ?? "", source.href);
119
- if (!parsed) return;
120
- const context = compact(`${node.text()} ${nearbyContext(node)}`).slice(0, MAX_CONTEXT_CHARS);
121
- const network = socialNetwork(parsed.hostname);
122
- if (network && !SOCIAL_NOISE.test(`${parsed.pathname}${parsed.search}`)) {
123
- const url2 = normalizedSocialUrl(parsed);
124
- if (!socialProfiles.has(url2)) socialProfiles.set(url2, { url: url2, network, sourceUrl: source.href, context });
125
- return;
126
- }
127
- if (parsed.origin !== source.origin || !CONTACT_PATH.test(parsed.pathname)) return;
128
- parsed.hash = "";
129
- const url = parsed.href;
130
- if (!candidatePageUrls.has(url)) candidatePageUrls.set(url, { url, sourceUrl: source.href, context });
131
- });
132
- $("form").each((_index, element) => {
133
- const node = $(element);
134
- const action = safeHttpUrl(node.attr("action") || source.href, source.href);
135
- if (!action || action.origin !== source.origin) return;
136
- action.hash = "";
137
- const url = action.href;
138
- if (!contactForms.has(url)) contactForms.set(url, { url, sourceUrl: source.href, context: nearbyContext(node) });
139
- });
140
- return {
141
- emails: [...emails.values()].slice(0, MAX_EMAILS),
142
- socialProfiles: [...socialProfiles.values()].slice(0, MAX_SOCIAL_PROFILES),
143
- contactForms: [...contactForms.values()].slice(0, MAX_CONTACT_FORMS),
144
- candidatePageUrls: [...candidatePageUrls.values()].slice(0, MAX_CANDIDATE_PAGE_URLS)
145
- };
146
- }
147
-
148
- // src/mcp/xray-install-contract.ts
149
- import { createHash } from "crypto";
150
- var XRAY_INSTALL_PROTOCOL_VERSION = "3.0.0";
151
- var XRAY_SETUP_TOOL = "analytics_setup_site";
152
- var XRAY_TRIGGER_PHRASE = "Install X-Ray";
153
- var XRAY_CANONICAL_GOAL = "Install X-Ray and tell me what drives customers";
154
- var CONTINUATION = `Call ${XRAY_SETUP_TOOL} with { "startUrl": "<site URL>", "goal": "${XRAY_CANONICAL_GOAL}" }; on every later call, reuse the returned setupId and revision.`;
155
- var LOCAL_JSON = JSON.stringify({
156
- mcpServers: {
157
- "mcp-scraper": {
158
- command: "npx",
159
- args: ["-y", "--package", "mcp-scraper@latest", "mcp-scraper"],
160
- env: { MCP_SCRAPER_API_KEY: "${env:MCP_SCRAPER_API_KEY}" }
161
- }
162
- }
163
- }, null, 2);
164
- var HOSTED_JSON = JSON.stringify({
165
- mcpServers: {
166
- "mcp-scraper": { url: "https://mcpscraper.dev/mcp" }
167
- }
168
- }, null, 2);
169
- var XRAY_INSTALL_CONTRACT_PAYLOAD = {
170
- schema: "https://mcpscraper.dev/schemas/xray-install-contract-v1.json",
171
- protocolVersion: XRAY_INSTALL_PROTOCOL_VERSION,
172
- product: "Thorbit X-Ray",
173
- triggerPhrases: [XRAY_TRIGGER_PHRASE, "Install X-Ray on this site and tell me what drives customers"],
174
- canonicalUrls: {
175
- markdown: "https://mcpscraper.dev/agent-install.md",
176
- json: "https://mcpscraper.dev/.well-known/xray-install.json",
177
- hostedMcp: "https://mcpscraper.dev/mcp"
178
- },
179
- facade: {
180
- tool: XRAY_SETUP_TOOL,
181
- initialCall: { startUrl: "<site URL>", goal: XRAY_CANONICAL_GOAL },
182
- continuationRule: "Treat setupId plus revision as the durable continuation handle. Reuse both after approval, runtime publication, verification, retry, or return."
183
- },
184
- topologies: {
185
- local_stdio: {
186
- label: "Local npm MCP process",
187
- authority: "The client starts MCP Scraper locally. After the one-time Pixel insertion, X-Ray setup publishes only owner-confirmed runtime configuration.",
188
- secretHandling: "Set MCP_SCRAPER_API_KEY in the local environment or client secret field. Never paste it into chat or commit it."
189
- },
190
- hosted_remote: {
191
- label: "Hosted authenticated MCP",
192
- authority: "The hosted MCP inspects approved public pages and publishes owner-confirmed X-Ray runtime configuration. It cannot read or write a caller-local path.",
193
- secretHandling: "Use the MCP OAuth/connection flow. Never put a credential in a URL, prompt, or config shown in chat."
194
- }
195
- },
196
- hosts: [
197
- {
198
- id: "codex",
199
- label: "Codex",
200
- supportedTopologies: ["local_stdio", "hosted_remote"],
201
- mcpConfigMode: "toml",
202
- workspaceRead: true,
203
- workspaceWrite: true,
204
- restartRequired: true,
205
- exactConfig: '[mcp_servers.mcp-scraper]\ncommand = "npx"\nargs = ["-y", "--package", "mcp-scraper@latest", "mcp-scraper"]\nenv_vars = ["MCP_SCRAPER_API_KEY"]',
206
- continuation: CONTINUATION,
207
- rollback: "Remove the mcp_servers.mcp-scraper table, then restart Codex. Pixel removal is a separate owner-controlled site action."
208
- },
209
- {
210
- id: "claude-code",
211
- label: "Claude Code",
212
- supportedTopologies: ["local_stdio", "hosted_remote"],
213
- mcpConfigMode: "command",
214
- workspaceRead: true,
215
- workspaceWrite: true,
216
- restartRequired: true,
217
- exactConfig: 'claude mcp add mcp-scraper --scope user --env MCP_SCRAPER_API_KEY="$MCP_SCRAPER_API_KEY" -- npx -y --package mcp-scraper@latest mcp-scraper',
218
- continuation: CONTINUATION,
219
- rollback: "Run: claude mcp remove mcp-scraper -s user. Pixel removal is a separate owner-controlled site action."
220
- },
221
- {
222
- id: "claude-desktop",
223
- label: "Claude Desktop",
224
- supportedTopologies: ["local_stdio"],
225
- mcpConfigMode: "desktop_extension",
226
- workspaceRead: false,
227
- workspaceWrite: false,
228
- restartRequired: true,
229
- exactConfig: "Install https://mcpscraper.dev/downloads/mcp-scraper.mcpb and enter the API key only in Claude Desktop's protected configuration field.",
230
- continuation: CONTINUATION,
231
- rollback: "Remove the MCP Scraper extension in Claude Desktop. Pixel removal is a separate owner-controlled site action."
232
- },
233
- {
234
- id: "cursor",
235
- label: "Cursor",
236
- supportedTopologies: ["local_stdio", "hosted_remote"],
237
- mcpConfigMode: "json",
238
- workspaceRead: true,
239
- workspaceWrite: true,
240
- restartRequired: true,
241
- exactConfig: LOCAL_JSON,
242
- continuation: CONTINUATION,
243
- rollback: "Remove mcp-scraper from .cursor/mcp.json (or the user MCP settings), then restart Cursor."
244
- },
245
- {
246
- id: "windsurf",
247
- label: "Windsurf",
248
- supportedTopologies: ["local_stdio"],
249
- mcpConfigMode: "json",
250
- workspaceRead: true,
251
- workspaceWrite: true,
252
- restartRequired: true,
253
- exactConfig: LOCAL_JSON,
254
- continuation: CONTINUATION,
255
- rollback: "Remove mcp-scraper from the Windsurf MCP configuration, then restart Windsurf."
256
- },
257
- {
258
- id: "cline",
259
- label: "Cline",
260
- supportedTopologies: ["local_stdio"],
261
- mcpConfigMode: "json",
262
- workspaceRead: true,
263
- workspaceWrite: true,
264
- restartRequired: true,
265
- exactConfig: LOCAL_JSON,
266
- continuation: CONTINUATION,
267
- rollback: "Remove mcp-scraper from Cline MCP Servers, then restart the extension host."
268
- },
269
- {
270
- id: "user-action-only",
271
- label: "User-action-only MCP host",
272
- supportedTopologies: ["hosted_remote"],
273
- mcpConfigMode: "handoff_only",
274
- workspaceRead: false,
275
- workspaceWrite: false,
276
- restartRequired: false,
277
- exactConfig: HOSTED_JSON,
278
- continuation: CONTINUATION,
279
- rollback: "Disconnect MCP Scraper from the host. Pixel removal remains an owner-controlled site action."
280
- }
281
- ],
282
- tagBootstrap: {
283
- requirement: "one_global_pixel_once",
284
- ongoingConfiguration: "xray_runtime_manifest",
285
- sourceAccessRequiredAfterInstall: false,
286
- supportedInsertionPaths: ["manual", "tag_manager", "cms_plugin", "coding_host"]
287
- },
288
- workflow: [
289
- "Resolve Thorbit X-Ray entitlement and the target site without consuming MCP Scraper Credits.",
290
- "Inspect the whole approved public site; enumerate distinct renderers, routes, and Pixel coverage.",
291
- "Infer the business model and customer outcome, then propose one privacy-safe measurement plan.",
292
- "If the Pixel is absent, return the canonical one-time tag and exact uncovered surfaces; do not claim ongoing control there.",
293
- "Publish an approved measurement revision as a digest-bound runtime manifest; source or repository access is not required.",
294
- "For SaaS, verify the measurement foundation: global tag coverage, ad-source capture, lifecycle events, currency and timezone, consent, and observed outcome events.",
295
- "Optionally publish a structured post-purchase survey after approved disclosure evidence; reported influence remains separate from deterministic touches.",
296
- "Compare observed attribution with customer-reported influence only with explicit weights totaling 1,000,000 micros and a named methodology version.",
297
- "Keep view-through reporting unavailable until event-level provider evidence can join to an eligible X-Ray subject; aggregate impressions never qualify.",
298
- "Verify route coverage, consent, PageViews, manifest convergence, bounded rule matches, and persisted definition/version evidence.",
299
- "Require a separate exact-revision approval before any canary or real-visitor experiment activation.",
300
- "Return first customer-journey answers when sufficient, or exact evidence-readiness thresholds when not.",
301
- "On \u201Cimprove\u201D, propose one highest-value uncovered outcome, require approval when needed, and reverify."
302
- ],
303
- safetyRules: [
304
- "Never invent or hand-author tracking code; use the canonical product-authored Pixel snippet returned by analytics_setup_site.",
305
- "Never ask for or print API keys, OAuth tokens, signed URLs, cookies, or other credentials.",
306
- "Never call ad platforms directly or silently activate events, definitions, funnels, or delivery destinations.",
307
- "Never treat a CTA click as a customer, sale, or revenue event without verified outcome evidence.",
308
- "Never claim full-domain installation while an in-scope route or renderer is missing, excluded without approval, or unverifiable.",
309
- "Never treat event-name presence as verification; require persisted definition ID and version evidence.",
310
- "Never accept survey invitation tokens or exchange codes through MCP inputs or prompts.",
311
- "Never treat customer-reported influence as identity, CRM mutation authority, ad-activation evidence, or a deterministic touch."
312
- ],
313
- terminalStates: ["complete", "tag_required", "input_required", "approval_required", "runtime_convergence_required", "not_ready", "error"]
314
- };
315
- function canonicalJson(value) {
316
- if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
317
- if (value && typeof value === "object") {
318
- return `{${Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, item]) => `${JSON.stringify(key)}:${canonicalJson(item)}`).join(",")}}`;
319
- }
320
- return JSON.stringify(value);
321
- }
322
- function xrayInstallDigestPayload() {
323
- return canonicalJson(XRAY_INSTALL_CONTRACT_PAYLOAD);
324
- }
325
- var XRAY_INSTALL_PROTOCOL_DIGEST = `sha256:${createHash("sha256").update(xrayInstallDigestPayload(), "utf8").digest("hex")}`;
326
- var XRAY_INSTALL_CONTRACT = {
327
- ...XRAY_INSTALL_CONTRACT_PAYLOAD,
328
- protocolDigest: XRAY_INSTALL_PROTOCOL_DIGEST
329
- };
330
- function xrayHostRecipe(host) {
331
- const recipe = XRAY_INSTALL_CONTRACT.hosts.find((candidate) => candidate.id === host);
332
- if (!recipe) throw new Error(`Unknown X-Ray host: ${host}`);
333
- return recipe;
334
- }
335
- function renderXraySetupPrompt() {
336
- return [
337
- "# Install X-Ray",
338
- "",
339
- `Protocol: ${XRAY_INSTALL_PROTOCOL_VERSION} (${XRAY_INSTALL_PROTOCOL_DIGEST})`,
340
- "",
341
- `Use ${XRAY_SETUP_TOOL} as the single conversational setup facade. Ask for the target site URL if it is missing, then call it with the goal \u201C${XRAY_CANONICAL_GOAL}\u201D.`,
342
- "",
343
- "Continue the same setup with its setupId and revision. Confirm one global Pixel across all public rendering surfaces; present one measurement plan; publish it as a digest-bound runtime manifest after exact-revision approval; verify manifest and rule versions; then return first customer-journey answers or explicit readiness thresholds. For SaaS, verify the provider-neutral measurement foundation. Browser events never prove paid revenue. GitHub or repository access is not required after the tag is installed.",
344
- "",
345
- "Do not request pasted credentials, invent tracking code, silently activate measurement, call ad platforms directly, or claim customers/revenue from CTA clicks."
346
- ].join("\n");
347
- }
348
- function xrayInstallJson(contract = XRAY_INSTALL_CONTRACT) {
349
- return JSON.parse(JSON.stringify(contract));
350
- }
351
-
352
- // src/leads/contact-enrichment.ts
353
- var GENERIC_EMAIL_PREFIXES = /* @__PURE__ */ new Set([
354
- "admin",
355
- "contact",
356
- "customerservice",
357
- "hello",
358
- "help",
359
- "info",
360
- "inquiries",
361
- "mail",
362
- "office",
363
- "reception",
364
- "sales",
365
- "service",
366
- "support",
367
- "team"
368
- ]);
369
- var CONTACT_PATH2 = /(?:^|\/)(?:about|bio|contact|contact-us|meet|our-team|people|profile|staff|team)(?:\/|$)/i;
370
- var TEAM_CONTEXT = /\b(?:our team|meet the team|staff|agents?|brokers?|realtors?|people)\b/i;
371
- var OFFICE_CONTEXT = /\b(?:brokerage|company|corporate|front desk|main office|office|organization|reception)\b/i;
372
- function normalizeWords(value) {
373
- return value.toLowerCase().replace(/[^a-z0-9]+/g, " ").trim().split(/\s+/).filter(Boolean);
374
- }
375
- function safeHttpUrl2(value, base) {
376
- try {
377
- const parsed = new URL(value, base);
378
- if (!["http:", "https:"].includes(parsed.protocol) || parsed.username || parsed.password) return null;
379
- return parsed;
380
- } catch {
381
- return null;
382
- }
383
- }
384
- function classifyEmailScope(evidence, entity) {
385
- const local = evidence.value.split("@")[0].toLowerCase().replace(/[^a-z0-9]+/g, " ");
386
- const nameWords = normalizeWords(entity.name).filter((word) => word.length > 1);
387
- const pagePath = safeHttpUrl2(evidence.sourceUrl)?.pathname.toLowerCase() ?? "";
388
- const combinedContext = `${evidence.context} ${pagePath}`.toLowerCase();
389
- const generic = GENERIC_EMAIL_PREFIXES.has(local.replace(/\s+/g, ""));
390
- const nameHits = nameWords.filter((word) => new RegExp(`(?:^|[^a-z0-9])${word}(?:$|[^a-z0-9])`, "i").test(`${local} ${combinedContext}`)).length;
391
- const hasExactPersonContext = entity.entityType === "person" && nameWords.length >= 2 && nameHits >= Math.min(2, nameWords.length) && (local.includes(nameWords[0]) || pagePath.includes(nameWords.join("-")) || pagePath.includes(nameWords.join("/")));
392
- if (!generic && hasExactPersonContext) return { scope: "person", reasons: ["named_person_page_context"] };
393
- if (generic || OFFICE_CONTEXT.test(combinedContext)) return { scope: "office", reasons: generic ? ["generic_mailbox_prefix"] : ["organization_page_context"] };
394
- if (TEAM_CONTEXT.test(combinedContext)) return { scope: "team", reasons: ["team_page_context"] };
395
- if (entity.entityType === "business") {
396
- const organizationWords = normalizeWords(entity.organization || entity.name).filter((word) => word.length > 2);
397
- if (organizationWords.some((word) => combinedContext.includes(word))) return { scope: "team", reasons: ["business_context"] };
398
- }
399
- return { scope: "unknown", reasons: ["public_occurrence_without_identity_evidence"] };
400
- }
401
- function selectPrimaryEmail(evidence, entity) {
402
- const scoped = evidence.map((item) => ({ ...item, ...classifyEmailScope(item, entity) }));
403
- const allowed = entity.entityType === "person" ? ["person", "team"] : ["person", "team", "office"];
404
- const sourcePriority = ["provided", "first_party_page", "structured_data", "serp_snippet"];
405
- const priority = ["person", "team", "office"];
406
- return scoped.filter((item) => allowed.includes(item.scope)).sort((a, b) => {
407
- const sourceDelta = sourcePriority.indexOf(a.sourceType) - sourcePriority.indexOf(b.sourceType);
408
- return sourceDelta || priority.indexOf(a.scope) - priority.indexOf(b.scope);
409
- })[0] ?? null;
410
- }
411
- function contactAttemptUrls(seedUrl, discoveredLinks, maxPages) {
412
- if (!Number.isInteger(maxPages) || maxPages < 1 || maxPages > 4) throw new Error("maxPages must be an integer from 1 to 4");
413
- const seed = safeHttpUrl2(seedUrl);
414
- if (!seed) return [];
415
- seed.hash = "";
416
- const urls = /* @__PURE__ */ new Set([seed.href]);
417
- const discovered = discoveredLinks.map((item) => typeof item === "string" ? item : item.url).map((item) => safeHttpUrl2(item, seed.href)).filter((item) => Boolean(item && item.origin === seed.origin && CONTACT_PATH2.test(item.pathname)));
418
- for (const item of discovered) {
419
- item.hash = "";
420
- urls.add(item.href);
421
- }
422
- for (const pathname of ["/contact", "/contact-us"]) urls.add(new URL(pathname, seed.origin).href);
423
- return [...urls].slice(0, maxPages);
424
- }
425
-
426
- // src/leads/serp-email-evidence.ts
427
- var EMAIL_CANDIDATE = /[A-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[A-Z0-9.-]+\.[A-Z]{2,}/gi;
428
- var DEVELOPER_CONTEXT2 = /\b(?:website|site|web)\s+(?:by|built|design(?:ed)?|develop(?:ed)?|powered)\b|\b(?:developer|webmaster)\b/i;
429
- var MAX_CONTEXT_CHARS2 = 280;
430
- var MAX_EMAILS2 = 10;
431
- function normalizeHostname(hostname) {
432
- return hostname.toLowerCase().replace(/^www\./, "").replace(/\.$/, "");
433
- }
434
- function isLocalHostname(hostname) {
435
- const host = normalizeHostname(hostname);
436
- return host === "localhost" || host.endsWith(".localhost") || host.endsWith(".local") || !host.includes(".") && !host.includes(":");
437
- }
438
- function parsePublicLiteralUrl(websiteUrl) {
439
- try {
440
- const parsed = new URL(websiteUrl.trim());
441
- if (!["http:", "https:"].includes(parsed.protocol) || parsed.username || parsed.password) return null;
442
- if (isLocalHostname(parsed.hostname) || isPrivateIpAddress(parsed.hostname)) return null;
443
- return parsed;
444
- } catch {
445
- return null;
446
- }
447
- }
448
- function compactContext(title, snippet) {
449
- return `${title} ${snippet ?? ""}`.replace(/\s+/g, " ").trim().slice(0, MAX_CONTEXT_CHARS2);
450
- }
451
- function buildSerpEmailQuery(websiteUrl) {
452
- const parsed = parsePublicLiteralUrl(websiteUrl);
453
- if (!parsed) return null;
454
- parsed.hostname = normalizeHostname(parsed.hostname);
455
- return `email for ${parsed.protocol}//${parsed.host}/`;
456
- }
457
- function extractSerpEmailEvidence({
458
- websiteUrl,
459
- results
460
- }) {
461
- const parsedWebsite = parsePublicLiteralUrl(websiteUrl);
462
- if (!parsedWebsite) return [];
463
- const websiteDomain = normalizeHostname(parsedWebsite.hostname);
464
- const accepted = /* @__PURE__ */ new Map();
465
- const orderedResults = results.map((result, index) => ({ result, index })).sort((left, right) => {
466
- const leftPosition = Number.isFinite(left.result.position) ? left.result.position : Number.MAX_SAFE_INTEGER;
467
- const rightPosition = Number.isFinite(right.result.position) ? right.result.position : Number.MAX_SAFE_INTEGER;
468
- return leftPosition - rightPosition || left.index - right.index;
469
- });
470
- for (const { result } of orderedResults) {
471
- const searchableText = `${result.title} ${result.snippet ?? ""}`;
472
- const context = compactContext(result.title, result.snippet);
473
- for (const rawCandidate of searchableText.match(EMAIL_CANDIDATE) ?? []) {
474
- const value = normalizeEmail(rawCandidate);
475
- if (!value || DEVELOPER_CONTEXT2.test(searchableText) || normalizeHostname(value.slice(value.lastIndexOf("@") + 1)) !== websiteDomain) continue;
476
- if (accepted.has(value)) continue;
477
- accepted.set(value, {
478
- value,
479
- sourceUrl: result.url,
480
- sourceTitle: result.title,
481
- sourcePosition: result.position,
482
- context,
483
- sourceType: "serp_snippet"
484
- });
485
- }
486
- }
487
- return [...accepted.values()].slice(0, MAX_EMAILS2);
488
- }
489
-
490
- export {
491
- extractContactEvidence,
492
- classifyEmailScope,
493
- selectPrimaryEmail,
494
- contactAttemptUrls,
495
- buildSerpEmailQuery,
496
- extractSerpEmailEvidence,
497
- XRAY_INSTALL_PROTOCOL_VERSION,
498
- XRAY_SETUP_TOOL,
499
- XRAY_CANONICAL_GOAL,
500
- XRAY_INSTALL_PROTOCOL_DIGEST,
501
- XRAY_INSTALL_CONTRACT,
502
- xrayHostRecipe,
503
- renderXraySetupPrompt,
504
- xrayInstallJson
505
- };